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/.gitattributes b/.gitattributes index 9158b397935..6ab2e8bf481 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,47 @@ +#-------------------------------------------------------------------- +# Line endings +#-------------------------------------------------------------------- +# Source code - always LF +*.zig text eol=lf +*.c text eol=lf +*.h text eol=lf +*.cpp text eol=lf +*.m text eol=lf +*.swift text eol=lf +*.py text eol=lf +*.sh text eol=lf +*.glsl text eol=lf +*.blp text eol=lf + +# Config/build files - always LF +*.zon text eol=lf +*.nix text eol=lf +*.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +CMakeLists.txt text eol=lf +*.cmake text eol=lf +Makefile text eol=lf + +# Text data files - always LF (embedded in Zig, parsed with \n split) +*.txt text eol=lf + +# Windows resource files - preserve as-is (native Windows tooling) +*.rc -text +*.manifest -text + +# Binary files +*.png binary +*.ico binary +*.icns binary +*.ttf binary +*.otf binary + +#-------------------------------------------------------------------- +# Linguist +#-------------------------------------------------------------------- build.zig.zon.nix linguist-generated=true build.zig.zon.txt linguist-generated=true build.zig.zon.json linguist-generated=true diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index cbbbd40ae86..aecc67da9e2 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -18,30 +18,186 @@ # 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 +alosarjos +amadeus +andrejdaskalov +anhthang +anmitalidev +anthonyzhoon +atomk +balazs-szucs +barutsrb bennettp123 +benodiwal bernsno +beryesa +bitigchi bkircher +bo2themax +brentschroeter +brianc442 +cespare +charliie-dev +chernetskyi +chronologos +cmwetherell +crayxt +craziestowl +curtismoncoq +d-dudas +-daedaevibin daiimus +damyanbogoev +danulqua +dariogriffo +davidsanchez222 +deblasis +dervedro +devsunb +diaaeddin +dmehala doprz +douglance +douglas +drepper +dzhlobo +ekaterinepapava elias8 +ephemera eriksremess +faukah filip7 +flou +francescarpi +gagbo +ghokun +gmile +gordonbondon +gpanders +guilhermetk hakonhagland +halosatrio +heaths +heddxh heddxh +-highimpact-dev Disrespectful AI user +hlcfan hqnna +hulet +i999rri +icodesign +j0hnm4r5 +jacobsandlund jake-stewart jcollie +jesusvazquez +jguthmiller +jmcgover +johnslavik +josephmart +jparise juniqlim +karesansui-u +kawarimidoll +kenvandine +khipp +kirwiisp +kjvdven +kloneets +-kody-w +koranir +kristina8888 +kristofersoler +laxystem +liby +linustalacko +lonsagisawa +luisnquin +lynicis +mac0ne mahnokropotkinvich +marijagjorgjieva +markdorison +markhuot +marler8997 marrocco-simone +matkotiric +micaeljarniac +michielvk +miguelelgallo +mihi314 mikailmm +misairuzame +mischief mitchellh +miupa +molechowski +mrconnorkenway +mrmage +mtak +natesmyth +neo773 +nicholas-ochoa +nicosuave +nmggithub +noib3 +nwehg +ocean6954 +oshdubh +paaloeye +pan93412 +pangoraw +pauley-unsaturated peilingjiang peterdavehello +philocalyst +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 +tdgroot +tdslot +ticclick +tnagatomi +trag1c +tristan957 +turbolent tweedbeetle +uhojin +unphased +uzaaft +vaughanandrews +viruslobster +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..34e7652a7b9 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -11,16 +11,18 @@ on: jobs: update-milestone: + # Ignore bot-authored pull requests (dependabot, app bots, etc) + # and CI-only PRs. + if: github.event_name == 'issues' || (github.event.pull_request.user.type != 'Bot' && !startsWith(github.event.pull_request.title, 'ci:')) runs-on: namespace-profile-ghostty-sm name: Milestone Update 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 +30,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..f0f175b651e 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -41,16 +41,16 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index a24e5a38968..1a945f49878 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,17 +83,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -113,7 +113,7 @@ jobs: nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: source-tarball path: |- @@ -130,27 +130,37 @@ 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 - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version - 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..28e56e7fb8a 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -37,10 +37,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -158,22 +163,22 @@ jobs: github.ref_name == 'main' ) ) - runs-on: namespace-profile-ghostty-md + runs-on: namespace-profile-ghostty-sm env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -190,7 +195,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -201,6 +206,60 @@ jobs: ghostty-source.tar.gz.minisig token: ${{ secrets.GH_RELEASE_TOKEN }} + source-tarball-lib-vt: + needs: [setup] + if: | + needs.setup.outputs.should_skip != 'true' && + ( + github.event_name == 'workflow_dispatch' || + ( + github.repository_owner == 'ghostty-org' && + github.ref_name == 'main' + ) + ) + runs-on: namespace-profile-ghostty-sm + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + path: | + /nix + /zig + - uses: cachix/install-nix-action@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - name: Create Tarball + run: | + rm -rf zig-out/dist + nix develop -c zig build dist -Demit-lib-vt=true + cp zig-out/dist/*.tar.gz libghostty-vt-source.tar.gz + + - name: Sign Tarball + run: | + echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key + echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password + nix develop -c minisign -S -m libghostty-vt-source.tar.gz -s minisign.key < minisign.password + + - name: Update Release + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + with: + name: 'Ghostty Tip ("Nightly")' + prerelease: true + tag_name: tip + target_commitish: ${{ github.sha }} + files: | + libghostty-vt-source.tar.gz + libghostty-vt-source.tar.gz.minisig + token: ${{ secrets.GH_RELEASE_TOKEN }} + build-macos: needs: [setup] if: | @@ -219,6 +278,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,17 +287,25 @@ 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: determinate: true - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version @@ -244,7 +313,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 +332,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 +380,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 @@ -360,7 +432,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -438,7 +510,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 +534,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,17 +543,25 @@ 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: determinate: true - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version @@ -487,7 +569,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 +588,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 +636,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 @@ -596,7 +681,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -646,6 +731,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,17 +740,25 @@ 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: determinate: true - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version @@ -671,7 +766,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 +785,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 +833,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 @@ -780,7 +878,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true 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..587e2ed704e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,30 +11,108 @@ 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@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter_every + with: + token: "" + predicate-quantifier: "every" + filters: | + code: + - '**' + - '!.github/VOUCHED.td' + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter_any + with: + 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-dist-lib-vt + - build-examples-zig + - build-examples-cmake + - build-examples-cmake-windows + - build-cmake - build-flatpak - build-libghostty-vt + - build-libghostty-vt-android + - build-libghostty-vt-macos + - build-libghostty-vt-windows - build-linux - build-linux-libghostty - build-nix - build-macos - build-macos-freetype - build-snap - - build-windows - test - test-simd - test-gtk - test-sentry-linux - test-i18n + - test-fuzz-libghostty + - test-lib-vt - test-macos + - test-windows - pinact - prettier + - swiftlint - alejandra - typos - shellcheck @@ -77,17 +155,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -95,23 +173,34 @@ jobs: - name: Build Benchmarks run: nix develop -c zig build -Demit-bench - build-examples: + list-examples: + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip + runs-on: namespace-profile-ghostty-xsm + outputs: + zig: ${{ steps.list.outputs.zig }} + cmake: ${{ steps.list.outputs.cmake }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - id: list + name: List example directories + run: | + zig=$(ls example/*/build.zig.zon 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "$zig" | jq . + echo "zig=$zig" >> "$GITHUB_OUTPUT" + cmake=$(ls example/*/CMakeLists.txt 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "$cmake" | jq . + echo "cmake=$cmake" >> "$GITHUB_OUTPUT" + + build-examples-zig: strategy: fail-fast: false matrix: - dir: - [ - c-vt, - c-vt-key-encode, - c-vt-paste, - c-vt-sgr, - zig-formatter, - zig-vt, - zig-vt-stream, - ] + dir: ${{ fromJSON(needs.list-examples.outputs.zig) }} name: Example ${{ matrix.dir }} - runs-on: namespace-profile-ghostty-sm - needs: test + runs-on: namespace-profile-ghostty-xsm + needs: [test, list-examples] env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache @@ -120,17 +209,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -140,6 +229,104 @@ jobs: cd example/${{ matrix.dir }} nix develop -c zig build + build-examples-cmake: + strategy: + fail-fast: false + matrix: + dir: ${{ fromJSON(needs.list-examples.outputs.cmake) }} + name: Example ${{ matrix.dir }} + runs-on: namespace-profile-ghostty-xsm + needs: [test, list-examples] + 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build Example + run: | + cd example/${{ matrix.dir }} + nix develop -c cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=${{ github.workspace }} + nix develop -c cmake --build build + + build-examples-cmake-windows: + strategy: + fail-fast: false + matrix: + dir: ${{ fromJSON(needs.list-examples.outputs.cmake) }} + name: Example ${{ matrix.dir }} (Windows) + runs-on: namespace-profile-ghostty-windows + timeout-minutes: 45 + needs: [test, list-examples] + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + + - name: Build Example + shell: pwsh + run: | + cd example/${{ matrix.dir }} + cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=${{ github.workspace }} + cmake --build build + + build-cmake: + 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build + run: | + nix develop -c cmake -B build + nix develop -c cmake --build build + + - name: Verify artifacts + run: | + test -f zig-out/lib/libghostty-vt.so.0.1.0 + test -d zig-out/include/ghostty + ls -la zig-out/lib/ + ls -la zig-out/include/ghostty/ + build-flatpak: strategy: fail-fast: false @@ -153,17 +340,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -187,17 +374,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -231,27 +418,131 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build run: | - nix develop -c zig build lib-vt \ + nix develop -c zig build -Demit-lib-vt \ -Dtarget=${{ matrix.target }} \ -Dsimd=false + # lib-vt requires macOS runner for macOS/iOS builds because 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@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.3.app + + - name: Build + run: | + nix develop -c zig build -Demit-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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + 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 -Demit-lib-vt \ + -Dtarget=${{ matrix.target }} + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + + build-libghostty-vt-windows: + runs-on: namespace-profile-ghostty-windows + timeout-minutes: 45 + needs: test + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + + - name: Test libghostty-vt + run: zig build test-lib-vt + + - name: Build libghostty-vt + run: zig build -Demit-lib-vt + build-linux: strategy: fail-fast: false @@ -267,17 +558,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -296,17 +587,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -329,17 +620,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -363,7 +654,7 @@ jobs: run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main' build-dist: - runs-on: namespace-profile-ghostty-md + runs-on: namespace-profile-ghostty-sm needs: test outputs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} @@ -375,17 +666,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -398,12 +689,54 @@ 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: |- ghostty-source.tar.gz + build-dist-lib-vt: + 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build and Check Source Tarball + run: | + rm -rf zig-out/dist + nix develop -c zig build distcheck -Demit-lib-vt=true + + - name: Verify tarball size + run: | + tarball=$(ls zig-out/dist/*.tar.gz) + size=$(stat --format=%s "$tarball") + max=$((5 * 1024 * 1024)) + echo "Tarball size: $size bytes (max: $max)" + if [ "$size" -gt "$max" ]; then + echo "ERROR: tarball exceeds 5 MB" + exit 1 + fi + trigger-snap: if: github.event_name != 'pull_request' runs-on: namespace-profile-ghostty-xsm @@ -443,21 +776,32 @@ 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: determinate: true - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version @@ -475,32 +819,49 @@ 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: determinate: true - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version @@ -509,85 +870,19 @@ 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: | nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_freetype - build-windows: - runs-on: windows-2022 - # this will not stop other jobs from running - continue-on-error: true - timeout-minutes: 45 - needs: test - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # This could be from a script if we wanted to but inlining here for now - # in one place. - # Using powershell so that we do not need to install WSL components. Also, - # WSLv1 is only installed on Github runners. - - name: Install zig - shell: pwsh - run: | - # Get the zig version from build.zig.zon so that it only needs to be updated - $fileContent = Get-Content -Path "build.zig.zon" -Raw - $pattern = 'minimum_zig_version\s*=\s*"([^"]+)"' - $zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value - $version = "zig-x86_64-windows-$zigVersion" - Write-Output $version - $uri = "https://ziglang.org/download/$zigVersion/$version.zip" - Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip" - Expand-Archive -Path ".\zig-windows.zip" -DestinationPath ".\" -Force - Remove-Item -Path ".\zig-windows.zip" - Rename-Item -Path ".\$version" -NewName ".\zig" - Write-Host "Zig installed." - .\zig\zig.exe version - - - name: Generate build testing script - shell: pwsh - run: | - # Generate a script so that we can swallow the errors - $scriptContent = @" - .\zig\zig.exe build test 2>&1 | Out-File -FilePath "build.log" -Append - exit 0 - "@ - $scriptPath = "zigbuild.ps1" - # Write the script content to a file - $scriptContent | Set-Content -Path $scriptPath - Write-Host "Script generated at: $scriptPath" - - - name: Test Windows - shell: pwsh - run: .\zigbuild.ps1 -ErrorAction SilentlyContinue - - - name: Generate build script - shell: pwsh - run: | - # Generate a script so that we can swallow the errors - $scriptContent = @" - .\zig\zig.exe build 2>&1 | Out-File -FilePath "build.log" -Append - exit 0 - "@ - $scriptPath = "zigbuild.ps1" - # Write the script content to a file - $scriptContent | Set-Content -Path $scriptPath - Write-Host "Script generated at: $scriptPath" - - - name: Build Windows - shell: pwsh - run: .\zigbuild.ps1 -ErrorAction SilentlyContinue - - - name: Dump logs - shell: pwsh - 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,17 +899,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -629,6 +924,36 @@ jobs: - name: Test System Build run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p + test-lib-vt: + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip + runs-on: namespace-profile-ghostty-md + 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Test + run: nix develop -c zig build test-lib-vt + test-gtk: strategy: fail-fast: false @@ -646,17 +971,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -694,17 +1019,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -729,17 +1054,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -751,21 +1076,32 @@ 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: determinate: true - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.2.app + run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Xcode Version run: xcodebuild -version @@ -777,12 +1113,27 @@ jobs: - name: test run: nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} + test-windows: + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip + runs-on: namespace-profile-ghostty-windows + timeout-minutes: 45 + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + + - name: Test + run: zig build -Dapp-runtime=none test + test-i18n: strategy: 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,17 +1144,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -812,8 +1163,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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + 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,15 +1219,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -841,7 +1238,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,15 +1250,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -872,7 +1270,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,15 +1280,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -898,8 +1297,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@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + 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,15 +1338,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -926,7 +1356,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,15 +1366,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -953,7 +1384,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,15 +1394,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -985,7 +1417,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,15 +1427,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1012,7 +1445,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,15 +1455,15 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1056,17 +1490,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1082,13 +1516,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@df198f982fcecfb8264bea3f1274b56a61b6dfdc # v0.0.12 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@f5814dcf37a16cce0624d5bec2ab879654294aa0 # v0.0.22 + uses: namespacelabs/nscloud-setup-buildx-action@d059ed7184f0bc7c8b27e8810cea153d02bcc6dd # v0.0.23 - 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 +1532,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,17 +1552,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 026b1e9df2a..4c9ff66f960 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,17 +22,17 @@ 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@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml index e0933aaf101..d0456b00a4f 100644 --- a/.github/workflows/vouch-check-issue.yml +++ b/.github/workflows/vouch-check-issue.yml @@ -8,13 +8,13 @@ jobs: check: runs-on: namespace-profile-ghostty-xsm steps: - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 id: app-token with: 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..0602553ca82 100644 --- a/.github/workflows/vouch-check-pr.yml +++ b/.github/workflows/vouch-check-pr.yml @@ -8,13 +8,13 @@ jobs: check: runs-on: namespace-profile-ghostty-xsm steps: - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 id: app-token with: 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..7288c4ab2a6 100644 --- a/.github/workflows/vouch-manage-by-discussion.yml +++ b/.github/workflows/vouch-manage-by-discussion.yml @@ -12,7 +12,7 @@ jobs: manage: runs-on: namespace-profile-ghostty-xsm steps: - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 id: app-token with: app-id: ${{ secrets.VOUCH_APP_ID }} @@ -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..6f61592ffe3 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -12,7 +12,7 @@ jobs: manage: runs-on: namespace-profile-ghostty-xsm steps: - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 id: app-token with: app-id: ${{ secrets.VOUCH_APP_ID }} @@ -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..0879c972290 --- /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@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + 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..699ac9a5f21 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ zig-cache/ .zig-cache/ zig-out/ +build-cmake/ +CMakeCache.txt +CMakeFiles/ +/build.zig.zon.bak /result* /.nixos-test-history example/*.wasm @@ -24,3 +28,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/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000000..326f5b37a64 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,231 @@ +# CMake wrapper for libghostty-vt +# +# This file delegates to `zig build -Demit-lib-vt` to produce the shared library, +# headers, and pkg-config file. It exists so that CMake-based projects can +# consume libghostty-vt without interacting with the Zig build system +# directly. However, downstream users do still require `zig` on the PATH. +# Please consult the Ghostty docs for the required Zig version: +# +# https://ghostty.org/docs/install/build +# +# Building within the Ghostty repo +# --------------------------------- +# +# cmake -B build +# cmake --build build +# cmake --install build --prefix /usr/local +# +# Pass extra flags to the Zig build with GHOSTTY_ZIG_BUILD_FLAGS: +# +# cmake -B build -DGHOSTTY_ZIG_BUILD_FLAGS="-Demit-macos-app=false" +# +# Integrating into a downstream CMake project +# --------------------------------------------- +# +# Option 1 — FetchContent (recommended, no manual install step): +# +# include(FetchContent) +# FetchContent_Declare(ghostty +# GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git +# GIT_TAG main +# ) +# FetchContent_MakeAvailable(ghostty) +# +# target_link_libraries(myapp PRIVATE ghostty-vt) # shared +# target_link_libraries(myapp PRIVATE ghostty-vt-static) # static +# +# To use a local checkout instead of fetching: +# +# cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=/path/to/ghostty +# +# Option 2 — find_package (after installing to a prefix): +# +# find_package(ghostty-vt REQUIRED) +# target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt) # shared +# target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt-static) # static +# +# See dist/cmake/README.md for more details and example/c-vt-cmake/ for a +# complete working example. + +cmake_minimum_required(VERSION 3.19) +project(ghostty-vt VERSION 0.1.0 LANGUAGES C) + +# --- Options ---------------------------------------------------------------- + +set(GHOSTTY_ZIG_BUILD_FLAGS "" CACHE STRING "Additional flags to pass to zig build") + +# Map CMake build types to Zig optimization levels. +if(CMAKE_BUILD_TYPE) + string(TOUPPER "${CMAKE_BUILD_TYPE}" _bt) + if(_bt STREQUAL "RELEASE" OR _bt STREQUAL "MINSIZEREL" OR _bt STREQUAL "RELWITHDEBINFO") + list(APPEND GHOSTTY_ZIG_BUILD_FLAGS "-Doptimize=ReleaseFast") + endif() + unset(_bt) +endif() + +# --- Find Zig ---------------------------------------------------------------- + +find_program(ZIG_EXECUTABLE zig REQUIRED) +message(STATUS "Found zig: ${ZIG_EXECUTABLE}") + +# --- Build via zig build ----------------------------------------------------- + +# The zig build installs into zig-out/ relative to the source tree. +set(ZIG_OUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/zig-out") + +# Shared library names (zig build produces both shared and static). +if(APPLE) + set(GHOSTTY_VT_LIBNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(GHOSTTY_VT_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt.0${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(GHOSTTY_VT_REALNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt.0.1.0${CMAKE_SHARED_LIBRARY_SUFFIX}") +elseif(WIN32) + set(GHOSTTY_VT_LIBNAME "ghostty-vt.dll") + set(GHOSTTY_VT_REALNAME "ghostty-vt.dll") + set(GHOSTTY_VT_IMPLIB "ghostty-vt.lib") +else() + set(GHOSTTY_VT_LIBNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(GHOSTTY_VT_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}.0") + set(GHOSTTY_VT_REALNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}.0.1.0") +endif() + +if(WIN32) + set(GHOSTTY_VT_SHARED_LIBRARY "${ZIG_OUT_DIR}/bin/${GHOSTTY_VT_REALNAME}") +else() + set(GHOSTTY_VT_SHARED_LIBRARY "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_REALNAME}") +endif() + +# Static library name. +# On Windows, the static lib is named "ghostty-vt-static.lib" to avoid +# colliding with the DLL import library "ghostty-vt.lib". +if(WIN32) + set(GHOSTTY_VT_STATIC_REALNAME "ghostty-vt-static.lib") +else() + set(GHOSTTY_VT_STATIC_REALNAME "libghostty-vt.a") +endif() +set(GHOSTTY_VT_STATIC_LIBRARY "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_STATIC_REALNAME}") + +# Ensure the output directories exist so CMake doesn't reject the +# INTERFACE_INCLUDE_DIRECTORIES before the zig build has run. +file(MAKE_DIRECTORY "${ZIG_OUT_DIR}/include") + +# Custom command: run zig build -Demit-lib-vt (produces both shared and static) +add_custom_command( + OUTPUT "${GHOSTTY_VT_SHARED_LIBRARY}" "${GHOSTTY_VT_STATIC_LIBRARY}" "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_IMPLIB}" + COMMAND "${ZIG_EXECUTABLE}" build -Demit-lib-vt ${GHOSTTY_ZIG_BUILD_FLAGS} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building libghostty-vt via zig build..." + USES_TERMINAL +) + +add_custom_target(zig_build_lib_vt ALL + DEPENDS "${GHOSTTY_VT_SHARED_LIBRARY}" "${GHOSTTY_VT_STATIC_LIBRARY}" +) + +# Tell CMake's clean target to also remove Zig's output directory. +set_property(DIRECTORY APPEND PROPERTY + ADDITIONAL_CLEAN_FILES "${ZIG_OUT_DIR}" +) + +# --- IMPORTED library targets ------------------------------------------------ + +# Shared +add_library(ghostty-vt SHARED IMPORTED GLOBAL) +set_target_properties(ghostty-vt PROPERTIES + IMPORTED_LOCATION "${GHOSTTY_VT_SHARED_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${ZIG_OUT_DIR}/include" +) +if(APPLE) + set_target_properties(ghostty-vt PROPERTIES + IMPORTED_SONAME "@rpath/${GHOSTTY_VT_SONAME}" + ) +elseif(WIN32) + set_target_properties(ghostty-vt PROPERTIES + IMPORTED_IMPLIB "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_IMPLIB}" + ) +else() + set_target_properties(ghostty-vt PROPERTIES + IMPORTED_SONAME "${GHOSTTY_VT_SONAME}" + ) +endif() +add_dependencies(ghostty-vt zig_build_lib_vt) + +# Static +# +# When linking the static library, consumers must also link its transitive +# dependencies. By default (with SIMD enabled), these are: +# - libc +# - libc++ (or libstdc++ on Linux) +# - highway +# - simdutf +# +# Building with -Dsimd=false removes the C++ / highway / simdutf +# dependencies, leaving only libc. +add_library(ghostty-vt-static STATIC IMPORTED GLOBAL) +set_target_properties(ghostty-vt-static PROPERTIES + IMPORTED_LOCATION "${GHOSTTY_VT_STATIC_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${ZIG_OUT_DIR}/include" + INTERFACE_COMPILE_DEFINITIONS "GHOSTTY_STATIC" +) +if(WIN32) + # On Windows, the Zig standard library uses NT API functions + # (NtClose, NtCreateSection, etc.) and kernel32 functions that + # consumers must link when using the static library. + set_target_properties(ghostty-vt-static PROPERTIES + INTERFACE_LINK_LIBRARIES "ntdll;kernel32" + ) +endif() +add_dependencies(ghostty-vt-static zig_build_lib_vt) + +# --- Install ------------------------------------------------------------------ + +include(GNUInstallDirs) + +# Install shared library +if(WIN32) + # On Windows, install the DLL and PDB to bin/ and the import library to lib/ + install(FILES "${GHOSTTY_VT_SHARED_LIBRARY}" "${ZIG_OUT_DIR}/bin/ghostty-vt.pdb" TYPE BIN) + install(FILES "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_IMPLIB}" TYPE LIB) +else() + install(FILES "${GHOSTTY_VT_SHARED_LIBRARY}" TYPE LIB) + # Install symlinks + install(CODE " + execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink + \"${GHOSTTY_VT_REALNAME}\" + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${GHOSTTY_VT_SONAME}\") + execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink + \"${GHOSTTY_VT_SONAME}\" + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${GHOSTTY_VT_LIBNAME}\") + ") +endif() + +# Install static library +install(FILES "${GHOSTTY_VT_STATIC_LIBRARY}" TYPE LIB) + +# Install headers +install(DIRECTORY "${ZIG_OUT_DIR}/include/ghostty" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + +# --- CMake package config for find_package() ---------------------------------- + +include(CMakePackageConfigHelpers) + +# Generate the config file +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/dist/cmake/ghostty-vt-config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt" +) + +# Generate the version file +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion +) + +# Install the config files +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt" +) 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/README.md b/README.md index 9619680979c..293f5a6e29f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@

Fast, native, feature-rich terminal emulator pushing modern features.
+ A native GUI or embeddable library via libghostty. +
About · Download @@ -26,20 +28,13 @@ fast, feature-rich, and native. While there are many excellent terminal emulators available, they all force you to choose between speed, features, or native UIs. Ghostty provides all three. -In all categories, I am not trying to claim that Ghostty is the -best (i.e. the fastest, most feature-rich, or most native). But -Ghostty is competitive in all three categories and Ghostty -doesn't make you choose between them. - -Ghostty also intends to push the boundaries of what is possible with a -terminal emulator by exposing modern, opt-in features that enable CLI tool -developers to build more feature rich, interactive applications. - -While aiming for this ambitious goal, our first step is to make Ghostty -one of the best fully standards compliant terminal emulator, remaining -compatible with all existing shells and software while supporting all of -the latest terminal innovations in the ecosystem. You can use Ghostty -as a drop-in replacement for your existing terminal emulator. +**`libghostty`** is a cross-platform, zero-dependency C and Zig library +for building terminal emulators or utilizing terminal functionality +(such as style parsing). Anyone can use `libghostty` to build a terminal +emulator or embed a terminal into their own applications. See +[Ghostling](https://github.com/ghostty-org/ghostling) for a minimal complete project +example or the [`examples` directory](https://github.com/ghostty-org/ghostty/tree/main/example) +for smaller examples of using `libghostty` in C and Zig. For more details, see [About Ghostty](https://ghostty.org/docs/about). @@ -61,30 +56,37 @@ to get involved with Ghostty's development as well should also read the ## Roadmap and Status +Ghostty is stable and in use by millions of people and machines daily. + The high-level ambitious plan for the project, in order: -| # | Step | Status | -| :-: | --------------------------------------------------------- | :----: | -| 1 | Standards-compliant terminal emulation | ✅ | -| 2 | Competitive performance | ✅ | -| 3 | Basic customizability -- fonts, bg colors, etc. | ✅ | -| 4 | Richer windowing features -- multi-window, tabbing, panes | ✅ | -| 5 | Native Platform Experiences (i.e. Mac Preference Panel) | âš ï¸ | -| 6 | Cross-platform `libghostty` for Embeddable Terminals | âš ï¸ | -| 7 | Windows Terminals (including PowerShell, Cmd, WSL) | ⌠| -| N | Fancy features (to be expanded upon later) | ⌠| +| # | Step | Status | +| :-: | ------------------------------------------------------- | :----: | +| 1 | Standards-compliant terminal emulation | ✅ | +| 2 | Competitive performance | ✅ | +| 3 | Rich windowing features -- multi-window, tabbing, panes | ✅ | +| 4 | Native Platform Experiences | ✅ | +| 5 | Cross-platform `libghostty` for Embeddable Terminals | ✅ | +| 6 | Ghostty-only Terminal Control Sequences | ⌠| Additional details for each step in the big roadmap below: #### Standards-Compliant Terminal Emulation -Ghostty implements enough control sequences to be used by hundreds of -testers daily for over the past year. Further, we've done a -[comprehensive xterm audit](https://github.com/ghostty-org/ghostty/issues/632) +Ghostty implements all of the regularly used control sequences and +can run every mainstream terminal program without issue. For legacy sequences, +we've done a [comprehensive xterm audit](https://github.com/ghostty-org/ghostty/issues/632) comparing Ghostty's behavior to xterm and building a set of conformance test cases. -We believe Ghostty is one of the most compliant terminal emulators available. +In addition to legacy sequences (what you'd call real "terminal" emulation), +Ghostty also supports more modern sequences than almost any other terminal +emulator. These features include things like the Kitty graphics protocol, +Kitty image protocol, clipboard sequences, synchronized rendering, +light/dark mode notifications, and many, many more. + +We believe Ghostty is one of the most compliant and feature-rich terminal +emulators available. Terminal behavior is partially a de jure standard (i.e. [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/)) @@ -96,33 +98,30 @@ views as a "standard." #### Competitive Performance -We need better benchmarks to continuously verify this, but Ghostty is -generally in the same performance category as the other highest performing -terminal emulators. - -For rendering, we have a multi-renderer architecture that uses OpenGL on -Linux and Metal on macOS. As far as I'm aware, we're the only terminal -emulator other than iTerm that uses Metal directly. And we're the only -terminal emulator that has a Metal renderer that supports ligatures (iTerm -uses a CPU renderer if ligatures are enabled). We can maintain around 60fps -under heavy load and much more generally -- though the terminal is -usually rendering much lower due to little screen changes. - -For IO, we have a dedicated IO thread that maintains very little jitter -under heavy IO load (i.e. `cat .txt`). On benchmarks for IO, -we're usually within a small margin of other fast terminal emulators. -For example, reading a dump of plain text is 4x faster compared to iTerm and -Kitty, and 2x faster than Terminal.app. Alacritty is very fast but we're still -around the same speed (give or take) and our app experience is much more -feature rich. +Ghostty is generally in the same performance category as the other highest +performing terminal emulators. -> [!NOTE] -> Despite being _very fast_, there is a lot of room for improvement here. +"The same performance category" means that Ghostty is much faster than +traditional or "slow" terminals and is within an unnoticeable margin of the +well-known "fast" terminals. For example, Ghostty and Alacritty are usually within +a few percentage points of each other on various benchmarks, but are both +something like 100x faster than Terminal.app and iTerm. However, Ghostty +is much more feature rich than Alacritty and has a much more native app +experience. + +This performance is achieved through high-level architectural decisions and +low-level optimizations. At a high-level, Ghostty has a multi-threaded +architecture with a dedicated read thread, write thread, and render thread +per terminal. Our renderer uses OpenGL on Linux and Metal on macOS. +Our read thread has a heavily optimized terminal parser that leverages +CPU-specific SIMD instructions. Etc. -#### Richer Windowing Features +#### Rich Windowing Features The Mac and Linux (build with GTK) apps support multi-window, tabbing, and -splits. +splits with additional features such as tab renaming, coloring, etc. These +features allow for a higher degree of organization and customization than +single-window terminals. #### Native Platform Experiences @@ -133,10 +132,15 @@ in Zig but we do a lot of platform-native things: - The macOS app is a true SwiftUI-based application with all the things you would expect such as real windowing, menu bars, a settings GUI, etc. - macOS uses a true Metal renderer with CoreText for font discovery. +- macOS supports AppleScript, Apple Shortcuts (AppIntents), etc. - The Linux app is built with GTK. +- The Linux app integrates deeply with systemd if available for things + like always-on, new windows in a single instance, cgroup isolation, etc. -There are more improvements to be made. The macOS settings window is still -a work-in-progress. Similar improvements will follow with Linux. +Our goal with Ghostty is for users of whatever platform they run Ghostty +on to think that Ghostty was built for their platform first and maybe even +exclusively. We want Ghostty to feel like a native app on every platform, +for the best definition of "native" on each platform. #### Cross-platform `libghostty` for Embeddable Terminals @@ -151,15 +155,34 @@ terminal state. This is covered in more detail in this [blog post](https://mitchellh.com/writing/libghostty-is-coming). `libghostty-vt` is already available and usable today for Zig and C and -is compatible for macOS, Linux, Windows, and WebAssembly. At the time of -writing this, the API isn't stable yet and we haven't tagged an official -release, but the core logic is well proven (since Ghostty uses it) and -we're working hard on it now. - -The ultimate goal is not hypothetical! The macOS app is a `libghostty` consumer. -The macOS app is a native Swift app developed in Xcode and `main()` is -within Swift. The Swift app links to `libghostty` and uses the C API to -render terminals. +is compatible for macOS, Linux, Windows, and WebAssembly. The functionality +is extremely stable (since its been proven in Ghostty GUI for a long time), +but the API signatures are still in flux. + +`libghostty` is already heavily in use. See [`examples`](https://github.com/ghostty-org/ghostty/tree/main/example) +for small examples of using `libghostty` in C and Zig or the +[Ghostling](https://github.com/ghostty-org/ghostling) project for a +complete example. See [awesome-libghostty](https://github.com/Uzaaft/awesome-libghostty) +for a list of projects and resources related to `libghostty`. + +We haven't tagged libghostty with a version yet and we're still working +on a better docs experience, but our [Doxygen website](https://libghostty.tip.ghostty.org/) +is a good resource for the C API. + +#### Ghostty-only Terminal Control Sequences + +We want and believe that terminal applications can and should be able +to do so much more. We've worked hard to support a wide variety of modern +sequences created by other terminal emulators towards this end, but we also +want to fill the gaps by creating our own sequences. + +We've been hesitant to do this up until now because we don't want to create +more fragmentation in the terminal ecosystem by creating sequences that only +work in Ghostty. But, we do want to balance that with the desire to push the +terminal forward with stagnant standards and the slow pace of change in the +terminal ecosystem. + +We haven't done any of this yet. ## Crash Reports diff --git a/build.zig b/build.zig index fa68b91b484..173af3d2f5c 100644 --- a/build.zig +++ b/build.zig @@ -3,11 +3,17 @@ const assert = std.debug.assert; const builtin = @import("builtin"); const buildpkg = @import("src/build/main.zig"); -const appVersion = @import("build.zig.zon").version; -const minimumZigVersion = @import("build.zig.zon").minimum_zig_version; +/// App version from build.zig.zon. +const app_zon_version = @import("build.zig.zon").version; + +/// Libghostty version. We use a separate version from the app. +const lib_version = "0.1.0"; + +/// Minimum required zig version. +const minimum_zig_version = @import("build.zig.zon").minimum_zig_version; comptime { - buildpkg.requireZig(minimumZigVersion); + buildpkg.requireZig(minimum_zig_version); } pub fn build(b: *std.Build) !void { @@ -15,7 +21,23 @@ pub fn build(b: *std.Build) !void { // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. - const config = try buildpkg.Config.init(b, appVersion); + // If we have a VERSION file (present in source tarballs) then we + // use that as the version source of truth. Otherwise we fall back + // to what is in the build.zig.zon. + const file_version: ?[]const u8 = if (b.build_root.handle.readFileAlloc( + b.allocator, + "VERSION", + 128, + )) |content| std.mem.trim( + u8, + content, + &std.ascii.whitespace, + ) else |_| null; + + const config = try buildpkg.Config.init( + b, + file_version orelse app_zon_version, + ); const test_filters = b.option( [][]const u8, "test-filter", @@ -35,7 +57,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 +83,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); @@ -91,16 +114,6 @@ pub fn build(b: *std.Build) !void { check_step.dependOn(dist.install_step); } - // libghostty (internal, big) - const libghostty_shared = try buildpkg.GhosttyLib.initShared( - b, - &deps, - ); - const libghostty_static = try buildpkg.GhosttyLib.initStatic( - b, - &deps, - ); - // libghostty-vt const libghostty_vt_shared = shared: { if (config.target.result.cpu.arch.isWasm()) { @@ -115,9 +128,31 @@ pub fn build(b: *std.Build) !void { &mod, ); }; - libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(b.getInstallStep()); + // libghostty-vt static lib + const libghostty_vt_static = try buildpkg.GhosttyLibVt.initStatic( + b, + &mod, + ); + if (config.is_dep) { + // If we're a dependency, we need to install everything as-is + // so that dep.artifact("ghostty-vt-static") works. + libghostty_vt_static.install(b.getInstallStep()); + } else { + // If we're not a dependency, we rename the static lib to + // be idiomatic. On Windows, we use a distinct name to avoid + // colliding with the DLL import library (ghostty-vt.lib). + const static_lib_name = if (config.target.result.os.tag == .windows) + "ghostty-vt-static.lib" + else + "libghostty-vt.a"; + b.getInstallStep().dependOn(&b.addInstallLibFile( + libghostty_vt_static.output, + static_lib_name, + ).step); + } + // Helpgen if (config.emit_helpgen) deps.help_strings.install(); @@ -128,26 +163,34 @@ pub fn build(b: *std.Build) !void { resources.install(); if (i18n) |v| v.install(); } - } else { - // Libghostty + } else if (!config.emit_lib_vt) { + // The macOS Ghostty Library // - // Note: libghostty is not stable for general purpose use. It is used - // heavily by Ghostty on macOS but it isn't built to be reusable yet. - // As such, these build steps are lacking. For example, the Darwin - // build only produces an xcframework. + // This is NOT libghostty (even though its named that for historical + // reasons). It is just the glue between Ghostty GUI on macOS and + // the full Ghostty GUI core. + const lib_shared = try buildpkg.GhosttyLib.initShared(b, &deps); + const lib_static = try buildpkg.GhosttyLib.initStatic(b, &deps); // We shouldn't have this guard but we don't currently // build on macOS this way ironically so we need to fix that. if (!config.target.result.os.tag.isDarwin()) { - libghostty_shared.installHeader(); // Only need one header - libghostty_shared.install("libghostty.so"); - libghostty_static.install("libghostty.a"); + lib_shared.installHeader(); // Only need one header + if (config.target.result.os.tag == .windows) { + lib_shared.install("ghostty.dll"); + lib_static.install("ghostty-static.lib"); + } else { + lib_shared.install("libghostty.so"); + lib_static.install("libghostty.a"); + } } } // macOS only artifacts. These will error if they're initialized for // other targets. - if (config.target.result.os.tag.isDarwin()) { + if (config.target.result.os.tag.isDarwin() and + (config.emit_xcframework or config.emit_macos_app)) + { // Ghostty xcframework const xcframework = try buildpkg.GhosttyXCFramework.init( b, @@ -202,7 +245,9 @@ pub fn build(b: *std.Build) !void { // On macOS we can run the macOS app. For "run" we always force // a native-only build so that we can run as quickly as possible. - if (config.target.result.os.tag.isDarwin()) { + if (config.target.result.os.tag.isDarwin() and + (config.emit_xcframework or config.emit_macos_app)) + { const xcframework_native = try buildpkg.GhosttyXCFramework.init( b, &deps, @@ -291,6 +336,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..f4ecce715a7 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 @@ -91,8 +91,8 @@ .lazy = true, }, .wayland_protocols = .{ - .url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", - .hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", + .url = "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + .hash = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", .lazy = true, }, .plasma_wayland_protocols = .{ @@ -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-20260323-152405-a2c7b60.tgz", + .hash = "N-V-__8AAL6FAwBDPampKgDjoxlJYDIn2jv0VaINS4W6CXJN", .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..ccadd6ac973 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-__8AAL6FAwBDPampKgDjoxlJYDIn2jv0VaINS4W6CXJN": { "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-20260323-152405-a2c7b60.tgz", + "hash": "sha256-fWgXdUXh2/dNZqERzEu9hz4xyy4nl+GUjLMpUMrsRnA=" }, "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", @@ -139,6 +139,11 @@ "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", "hash": "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg=" }, + "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA": { + "name": "wayland_protocols", + "url": "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + "hash": "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM=" + }, "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs": { "name": "wuffs", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", @@ -146,7 +151,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..23464bd5e95 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -171,11 +171,11 @@ in }; } { - name = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z"; + name = "N-V-__8AAL6FAwBDPampKgDjoxlJYDIn2jv0VaINS4W6CXJN"; 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-20260323-152405-a2c7b60.tgz"; + hash = "sha256-fWgXdUXh2/dNZqERzEu9hz4xyy4nl+GUjLMpUMrsRnA="; }; } { @@ -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="; }; } { @@ -306,6 +306,14 @@ in hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="; }; } + { + name = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA"; + path = fetchZigArtifact { + name = "wayland_protocols"; + url = "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz"; + hash = "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM="; + }; + } { name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; path = fetchZigArtifact { @@ -318,7 +326,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..f9b0322dd6c 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-20260323-152405-a2c7b60.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,4 @@ 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 +https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz diff --git a/dist/cmake/README.md b/dist/cmake/README.md new file mode 100644 index 00000000000..ed8b86453c2 --- /dev/null +++ b/dist/cmake/README.md @@ -0,0 +1,67 @@ +# CMake Support for libghostty-vt + +The top-level `CMakeLists.txt` wraps the Zig build system so that CMake +projects can consume libghostty-vt without invoking `zig build` manually. +Running `cmake --build` triggers `zig build -Demit-lib-vt` automatically. + +This means downstream projects do require a working Zig compiler on +`PATH` to build, but don't need to know any Zig-specific details. + +## Using FetchContent (recommended) + +Add the following to your project's `CMakeLists.txt`: + +```cmake +include(FetchContent) +FetchContent_Declare(ghostty + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git + GIT_TAG main +) +FetchContent_MakeAvailable(ghostty) + +add_executable(myapp main.c) +target_link_libraries(myapp PRIVATE ghostty-vt) +``` + +This fetches the Ghostty source, builds libghostty-vt via Zig during your +CMake build, and links it into your target. Headers are added to the +include path automatically. + +### Using a local checkout + +If you already have the Ghostty source checked out, skip the download by +pointing CMake at it: + +```shell-session +cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=/path/to/ghostty +cmake --build build +``` + +## Using find_package (install-based) + +Build and install libghostty-vt first: + +```shell-session +cd /path/to/ghostty +cmake -B build +cmake --build build +cmake --install build --prefix /usr/local +``` + +Then in your project: + +```cmake +find_package(ghostty-vt REQUIRED) + +add_executable(myapp main.c) +target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt) +``` + +## Files + +- `ghostty-vt-config.cmake.in` — template for the CMake package config + file installed alongside the library, enabling `find_package()` support. + +## Example + +See `example/c-vt-cmake/` for a complete working example. diff --git a/dist/cmake/ghostty-vt-config.cmake.in b/dist/cmake/ghostty-vt-config.cmake.in new file mode 100644 index 00000000000..8e1d757296f --- /dev/null +++ b/dist/cmake/ghostty-vt-config.cmake.in @@ -0,0 +1,65 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +set(_ghostty_vt_libdir "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@") + +# Shared library target +if(NOT TARGET ghostty-vt::ghostty-vt) + add_library(ghostty-vt::ghostty-vt SHARED IMPORTED) + + if(WIN32) + set(_ghostty_vt_shared_location "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_BINDIR@/@GHOSTTY_VT_REALNAME@") + else() + set(_ghostty_vt_shared_location "${_ghostty_vt_libdir}/@GHOSTTY_VT_REALNAME@") + endif() + + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_LOCATION "${_ghostty_vt_shared_location}" + INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@" + ) + unset(_ghostty_vt_shared_location) + + if(APPLE) + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_SONAME "@rpath/@GHOSTTY_VT_SONAME@" + INTERFACE_LINK_DIRECTORIES "${_ghostty_vt_libdir}" + ) + # Ensure consumers can find the @rpath dylib at runtime + set_property(TARGET ghostty-vt::ghostty-vt APPEND PROPERTY + INTERFACE_LINK_OPTIONS "LINKER:-rpath,${_ghostty_vt_libdir}" + ) + elseif(WIN32) + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_IMPLIB "${_ghostty_vt_libdir}/@GHOSTTY_VT_IMPLIB@" + ) + else() + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_SONAME "@GHOSTTY_VT_SONAME@" + ) + endif() +endif() + +# Static library target +# +# Consumers must link transitive dependencies themselves. By default (with +# SIMD enabled): libc, libc++ (or libstdc++ on Linux), highway, and +# simdutf. Building with -Dsimd=false removes the C++ / highway / simdutf +# dependencies. +if(NOT TARGET ghostty-vt::ghostty-vt-static) + add_library(ghostty-vt::ghostty-vt-static STATIC IMPORTED) + + set_target_properties(ghostty-vt::ghostty-vt-static PROPERTIES + IMPORTED_LOCATION "${_ghostty_vt_libdir}/@GHOSTTY_VT_STATIC_REALNAME@" + INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@" + ) + if(WIN32) + set_target_properties(ghostty-vt::ghostty-vt-static PROPERTIES + INTERFACE_LINK_LIBRARIES "ntdll;kernel32" + ) + endif() +endif() + +unset(_ghostty_vt_libdir) + +check_required_components(ghostty-vt) 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/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop index 5e83513901a..b9ac7fd2601 100755 --- a/dist/linux/ghostty_dolphin.desktop +++ b/dist/linux/ghostty_dolphin.desktop @@ -7,5 +7,4 @@ Actions=RunGhosttyDir [Desktop Action RunGhosttyDir] Name=Open Ghostty Here Icon=com.mitchellh.ghostty -Exec=ghostty --working-directory=%F --gtk-single-instance=false - +Exec=ghostty +new-window --working-directory=%F diff --git a/example/.gitignore b/example/.gitignore index 3fa248f4cae..6f372bc4d1c 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules/ example.wasm* +build/ diff --git a/example/AGENTS.md b/example/AGENTS.md new file mode 100644 index 00000000000..280b97e1842 --- /dev/null +++ b/example/AGENTS.md @@ -0,0 +1,39 @@ +# Example Libghostty Projects + +Each example is a standalone project with its own `build.zig`, +`build.zig.zon`, `README.md`, and `src/main.c` (or `.zig`). Examples are +auto-discovered by CI via `example/*/build.zig.zon`, so no workflow file +edits are needed when adding a new example. + +## Adding a New Example + +1. Copy an existing example directory (e.g., `c-vt-encode-focus/`) as a + starting point. +2. Update `build.zig.zon`: change `.name`, generate a **new unique** + `.fingerprint` value (a random `u64` hex literal), and keep + `.minimum_zig_version` matching the others. +3. Update `build.zig`: change the executable `.name` to match the directory. +4. Write a `README.md` following the existing format. + +## Doxygen Snippet Tags + +Example source files use Doxygen `@snippet` tags so the corresponding +header in `include/ghostty/vt/` can reference them. Wrap the relevant +code with `//! [snippet-name]` markers: + +```c +//! [my-snippet] +int main() { ... } +//! [my-snippet] +``` + +The header then uses `@snippet

/src/main.c my-snippet` instead of +inline `@code` blocks. Never duplicate example code inline in the +headers — always use `@snippet`. When modifying example code, keep the +snippet markers in sync with the headers in `include/ghostty/vt/`. + +## Conventions + +- Executable names use underscores: `c_vt_encode_focus` (not hyphens). +- All C examples link `ghostty-vt` via `lazyDependency("ghostty", ...)`. +- `build.zig` files follow a common template — keep them consistent. diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000000..25e41aeeb64 --- /dev/null +++ b/example/README.md @@ -0,0 +1,17 @@ +# Examples + +Standalone projects demonstrating the Ghostty library APIs. +The directories starting with `c-` use the C API and the directories +starting with `zig-` use the Zig API. + +Every example can be built and run using `zig build` and `zig build run` +from within the respective example directory. +Even the C API examples use the Zig build system (not the language) to +build the project. + +## Running an Example + +```shell-session +cd example/ +zig build run +``` diff --git a/example/c-vt-build-info/README.md b/example/c-vt-build-info/README.md new file mode 100644 index 00000000000..08fc1cb3c5e --- /dev/null +++ b/example/c-vt-build-info/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` Build Info + +This contains a simple example of how to use the `ghostty-vt` build info +API to query compile-time build configuration. + +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-build-info/build.zig b/example/c-vt-build-info/build.zig new file mode 100644 index 00000000000..2cd3d307a09 --- /dev/null +++ b/example/c-vt-build-info/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_build_info", + .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-build-info/build.zig.zon b/example/c-vt-build-info/build.zig.zon new file mode 100644 index 00000000000..14966615ae3 --- /dev/null +++ b/example/c-vt-build-info/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_build_info, + .version = "0.0.0", + .fingerprint = 0xc6b57ed4f83fb16, + .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-build-info/src/main.c b/example/c-vt-build-info/src/main.c new file mode 100644 index 00000000000..14834144694 --- /dev/null +++ b/example/c-vt-build-info/src/main.c @@ -0,0 +1,45 @@ +#include +#include + +//! [build-info-query] +void query_build_info() { + bool simd = false; + bool kitty_graphics = false; + bool tmux_control_mode = false; + + ghostty_build_info(GHOSTTY_BUILD_INFO_SIMD, &simd); + ghostty_build_info(GHOSTTY_BUILD_INFO_KITTY_GRAPHICS, &kitty_graphics); + ghostty_build_info(GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE, &tmux_control_mode); + + printf("SIMD: %s\n", simd ? "enabled" : "disabled"); + printf("Kitty graphics: %s\n", kitty_graphics ? "enabled" : "disabled"); + printf("Tmux control mode: %s\n", tmux_control_mode ? "enabled" : "disabled"); + + GhosttyString version_string = {0}; + size_t version_major = 0; + size_t version_minor = 0; + size_t version_patch = 0; + GhosttyString version_build = {0}; + + ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_STRING, &version_string); + ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_MAJOR, &version_major); + ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_MINOR, &version_minor); + ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_PATCH, &version_patch); + ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_BUILD, &version_build); + + printf("Version: %.*s\n", (int)version_string.len, version_string.ptr); + printf("Version major: %zu\n", version_major); + printf("Version minor: %zu\n", version_minor); + printf("Version patch: %zu\n", version_patch); + if (version_build.len > 0) { + printf("Version build: %.*s\n", (int)version_build.len, version_build.ptr); + } else { + printf("Version build: (none)\n"); + } +} +//! [build-info-query] + +int main() { + query_build_info(); + return 0; +} diff --git a/example/c-vt-cmake-static/CMakeLists.txt b/example/c-vt-cmake-static/CMakeLists.txt new file mode 100644 index 00000000000..bb4b1ac35f1 --- /dev/null +++ b/example/c-vt-cmake-static/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.19) +project(c-vt-cmake-static LANGUAGES C) + +include(FetchContent) +FetchContent_Declare(ghostty + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git + GIT_TAG main +) +set(GHOSTTY_ZIG_BUILD_FLAGS "-Dsimd=false" CACHE STRING "" FORCE) +FetchContent_MakeAvailable(ghostty) + +add_executable(c_vt_cmake_static src/main.c) +target_link_libraries(c_vt_cmake_static PRIVATE ghostty-vt-static) diff --git a/example/c-vt-cmake-static/README.md b/example/c-vt-cmake-static/README.md new file mode 100644 index 00000000000..6aa503e0411 --- /dev/null +++ b/example/c-vt-cmake-static/README.md @@ -0,0 +1,21 @@ +# c-vt-cmake-static + +Demonstrates consuming libghostty-vt as a **static** library from a CMake +project using `FetchContent`. Creates a terminal, writes VT sequences into +it, and formats the screen contents as plain text. + +## Building + +```shell-session +cd example/c-vt-cmake-static +cmake -B build +cmake --build build +./build/c_vt_cmake_static +``` + +To build against a local checkout instead of fetching from GitHub: + +```shell-session +cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../.. +cmake --build build +``` diff --git a/example/c-vt-cmake-static/src/main.c b/example/c-vt-cmake-static/src/main.c new file mode 100644 index 00000000000..233bd34d16b --- /dev/null +++ b/example/c-vt-cmake-static/src/main.c @@ -0,0 +1,52 @@ +#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 some VT-encoded content into the terminal + const char *commands[] = { + "Hello from a \033[1mCMake\033[0m-built program (static)!\r\n", + "Line 2: \033[4munderlined\033[0m text\r\n", + "Line 3: \033[31mred\033[0m \033[32mgreen\033[0m \033[34mblue\033[0m\r\n", + }; + 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])); + } + + // Format the terminal contents as plain text + 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); + + uint8_t *buf = NULL; + size_t len = 0; + result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + printf("Plain text (%zu bytes):\n", len); + fwrite(buf, 1, len, stdout); + printf("\n"); + + ghostty_free(NULL, buf, len); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/c-vt-cmake/CMakeLists.txt b/example/c-vt-cmake/CMakeLists.txt new file mode 100644 index 00000000000..ff6e35bc1a5 --- /dev/null +++ b/example/c-vt-cmake/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.19) +project(c-vt-cmake LANGUAGES C) + +include(FetchContent) +FetchContent_Declare(ghostty + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git + GIT_TAG main +) +FetchContent_MakeAvailable(ghostty) + +add_executable(c_vt_cmake src/main.c) +target_link_libraries(c_vt_cmake PRIVATE ghostty-vt) diff --git a/example/c-vt-cmake/README.md b/example/c-vt-cmake/README.md new file mode 100644 index 00000000000..d76ca946d78 --- /dev/null +++ b/example/c-vt-cmake/README.md @@ -0,0 +1,21 @@ +# c-vt-cmake + +Demonstrates consuming libghostty-vt from a CMake project using +`FetchContent`. Creates a terminal, writes VT sequences into it, and +formats the screen contents as plain text. + +## Building + +```shell-session +cd example/c-vt-cmake +cmake -B build +cmake --build build +./build/c_vt_cmake +``` + +To build against a local checkout instead of fetching from GitHub: + +```shell-session +cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../.. +cmake --build build +``` diff --git a/example/c-vt-cmake/src/main.c b/example/c-vt-cmake/src/main.c new file mode 100644 index 00000000000..9925864511f --- /dev/null +++ b/example/c-vt-cmake/src/main.c @@ -0,0 +1,52 @@ +#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 some VT-encoded content into the terminal + const char *commands[] = { + "Hello from a \033[1mCMake\033[0m-built program!\r\n", + "Line 2: \033[4munderlined\033[0m text\r\n", + "Line 3: \033[31mred\033[0m \033[32mgreen\033[0m \033[34mblue\033[0m\r\n", + }; + 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])); + } + + // Format the terminal contents as plain text + 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); + + uint8_t *buf = NULL; + size_t len = 0; + result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + printf("Plain text (%zu bytes):\n", len); + fwrite(buf, 1, len, stdout); + printf("\n"); + + ghostty_free(NULL, buf, len); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/c-vt-colors/README.md b/example/c-vt-colors/README.md new file mode 100644 index 00000000000..881abfcc21a --- /dev/null +++ b/example/c-vt-colors/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Terminal Colors + +This contains a simple example of how to set default terminal colors, +read effective and default color values, and observe how OSC overrides +layer on top of defaults using the `ghostty-vt` C library. + +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-colors/build.zig b/example/c-vt-colors/build.zig new file mode 100644 index 00000000000..ddb62ece389 --- /dev/null +++ b/example/c-vt-colors/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_colors", + .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-colors/build.zig.zon b/example/c-vt-colors/build.zig.zon new file mode 100644 index 00000000000..3d0023d3d1e --- /dev/null +++ b/example/c-vt-colors/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_colors, + .version = "0.0.0", + .fingerprint = 0xe7ec4247f16d4fce, + .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-colors/src/main.c b/example/c-vt-colors/src/main.c new file mode 100644 index 00000000000..6838527f23e --- /dev/null +++ b/example/c-vt-colors/src/main.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include + +//! [colors-set-defaults] +/// Set up a dark color theme with custom palette entries. +void set_color_theme(GhosttyTerminal terminal) { + // Set default foreground (light gray) and background (dark) + GhosttyColorRgb fg = { .r = 0xDD, .g = 0xDD, .b = 0xDD }; + GhosttyColorRgb bg = { .r = 0x1E, .g = 0x1E, .b = 0x2E }; + GhosttyColorRgb cursor = { .r = 0xF5, .g = 0xE0, .b = 0xDC }; + + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, &fg); + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, &bg); + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, &cursor); + + // Set a custom palette — start from the built-in default and override + // the first 8 entries with a custom dark theme. + GhosttyColorRgb palette[256]; + ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_PALETTE, palette); + + palette[GHOSTTY_COLOR_NAMED_BLACK] = (GhosttyColorRgb){ 0x45, 0x47, 0x5A }; + palette[GHOSTTY_COLOR_NAMED_RED] = (GhosttyColorRgb){ 0xF3, 0x8B, 0xA8 }; + palette[GHOSTTY_COLOR_NAMED_GREEN] = (GhosttyColorRgb){ 0xA6, 0xE3, 0xA1 }; + palette[GHOSTTY_COLOR_NAMED_YELLOW] = (GhosttyColorRgb){ 0xF9, 0xE2, 0xAF }; + palette[GHOSTTY_COLOR_NAMED_BLUE] = (GhosttyColorRgb){ 0x89, 0xB4, 0xFA }; + palette[GHOSTTY_COLOR_NAMED_MAGENTA] = (GhosttyColorRgb){ 0xF5, 0xC2, 0xE7 }; + palette[GHOSTTY_COLOR_NAMED_CYAN] = (GhosttyColorRgb){ 0x94, 0xE2, 0xD5 }; + palette[GHOSTTY_COLOR_NAMED_WHITE] = (GhosttyColorRgb){ 0xBA, 0xC2, 0xDE }; + + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, palette); +} +//! [colors-set-defaults] + +//! [colors-read] +/// Print the effective and default values for a color, showing how +/// OSC overrides layer on top of defaults. +void print_color(GhosttyTerminal terminal, + const char* name, + GhosttyTerminalData effective_data, + GhosttyTerminalData default_data) { + GhosttyColorRgb color; + + GhosttyResult res = ghostty_terminal_get(terminal, effective_data, &color); + if (res == GHOSTTY_SUCCESS) { + printf(" %-12s effective: #%02X%02X%02X", name, color.r, color.g, color.b); + } else { + printf(" %-12s effective: (not set)", name); + } + + res = ghostty_terminal_get(terminal, default_data, &color); + if (res == GHOSTTY_SUCCESS) { + printf(" default: #%02X%02X%02X\n", color.r, color.g, color.b); + } else { + printf(" default: (not set)\n"); + } +} + +void print_all_colors(GhosttyTerminal terminal, const char* label) { + printf("%s:\n", label); + print_color(terminal, "foreground", + GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND, + GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT); + print_color(terminal, "background", + GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND, + GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT); + print_color(terminal, "cursor", + GHOSTTY_TERMINAL_DATA_COLOR_CURSOR, + GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT); + + // Show palette index 0 (black) as an example + GhosttyColorRgb palette[256]; + ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_PALETTE, palette); + printf(" %-12s effective: #%02X%02X%02X", "palette[0]", + palette[0].r, palette[0].g, palette[0].b); + + ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT, + palette); + printf(" default: #%02X%02X%02X\n", palette[0].r, palette[0].g, palette[0].b); +} +//! [colors-read] + +//! [colors-main] +int main() { + // Create a terminal + GhosttyTerminal terminal = NULL; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) { + fprintf(stderr, "Failed to create terminal\n"); + return 1; + } + + // Before setting any colors, everything is unset + print_all_colors(terminal, "Before setting defaults"); + + // Set our color theme defaults + set_color_theme(terminal); + print_all_colors(terminal, "\nAfter setting defaults"); + + // Simulate an OSC override (e.g. a program running inside the + // terminal changes the foreground via OSC 10) + const char* osc_fg = "\x1B]10;rgb:FF/00/00\x1B\\"; + ghostty_terminal_vt_write(terminal, (const uint8_t*)osc_fg, + strlen(osc_fg)); + print_all_colors(terminal, "\nAfter OSC foreground override"); + + // Clear the foreground default — the OSC override is still active + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, NULL); + print_all_colors(terminal, "\nAfter clearing foreground default"); + + ghostty_terminal_free(terminal); + return 0; +} +//! [colors-main] diff --git a/example/c-vt-effects/README.md b/example/c-vt-effects/README.md new file mode 100644 index 00000000000..5f5a22b14bb --- /dev/null +++ b/example/c-vt-effects/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Terminal Effects + +This contains a simple example of how to register and use terminal +effect callbacks (`write_pty`, `bell`, `title_changed`) with the +`ghostty-vt` C library. + +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-effects/build.zig b/example/c-vt-effects/build.zig new file mode 100644 index 00000000000..c3b1af73b75 --- /dev/null +++ b/example/c-vt-effects/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_effects", + .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-effects/build.zig.zon b/example/c-vt-effects/build.zig.zon new file mode 100644 index 00000000000..0275f4f6835 --- /dev/null +++ b/example/c-vt-effects/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_effects, + .version = "0.0.0", + .fingerprint = 0xc02634cd65f5b583, + .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-effects/src/main.c b/example/c-vt-effects/src/main.c new file mode 100644 index 00000000000..1e3a3d64549 --- /dev/null +++ b/example/c-vt-effects/src/main.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include + +//! [effects-write-pty] +void on_write_pty(GhosttyTerminal terminal, + void* userdata, + const uint8_t* data, + size_t len) { + (void)terminal; + (void)userdata; + printf(" write_pty (%zu bytes): ", len); + fwrite(data, 1, len, stdout); + printf("\n"); +} +//! [effects-write-pty] + +//! [effects-bell] +void on_bell(GhosttyTerminal terminal, void* userdata) { + (void)terminal; + int* count = (int*)userdata; + (*count)++; + printf(" bell! (count=%d)\n", *count); +} +//! [effects-bell] + +//! [effects-title-changed] +void on_title_changed(GhosttyTerminal terminal, void* userdata) { + (void)userdata; + // Query the cursor position to confirm the terminal processed the + // title change (the title itself is tracked by the embedder via the + // OSC parser or its own state). + uint16_t col = 0; + ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_CURSOR_X, &col); + printf(" title changed (cursor at col %u)\n", col); +} +//! [effects-title-changed] + +//! [effects-register] +int main() { + // Create a terminal + GhosttyTerminal terminal = NULL; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) { + fprintf(stderr, "Failed to create terminal\n"); + return 1; + } + + // Set up userdata — a simple bell counter + int bell_count = 0; + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_USERDATA, &bell_count); + + // Register effect callbacks + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, + (const void *)on_write_pty); + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_BELL, + (const void *)on_bell); + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, + (const void *)on_title_changed); + + // Feed VT data that triggers effects: + + // 1. Bell (BEL = 0x07) + printf("Sending BEL:\n"); + const uint8_t bel = 0x07; + ghostty_terminal_vt_write(terminal, &bel, 1); + + // 2. Title change (OSC 2 ; ST) + printf("Sending title change:\n"); + const char* title_seq = "\x1B]2;Hello Effects\x1B\\"; + ghostty_terminal_vt_write(terminal, (const uint8_t*)title_seq, + strlen(title_seq)); + + // 3. Device status report (DECRQM for wraparound mode ?7) + // triggers write_pty with the response + printf("Sending DECRQM query:\n"); + const char* decrqm = "\x1B[?7$p"; + ghostty_terminal_vt_write(terminal, (const uint8_t*)decrqm, + strlen(decrqm)); + + // 4. Another bell to show the counter increments + printf("Sending another BEL:\n"); + ghostty_terminal_vt_write(terminal, &bel, 1); + + printf("Total bells: %d\n", bell_count); + + ghostty_terminal_free(terminal); + return 0; +} +//! [effects-register] diff --git a/example/c-vt-encode-focus/README.md b/example/c-vt-encode-focus/README.md new file mode 100644 index 00000000000..f433e88084e --- /dev/null +++ b/example/c-vt-encode-focus/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` Encode Focus + +This contains a simple example of how to use the `ghostty-vt` focus +encoding API to encode focus gained/lost events into escape sequences. + +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-encode-focus/build.zig b/example/c-vt-encode-focus/build.zig new file mode 100644 index 00000000000..2904371fb1b --- /dev/null +++ b/example/c-vt-encode-focus/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_encode_focus", + .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-encode-focus/build.zig.zon b/example/c-vt-encode-focus/build.zig.zon new file mode 100644 index 00000000000..0da20475cdb --- /dev/null +++ b/example/c-vt-encode-focus/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_encode_focus, + .version = "0.0.0", + .fingerprint = 0x89f01fd829fcc550, + .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-encode-focus/src/main.c b/example/c-vt-encode-focus/src/main.c new file mode 100644 index 00000000000..15854792f62 --- /dev/null +++ b/example/c-vt-encode-focus/src/main.c @@ -0,0 +1,20 @@ +#include <stdio.h> +#include <ghostty/vt.h> + +//! [focus-encode] +int main() { + char buf[8]; + size_t written = 0; + + GhosttyResult result = ghostty_focus_encode( + GHOSTTY_FOCUS_GAINED, buf, sizeof(buf), &written); + + if (result == GHOSTTY_SUCCESS) { + printf("Encoded %zu bytes: ", written); + fwrite(buf, 1, written, stdout); + printf("\n"); + } + + return 0; +} +//! [focus-encode] diff --git a/example/c-vt-key-encode/README.md b/example/c-vt-encode-key/README.md similarity index 100% rename from example/c-vt-key-encode/README.md rename to example/c-vt-encode-key/README.md diff --git a/example/c-vt-encode-key/build.zig b/example/c-vt-encode-key/build.zig new file mode 100644 index 00000000000..de878a7adc9 --- /dev/null +++ b/example/c-vt-encode-key/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_encode_key", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-key-encode/build.zig.zon b/example/c-vt-encode-key/build.zig.zon similarity index 100% rename from example/c-vt-key-encode/build.zig.zon rename to example/c-vt-encode-key/build.zig.zon diff --git a/example/c-vt-encode-key/src/main.c b/example/c-vt-encode-key/src/main.c new file mode 100644 index 00000000000..99b782022fb --- /dev/null +++ b/example/c-vt-encode-key/src/main.c @@ -0,0 +1,40 @@ +#include <assert.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <ghostty/vt.h> + +//! [key-encode] +int main() { + // Create encoder + GhosttyKeyEncoder encoder; + GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + assert(result == GHOSTTY_SUCCESS); + + // Enable Kitty keyboard protocol with all features + ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, + &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + + // Create and configure key event for Ctrl+C press + GhosttyKeyEvent event; + result = ghostty_key_event_new(NULL, &event); + assert(result == GHOSTTY_SUCCESS); + ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); + ghostty_key_event_set_key(event, GHOSTTY_KEY_C); + ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + + // Encode the key event + char buf[128]; + size_t written = 0; + result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + assert(result == GHOSTTY_SUCCESS); + + // Use the encoded sequence (e.g., write to terminal) + fwrite(buf, 1, written, stdout); + + // Cleanup + ghostty_key_event_free(event); + ghostty_key_encoder_free(encoder); + return 0; +} +//! [key-encode] diff --git a/example/c-vt-encode-mouse/README.md b/example/c-vt-encode-mouse/README.md new file mode 100644 index 00000000000..754e098052f --- /dev/null +++ b/example/c-vt-encode-mouse/README.md @@ -0,0 +1,23 @@ +# Example: `ghostty-vt` C Mouse Encoding + +This example demonstrates how to use the `ghostty-vt` C library to encode mouse +events into terminal escape sequences. + +This example specifically shows how to: + +1. Create a mouse encoder with the C API +2. Configure tracking mode and output format (this example uses SGR) +3. Set terminal geometry for pixel-to-cell coordinate mapping +4. Create and configure a mouse event +5. Encode the mouse event into a terminal escape sequence + +The example encodes a left button press at pixel position (50, 40) using SGR +format, producing an escape sequence like `\x1b[<0;6;3M`. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-encode-mouse/build.zig b/example/c-vt-encode-mouse/build.zig new file mode 100644 index 00000000000..4fdb353c011 --- /dev/null +++ b/example/c-vt-encode-mouse/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_encode_mouse", + .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-encode-mouse/build.zig.zon b/example/c-vt-encode-mouse/build.zig.zon new file mode 100644 index 00000000000..1ab5da284c6 --- /dev/null +++ b/example/c-vt-encode-mouse/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt, + .version = "0.0.0", + .fingerprint = 0x413a8529a6dd3c51, + .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-encode-mouse/src/main.c b/example/c-vt-encode-mouse/src/main.c new file mode 100644 index 00000000000..d75ed9c5483 --- /dev/null +++ b/example/c-vt-encode-mouse/src/main.c @@ -0,0 +1,52 @@ +#include <assert.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <ghostty/vt.h> + +//! [mouse-encode] +int main() { + // Create encoder + GhosttyMouseEncoder encoder; + GhosttyResult result = ghostty_mouse_encoder_new(NULL, &encoder); + assert(result == GHOSTTY_SUCCESS); + + // Configure SGR format with normal tracking + ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_EVENT, + &(GhosttyMouseTrackingMode){GHOSTTY_MOUSE_TRACKING_NORMAL}); + ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_FORMAT, + &(GhosttyMouseFormat){GHOSTTY_MOUSE_FORMAT_SGR}); + + // Set terminal geometry for coordinate mapping + ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE, + &(GhosttyMouseEncoderSize){ + .size = sizeof(GhosttyMouseEncoderSize), + .screen_width = 800, .screen_height = 600, + .cell_width = 10, .cell_height = 20, + }); + + // Create and configure a left button press event + GhosttyMouseEvent event; + result = ghostty_mouse_event_new(NULL, &event); + assert(result == GHOSTTY_SUCCESS); + ghostty_mouse_event_set_action(event, GHOSTTY_MOUSE_ACTION_PRESS); + ghostty_mouse_event_set_button(event, GHOSTTY_MOUSE_BUTTON_LEFT); + ghostty_mouse_event_set_position(event, + (GhosttyMousePosition){.x = 50.0f, .y = 40.0f}); + + // Encode the mouse event + char buf[128]; + size_t written = 0; + result = ghostty_mouse_encoder_encode(encoder, event, + buf, sizeof(buf), &written); + assert(result == GHOSTTY_SUCCESS); + + // Use the encoded sequence (e.g., write to terminal) + fwrite(buf, 1, written, stdout); + + // Cleanup + ghostty_mouse_event_free(event); + ghostty_mouse_encoder_free(encoder); + return 0; +} +//! [mouse-encode] 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..56f9d1220f1 --- /dev/null +++ b/example/c-vt-formatter/src/main.c @@ -0,0 +1,63 @@ +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ghostty/vt.h> + +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 + ghostty_free(NULL, buf, len); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/c-vt-grid-traverse/README.md b/example/c-vt-grid-traverse/README.md new file mode 100644 index 00000000000..f9a15851a91 --- /dev/null +++ b/example/c-vt-grid-traverse/README.md @@ -0,0 +1,19 @@ +# Example: `ghostty-vt` Grid Traversal + +This contains a simple example of how to use the `ghostty-vt` terminal and +grid reference APIs to create a terminal, write content into it, and then +traverse the entire grid cell-by-cell using grid refs to inspect codepoints, +row state, and styles. + +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-grid-traverse/build.zig b/example/c-vt-grid-traverse/build.zig new file mode 100644 index 00000000000..caf1740283f --- /dev/null +++ b/example/c-vt-grid-traverse/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_grid_traverse", + .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-grid-traverse/build.zig.zon b/example/c-vt-grid-traverse/build.zig.zon new file mode 100644 index 00000000000..21b6cea184e --- /dev/null +++ b/example/c-vt-grid-traverse/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_grid_traverse, + .version = "0.0.0", + .fingerprint = 0xf694dd12db9be040, + .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-grid-traverse/src/main.c b/example/c-vt-grid-traverse/src/main.c new file mode 100644 index 00000000000..f07169eb683 --- /dev/null +++ b/example/c-vt-grid-traverse/src/main.c @@ -0,0 +1,85 @@ +#include <assert.h> +#include <stdio.h> +#include <string.h> +#include <ghostty/vt.h> + +//! [grid-ref-traverse] +int main() { + // Create a small terminal + GhosttyTerminal terminal; + GhosttyTerminalOptions opts = { + .cols = 10, + .rows = 3, + .max_scrollback = 0, + }; + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + assert(result == GHOSTTY_SUCCESS); + + // Write some content so the grid has interesting data + const char *text = "Hello!\r\n" // Row 0: H e l l o ! + "World\r\n" // Row 1: W o r l d + "\033[1mBold"; // Row 2: B o l d (bold style) + ghostty_terminal_vt_write( + terminal, (const uint8_t *)text, strlen(text)); + + // Get terminal dimensions + uint16_t cols, rows; + ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLS, &cols); + ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_ROWS, &rows); + + // Traverse the entire grid using grid refs + for (uint16_t row = 0; row < rows; row++) { + printf("Row %u: ", row); + for (uint16_t col = 0; col < cols; col++) { + // Resolve the point to a grid reference + GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); + GhosttyPoint pt = { + .tag = GHOSTTY_POINT_TAG_ACTIVE, + .value = { .coordinate = { .x = col, .y = row } }, + }; + result = ghostty_terminal_grid_ref(terminal, pt, &ref); + assert(result == GHOSTTY_SUCCESS); + + // Read the cell from the grid ref + GhosttyCell cell; + result = ghostty_grid_ref_cell(&ref, &cell); + assert(result == GHOSTTY_SUCCESS); + + // Check if the cell has text + bool has_text = false; + ghostty_cell_get(cell, GHOSTTY_CELL_DATA_HAS_TEXT, &has_text); + + if (has_text) { + uint32_t codepoint = 0; + ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CODEPOINT, &codepoint); + printf("%c", (char)codepoint); + } else { + printf("."); + } + } + + // Also inspect the row for wrap state + GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); + GhosttyPoint pt = { + .tag = GHOSTTY_POINT_TAG_ACTIVE, + .value = { .coordinate = { .x = 0, .y = row } }, + }; + ghostty_terminal_grid_ref(terminal, pt, &ref); + + GhosttyRow grid_row; + ghostty_grid_ref_row(&ref, &grid_row); + + bool wrap = false; + ghostty_row_get(grid_row, GHOSTTY_ROW_DATA_WRAP, &wrap); + printf(" (wrap=%s", wrap ? "true" : "false"); + + // Check the style of the first cell with text + GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); + ghostty_grid_ref_style(&ref, &style); + printf(", bold=%s)\n", style.bold ? "true" : "false"); + } + + ghostty_terminal_free(terminal); + return 0; +} +//! [grid-ref-traverse] diff --git a/example/c-vt-key-encode/build.zig b/example/c-vt-key-encode/build.zig deleted file mode 100644 index b4b759744d3..00000000000 --- a/example/c-vt-key-encode/build.zig +++ /dev/null @@ -1,42 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const run_step = b.step("run", "Run the app"); - - const exe_mod = b.createModule(.{ - .target = target, - .optimize = optimize, - }); - exe_mod.addCSourceFiles(.{ - .root = b.path("src"), - .files = &.{"main.c"}, - }); - - // You'll want to use a lazy dependency here so that ghostty is only - // downloaded if you actually need it. - if (b.lazyDependency("ghostty", .{ - // Setting simd to false will force a pure static build that - // doesn't even require libc, but it has a significant performance - // penalty. If your embedding app requires libc anyway, you should - // always keep simd enabled. - // .simd = false, - })) |dep| { - exe_mod.linkLibrary(dep.artifact("ghostty-vt")); - } - - // Exe - const exe = b.addExecutable(.{ - .name = "c_vt_key_encode", - .root_module = exe_mod, - }); - b.installArtifact(exe); - - // Run - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); - run_step.dependOn(&run_cmd.step); -} diff --git a/example/c-vt-key-encode/src/main.c b/example/c-vt-key-encode/src/main.c deleted file mode 100644 index 82444f99d2e..00000000000 --- a/example/c-vt-key-encode/src/main.c +++ /dev/null @@ -1,59 +0,0 @@ -#include <assert.h> -#include <stddef.h> -#include <stdio.h> -#include <string.h> -#include <ghostty/vt.h> - -int main() { - GhosttyKeyEncoder encoder; - GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); - assert(result == GHOSTTY_SUCCESS); - - // Set kitty flags with all features enabled - ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); - - // Create key event - GhosttyKeyEvent event; - result = ghostty_key_event_new(NULL, &event); - assert(result == GHOSTTY_SUCCESS); - ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_RELEASE); - ghostty_key_event_set_key(event, GHOSTTY_KEY_CONTROL_LEFT); - ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); - printf("Encoding event: left ctrl release with all Kitty flags enabled\n"); - - // Optionally, encode with null buffer to get required size. You can - // skip this step and provide a sufficiently large buffer directly. - // If there isn't enoug hspace, the function will return an out of memory - // error. - size_t required = 0; - result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); - assert(result == GHOSTTY_OUT_OF_MEMORY); - printf("Required buffer size: %zu bytes\n", required); - - // Encode the key event. We don't use our required size above because - // that was just an example; we know 128 bytes is enough. - char buf[128]; - size_t written = 0; - result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); - assert(result == GHOSTTY_SUCCESS); - printf("Encoded %zu bytes\n", written); - - // Print the encoded sequence (hex and string) - printf("Hex: "); - for (size_t i = 0; i < written; i++) printf("%02x ", (unsigned char)buf[i]); - printf("\n"); - - printf("String: "); - for (size_t i = 0; i < written; i++) { - if (buf[i] == 0x1b) { - printf("\\x1b"); - } else { - printf("%c", buf[i]); - } - } - printf("\n"); - - ghostty_key_event_free(event); - ghostty_key_encoder_free(encoder); - return 0; -} diff --git a/example/c-vt-modes/README.md b/example/c-vt-modes/README.md new file mode 100644 index 00000000000..bd43c17996b --- /dev/null +++ b/example/c-vt-modes/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Mode Utilities + +This contains a simple example of how to use the `ghostty-vt` mode +utilities to pack and unpack terminal mode identifiers and encode +DECRPM responses. + +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-modes/build.zig b/example/c-vt-modes/build.zig new file mode 100644 index 00000000000..1a4b3f8d82f --- /dev/null +++ b/example/c-vt-modes/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_modes", + .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-modes/build.zig.zon b/example/c-vt-modes/build.zig.zon new file mode 100644 index 00000000000..bdfeefdcac6 --- /dev/null +++ b/example/c-vt-modes/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_modes, + .version = "0.0.0", + .fingerprint = 0x67ce079ebc70a02a, + .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-modes/src/main.c b/example/c-vt-modes/src/main.c new file mode 100644 index 00000000000..e957c97777e --- /dev/null +++ b/example/c-vt-modes/src/main.c @@ -0,0 +1,45 @@ +#include <stdio.h> +#include <ghostty/vt.h> + +//! [modes-pack-unpack] +void modes_example() { + // Create a mode for DEC mode 25 (cursor visible) + GhosttyMode tag = ghostty_mode_new(25, false); + printf("value=%u ansi=%d packed=0x%04x\n", + ghostty_mode_value(tag), + ghostty_mode_ansi(tag), + tag); + + // Create a mode for ANSI mode 4 (insert mode) + GhosttyMode ansi_tag = ghostty_mode_new(4, true); + printf("value=%u ansi=%d packed=0x%04x\n", + ghostty_mode_value(ansi_tag), + ghostty_mode_ansi(ansi_tag), + ansi_tag); +} +//! [modes-pack-unpack] + +//! [modes-decrpm] +void decrpm_example() { + char buf[32]; + size_t written = 0; + + // Encode a report that DEC mode 25 (cursor visible) is set + GhosttyResult result = ghostty_mode_report_encode( + GHOSTTY_MODE_CURSOR_VISIBLE, + GHOSTTY_MODE_REPORT_SET, + buf, sizeof(buf), &written); + + if (result == GHOSTTY_SUCCESS) { + printf("Encoded %zu bytes: ", written); + fwrite(buf, 1, written, stdout); + printf("\n"); // prints: ESC[?25;1$y + } +} +//! [modes-decrpm] + +int main() { + modes_example(); + decrpm_example(); + return 0; +} diff --git a/example/c-vt-paste/README.md b/example/c-vt-paste/README.md index 0f911771f8e..377cd3c3b88 100644 --- a/example/c-vt-paste/README.md +++ b/example/c-vt-paste/README.md @@ -1,7 +1,7 @@ -# Example: `ghostty-vt` Paste Safety Check +# Example: `ghostty-vt` Paste Utilities This contains a simple example of how to use the `ghostty-vt` paste -utilities to check if paste data is safe. +utilities to check if paste data is safe and encode it for terminal input. 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 diff --git a/example/c-vt-paste/src/main.c b/example/c-vt-paste/src/main.c index 153861ca9ef..e6e4b3d61c6 100644 --- a/example/c-vt-paste/src/main.c +++ b/example/c-vt-paste/src/main.c @@ -2,18 +2,41 @@ #include <string.h> #include <ghostty/vt.h> -int main() { - // Test safe paste data - const char *safe_data = "hello world"; +//! [paste-safety] +void safety_example() { + const char* safe_data = "hello world"; + const char* unsafe_data = "rm -rf /\n"; + if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { - printf("'%s' is safe to paste\n", safe_data); + printf("Safe to paste\n"); } - // Test unsafe paste data with newline - const char *unsafe_newline = "rm -rf /\n"; - if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) { - printf("'%s' is UNSAFE - contains newline\n", unsafe_newline); + if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { + printf("Unsafe! Contains newline\n"); } +} +//! [paste-safety] + +//! [paste-encode] +void encode_example() { + // The input buffer is modified in place (unsafe bytes are stripped). + char data[] = "hello\nworld"; + char buf[64]; + size_t written = 0; + + GhosttyResult result = ghostty_paste_encode( + data, strlen(data), true, buf, sizeof(buf), &written); + + if (result == GHOSTTY_SUCCESS) { + printf("Encoded %zu bytes: ", written); + fwrite(buf, 1, written, stdout); + printf("\n"); + } +} +//! [paste-encode] + +int main() { + safety_example(); // Test unsafe paste data with bracketed paste end sequence const char *unsafe_escape = "evil\x1b[201~code"; @@ -27,5 +50,7 @@ int main() { printf("Empty data is safe\n"); } + encode_example(); + return 0; } diff --git a/example/c-vt-render/README.md b/example/c-vt-render/README.md new file mode 100644 index 00000000000..3725ed46f15 --- /dev/null +++ b/example/c-vt-render/README.md @@ -0,0 +1,19 @@ +# Example: `ghostty-vt` Render State + +This contains an example of how to use the `ghostty-vt` render-state API +to create a render state, update it from terminal content, iterate rows +and cells, read styles and colors, inspect cursor state, and manage dirty +tracking. + +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-render/build.zig b/example/c-vt-render/build.zig new file mode 100644 index 00000000000..15e3e540528 --- /dev/null +++ b/example/c-vt-render/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_render", + .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-render/build.zig.zon b/example/c-vt-render/build.zig.zon new file mode 100644 index 00000000000..3919970f95a --- /dev/null +++ b/example/c-vt-render/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_render, + .version = "0.0.0", + .fingerprint = 0xb10e18b2fab773c9, + .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-render/src/main.c b/example/c-vt-render/src/main.c new file mode 100644 index 00000000000..0714d416033 --- /dev/null +++ b/example/c-vt-render/src/main.c @@ -0,0 +1,234 @@ +#include <assert.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <ghostty/vt.h> + +/// Helper: resolve a style color to an RGB value using the palette. +static GhosttyColorRgb resolve_color(GhosttyStyleColor color, + const GhosttyRenderStateColors* colors, + GhosttyColorRgb fallback) { + switch (color.tag) { + case GHOSTTY_STYLE_COLOR_RGB: + return color.value.rgb; + case GHOSTTY_STYLE_COLOR_PALETTE: + return colors->palette[color.value.palette]; + default: + return fallback; + } +} + +int main(void) { + GhosttyResult result; + + //! [render-state-update] + // Create a terminal and render state, then update the render state + // from the terminal. The render state captures a snapshot of everything + // needed to draw a frame. + GhosttyTerminal terminal = NULL; + GhosttyTerminalOptions terminal_opts = { + .cols = 40, + .rows = 5, + .max_scrollback = 10000, + }; + result = ghostty_terminal_new(NULL, &terminal, terminal_opts); + assert(result == GHOSTTY_SUCCESS); + + GhosttyRenderState render_state = NULL; + result = ghostty_render_state_new(NULL, &render_state); + assert(result == GHOSTTY_SUCCESS); + + // Feed some styled content into the terminal. + const char* content = + "Hello, \033[1;32mworld\033[0m!\r\n" // bold green "world" + "\033[4munderlined\033[0m text\r\n" // underlined text + "\033[38;2;255;128;0morange\033[0m\r\n"; // 24-bit orange fg + ghostty_terminal_vt_write( + terminal, (const uint8_t*)content, strlen(content)); + + result = ghostty_render_state_update(render_state, terminal); + assert(result == GHOSTTY_SUCCESS); + //! [render-state-update] + + //! [render-dirty-check] + // Check the global dirty state to decide how much work the renderer + // needs to do. After rendering, reset it to false. + GhosttyRenderStateDirty dirty; + result = ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_DIRTY, &dirty); + assert(result == GHOSTTY_SUCCESS); + + switch (dirty) { + case GHOSTTY_RENDER_STATE_DIRTY_FALSE: + printf("Frame is clean, nothing to draw.\n"); + break; + case GHOSTTY_RENDER_STATE_DIRTY_PARTIAL: + printf("Partial redraw needed.\n"); + break; + case GHOSTTY_RENDER_STATE_DIRTY_FULL: + printf("Full redraw needed.\n"); + break; + } + //! [render-dirty-check] + + //! [render-colors] + // Retrieve colors (background, foreground, palette) from the render + // state. These are needed to resolve palette-indexed cell colors. + GhosttyRenderStateColors colors = + GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); + result = ghostty_render_state_colors_get(render_state, &colors); + assert(result == GHOSTTY_SUCCESS); + + printf("Background: #%02x%02x%02x\n", + colors.background.r, colors.background.g, colors.background.b); + printf("Foreground: #%02x%02x%02x\n", + colors.foreground.r, colors.foreground.g, colors.foreground.b); + //! [render-colors] + + //! [render-cursor] + // Read cursor position and visual style from the render state. + bool cursor_visible = false; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, + &cursor_visible); + + bool cursor_in_viewport = false; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, + &cursor_in_viewport); + + if (cursor_visible && cursor_in_viewport) { + uint16_t cx, cy; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cx); + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cy); + + GhosttyRenderStateCursorVisualStyle style; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, + &style); + + const char* style_name = "unknown"; + switch (style) { + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR: + style_name = "bar"; + break; + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK: + style_name = "block"; + break; + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE: + style_name = "underline"; + break; + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW: + style_name = "hollow"; + break; + } + printf("Cursor at (%u, %u), style: %s\n", cx, cy, style_name); + } + //! [render-cursor] + + //! [render-row-iterate] + // Iterate rows via the row iterator. For each dirty row, iterate its + // cells, read codepoints/graphemes and styles, and emit ANSI-colored + // output as a simple "renderer". + GhosttyRenderStateRowIterator row_iter = NULL; + result = ghostty_render_state_row_iterator_new(NULL, &row_iter); + assert(result == GHOSTTY_SUCCESS); + + result = ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, &row_iter); + assert(result == GHOSTTY_SUCCESS); + + GhosttyRenderStateRowCells cells = NULL; + result = ghostty_render_state_row_cells_new(NULL, &cells); + assert(result == GHOSTTY_SUCCESS); + + int row_index = 0; + while (ghostty_render_state_row_iterator_next(row_iter)) { + // Check per-row dirty state; a real renderer would skip clean rows. + bool row_dirty = false; + ghostty_render_state_row_get( + row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, &row_dirty); + + printf("Row %2d [%s]: ", row_index, + row_dirty ? "dirty" : "clean"); + + // Get cells for this row (reuses the same cells handle). + result = ghostty_render_state_row_get( + row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &cells); + assert(result == GHOSTTY_SUCCESS); + + while (ghostty_render_state_row_cells_next(cells)) { + // Get the grapheme length; 0 means the cell is empty. + uint32_t grapheme_len = 0; + ghostty_render_state_row_cells_get( + cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, + &grapheme_len); + + if (grapheme_len == 0) { + putchar(' '); + continue; + } + + // Read the style for this cell. Returns the default style for + // cells that have no explicit styling. + GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); + ghostty_render_state_row_cells_get( + cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, &style); + + // Resolve foreground color for this cell. + GhosttyColorRgb fg = + resolve_color(style.fg_color, &colors, colors.foreground); + + // Emit ANSI true-color escape for the foreground. + printf("\033[38;2;%u;%u;%um", fg.r, fg.g, fg.b); + if (style.bold) printf("\033[1m"); + if (style.underline) printf("\033[4m"); + + // Read grapheme codepoints into a buffer and print them. + // The buffer must be at least grapheme_len elements. + uint32_t codepoints[16]; + uint32_t len = grapheme_len < 16 ? grapheme_len : 16; + ghostty_render_state_row_cells_get( + cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, + codepoints); + + for (uint32_t i = 0; i < len; i++) { + // Simple ASCII print; a real renderer would handle UTF-8. + if (codepoints[i] < 128) + putchar((char)codepoints[i]); + else + printf("U+%04X", codepoints[i]); + } + + printf("\033[0m"); // Reset style after each cell. + } + + printf("\n"); + + // Clear per-row dirty flag after "rendering" it. + bool clean = false; + ghostty_render_state_row_set( + row_iter, GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, &clean); + + row_index++; + } + //! [render-row-iterate] + + //! [render-dirty-reset] + // After finishing the frame, reset the global dirty state so the next + // update can report changes accurately. + GhosttyRenderStateDirty clean_state = GHOSTTY_RENDER_STATE_DIRTY_FALSE; + result = ghostty_render_state_set( + render_state, GHOSTTY_RENDER_STATE_OPTION_DIRTY, &clean_state); + assert(result == GHOSTTY_SUCCESS); + //! [render-dirty-reset] + + // Cleanup + ghostty_render_state_row_cells_free(cells); + ghostty_render_state_row_iterator_free(row_iter); + ghostty_render_state_free(render_state); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/c-vt-sgr/src/main.c b/example/c-vt-sgr/src/main.c index 21a5297265a..e213c0c93f1 100644 --- a/example/c-vt-sgr/src/main.c +++ b/example/c-vt-sgr/src/main.c @@ -2,12 +2,43 @@ #include <stdio.h> #include <ghostty/vt.h> -int main() { +//! [sgr-basic] +void basic_example() { // Create parser GhosttySgrParser parser; GhosttyResult result = ghostty_sgr_new(NULL, &parser); assert(result == GHOSTTY_SUCCESS); + // Parse "bold, red foreground" sequence: ESC[1;31m + uint16_t params[] = {1, 31}; + result = ghostty_sgr_set_params(parser, params, NULL, 2); + assert(result == GHOSTTY_SUCCESS); + + // Iterate through attributes + GhosttySgrAttribute attr; + while (ghostty_sgr_next(parser, &attr)) { + switch (attr.tag) { + case GHOSTTY_SGR_ATTR_BOLD: + printf("Bold enabled\n"); + break; + case GHOSTTY_SGR_ATTR_FG_8: + printf("Foreground color: %d\n", attr.value.fg_8); + break; + default: + break; + } + } + + // Cleanup + ghostty_sgr_free(parser); +} +//! [sgr-basic] + +void advanced_example() { + GhosttySgrParser parser; + GhosttyResult result = ghostty_sgr_new(NULL, &parser); + assert(result == GHOSTTY_SUCCESS); + // Parse a complex SGR sequence from Kakoune // This corresponds to the escape sequence: // ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m @@ -26,10 +57,9 @@ int main() { result = ghostty_sgr_set_params(parser, params, separators, sizeof(params) / sizeof(params[0])); assert(result == GHOSTTY_SUCCESS); - printf("Parsing Kakoune SGR sequence:\n"); + printf("\nParsing Kakoune SGR sequence:\n"); printf("ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m\n\n"); - // Iterate through attributes GhosttySgrAttribute attr; int count = 0; while (ghostty_sgr_next(parser, &attr)) { @@ -124,8 +154,11 @@ int main() { } printf("\nTotal attributes parsed: %d\n", count); - - // Cleanup ghostty_sgr_free(parser); +} + +int main() { + basic_example(); + advanced_example(); return 0; } diff --git a/example/c-vt-size-report/README.md b/example/c-vt-size-report/README.md new file mode 100644 index 00000000000..0e6ef2c8575 --- /dev/null +++ b/example/c-vt-size-report/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` Size Report Encoding + +This contains a simple example of how to use the `ghostty-vt` size report +encoding API to encode terminal size reports into escape sequences. + +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-size-report/build.zig b/example/c-vt-size-report/build.zig new file mode 100644 index 00000000000..fbd0f5e2309 --- /dev/null +++ b/example/c-vt-size-report/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_size_report", + .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-size-report/build.zig.zon b/example/c-vt-size-report/build.zig.zon new file mode 100644 index 00000000000..71d10d343ed --- /dev/null +++ b/example/c-vt-size-report/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_size_report, + .version = "0.0.0", + .fingerprint = 0x17e8cdb658fab232, + .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-size-report/src/main.c b/example/c-vt-size-report/src/main.c new file mode 100644 index 00000000000..99e9c10dc2c --- /dev/null +++ b/example/c-vt-size-report/src/main.c @@ -0,0 +1,27 @@ +#include <stdio.h> +#include <ghostty/vt.h> + +//! [size-report-encode] +int main() { + GhosttySizeReportSize size = { + .rows = 24, + .columns = 80, + .cell_width = 9, + .cell_height = 18, + }; + + char buf[64]; + size_t written = 0; + + GhosttyResult result = ghostty_size_report_encode( + GHOSTTY_SIZE_REPORT_MODE_2048, size, buf, sizeof(buf), &written); + + if (result == GHOSTTY_SUCCESS) { + printf("Encoded %zu bytes: ", written); + fwrite(buf, 1, written, stdout); + printf("\n"); + } + + return 0; +} +//! [size-report-encode] diff --git a/example/c-vt-static/README.md b/example/c-vt-static/README.md new file mode 100644 index 00000000000..52da4ddb0c7 --- /dev/null +++ b/example/c-vt-static/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Static Linking + +This contains a simple example of how to statically link the `ghostty-vt` +C library with a C program using the `ghostty-vt-static` artifact. It is +otherwise identical to the `c-vt` example. + +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-static/build.zig b/example/c-vt-static/build.zig new file mode 100644 index 00000000000..0e53d69c529 --- /dev/null +++ b/example/c-vt-static/build.zig @@ -0,0 +1,44 @@ +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| { + // Use "ghostty-vt-static" for static linking instead of + // "ghostty-vt" which provides a shared library. + exe_mod.linkLibrary(dep.artifact("ghostty-vt-static")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_static", + .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-static/build.zig.zon b/example/c-vt-static/build.zig.zon new file mode 100644 index 00000000000..413bf66fb9a --- /dev/null +++ b/example/c-vt-static/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_static, + .version = "0.0.0", + .fingerprint = 0xa592a9fdd5d87ed2, + .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-static/src/main.c b/example/c-vt-static/src/main.c new file mode 100644 index 00000000000..b1297d7a76d --- /dev/null +++ b/example/c-vt-static/src/main.c @@ -0,0 +1,36 @@ +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <ghostty/vt.h> + +int main() { + GhosttyOscParser parser; + if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { + return 1; + } + + // Setup change window title command to change the title to "hello" + ghostty_osc_next(parser, '0'); + ghostty_osc_next(parser, ';'); + const char *title = "hello"; + for (size_t i = 0; i < strlen(title); i++) { + ghostty_osc_next(parser, title[i]); + } + + // End parsing and get command + GhosttyOscCommand command = ghostty_osc_end(parser, 0); + + // Get and print command type + GhosttyOscCommandType type = ghostty_osc_command_type(command); + printf("Command type: %d\n", type); + + // Extract and print the title + if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) { + printf("Extracted title: %s\n", title); + } else { + printf("Failed to extract title\n"); + } + + ghostty_osc_free(parser); + return 0; +} diff --git a/example/c-vt-stream/README.md b/example/c-vt-stream/README.md new file mode 100644 index 00000000000..6620537ed47 --- /dev/null +++ b/example/c-vt-stream/README.md @@ -0,0 +1,19 @@ +# Example: VT Stream Processing in C + +This contains a simple example of how to use `ghostty_terminal_vt_write` +to parse and process VT sequences in C. This is the C equivalent of +the `zig-vt-stream` example, ideal for read-only terminal applications +such as replay tooling, CI log viewers, and PaaS builder output. + +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-stream/build.zig b/example/c-vt-stream/build.zig new file mode 100644 index 00000000000..21575ddd6bc --- /dev/null +++ b/example/c-vt-stream/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_stream", + .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-stream/build.zig.zon b/example/c-vt-stream/build.zig.zon new file mode 100644 index 00000000000..4c37e852c54 --- /dev/null +++ b/example/c-vt-stream/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_stream, + .version = "0.0.0", + .fingerprint = 0xd5bb3fc45e3f4dfc, + .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-stream/src/main.c b/example/c-vt-stream/src/main.c new file mode 100644 index 00000000000..7063e1f1472 --- /dev/null +++ b/example/c-vt-stream/src/main.c @@ -0,0 +1,74 @@ +#include <assert.h> +#include <stdio.h> +#include <string.h> +#include <ghostty/vt.h> + +int main(void) { + //! [vt-stream-init] + // Create a terminal + GhosttyTerminal terminal; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + assert(result == GHOSTTY_SUCCESS); + //! [vt-stream-init] + + //! [vt-stream-write] + // Feed VT data into the terminal + const char *text = "Hello, World!\r\n"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + + // ANSI color codes: ESC[1;32m = bold green, ESC[0m = reset + text = "\x1b[1;32mGreen Text\x1b[0m\r\n"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + + // Cursor positioning: ESC[1;1H = move to row 1, column 1 + text = "\x1b[1;1HTop-left corner\r\n"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + + // Cursor movement: ESC[5B = move down 5 lines + text = "\x1b[5B"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + text = "Moved down!\r\n"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + + // Erase line: ESC[2K = clear entire line + text = "\x1b[2K"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + text = "New content\r\n"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + + // Multiple lines + text = "Line A\r\nLine B\r\nLine C\r\n"; + ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text)); + //! [vt-stream-write] + + //! [vt-stream-read] + // Get the final terminal state as a plain string using the formatter + 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); + + uint8_t *buf = NULL; + size_t len = 0; + result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + fwrite(buf, 1, len, stdout); + printf("\n"); + + ghostty_free(NULL, buf, len); + ghostty_formatter_free(formatter); + //! [vt-stream-read] + + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/cpp-vt-stream/README.md b/example/cpp-vt-stream/README.md new file mode 100644 index 00000000000..7dccbe30166 --- /dev/null +++ b/example/cpp-vt-stream/README.md @@ -0,0 +1,19 @@ +# Example: VT Stream Processing in C++ + +This contains a simple example of how to use `ghostty_terminal_vt_write` +to parse and process VT sequences in C++. This is a simplified C++ port +of the `c-vt-stream` example that verifies libghostty compiles in C++ +mode. + +> [!IMPORTANT] +> +> **`libghostty` is a C library.** This example is only here so our CI +> verifies that the library can be built in used from C++ files. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/cpp-vt-stream/build.zig b/example/cpp-vt-stream/build.zig new file mode 100644 index 00000000000..c1fd8708164 --- /dev/null +++ b/example/cpp-vt-stream/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.cpp"}, + }); + + // 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 = "cpp_vt_stream", + .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/cpp-vt-stream/build.zig.zon b/example/cpp-vt-stream/build.zig.zon new file mode 100644 index 00000000000..bc6a39a0e77 --- /dev/null +++ b/example/cpp-vt-stream/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .cpp_vt_stream, + .version = "0.0.0", + .fingerprint = 0x112f5d044ef8c2ac, + .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/cpp-vt-stream/src/main.cpp b/example/cpp-vt-stream/src/main.cpp new file mode 100644 index 00000000000..a77f98ad506 --- /dev/null +++ b/example/cpp-vt-stream/src/main.cpp @@ -0,0 +1,49 @@ +#include <cassert> +#include <cstdio> +#include <cstring> +#include <ghostty/vt.h> + +int main() { + // Create a terminal + GhosttyTerminal terminal; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + GhosttyResult result = ghostty_terminal_new(nullptr, &terminal, opts); + assert(result == GHOSTTY_SUCCESS); + + // Feed VT data into the terminal + const char *text = "Hello from C++!\r\n"; + ghostty_terminal_vt_write(terminal, reinterpret_cast<const uint8_t *>(text), std::strlen(text)); + + text = "\x1b[1;32mGreen Text\x1b[0m\r\n"; + ghostty_terminal_vt_write(terminal, reinterpret_cast<const uint8_t *>(text), std::strlen(text)); + + text = "\x1b[1;1HTop-left corner\r\n"; + ghostty_terminal_vt_write(terminal, reinterpret_cast<const uint8_t *>(text), std::strlen(text)); + + // Get the final terminal state as a plain string + 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(nullptr, &formatter, terminal, fmt_opts); + assert(result == GHOSTTY_SUCCESS); + + uint8_t *buf = nullptr; + size_t len = 0; + result = ghostty_formatter_format_alloc(formatter, nullptr, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + std::fwrite(buf, 1, len, stdout); + std::printf("\n"); + + ghostty_free(nullptr, buf, len); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/wasm-key-encode/README.md b/example/wasm-key-encode/README.md index ccd906cf793..e528449911c 100644 --- a/example/wasm-key-encode/README.md +++ b/example/wasm-key-encode/README.md @@ -8,7 +8,7 @@ to encode key events into terminal escape sequences. First, build the WebAssembly module: ```bash -zig build lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall +zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall ``` This will create `zig-out/bin/ghostty-vt.wasm`. diff --git a/example/wasm-sgr/README.md b/example/wasm-sgr/README.md index a107c910d43..465d6fdbb3c 100644 --- a/example/wasm-sgr/README.md +++ b/example/wasm-sgr/README.md @@ -9,7 +9,7 @@ styling attributes. First, build the WebAssembly module: ```bash -zig build lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall +zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall ``` This will create `zig-out/bin/ghostty-vt.wasm`. diff --git a/example/wasm-vt/README.md b/example/wasm-vt/README.md new file mode 100644 index 00000000000..92e928405c6 --- /dev/null +++ b/example/wasm-vt/README.md @@ -0,0 +1,39 @@ +# WebAssembly VT Terminal Example + +This example demonstrates how to use the Ghostty VT library from WebAssembly +to initialize a terminal, write VT-encoded data to it, and format the +terminal contents as plain text. + +## Building + +First, build the WebAssembly module: + +```bash +zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall +``` + +This will create `zig-out/bin/ghostty-vt.wasm`. + +## Running + +**Important:** You must serve this via HTTP, not open it as a file directly. +Browsers block loading WASM files from `file://` URLs. + +From the **root of the ghostty repository**, serve with a local HTTP server: + +```bash +# Using Python (recommended) +python3 -m http.server 8000 + +# Or using Node.js +npx serve . + +# Or using PHP +php -S localhost:8000 +``` + +Then open your browser to: + +``` +http://localhost:8000/example/wasm-vt/ +``` diff --git a/example/wasm-vt/index.html b/example/wasm-vt/index.html new file mode 100644 index 00000000000..d720e23757a --- /dev/null +++ b/example/wasm-vt/index.html @@ -0,0 +1,342 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Ghostty VT Terminal - WebAssembly Example + + + +

Ghostty VT Terminal - WebAssembly Example

+

This example demonstrates initializing a terminal, writing VT-encoded data to it, and formatting the output using the Ghostty VT WebAssembly module.

+ +
Loading WebAssembly module...
+ +
+

Terminal Size

+
+ + +
+

VT Input

+ +

Use \x1b for ESC, \r\n for CR+LF. Press "Run" to process.

+ +
+ +
Waiting for input...
+ +

Note: This example must be served via HTTP (not opened directly as a file). See the README for instructions.

+ + + + 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..f3a4814bbd2 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,14 +52,15 @@ "root": { "inputs": { "flake-compat": "flake-compat", - "flake-utils": "flake-utils", "home-manager": "home-manager", "nixpkgs": "nixpkgs", + "systems": "systems", "zig": "zig", "zon2nix": "zon2nix" } }, "systems": { + "flake": false, "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -97,19 +80,19 @@ "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..fec675cf0cc 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 = { @@ -18,12 +17,17 @@ flake = false; }; + systems = { + url = "github:nix-systems/default"; + flake = false; + }; + zig = { url = "github:mitchellh/zig-overlay"; inputs = { nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; flake-compat.follows = "flake-compat"; + systems.follows = "systems"; }; }; @@ -80,6 +84,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..6a7e4e521c7 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-20260323-152405-a2c7b60.tgz", + "dest": "vendor/p/N-V-__8AAL6FAwBDPampKgDjoxlJYDIn2jv0VaINS4W6CXJN", + "sha256": "7d68177545e1dbf74d66a111cc4bbd873e31cb2e2797e1948cb32950caec4670" }, { "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", @@ -167,6 +167,12 @@ "dest": "vendor/p/N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", "sha256": "5cedcadde81b75e60f23e5e83b5dd2b8eb4efb9f8f79bd7a347d148aeb0530f8" }, + { + "type": "archive", + "url": "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + "dest": "vendor/p/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", + "sha256": "dd2df14ab5f41038257aaedcc4b5fb9ac0ee018f3f0f94af9097028e60d33223" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", @@ -175,7 +181,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..ba5649df210 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -17,11 +17,38 @@ extern "C" { #include #include +#ifdef _MSC_VER +#include +typedef SSIZE_T ssize_t; +#endif + //------------------------------------------------------------------- // Macros #define GHOSTTY_SUCCESS 0 +// Symbol visibility for shared library builds. On Windows, functions +// are exported from the DLL when building and imported when consuming. +// On other platforms with GCC/Clang, functions are marked with default +// visibility so they remain accessible when the library is built with +// -fvisibility=hidden. For static library builds, define GHOSTTY_STATIC +// before including this header to make this a no-op. +#ifndef GHOSTTY_API +#if defined(GHOSTTY_STATIC) + #define GHOSTTY_API +#elif defined(_WIN32) || defined(_WIN64) + #ifdef GHOSTTY_BUILD_SHARED + #define GHOSTTY_API __declspec(dllexport) + #else + #define GHOSTTY_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) && __GNUC__ >= 4 + #define GHOSTTY_API __attribute__((visibility("default"))) +#else + #define GHOSTTY_API +#endif +#endif + //------------------------------------------------------------------- // Types @@ -463,6 +490,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 +542,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 +619,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 +751,7 @@ typedef struct { // renderer.Health typedef enum { - GHOSTTY_RENDERER_HEALTH_OK, + GHOSTTY_RENDERER_HEALTH_HEALTHY, GHOSTTY_RENDERER_HEALTH_UNHEALTHY, } ghostty_action_renderer_health_e; @@ -905,6 +947,7 @@ typedef enum { GHOSTTY_ACTION_SEARCH_SELECTED, GHOSTTY_ACTION_READONLY, GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD, + GHOSTTY_ACTION_SET_TAB_TITLE, } ghostty_action_tag_e; typedef union { @@ -945,6 +988,7 @@ typedef union { ghostty_action_search_total_s search_total; ghostty_action_search_selected_s search_selected; ghostty_action_readonly_e readonly; + ghostty_action_set_title_s set_tab_title; } ghostty_action_u; typedef struct { @@ -953,7 +997,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)( @@ -1015,144 +1059,146 @@ typedef enum { //------------------------------------------------------------------- // Published API -int ghostty_init(uintptr_t, char**); -void ghostty_cli_try_action(void); -ghostty_info_s ghostty_info(void); -const char* ghostty_translate(const char*); -void ghostty_string_free(ghostty_string_s); - -ghostty_config_t ghostty_config_new(); -void ghostty_config_free(ghostty_config_t); -ghostty_config_t ghostty_config_clone(ghostty_config_t); -void ghostty_config_load_cli_args(ghostty_config_t); -void ghostty_config_load_file(ghostty_config_t, const char*); -void ghostty_config_load_default_files(ghostty_config_t); -void ghostty_config_load_recursive_files(ghostty_config_t); -void ghostty_config_finalize(ghostty_config_t); -bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t); -ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, - const char*, - uintptr_t); -uint32_t ghostty_config_diagnostics_count(ghostty_config_t); -ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); -ghostty_string_s ghostty_config_open_path(void); - -ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, - ghostty_config_t); -void ghostty_app_free(ghostty_app_t); -void ghostty_app_tick(ghostty_app_t); -void* ghostty_app_userdata(ghostty_app_t); -void ghostty_app_set_focus(ghostty_app_t, bool); -bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); -bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s); -void ghostty_app_keyboard_changed(ghostty_app_t); -void ghostty_app_open_config(ghostty_app_t); -void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); -bool ghostty_app_needs_confirm_quit(ghostty_app_t); -bool ghostty_app_has_global_keybinds(ghostty_app_t); -void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); - -ghostty_surface_config_s ghostty_surface_config_new(); - -ghostty_surface_t ghostty_surface_new(ghostty_app_t, - const ghostty_surface_config_s*); -void ghostty_surface_free(ghostty_surface_t); -void* ghostty_surface_userdata(ghostty_surface_t); -ghostty_app_t ghostty_surface_app(ghostty_surface_t); -ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); -void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); -bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); -bool ghostty_surface_process_exited(ghostty_surface_t); -void ghostty_surface_refresh(ghostty_surface_t); -void ghostty_surface_draw(ghostty_surface_t); -void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); -void ghostty_surface_set_focus(ghostty_surface_t, bool); -void ghostty_surface_set_occlusion(ghostty_surface_t, bool); -void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); -ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); -void ghostty_surface_set_color_scheme(ghostty_surface_t, - ghostty_color_scheme_e); -ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, - ghostty_input_mods_e); -bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); -bool ghostty_surface_key_is_binding(ghostty_surface_t, - ghostty_input_key_s, - ghostty_binding_flags_e*); -void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); -void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); -bool ghostty_surface_mouse_captured(ghostty_surface_t); -bool ghostty_surface_mouse_button(ghostty_surface_t, - ghostty_input_mouse_state_e, - ghostty_input_mouse_button_e, - ghostty_input_mods_e); -void ghostty_surface_mouse_pos(ghostty_surface_t, - double, - double, - ghostty_input_mods_e); -void ghostty_surface_mouse_scroll(ghostty_surface_t, - double, - double, - ghostty_input_scroll_mods_t); -void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); -void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*); -void ghostty_surface_request_close(ghostty_surface_t); -void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); -void ghostty_surface_split_focus(ghostty_surface_t, - ghostty_action_goto_split_e); -void ghostty_surface_split_resize(ghostty_surface_t, - ghostty_action_resize_split_direction_e, - uint16_t); -void ghostty_surface_split_equalize(ghostty_surface_t); -bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); -void ghostty_surface_complete_clipboard_request(ghostty_surface_t, - const char*, - void*, - bool); -bool ghostty_surface_has_selection(ghostty_surface_t); -bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); -bool ghostty_surface_read_text(ghostty_surface_t, - ghostty_selection_s, - ghostty_text_s*); -void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); +GHOSTTY_API int ghostty_init(uintptr_t, char**); +GHOSTTY_API void ghostty_cli_try_action(void); +GHOSTTY_API ghostty_info_s ghostty_info(void); +GHOSTTY_API const char* ghostty_translate(const char*); +GHOSTTY_API void ghostty_string_free(ghostty_string_s); + +GHOSTTY_API ghostty_config_t ghostty_config_new(); +GHOSTTY_API void ghostty_config_free(ghostty_config_t); +GHOSTTY_API ghostty_config_t ghostty_config_clone(ghostty_config_t); +GHOSTTY_API void ghostty_config_load_cli_args(ghostty_config_t); +GHOSTTY_API void ghostty_config_load_file(ghostty_config_t, const char*); +GHOSTTY_API void ghostty_config_load_default_files(ghostty_config_t); +GHOSTTY_API void ghostty_config_load_recursive_files(ghostty_config_t); +GHOSTTY_API void ghostty_config_finalize(ghostty_config_t); +GHOSTTY_API bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t); +GHOSTTY_API ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, + const char*, + uintptr_t); +GHOSTTY_API uint32_t ghostty_config_diagnostics_count(ghostty_config_t); +GHOSTTY_API ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); +GHOSTTY_API ghostty_string_s ghostty_config_open_path(void); + +GHOSTTY_API ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, + ghostty_config_t); +GHOSTTY_API void ghostty_app_free(ghostty_app_t); +GHOSTTY_API void ghostty_app_tick(ghostty_app_t); +GHOSTTY_API void* ghostty_app_userdata(ghostty_app_t); +GHOSTTY_API void ghostty_app_set_focus(ghostty_app_t, bool); +GHOSTTY_API bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); +GHOSTTY_API bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s); +GHOSTTY_API void ghostty_app_keyboard_changed(ghostty_app_t); +GHOSTTY_API void ghostty_app_open_config(ghostty_app_t); +GHOSTTY_API void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); +GHOSTTY_API bool ghostty_app_needs_confirm_quit(ghostty_app_t); +GHOSTTY_API bool ghostty_app_has_global_keybinds(ghostty_app_t); +GHOSTTY_API void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); + +GHOSTTY_API ghostty_surface_config_s ghostty_surface_config_new(); + +GHOSTTY_API ghostty_surface_t ghostty_surface_new(ghostty_app_t, + const ghostty_surface_config_s*); +GHOSTTY_API void ghostty_surface_free(ghostty_surface_t); +GHOSTTY_API void* ghostty_surface_userdata(ghostty_surface_t); +GHOSTTY_API ghostty_app_t ghostty_surface_app(ghostty_surface_t); +GHOSTTY_API ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); +GHOSTTY_API void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); +GHOSTTY_API bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_process_exited(ghostty_surface_t); +GHOSTTY_API void ghostty_surface_refresh(ghostty_surface_t); +GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t); +GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); +GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool); +GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool); +GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); +GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); +GHOSTTY_API void ghostty_surface_set_color_scheme(ghostty_surface_t, + ghostty_color_scheme_e); +GHOSTTY_API ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, + ghostty_input_mods_e); +GHOSTTY_API bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); +GHOSTTY_API bool ghostty_surface_key_is_binding(ghostty_surface_t, + ghostty_input_key_s, + ghostty_binding_flags_e*); +GHOSTTY_API void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); +GHOSTTY_API void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); +GHOSTTY_API bool ghostty_surface_mouse_captured(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_mouse_button(ghostty_surface_t, + ghostty_input_mouse_state_e, + ghostty_input_mouse_button_e, + ghostty_input_mods_e); +GHOSTTY_API void ghostty_surface_mouse_pos(ghostty_surface_t, + double, + double, + ghostty_input_mods_e); +GHOSTTY_API void ghostty_surface_mouse_scroll(ghostty_surface_t, + double, + double, + ghostty_input_scroll_mods_t); +GHOSTTY_API void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); +GHOSTTY_API void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*); +GHOSTTY_API void ghostty_surface_request_close(ghostty_surface_t); +GHOSTTY_API void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); +GHOSTTY_API void ghostty_surface_split_focus(ghostty_surface_t, + ghostty_action_goto_split_e); +GHOSTTY_API void ghostty_surface_split_resize(ghostty_surface_t, + ghostty_action_resize_split_direction_e, + uint16_t); +GHOSTTY_API void ghostty_surface_split_equalize(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); +GHOSTTY_API void ghostty_surface_complete_clipboard_request(ghostty_surface_t, + const char*, + void*, + bool); +GHOSTTY_API bool ghostty_surface_has_selection(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_select_cursor_cell(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_clear_selection(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); +GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t, + ghostty_selection_s, + ghostty_text_s*); +GHOSTTY_API void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); #ifdef __APPLE__ -void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); -void* ghostty_surface_quicklook_font(ghostty_surface_t); -bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); +GHOSTTY_API void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); +GHOSTTY_API void* ghostty_surface_quicklook_font(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); #endif -ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); -void ghostty_inspector_free(ghostty_surface_t); -void ghostty_inspector_set_focus(ghostty_inspector_t, bool); -void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double); -void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t); -void ghostty_inspector_mouse_button(ghostty_inspector_t, - ghostty_input_mouse_state_e, - ghostty_input_mouse_button_e, - ghostty_input_mods_e); -void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double); -void ghostty_inspector_mouse_scroll(ghostty_inspector_t, - double, - double, - ghostty_input_scroll_mods_t); -void ghostty_inspector_key(ghostty_inspector_t, - ghostty_input_action_e, - ghostty_input_key_e, - ghostty_input_mods_e); -void ghostty_inspector_text(ghostty_inspector_t, const char*); +GHOSTTY_API ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); +GHOSTTY_API void ghostty_inspector_free(ghostty_surface_t); +GHOSTTY_API void ghostty_inspector_set_focus(ghostty_inspector_t, bool); +GHOSTTY_API void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double); +GHOSTTY_API void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t); +GHOSTTY_API void ghostty_inspector_mouse_button(ghostty_inspector_t, + ghostty_input_mouse_state_e, + ghostty_input_mouse_button_e, + ghostty_input_mods_e); +GHOSTTY_API void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double); +GHOSTTY_API void ghostty_inspector_mouse_scroll(ghostty_inspector_t, + double, + double, + ghostty_input_scroll_mods_t); +GHOSTTY_API void ghostty_inspector_key(ghostty_inspector_t, + ghostty_input_action_e, + ghostty_input_key_e, + ghostty_input_mods_e); +GHOSTTY_API void ghostty_inspector_text(ghostty_inspector_t, const char*); #ifdef __APPLE__ -bool ghostty_inspector_metal_init(ghostty_inspector_t, void*); -void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*); -bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); +GHOSTTY_API bool ghostty_inspector_metal_init(ghostty_inspector_t, void*); +GHOSTTY_API void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*); +GHOSTTY_API bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); #endif // APIs I'd like to get rid of eventually but are still needed for now. // Don't use these unless you know what you're doing. -void ghostty_set_window_background_blur(ghostty_app_t, void*); +GHOSTTY_API void ghostty_set_window_background_blur(ghostty_app_t, void*); // Benchmark API, if available. -bool ghostty_benchmark_cli(const char*, const char*); +GHOSTTY_API bool ghostty_benchmark_cli(const char*, const char*); #ifdef __cplusplus } diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 4f8fef88ecc..2a52f4b087d 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -28,33 +28,55 @@ * @section groups_sec API Reference * * The API is organized into the following groups: - * - @ref key "Key Encoding" - Encode key events into terminal sequences + * - @ref terminal "Terminal" - Complete terminal emulator state and rendering + * - @ref render "Render State" - Incremental render state updates for custom renderers + * - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences * - @ref paste "Paste Utilities" - Validate paste data safety + * - @ref build_info "Build Info" - Query compile-time build configuration * - @ref allocator "Memory Management" - Memory management and custom allocators * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions * + * Encoding related APIs: + * - @ref focus "Focus Encoding" - Encode focus in/out events into terminal sequences + * - @ref key "Key Encoding" - Encode key events into terminal sequences + * - @ref mouse "Mouse Encoding" - Encode mouse events into terminal sequences + * * @section examples_sec Examples * * Complete working examples: + * - @ref c-vt-build-info/src/main.c - Build info query example * - @ref c-vt/src/main.c - OSC parser example - * - @ref c-vt-key-encode/src/main.c - Key encoding example + * - @ref c-vt-encode-key/src/main.c - Key encoding example + * - @ref c-vt-encode-mouse/src/main.c - Mouse 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 + * - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs * */ +/** @example c-vt-build-info/src/main.c + * This example demonstrates how to query compile-time build configuration + * such as SIMD support, Kitty graphics, and tmux control mode availability. + */ + /** @example c-vt/src/main.c * This example demonstrates how to use the OSC parser to parse an OSC sequence, * extract command information, and retrieve command-specific data like window titles. */ -/** @example c-vt-key-encode/src/main.c +/** @example c-vt-encode-key/src/main.c * This example demonstrates how to use the key encoder to convert key events * into terminal escape sequences using the Kitty keyboard protocol. */ +/** @example c-vt-encode-mouse/src/main.c + * This example demonstrates how to use the mouse encoder to convert mouse events + * into terminal escape sequences using the SGR mouse format. + */ + /** @example c-vt-paste/src/main.c * This example demonstrates how to use the paste utilities to check if * paste data is safe before sending it to the terminal. @@ -65,6 +87,17 @@ * 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. + */ + +/** @example c-vt-grid-traverse/src/main.c + * This example demonstrates how to traverse the entire terminal grid using + * grid refs to inspect cell codepoints, row wrap state, and cell styles. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -72,12 +105,25 @@ extern "C" { #endif -#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include #include +#include +#include #include +#include +#include #include #ifdef __cplusplus diff --git a/include/ghostty/vt/allocator.h b/include/ghostty/vt/allocator.h index 4cebe91bb10..2e8685e8445 100644 --- a/include/ghostty/vt/allocator.h +++ b/include/ghostty/vt/allocator.h @@ -10,6 +10,7 @@ #include #include #include +#include /** @defgroup allocator Memory Management * @@ -44,6 +45,24 @@ * 2. Create a GhosttyAllocator struct with your vtable and context * 3. Pass the allocator to functions that accept one * + * ## Alloc/Free Helpers + * + * ghostty_alloc() and ghostty_free() provide a simple malloc/free-style + * interface for allocating and freeing byte buffers through the library's + * allocator. These are useful when: + * + * - You need to allocate a buffer to pass into a libghostty-vt function + * (e.g. preparing input data for ghostty_terminal_vt_write()). + * - You need to free a buffer returned by a libghostty-vt function + * (e.g. the output of ghostty_formatter_format_alloc()). + * - You are on a platform where the library's internal allocator differs + * from the consumer's C runtime (e.g. Windows, where Zig's libc and + * MSVC's CRT maintain separate heaps), so calling the standard C + * free() on library-allocated memory would be undefined behavior. + * + * Always use the same allocator (or NULL) for both the allocation and + * the corresponding free. + * * @{ */ @@ -191,6 +210,46 @@ typedef struct GhosttyAllocator { const GhosttyAllocatorVtable *vtable; } GhosttyAllocator; +/** + * Allocate a buffer of `len` bytes. + * + * Uses the provided allocator, or the default allocator if NULL is passed. + * The returned buffer must be freed with ghostty_free() using the same + * allocator. + * + * @param allocator Pointer to the allocator to use, or NULL for the default + * @param len Number of bytes to allocate + * @return Pointer to the allocated buffer, or NULL if allocation failed + * + * @ingroup allocator + */ +GHOSTTY_API uint8_t* ghostty_alloc(const GhosttyAllocator* allocator, size_t len); + +/** + * Free memory that was allocated by a libghostty-vt function. + * + * Use this to free buffers returned by functions such as + * ghostty_formatter_format_alloc(). Pass the same allocator that was + * used for the allocation, or NULL if the default allocator was used. + * + * On platforms where the library's internal allocator differs from the + * consumer's C runtime (e.g. Windows, where Zig's libc and MSVC's CRT + * maintain separate heaps), calling the standard C free() on memory + * allocated by the library causes undefined behavior. This function + * guarantees the correct allocator is used regardless of platform. + * + * It is safe to pass a NULL pointer; the call is a no-op in that case. + * + * @param allocator Pointer to the allocator that was used to allocate the + * memory, or NULL if the default allocator was used + * @param ptr Pointer to the memory to free (may be NULL) + * @param len Length of the allocation in bytes (must match the original + * allocation size) + * + * @ingroup allocator + */ +GHOSTTY_API void ghostty_free(const GhosttyAllocator* allocator, uint8_t* ptr, size_t len); + /** @} */ #endif /* GHOSTTY_VT_ALLOCATOR_H */ diff --git a/include/ghostty/vt/build_info.h b/include/ghostty/vt/build_info.h new file mode 100644 index 00000000000..7f77a769b6e --- /dev/null +++ b/include/ghostty/vt/build_info.h @@ -0,0 +1,140 @@ +/** + * @file build_info.h + * + * Build info - query compile-time build configuration of libghostty-vt. + */ + +#ifndef GHOSTTY_VT_BUILD_INFO_H +#define GHOSTTY_VT_BUILD_INFO_H + +/** @defgroup build_info Build Info + * + * Query compile-time build configuration of libghostty-vt. + * + * These values reflect the options the library was built with and are + * constant for the lifetime of the process. + * + * ## Basic Usage + * + * Use ghostty_build_info() to query individual build options: + * + * @snippet c-vt-build-info/src/main.c build-info-query + * + * @{ + */ + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Build optimization mode. + */ +typedef enum { + GHOSTTY_OPTIMIZE_DEBUG = 0, + GHOSTTY_OPTIMIZE_RELEASE_SAFE = 1, + GHOSTTY_OPTIMIZE_RELEASE_SMALL = 2, + GHOSTTY_OPTIMIZE_RELEASE_FAST = 3, +} GhosttyOptimizeMode; + +/** + * Build info data types that can be queried. + * + * Each variant documents the expected output pointer type. + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_BUILD_INFO_INVALID = 0, + + /** + * Whether SIMD-accelerated code paths are enabled. + * + * Output type: bool * + */ + GHOSTTY_BUILD_INFO_SIMD = 1, + + /** + * Whether Kitty graphics protocol support is available. + * + * Output type: bool * + */ + GHOSTTY_BUILD_INFO_KITTY_GRAPHICS = 2, + + /** + * Whether tmux control mode support is available. + * + * Output type: bool * + */ + GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE = 3, + + /** + * The optimization mode the library was built with. + * + * Output type: GhosttyOptimizeMode * + */ + GHOSTTY_BUILD_INFO_OPTIMIZE = 4, + + /** + * The full version string (e.g. "1.2.3" or "1.2.3-dev+abcdef"). + * + * Output type: GhosttyString * + */ + GHOSTTY_BUILD_INFO_VERSION_STRING = 5, + + /** + * The major version number. + * + * Output type: size_t * + */ + GHOSTTY_BUILD_INFO_VERSION_MAJOR = 6, + + /** + * The minor version number. + * + * Output type: size_t * + */ + GHOSTTY_BUILD_INFO_VERSION_MINOR = 7, + + /** + * The patch version number. + * + * Output type: size_t * + */ + GHOSTTY_BUILD_INFO_VERSION_PATCH = 8, + + /** + * The build metadata string (e.g. commit hash). Has zero length if + * no build metadata is present. + * + * Output type: GhosttyString * + */ + GHOSTTY_BUILD_INFO_VERSION_BUILD = 9, +} GhosttyBuildInfo; + +/** + * Query a compile-time build configuration value. + * + * The caller must pass a pointer to the correct output type for the + * requested data (see GhosttyBuildInfo variants for types). + * + * @param data The build info field to query + * @param out Pointer to store the result (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * data type is invalid + * + * @ingroup build_info + */ +GHOSTTY_API GhosttyResult ghostty_build_info(GhosttyBuildInfo data, void *out); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_BUILD_INFO_H */ diff --git a/include/ghostty/vt/color.h b/include/ghostty/vt/color.h index 0d57b8db4ab..9dc21864eb9 100644 --- a/include/ghostty/vt/color.h +++ b/include/ghostty/vt/color.h @@ -8,6 +8,7 @@ #define GHOSTTY_VT_COLOR_H #include +#include #ifdef __cplusplus extern "C" { @@ -84,7 +85,7 @@ typedef uint8_t GhosttyColorPaletteIndex; * * @ingroup sgr */ -void ghostty_color_rgb_get(GhosttyColorRgb color, +GHOSTTY_API void ghostty_color_rgb_get(GhosttyColorRgb color, uint8_t* r, uint8_t* g, uint8_t* b); diff --git a/include/ghostty/vt/device.h b/include/ghostty/vt/device.h new file mode 100644 index 00000000000..fdf6bca7d4c --- /dev/null +++ b/include/ghostty/vt/device.h @@ -0,0 +1,150 @@ +/** + * @file device.h + * + * Device types used by the terminal for device status and device attribute + * queries. + */ + +#ifndef GHOSTTY_VT_DEVICE_H +#define GHOSTTY_VT_DEVICE_H + +#include +#include + +/* DA1 conformance levels (Pp parameter). */ +#define GHOSTTY_DA_CONFORMANCE_VT100 1 +#define GHOSTTY_DA_CONFORMANCE_VT101 1 +#define GHOSTTY_DA_CONFORMANCE_VT102 6 +#define GHOSTTY_DA_CONFORMANCE_VT125 12 +#define GHOSTTY_DA_CONFORMANCE_VT131 7 +#define GHOSTTY_DA_CONFORMANCE_VT132 4 +#define GHOSTTY_DA_CONFORMANCE_VT220 62 +#define GHOSTTY_DA_CONFORMANCE_VT240 62 +#define GHOSTTY_DA_CONFORMANCE_VT320 63 +#define GHOSTTY_DA_CONFORMANCE_VT340 63 +#define GHOSTTY_DA_CONFORMANCE_VT420 64 +#define GHOSTTY_DA_CONFORMANCE_VT510 65 +#define GHOSTTY_DA_CONFORMANCE_VT520 65 +#define GHOSTTY_DA_CONFORMANCE_VT525 65 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_2 62 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_3 63 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_4 64 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_5 65 + +/* DA1 feature codes (Ps parameters). */ +#define GHOSTTY_DA_FEATURE_COLUMNS_132 1 +#define GHOSTTY_DA_FEATURE_PRINTER 2 +#define GHOSTTY_DA_FEATURE_REGIS 3 +#define GHOSTTY_DA_FEATURE_SIXEL 4 +#define GHOSTTY_DA_FEATURE_SELECTIVE_ERASE 6 +#define GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS 8 +#define GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT 9 +#define GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS 15 +#define GHOSTTY_DA_FEATURE_LOCATOR 16 +#define GHOSTTY_DA_FEATURE_TERMINAL_STATE 17 +#define GHOSTTY_DA_FEATURE_WINDOWING 18 +#define GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING 21 +#define GHOSTTY_DA_FEATURE_ANSI_COLOR 22 +#define GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING 28 +#define GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR 29 +#define GHOSTTY_DA_FEATURE_CLIPBOARD 52 + +/* DA2 device type identifiers (Pp parameter). */ +#define GHOSTTY_DA_DEVICE_TYPE_VT100 0 +#define GHOSTTY_DA_DEVICE_TYPE_VT220 1 +#define GHOSTTY_DA_DEVICE_TYPE_VT240 2 +#define GHOSTTY_DA_DEVICE_TYPE_VT330 18 +#define GHOSTTY_DA_DEVICE_TYPE_VT340 19 +#define GHOSTTY_DA_DEVICE_TYPE_VT320 24 +#define GHOSTTY_DA_DEVICE_TYPE_VT382 32 +#define GHOSTTY_DA_DEVICE_TYPE_VT420 41 +#define GHOSTTY_DA_DEVICE_TYPE_VT510 61 +#define GHOSTTY_DA_DEVICE_TYPE_VT520 64 +#define GHOSTTY_DA_DEVICE_TYPE_VT525 65 + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Color scheme reported in response to a CSI ? 996 n query. + * + * @ingroup terminal + */ +typedef enum { + GHOSTTY_COLOR_SCHEME_LIGHT = 0, + GHOSTTY_COLOR_SCHEME_DARK = 1, +} GhosttyColorScheme; + +/** + * Primary device attributes (DA1) response data. + * + * Returned as part of GhosttyDeviceAttributes in response to a CSI c query. + * The conformance_level is the Pp parameter and features contains the Ps + * feature codes. + * + * @ingroup terminal + */ +typedef struct { + /** Conformance level (Pp parameter). E.g. 62 for VT220. */ + uint16_t conformance_level; + + /** DA1 feature codes. Only the first num_features entries are valid. */ + uint16_t features[64]; + + /** Number of valid entries in the features array. */ + size_t num_features; +} GhosttyDeviceAttributesPrimary; + +/** + * Secondary device attributes (DA2) response data. + * + * Returned as part of GhosttyDeviceAttributes in response to a CSI > c query. + * Response format: CSI > Pp ; Pv ; Pc c + * + * @ingroup terminal + */ +typedef struct { + /** Terminal type identifier (Pp). E.g. 1 for VT220. */ + uint16_t device_type; + + /** Firmware/patch version number (Pv). */ + uint16_t firmware_version; + + /** ROM cartridge registration number (Pc). Always 0 for emulators. */ + uint16_t rom_cartridge; +} GhosttyDeviceAttributesSecondary; + +/** + * Tertiary device attributes (DA3) response data. + * + * Returned as part of GhosttyDeviceAttributes in response to a CSI = c query. + * Response format: DCS ! | D...D ST (DECRPTUI). + * + * @ingroup terminal + */ +typedef struct { + /** Unit ID encoded as 8 uppercase hex digits in the response. */ + uint32_t unit_id; +} GhosttyDeviceAttributesTertiary; + +/** + * Device attributes response data for all three DA levels. + * + * Filled by the device_attributes callback in response to CSI c, + * CSI > c, or CSI = c queries. The terminal uses whichever sub-struct + * matches the request type. + * + * @ingroup terminal + */ +typedef struct { + GhosttyDeviceAttributesPrimary primary; + GhosttyDeviceAttributesSecondary secondary; + GhosttyDeviceAttributesTertiary tertiary; +} GhosttyDeviceAttributes; + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_DEVICE_H */ diff --git a/include/ghostty/vt/focus.h b/include/ghostty/vt/focus.h new file mode 100644 index 00000000000..6e4c9502ccf --- /dev/null +++ b/include/ghostty/vt/focus.h @@ -0,0 +1,75 @@ +/** + * @file focus.h + * + * Focus encoding - encode focus in/out events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_FOCUS_H +#define GHOSTTY_VT_FOCUS_H + +/** @defgroup focus Focus Encoding + * + * Utilities for encoding focus gained/lost events into terminal escape + * sequences (CSI I / CSI O) for focus reporting mode (mode 1004). + * + * ## Basic Usage + * + * Use ghostty_focus_encode() to encode a focus event into a caller-provided + * buffer. If the buffer is too small, the function returns + * GHOSTTY_OUT_OF_SPACE and sets the required size in the output parameter. + * + * ## Example + * + * @snippet c-vt-encode-focus/src/main.c focus-encode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Focus event types for focus reporting mode (mode 1004). + */ +typedef enum { + /** Terminal window gained focus */ + GHOSTTY_FOCUS_GAINED = 0, + /** Terminal window lost focus */ + GHOSTTY_FOCUS_LOST = 1, +} GhosttyFocusEvent; + +/** + * Encode a focus event into a terminal escape sequence. + * + * Encodes a focus gained (CSI I) or focus lost (CSI O) report into the + * provided buffer. + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param event The focus event to encode + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_focus_encode( + GhosttyFocusEvent event, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_FOCUS_H */ diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h new file mode 100644 index 00000000000..81efdb27ce9 --- /dev/null +++ b/include/ghostty/vt/formatter.h @@ -0,0 +1,225 @@ +/** + * @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 GhosttyFormatterImpl* 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 + */ +GHOSTTY_API 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 + */ +GHOSTTY_API 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 with + * ghostty_free(), passing the same allocator (or NULL for the default) + * that was used for the allocation. + * + * @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 + */ +GHOSTTY_API 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 + */ +GHOSTTY_API void ghostty_formatter_free(GhosttyFormatter formatter); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_FORMATTER_H */ diff --git a/include/ghostty/vt/grid_ref.h b/include/ghostty/vt/grid_ref.h new file mode 100644 index 00000000000..d3489ea7387 --- /dev/null +++ b/include/ghostty/vt/grid_ref.h @@ -0,0 +1,131 @@ +/** + * @file grid_ref.h + * + * Terminal grid reference type for referencing a resolved position in the + * terminal grid. + */ + +#ifndef GHOSTTY_VT_GRID_REF_H +#define GHOSTTY_VT_GRID_REF_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup grid_ref Grid Reference + * + * A grid reference is a resolved reference to a specific cell position in the + * terminal's internal page structure. Obtain a grid reference from + * ghostty_terminal_grid_ref(), then extract the cell or row via + * ghostty_grid_ref_cell() and ghostty_grid_ref_row(). + * + * A grid reference is only valid until the next update to the terminal + * instance. There is no guarantee that a grid reference will remain + * valid after ANY operation, even if a seemingly unrelated part of + * the grid is changed, so any information related to the grid reference + * should be read and cached immediately after obtaining the grid reference. + * + * This API is not meant to be used as the core of render loop. It isn't + * built to sustain the framerates needed for rendering large screens. + * Use the render state API for that. + * + * ## Example + * + * @snippet c-vt-grid-traverse/src/main.c grid-ref-traverse + * + * @{ + */ + +/** + * A resolved reference to a terminal cell position. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * + * @ingroup grid_ref + */ +typedef struct { + size_t size; + void *node; + uint16_t x; + uint16_t y; +} GhosttyGridRef; + +/** + * Get the cell from a grid reference. + * + * @param ref Pointer to the grid reference + * @param[out] out_cell On success, set to the cell at the ref's position (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_cell(const GhosttyGridRef *ref, + GhosttyCell *out_cell); + +/** + * Get the row from a grid reference. + * + * @param ref Pointer to the grid reference + * @param[out] out_row On success, set to the row at the ref's position (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_row(const GhosttyGridRef *ref, + GhosttyRow *out_row); + +/** + * Get the grapheme cluster codepoints for the cell at the grid reference's + * position. + * + * Writes the full grapheme cluster (the cell's primary codepoint followed by + * any combining codepoints) into the provided buffer. If the cell has no text, + * out_len is set to 0 and GHOSTTY_SUCCESS is returned. + * + * If the buffer is too small (or NULL), the function returns + * GHOSTTY_OUT_OF_SPACE and writes the required number of codepoints to + * out_len. The caller can then retry with a sufficiently sized buffer. + * + * @param ref Pointer to the grid reference + * @param buf Output buffer of uint32_t codepoints (may be NULL) + * @param buf_len Number of uint32_t elements in the buffer + * @param[out] out_len On success, the number of codepoints written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size in codepoints. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref, + uint32_t *buf, + size_t buf_len, + size_t *out_len); + +/** + * Get the style of the cell at the grid reference's position. + * + * @param ref Pointer to the grid reference + * @param[out] out_style On success, set to the cell's style (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_style(const GhosttyGridRef *ref, + GhosttyStyle *out_style); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_GRID_REF_H */ diff --git a/include/ghostty/vt/key.h b/include/ghostty/vt/key.h index 772b5d43bcf..61b95475357 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,49 +27,40 @@ * changing its properties. * 4. Free the encoder with ghostty_key_encoder_free() when done * + * For a complete working example, see example/c-vt-encode-key in the + * repository. + * * ## Example * + * @snippet c-vt-encode-key/src/main.c key-encode + * + * ## 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} - * #include - * #include - * #include - * - * int main() { - * // Create encoder - * GhosttyKeyEncoder encoder; - * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); - * assert(result == GHOSTTY_SUCCESS); - * - * // Enable Kitty keyboard protocol with all features - * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, - * &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); - * - * // Create and configure key event for Ctrl+C press - * GhosttyKeyEvent event; - * result = ghostty_key_event_new(NULL, &event); - * assert(result == GHOSTTY_SUCCESS); - * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); - * ghostty_key_event_set_key(event, GHOSTTY_KEY_C); - * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); - * - * // Encode the key event - * char buf[128]; - * size_t written = 0; - * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); - * assert(result == GHOSTTY_SUCCESS); - * - * // Use the encoded sequence (e.g., write to terminal) - * fwrite(buf, 1, written, stdout); - * - * // Cleanup - * ghostty_key_event_free(event); - * ghostty_key_encoder_free(encoder); - * return 0; - * } - * @endcode + * // 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}); * - * For a complete working example, see example/c-vt-key-encode in the - * repository. + * // 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..9d8282cec2d 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 /** @@ -21,7 +22,7 @@ * * @ingroup key */ -typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; +typedef struct GhosttyKeyEncoderImpl *GhosttyKeyEncoder; /** * Kitty keyboard protocol flags. @@ -118,7 +119,7 @@ typedef enum { * * @ingroup key */ -GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); +GHOSTTY_API GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); /** * Free a key encoder instance. @@ -130,7 +131,7 @@ GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, Ghostty * * @ingroup key */ -void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); +GHOSTTY_API void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); /** * Set an option on the key encoder. @@ -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. * @@ -149,7 +154,26 @@ void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); * * @ingroup key */ -void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); +GHOSTTY_API 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 + */ +GHOSTTY_API void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, GhosttyTerminal terminal); /** * Encode a key event into a terminal escape sequence. @@ -161,7 +185,7 @@ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOpti * typically don't generate escape sequences. Check the out_len parameter to * determine if any data was written. * - * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY + * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_SPACE * and out_len will contain the required buffer size. The caller can then * allocate a larger buffer and call the function again. * @@ -170,15 +194,15 @@ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOpti * @param out_buf Buffer to write the encoded sequence to * @param out_buf_size Size of the output buffer in bytes * @param out_len Pointer to store the number of bytes written (may be NULL) - * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer too small, or other error code * * ## Example: Calculate required buffer size * * @code{.c} - * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) + * // Query the required size with a NULL buffer (always returns OUT_OF_SPACE) * size_t required = 0; * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); - * assert(result == GHOSTTY_OUT_OF_MEMORY); + * assert(result == GHOSTTY_OUT_OF_SPACE); * * // Allocate buffer of required size * char *buf = malloc(required); @@ -204,7 +228,7 @@ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOpti * if (result == GHOSTTY_SUCCESS) { * // Write the encoded sequence to the terminal * write(pty_fd, buf, written); - * } else if (result == GHOSTTY_OUT_OF_MEMORY) { + * } else if (result == GHOSTTY_OUT_OF_SPACE) { * // Buffer too small, written contains required size * char *dynamic_buf = malloc(written); * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); @@ -216,6 +240,6 @@ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOpti * * @ingroup key */ -GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); +GHOSTTY_API GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); #endif /* GHOSTTY_VT_KEY_ENCODER_H */ diff --git a/include/ghostty/vt/key/event.h b/include/ghostty/vt/key/event.h index dbd2e9f841a..bcc9d8dec10 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 /** @@ -21,7 +21,7 @@ * * @ingroup key */ -typedef struct GhosttyKeyEvent *GhosttyKeyEvent; +typedef struct GhosttyKeyEventImpl *GhosttyKeyEvent; /** * Keyboard input event types. @@ -310,7 +310,7 @@ typedef enum { * * @ingroup key */ -GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); +GHOSTTY_API GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); /** * Free a key event instance. @@ -322,7 +322,7 @@ GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKe * * @ingroup key */ -void ghostty_key_event_free(GhosttyKeyEvent event); +GHOSTTY_API void ghostty_key_event_free(GhosttyKeyEvent event); /** * Set the key action (press, release, repeat). @@ -332,7 +332,7 @@ void ghostty_key_event_free(GhosttyKeyEvent event); * * @ingroup key */ -void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); +GHOSTTY_API void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); /** * Get the key action (press, release, repeat). @@ -342,7 +342,7 @@ void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action * * @ingroup key */ -GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); +GHOSTTY_API GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); /** * Set the physical key code. @@ -352,7 +352,7 @@ GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); * * @ingroup key */ -void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); +GHOSTTY_API void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); /** * Get the physical key code. @@ -362,7 +362,7 @@ void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); * * @ingroup key */ -GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); +GHOSTTY_API GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); /** * Set the modifier keys bitmask. @@ -372,7 +372,7 @@ GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); * * @ingroup key */ -void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); +GHOSTTY_API void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); /** * Get the modifier keys bitmask. @@ -382,7 +382,7 @@ void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); * * @ingroup key */ -GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); +GHOSTTY_API GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); /** * Set the consumed modifiers bitmask. @@ -392,7 +392,7 @@ GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); * * @ingroup key */ -void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); +GHOSTTY_API void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); /** * Get the consumed modifiers bitmask. @@ -402,7 +402,7 @@ void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods cons * * @ingroup key */ -GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); +GHOSTTY_API GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); /** * Set whether the key event is part of a composition sequence. @@ -412,7 +412,7 @@ GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); * * @ingroup key */ -void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); +GHOSTTY_API void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); /** * Get whether the key event is part of a composition sequence. @@ -422,10 +422,16 @@ void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); * * @ingroup key */ -bool ghostty_key_event_get_composing(GhosttyKeyEvent event); +GHOSTTY_API bool ghostty_key_event_get_composing(GhosttyKeyEvent event); /** - * Set the UTF-8 text generated by the key event. + * Set the UTF-8 text generated by the key for the current keyboard layout. + * + * Must contain the unmodified character before any Ctrl/Meta transformations. + * The encoder derives modifier sequences from the logical key and mods + * bitmask, not from this text. Do not pass C0 control characters + * (U+0000-U+001F, U+007F) or platform function key codes (e.g. macOS PUA + * U+F700-U+F8FF); pass NULL instead and let the encoder use the logical key. * * The key event does NOT take ownership of the text pointer. The caller * must ensure the string remains valid for the lifetime needed by the event. @@ -436,7 +442,7 @@ bool ghostty_key_event_get_composing(GhosttyKeyEvent event); * * @ingroup key */ -void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); +GHOSTTY_API void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); /** * Get the UTF-8 text generated by the key event. @@ -449,7 +455,7 @@ void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t * * @ingroup key */ -const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); +GHOSTTY_API const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); /** * Set the unshifted Unicode codepoint. @@ -459,7 +465,7 @@ const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); * * @ingroup key */ -void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); +GHOSTTY_API void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); /** * Get the unshifted Unicode codepoint. @@ -469,6 +475,6 @@ void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t c * * @ingroup key */ -uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); +GHOSTTY_API uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); #endif /* GHOSTTY_VT_KEY_EVENT_H */ diff --git a/include/ghostty/vt/modes.h b/include/ghostty/vt/modes.h new file mode 100644 index 00000000000..513aaf5a5a2 --- /dev/null +++ b/include/ghostty/vt/modes.h @@ -0,0 +1,196 @@ +/** + * @file modes.h + * + * Terminal mode utilities - pack and unpack ANSI/DEC mode identifiers. + */ + +#ifndef GHOSTTY_VT_MODES_H +#define GHOSTTY_VT_MODES_H + +/** @defgroup modes Mode Utilities + * + * Utilities for working with terminal modes. A mode is a compact + * 16-bit representation of a terminal mode identifier that encodes both + * the numeric mode value (up to 15 bits) and whether the mode is an ANSI + * mode or a DEC private mode (?-prefixed). + * + * The packed layout (least-significant bit first) is: + * - Bits 0–14: mode value (u15) + * - Bit 15: ANSI flag (0 = DEC private mode, 1 = ANSI mode) + * + * ## Example + * + * @snippet c-vt-modes/src/main.c modes-pack-unpack + * + * ## DECRPM Report Encoding + * + * Use ghostty_mode_report_encode() to encode a DECRPM response into a + * caller-provided buffer: + * + * @snippet c-vt-modes/src/main.c modes-decrpm + * + * @{ + */ + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @name ANSI Modes + * Modes for standard ANSI modes. + * @{ + */ +#define GHOSTTY_MODE_KAM (ghostty_mode_new(2, true)) /**< Keyboard action (disable keyboard) */ +#define GHOSTTY_MODE_INSERT (ghostty_mode_new(4, true)) /**< Insert mode */ +#define GHOSTTY_MODE_SRM (ghostty_mode_new(12, true)) /**< Send/receive mode */ +#define GHOSTTY_MODE_LINEFEED (ghostty_mode_new(20, true)) /**< Linefeed/new line mode */ +/** @} */ + +/** @name DEC Private Modes + * Modes for DEC private modes (?-prefixed). + * @{ + */ +#define GHOSTTY_MODE_DECCKM (ghostty_mode_new(1, false)) /**< Cursor keys */ +#define GHOSTTY_MODE_132_COLUMN (ghostty_mode_new(3, false)) /**< 132/80 column mode */ +#define GHOSTTY_MODE_SLOW_SCROLL (ghostty_mode_new(4, false)) /**< Slow scroll */ +#define GHOSTTY_MODE_REVERSE_COLORS (ghostty_mode_new(5, false)) /**< Reverse video */ +#define GHOSTTY_MODE_ORIGIN (ghostty_mode_new(6, false)) /**< Origin mode */ +#define GHOSTTY_MODE_WRAPAROUND (ghostty_mode_new(7, false)) /**< Auto-wrap mode */ +#define GHOSTTY_MODE_AUTOREPEAT (ghostty_mode_new(8, false)) /**< Auto-repeat keys */ +#define GHOSTTY_MODE_X10_MOUSE (ghostty_mode_new(9, false)) /**< X10 mouse reporting */ +#define GHOSTTY_MODE_CURSOR_BLINKING (ghostty_mode_new(12, false)) /**< Cursor blink */ +#define GHOSTTY_MODE_CURSOR_VISIBLE (ghostty_mode_new(25, false)) /**< Cursor visible (DECTCEM) */ +#define GHOSTTY_MODE_ENABLE_MODE_3 (ghostty_mode_new(40, false)) /**< Allow 132 column mode */ +#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_new(45, false)) /**< Reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_new(47, false)) /**< Alternate screen (legacy) */ +#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_new(66, false)) /**< Application keypad */ +#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_new(69, false)) /**< Left/right margin mode */ +#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_new(1000, false)) /**< Normal mouse tracking */ +#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_new(1002, false)) /**< Button-event mouse tracking */ +#define GHOSTTY_MODE_ANY_MOUSE (ghostty_mode_new(1003, false)) /**< Any-event mouse tracking */ +#define GHOSTTY_MODE_FOCUS_EVENT (ghostty_mode_new(1004, false)) /**< Focus in/out events */ +#define GHOSTTY_MODE_UTF8_MOUSE (ghostty_mode_new(1005, false)) /**< UTF-8 mouse format */ +#define GHOSTTY_MODE_SGR_MOUSE (ghostty_mode_new(1006, false)) /**< SGR mouse format */ +#define GHOSTTY_MODE_ALT_SCROLL (ghostty_mode_new(1007, false)) /**< Alternate scroll mode */ +#define GHOSTTY_MODE_URXVT_MOUSE (ghostty_mode_new(1015, false)) /**< URxvt mouse format */ +#define GHOSTTY_MODE_SGR_PIXELS_MOUSE (ghostty_mode_new(1016, false)) /**< SGR-Pixels mouse format */ +#define GHOSTTY_MODE_NUMLOCK_KEYPAD (ghostty_mode_new(1035, false)) /**< Ignore keypad with NumLock */ +#define GHOSTTY_MODE_ALT_ESC_PREFIX (ghostty_mode_new(1036, false)) /**< Alt key sends ESC prefix */ +#define GHOSTTY_MODE_ALT_SENDS_ESC (ghostty_mode_new(1039, false)) /**< Alt sends escape */ +#define GHOSTTY_MODE_REVERSE_WRAP_EXT (ghostty_mode_new(1045, false)) /**< Extended reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN (ghostty_mode_new(1047, false)) /**< Alternate screen */ +#define GHOSTTY_MODE_SAVE_CURSOR (ghostty_mode_new(1048, false)) /**< Save cursor (DECSC) */ +#define GHOSTTY_MODE_ALT_SCREEN_SAVE (ghostty_mode_new(1049, false)) /**< Alt screen + save cursor + clear */ +#define GHOSTTY_MODE_BRACKETED_PASTE (ghostty_mode_new(2004, false)) /**< Bracketed paste mode */ +#define GHOSTTY_MODE_SYNC_OUTPUT (ghostty_mode_new(2026, false)) /**< Synchronized output */ +#define GHOSTTY_MODE_GRAPHEME_CLUSTER (ghostty_mode_new(2027, false)) /**< Grapheme cluster mode */ +#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_new(2031, false)) /**< Report color scheme */ +#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_new(2048, false)) /**< In-band size reports */ +/** @} */ + +/** + * A packed 16-bit terminal mode. + * + * Encodes a mode value (bits 0–14) and an ANSI flag (bit 15) into a + * single 16-bit integer. Use the inline helper functions to construct + * and inspect modes rather than manipulating bits directly. + */ +typedef uint16_t GhosttyMode; + +/** + * Create a mode from a mode value and ANSI flag. + * + * @param value The numeric mode value (0–32767) + * @param ansi true for an ANSI mode, false for a DEC private mode + * @return The packed mode + * + * @ingroup modes + */ +static inline GhosttyMode ghostty_mode_new(uint16_t value, bool ansi) { + return (GhosttyMode)((value & 0x7FFF) | ((uint16_t)ansi << 15)); +} + +/** + * Extract the numeric mode value from a mode. + * + * @param mode The mode + * @return The mode value (0–32767) + * + * @ingroup modes + */ +static inline uint16_t ghostty_mode_value(GhosttyMode mode) { + return mode & 0x7FFF; +} + +/** + * Check whether a mode represents an ANSI mode. + * + * @param mode The mode + * @return true if this is an ANSI mode, false if it is a DEC private mode + * + * @ingroup modes + */ +static inline bool ghostty_mode_ansi(GhosttyMode mode) { + return (mode >> 15) != 0; +} + +/** + * DECRPM report state values. + * + * These correspond to the Ps2 parameter in a DECRPM response + * sequence (CSI ? Ps1 ; Ps2 $ y). + */ +typedef enum { + /** Mode is not recognized */ + GHOSTTY_MODE_REPORT_NOT_RECOGNIZED = 0, + /** Mode is set (enabled) */ + GHOSTTY_MODE_REPORT_SET = 1, + /** Mode is reset (disabled) */ + GHOSTTY_MODE_REPORT_RESET = 2, + /** Mode is permanently set */ + GHOSTTY_MODE_REPORT_PERMANENTLY_SET = 3, + /** Mode is permanently reset */ + GHOSTTY_MODE_REPORT_PERMANENTLY_RESET = 4, +} GhosttyModeReportState; + +/** + * Encode a DECRPM (DEC Private Mode Report) response sequence. + * + * Writes a mode report escape sequence into the provided buffer. + * The generated sequence has the form: + * - DEC private mode: CSI ? Ps1 ; Ps2 $ y + * - ANSI mode: CSI Ps1 ; Ps2 $ y + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param mode The mode identifying the mode to report on + * @param state The report state for this mode + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_mode_report_encode( + GhosttyMode mode, + GhosttyModeReportState state, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_MODES_H */ diff --git a/include/ghostty/vt/mouse.h b/include/ghostty/vt/mouse.h new file mode 100644 index 00000000000..4ba5f52e38b --- /dev/null +++ b/include/ghostty/vt/mouse.h @@ -0,0 +1,70 @@ +/** + * @file mouse.h + * + * Mouse encoding module - encode mouse events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_MOUSE_H +#define GHOSTTY_VT_MOUSE_H + +/** @defgroup mouse Mouse Encoding + * + * Utilities for encoding mouse events into terminal escape sequences, + * supporting X10, UTF-8, SGR, URxvt, and SGR-Pixels mouse protocols. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_mouse_encoder_new(). + * 2. Configure encoder options with ghostty_mouse_encoder_setopt() or + * ghostty_mouse_encoder_setopt_from_terminal(). + * 3. For each mouse event: + * - Create a mouse event with ghostty_mouse_event_new(). + * - Set event properties (action, button, modifiers, position). + * - Encode with ghostty_mouse_encoder_encode(). + * - Free the event with ghostty_mouse_event_free() or reuse it. + * 4. Free the encoder with ghostty_mouse_encoder_free() when done. + * + * For a complete working example, see example/c-vt-encode-mouse in the + * repository. + * + * ## Example + * + * @snippet c-vt-encode-mouse/src/main.c mouse-encode + * + * ## Example: Encoding with Terminal State + * + * When you have a GhosttyTerminal, you can sync its tracking mode and + * output format into the encoder automatically: + * + * @code{.c} + * // Create a terminal and feed it some VT data that enables mouse tracking + * GhosttyTerminal terminal; + * ghostty_terminal_new(NULL, &terminal, + * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * + * // Application might write data that enables mouse reporting, etc. + * ghostty_terminal_vt_write(terminal, vt_data, vt_len); + * + * // Create an encoder and sync its options from the terminal + * GhosttyMouseEncoder encoder; + * ghostty_mouse_encoder_new(NULL, &encoder); + * ghostty_mouse_encoder_setopt_from_terminal(encoder, terminal); + * + * // Encode a mouse event using the terminal-derived options + * char buf[128]; + * size_t written = 0; + * ghostty_mouse_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * ghostty_mouse_encoder_free(encoder); + * ghostty_terminal_free(terminal); + * @endcode + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_MOUSE_H */ diff --git a/include/ghostty/vt/mouse/encoder.h b/include/ghostty/vt/mouse/encoder.h new file mode 100644 index 00000000000..744b9930354 --- /dev/null +++ b/include/ghostty/vt/mouse/encoder.h @@ -0,0 +1,211 @@ +/** + * @file encoder.h + * + * Mouse event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_MOUSE_ENCODER_H +#define GHOSTTY_VT_MOUSE_ENCODER_H + +#include +#include +#include +#include +#include +#include +#include + +/** + * Opaque handle to a mouse encoder instance. + * + * This handle represents a mouse encoder that converts normalized + * mouse events into terminal escape sequences. + * + * @ingroup mouse + */ +typedef struct GhosttyMouseEncoderImpl *GhosttyMouseEncoder; + +/** + * Mouse tracking mode. + * + * @ingroup mouse + */ +typedef enum { + /** Mouse reporting disabled. */ + GHOSTTY_MOUSE_TRACKING_NONE = 0, + + /** X10 mouse mode. */ + GHOSTTY_MOUSE_TRACKING_X10 = 1, + + /** Normal mouse mode (button press/release only). */ + GHOSTTY_MOUSE_TRACKING_NORMAL = 2, + + /** Button-event tracking mode. */ + GHOSTTY_MOUSE_TRACKING_BUTTON = 3, + + /** Any-event tracking mode. */ + GHOSTTY_MOUSE_TRACKING_ANY = 4, +} GhosttyMouseTrackingMode; + +/** + * Mouse output format. + * + * @ingroup mouse + */ +typedef enum { + GHOSTTY_MOUSE_FORMAT_X10 = 0, + GHOSTTY_MOUSE_FORMAT_UTF8 = 1, + GHOSTTY_MOUSE_FORMAT_SGR = 2, + GHOSTTY_MOUSE_FORMAT_URXVT = 3, + GHOSTTY_MOUSE_FORMAT_SGR_PIXELS = 4, +} GhosttyMouseFormat; + +/** + * Mouse encoder size and geometry context. + * + * This describes the rendered terminal geometry used to convert + * surface-space positions into encoded coordinates. + * + * @ingroup mouse + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyMouseEncoderSize). */ + size_t size; + + /** Full screen width in pixels. */ + uint32_t screen_width; + + /** Full screen height in pixels. */ + uint32_t screen_height; + + /** Cell width in pixels. Must be non-zero. */ + uint32_t cell_width; + + /** Cell height in pixels. Must be non-zero. */ + uint32_t cell_height; + + /** Top padding in pixels. */ + uint32_t padding_top; + + /** Bottom padding in pixels. */ + uint32_t padding_bottom; + + /** Right padding in pixels. */ + uint32_t padding_right; + + /** Left padding in pixels. */ + uint32_t padding_left; +} GhosttyMouseEncoderSize; + +/** + * Mouse encoder option identifiers. + * + * These values are used with ghostty_mouse_encoder_setopt() to configure + * the behavior of the mouse encoder. + * + * @ingroup mouse + */ +typedef enum { + /** Mouse tracking mode (value: GhosttyMouseTrackingMode). */ + GHOSTTY_MOUSE_ENCODER_OPT_EVENT = 0, + + /** Mouse output format (value: GhosttyMouseFormat). */ + GHOSTTY_MOUSE_ENCODER_OPT_FORMAT = 1, + + /** Renderer size context (value: GhosttyMouseEncoderSize). */ + GHOSTTY_MOUSE_ENCODER_OPT_SIZE = 2, + + /** Whether any mouse button is currently pressed (value: bool). */ + GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED = 3, + + /** Whether to enable motion deduplication by last cell (value: bool). */ + GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL = 4, +} GhosttyMouseEncoderOption; + +/** + * Create a new mouse encoder instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyResult ghostty_mouse_encoder_new(const GhosttyAllocator *allocator, + GhosttyMouseEncoder *encoder); + +/** + * Free a mouse encoder instance. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_free(GhosttyMouseEncoder encoder); + +/** + * Set an option on the mouse encoder. + * + * A null pointer value does nothing. It does not reset to defaults. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to option value (type depends on option) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_setopt(GhosttyMouseEncoder encoder, + GhosttyMouseEncoderOption option, + const void *value); + +/** + * Set encoder options from a terminal's current state. + * + * This sets tracking mode and output format from terminal state. + * It does not modify size or any-button state. + * + * @param encoder The encoder handle, must not be NULL + * @param terminal The terminal handle, must not be NULL + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_setopt_from_terminal(GhosttyMouseEncoder encoder, + GhosttyTerminal terminal); + +/** + * Reset internal encoder state. + * + * This clears motion deduplication state (last tracked cell). + * + * @param encoder The encoder handle (may be NULL) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_reset(GhosttyMouseEncoder encoder); + +/** + * Encode a mouse event into a terminal escape sequence. + * + * Not all mouse events produce output. In such cases this returns + * GHOSTTY_SUCCESS with out_len set to 0. + * + * If the output buffer is too small, this returns GHOSTTY_OUT_OF_SPACE + * and out_len contains the required size. + * + * @param encoder The encoder handle, must not be NULL + * @param event The mouse event to encode, must not be NULL + * @param out_buf Buffer to write encoded bytes to, or NULL to query required size + * @param out_buf_size Size of out_buf in bytes + * @param out_len Pointer to store bytes written (or required bytes on failure) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer is too small, + * or another error code + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyResult ghostty_mouse_encoder_encode(GhosttyMouseEncoder encoder, + GhosttyMouseEvent event, + char *out_buf, + size_t out_buf_size, + size_t *out_len); + +#endif /* GHOSTTY_VT_MOUSE_ENCODER_H */ diff --git a/include/ghostty/vt/mouse/event.h b/include/ghostty/vt/mouse/event.h new file mode 100644 index 00000000000..5b72735fcaa --- /dev/null +++ b/include/ghostty/vt/mouse/event.h @@ -0,0 +1,193 @@ +/** + * @file event.h + * + * Mouse event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_MOUSE_EVENT_H +#define GHOSTTY_VT_MOUSE_EVENT_H + +#include +#include +#include +#include + +/** + * Opaque handle to a mouse event. + * + * This handle represents a normalized mouse input event containing + * action, button, modifiers, and surface-space position. + * + * @ingroup mouse + */ +typedef struct GhosttyMouseEventImpl *GhosttyMouseEvent; + +/** + * Mouse event action type. + * + * @ingroup mouse + */ +typedef enum { + /** Mouse button was pressed. */ + GHOSTTY_MOUSE_ACTION_PRESS = 0, + + /** Mouse button was released. */ + GHOSTTY_MOUSE_ACTION_RELEASE = 1, + + /** Mouse moved. */ + GHOSTTY_MOUSE_ACTION_MOTION = 2, +} GhosttyMouseAction; + +/** + * Mouse button identity. + * + * @ingroup mouse + */ +typedef enum { + GHOSTTY_MOUSE_BUTTON_UNKNOWN = 0, + GHOSTTY_MOUSE_BUTTON_LEFT = 1, + GHOSTTY_MOUSE_BUTTON_RIGHT = 2, + GHOSTTY_MOUSE_BUTTON_MIDDLE = 3, + GHOSTTY_MOUSE_BUTTON_FOUR = 4, + GHOSTTY_MOUSE_BUTTON_FIVE = 5, + GHOSTTY_MOUSE_BUTTON_SIX = 6, + GHOSTTY_MOUSE_BUTTON_SEVEN = 7, + GHOSTTY_MOUSE_BUTTON_EIGHT = 8, + GHOSTTY_MOUSE_BUTTON_NINE = 9, + GHOSTTY_MOUSE_BUTTON_TEN = 10, + GHOSTTY_MOUSE_BUTTON_ELEVEN = 11, +} GhosttyMouseButton; + +/** + * Mouse position in surface-space pixels. + * + * @ingroup mouse + */ +typedef struct { + float x; + float y; +} GhosttyMousePosition; + +/** + * Create a new mouse event instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param event Pointer to store the created event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyResult ghostty_mouse_event_new(const GhosttyAllocator *allocator, + GhosttyMouseEvent *event); + +/** + * Free a mouse event instance. + * + * @param event The mouse event handle to free (may be NULL) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_free(GhosttyMouseEvent event); + +/** + * Set the event action. + * + * @param event The event handle, must not be NULL + * @param action The action to set + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_action(GhosttyMouseEvent event, + GhosttyMouseAction action); + +/** + * Get the event action. + * + * @param event The event handle, must not be NULL + * @return The event action + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyMouseAction ghostty_mouse_event_get_action(GhosttyMouseEvent event); + +/** + * Set the event button. + * + * This sets a concrete button identity for the event. + * To represent "no button" (for motion events), use + * ghostty_mouse_event_clear_button(). + * + * @param event The event handle, must not be NULL + * @param button The button to set + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_button(GhosttyMouseEvent event, + GhosttyMouseButton button); + +/** + * Clear the event button. + * + * This sets the event button to "none". + * + * @param event The event handle, must not be NULL + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_clear_button(GhosttyMouseEvent event); + +/** + * Get the event button. + * + * @param event The event handle, must not be NULL + * @param out_button Output pointer for the button value (may be NULL) + * @return true if a button is set, false if no button is set + * + * @ingroup mouse + */ +GHOSTTY_API bool ghostty_mouse_event_get_button(GhosttyMouseEvent event, + GhosttyMouseButton *out_button); + +/** + * Set keyboard modifiers held during the event. + * + * @param event The event handle, must not be NULL + * @param mods Modifier bitmask + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_mods(GhosttyMouseEvent event, + GhosttyMods mods); + +/** + * Get keyboard modifiers held during the event. + * + * @param event The event handle, must not be NULL + * @return Modifier bitmask + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyMods ghostty_mouse_event_get_mods(GhosttyMouseEvent event); + +/** + * Set the event position in surface-space pixels. + * + * @param event The event handle, must not be NULL + * @param position The position to set + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_position(GhosttyMouseEvent event, + GhosttyMousePosition position); + +/** + * Get the event position in surface-space pixels. + * + * @param event The event handle, must not be NULL + * @return The current event position + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyMousePosition ghostty_mouse_event_get_position(GhosttyMouseEvent event); + +#endif /* GHOSTTY_VT_MOUSE_EVENT_H */ diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index f53077ab326..c86498090a6 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include /** @@ -21,7 +21,7 @@ * * @ingroup osc */ -typedef struct GhosttyOscParser *GhosttyOscParser; +typedef struct GhosttyOscParserImpl *GhosttyOscParser; /** * Opaque handle to a single OSC command. @@ -31,7 +31,7 @@ typedef struct GhosttyOscParser *GhosttyOscParser; * * @ingroup osc */ -typedef struct GhosttyOscCommand *GhosttyOscCommand; +typedef struct GhosttyOscCommandImpl *GhosttyOscCommand; /** @defgroup osc OSC Parser * @@ -123,7 +123,7 @@ typedef enum { * * @ingroup osc */ -GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); +GHOSTTY_API GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); /** * Free an OSC parser instance. @@ -135,7 +135,7 @@ GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParse * * @ingroup osc */ -void ghostty_osc_free(GhosttyOscParser parser); +GHOSTTY_API void ghostty_osc_free(GhosttyOscParser parser); /** * Reset an OSC parser instance to its initial state. @@ -148,7 +148,7 @@ void ghostty_osc_free(GhosttyOscParser parser); * * @ingroup osc */ -void ghostty_osc_reset(GhosttyOscParser parser); +GHOSTTY_API void ghostty_osc_reset(GhosttyOscParser parser); /** * Parse the next byte in an OSC sequence. @@ -165,7 +165,7 @@ void ghostty_osc_reset(GhosttyOscParser parser); * * @ingroup osc */ -void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); +GHOSTTY_API void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); /** * Finalize OSC parsing and retrieve the parsed command. @@ -195,7 +195,7 @@ void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); * * @ingroup osc */ -GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); +GHOSTTY_API GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); /** * Get the type of an OSC command. @@ -209,7 +209,7 @@ GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); * * @ingroup osc */ -GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); +GHOSTTY_API GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); /** * Extract data from an OSC command. @@ -226,7 +226,7 @@ GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); * * @ingroup osc */ -bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); +GHOSTTY_API bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); /** @} */ diff --git a/include/ghostty/vt/paste.h b/include/ghostty/vt/paste.h index d90f303d43e..b3df5be4e0d 100644 --- a/include/ghostty/vt/paste.h +++ b/include/ghostty/vt/paste.h @@ -9,41 +9,32 @@ /** @defgroup paste Paste Utilities * - * Utilities for validating paste data safety. + * Utilities for validating and encoding paste data for terminal input. * * ## Basic Usage * * Use ghostty_paste_is_safe() to check if paste data contains potentially * dangerous sequences before sending it to the terminal. * - * ## Example - * - * @code{.c} - * #include - * #include - * #include - * - * int main() { - * const char* safe_data = "hello world"; - * const char* unsafe_data = "rm -rf /\n"; - * - * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { - * printf("Safe to paste\n"); - * } - * - * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { - * printf("Unsafe! Contains newline\n"); - * } - * - * return 0; - * } - * @endcode + * Use ghostty_paste_encode() to encode paste data for writing to the pty, + * including bracketed paste wrapping and unsafe byte stripping. + * + * ## Examples + * + * ### Safety Check + * + * @snippet c-vt-paste/src/main.c paste-safety + * + * ### Encoding + * + * @snippet c-vt-paste/src/main.c paste-encode * * @{ */ #include #include +#include #ifdef __cplusplus extern "C" { @@ -64,7 +55,42 @@ extern "C" { * @param len The length of the data in bytes * @return true if the data is safe to paste, false otherwise */ -bool ghostty_paste_is_safe(const char* data, size_t len); +GHOSTTY_API bool ghostty_paste_is_safe(const char* data, size_t len); + +/** + * Encode paste data for writing to the terminal pty. + * + * This function prepares paste data for terminal input by: + * - Stripping unsafe control bytes (NUL, ESC, DEL, etc.) by replacing + * them with spaces + * - Wrapping the data in bracketed paste sequences if @p bracketed is true + * - Replacing newlines with carriage returns if @p bracketed is false + * + * The input @p data buffer is modified in place during encoding. The + * encoded result (potentially with bracketed paste prefix/suffix) is + * written to the output buffer. + * + * If the output buffer is too small, the function returns + * GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written. + * The caller can then retry with a sufficiently sized buffer. + * + * @param data The paste data to encode (modified in place, may be NULL) + * @param data_len The length of the input data in bytes + * @param bracketed Whether bracketed paste mode is active + * @param buf Output buffer to write the encoded result into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_paste_encode( + char* data, + size_t data_len, + bool bracketed, + char* buf, + size_t buf_len, + size_t* out_written); #ifdef __cplusplus } diff --git a/include/ghostty/vt/point.h b/include/ghostty/vt/point.h new file mode 100644 index 00000000000..f152a5c46f6 --- /dev/null +++ b/include/ghostty/vt/point.h @@ -0,0 +1,88 @@ +/** + * @file point.h + * + * Terminal point types for referencing locations in the terminal grid. + */ + +#ifndef GHOSTTY_VT_POINT_H +#define GHOSTTY_VT_POINT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup point Point + * + * Types for referencing x/y positions in the terminal grid under + * different coordinate systems (active area, viewport, full screen, + * scrollback history). + * + * @{ + */ + +/** + * A coordinate in the terminal grid. + * + * @ingroup point + */ +typedef struct { + /** Column (0-indexed). */ + uint16_t x; + + /** Row (0-indexed). May exceed page size for screen/history tags. */ + uint32_t y; +} GhosttyPointCoordinate; + +/** + * Point reference tag. + * + * Determines which coordinate system a point uses. + * + * @ingroup point + */ +typedef enum { + /** Active area where the cursor can move. */ + GHOSTTY_POINT_TAG_ACTIVE = 0, + + /** Visible viewport (changes when scrolled). */ + GHOSTTY_POINT_TAG_VIEWPORT = 1, + + /** Full screen including scrollback. */ + GHOSTTY_POINT_TAG_SCREEN = 2, + + /** Scrollback history only (before active area). */ + GHOSTTY_POINT_TAG_HISTORY = 3, +} GhosttyPointTag; + +/** + * Point value union. + * + * @ingroup point + */ +typedef union { + /** Coordinate (used for all tag variants). */ + GhosttyPointCoordinate coordinate; + + /** Padding for ABI compatibility. Do not use. */ + uint64_t _padding[2]; +} GhosttyPointValue; + +/** + * Tagged union for a point in the terminal grid. + * + * @ingroup point + */ +typedef struct { + GhosttyPointTag tag; + GhosttyPointValue value; +} GhosttyPoint; + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_POINT_H */ diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h new file mode 100644 index 00000000000..163a4e1d4dc --- /dev/null +++ b/include/ghostty/vt/render.h @@ -0,0 +1,603 @@ +/** + * @file render.h + * + * Render state for creating high performance renderers. + */ + +#ifndef GHOSTTY_VT_RENDER_H +#define GHOSTTY_VT_RENDER_H + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup render Render State + * + * Represents the state required to render a visible screen (a viewport) + * of a terminal instance. This is stateful and optimized for repeated + * updates from a single terminal instance and only updating dirty regions + * of the screen. + * + * The key design principle of this API is that it only needs read/write + * access to the terminal instance during the update call. This allows + * the render state to minimally impact terminal IO performance and also + * allows the renderer to be safely multi-threaded (as long as a lock is + * held during the update call to ensure exclusive access to the terminal + * instance). + * + * The basic usage of this API is: + * + * 1. Create an empty render state + * 2. Update it from a terminal instance whenever you need. + * 3. Read from the render state to get the data needed to draw your frame. + * + * ## Dirty Tracking + * + * Dirty tracking is a key feature of the render state that allows renderers + * to efficiently determine what parts of the screen have changed and only + * redraw changed regions. + * + * The render state API keeps track of dirty state at two independent layers: + * a global dirty state that indicates whether the entire frame is clean, + * partially dirty, or fully dirty, and a per-row dirty state that allows + * tracking which rows in a partially dirty frame have changed. + * + * The user of the render state API is expected to unset both of these. + * The `update` call does not unset dirty state, it only updates it. + * + * An extremely important detail: setting one dirty state doesn't unset + * the other. For example, setting the global dirty state to false does not + * reset the row-level dirty flags. So, the caller of the render state API must + * be careful to manage both layers of dirty state correctly. + * + * ## Examples + * + * ### Creating and updating render state + * @snippet c-vt-render/src/main.c render-state-update + * + * ### Checking dirty state + * @snippet c-vt-render/src/main.c render-dirty-check + * + * ### Reading colors + * @snippet c-vt-render/src/main.c render-colors + * + * ### Reading cursor state + * @snippet c-vt-render/src/main.c render-cursor + * + * ### Iterating rows and cells + * @snippet c-vt-render/src/main.c render-row-iterate + * + * ### Resetting dirty state after rendering + * @snippet c-vt-render/src/main.c render-dirty-reset + * + * @{ + */ + +/** + * Opaque handle to a render state instance. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateImpl* GhosttyRenderState; + +/** + * Opaque handle to a render-state row iterator. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator; + +/** + * Opaque handle to render-state row cells. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells; + +/** + * Dirty state of a render state after update. + * + * @ingroup render + */ +typedef enum { + /** Not dirty at all; rendering can be skipped. */ + GHOSTTY_RENDER_STATE_DIRTY_FALSE = 0, + + /** Some rows changed; renderer can redraw incrementally. */ + GHOSTTY_RENDER_STATE_DIRTY_PARTIAL = 1, + + /** Global state changed; renderer should redraw everything. */ + GHOSTTY_RENDER_STATE_DIRTY_FULL = 2, +} GhosttyRenderStateDirty; + +/** + * Visual style of the cursor. + * + * @ingroup render + */ +typedef enum { + /** Bar cursor (DECSCUSR 5, 6). */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR = 0, + + /** Block cursor (DECSCUSR 1, 2). */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK = 1, + + /** Underline cursor (DECSCUSR 3, 4). */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE = 2, + + /** Hollow block cursor. */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW = 3, +} GhosttyRenderStateCursorVisualStyle; + +/** + * Queryable data kinds for ghostty_render_state_get(). + * + * @ingroup render + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_DATA_INVALID = 0, + + /** Viewport width in cells (uint16_t). */ + GHOSTTY_RENDER_STATE_DATA_COLS = 1, + + /** Viewport height in cells (uint16_t). */ + GHOSTTY_RENDER_STATE_DATA_ROWS = 2, + + /** Current dirty state (GhosttyRenderStateDirty). */ + GHOSTTY_RENDER_STATE_DATA_DIRTY = 3, + + /** Populate a pre-allocated GhosttyRenderStateRowIterator with row data + * from the render state (GhosttyRenderStateRowIterator). Row data is + * only valid as long as the underlying render state is not updated. + * It is unsafe to use row data after updating the render state. + * */ + GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR = 4, + + /** Default/current background color (GhosttyColorRgb). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND = 5, + + /** Default/current foreground color (GhosttyColorRgb). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND = 6, + + /** Cursor color when explicitly set by terminal state (GhosttyColorRgb). + * Returns GHOSTTY_INVALID_VALUE if no explicit cursor color is set; + * use COLOR_CURSOR_HAS_VALUE to check first. */ + GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR = 7, + + /** Whether an explicit cursor color is set (bool). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE = 8, + + /** The active 256-color palette (GhosttyColorRgb[256]). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE = 9, + + /** The visual style of the cursor (GhosttyRenderStateCursorVisualStyle). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE = 10, + + /** Whether the cursor is visible based on terminal modes (bool). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE = 11, + + /** Whether the cursor should blink based on terminal modes (bool). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING = 12, + + /** Whether the cursor is at a password input field (bool). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT = 13, + + /** Whether the cursor is visible within the viewport (bool). + * If false, the cursor viewport position values are undefined. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE = 14, + + /** Cursor viewport x position in cells (uint16_t). + * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X = 15, + + /** Cursor viewport y position in cells (uint16_t). + * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y = 16, + + /** Whether the cursor is on the tail of a wide character (bool). + * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17, +} GhosttyRenderStateData; + +/** + * Settable options for ghostty_render_state_set(). + * + * @ingroup render + */ +typedef enum { + /** Set dirty state (GhosttyRenderStateDirty). */ + GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0, +} GhosttyRenderStateOption; + +/** + * Queryable data kinds for ghostty_render_state_row_get(). + * + * @ingroup render + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_ROW_DATA_INVALID = 0, + + /** Whether the current row is dirty (bool). */ + GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY = 1, + + /** The raw row value (GhosttyRow). */ + GHOSTTY_RENDER_STATE_ROW_DATA_RAW = 2, + + /** Populate a pre-allocated GhosttyRenderStateRowCells with cell data for + * the current row (GhosttyRenderStateRowCells). Cell data is only + * valid as long as the underlying render state is not updated. + * It is unsafe to use cell data after updating the render state. */ + GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3, +} GhosttyRenderStateRowData; + +/** + * Settable options for ghostty_render_state_row_set(). + * + * @ingroup render + */ +typedef enum { + /** Set dirty state for the current row (bool). */ + GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY = 0, +} GhosttyRenderStateRowOption; + +/** + * Render-state color information. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling + * ghostty_render_state_colors_get(). + * + * Example: + * @code + * GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); + * GhosttyResult result = ghostty_render_state_colors_get(state, &colors); + * @endcode + * + * @ingroup render + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateColors). */ + size_t size; + + /** The default/current background color for the render state. */ + GhosttyColorRgb background; + + /** The default/current foreground color for the render state. */ + GhosttyColorRgb foreground; + + /** The cursor color when explicitly set by terminal state. */ + GhosttyColorRgb cursor; + + /** + * True when cursor contains a valid explicit cursor color value. + * If this is false, the cursor color should be ignored; it will + * contain undefined data. + * */ + bool cursor_has_value; + + /** The active 256-color palette for this render state. */ + GhosttyColorRgb palette[256]; +} GhosttyRenderStateColors; + +/** + * Create a new render state instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param state Pointer to store the created render state handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_new(const GhosttyAllocator* allocator, + GhosttyRenderState* state); + +/** + * Free a render state instance. + * + * Releases all resources associated with the render state. After this call, + * the render state handle becomes invalid. + * + * @param state The render state handle to free (may be NULL) + * + * @ingroup render + */ +GHOSTTY_API void ghostty_render_state_free(GhosttyRenderState state); + +/** + * Update a render state instance from a terminal. + * + * This consumes terminal/screen dirty state in the same way as the internal + * render state update path. + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param terminal The terminal handle to read from (NULL returns GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `terminal` is NULL, GHOSTTY_OUT_OF_MEMORY if updating the state requires + * allocation and that allocation fails + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_update(GhosttyRenderState state, + GhosttyTerminal terminal); + +/** + * Get a value from a render state. + * + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateData). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is + * NULL or `data` is not a recognized enum value + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_get(GhosttyRenderState state, + GhosttyRenderStateData data, + void* out); + +/** + * Set an option on a render state. + * + * The `value` pointer must point to a value of the type corresponding to the + * requested option kind (see GhosttyRenderStateOption). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param option The option to set + * @param[in] value Pointer to the value to set (NULL returns + * GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `value` is NULL + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_set(GhosttyRenderState state, + GhosttyRenderStateOption option, + const void* value); + +/** + * Get the current color information from a render state. + * + * This writes as many fields as fit in the caller-provided sized struct. + * `out_colors->size` must be set by the caller (typically via + * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors)). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param[out] out_colors Sized output struct to receive render-state colors + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `out_colors` is NULL, or if `out_colors->size` is smaller than + * `sizeof(size_t)` + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state, + GhosttyRenderStateColors* out_colors); + +/** + * Create a new row iterator instance. + * + * All fields except the allocator are left undefined until populated + * via ghostty_render_state_get() with + * GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_iterator On success, receives the created iterator handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_iterator_new( + const GhosttyAllocator* allocator, + GhosttyRenderStateRowIterator* out_iterator); + +/** + * Free a render-state row iterator. + * + * @param iterator The iterator handle to free (may be NULL) + * + * @ingroup render + */ +GHOSTTY_API void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator); + +/** + * Move a render-state row iterator to the next row. + * + * Returns true if the iterator moved successfully and row data is + * available to read at the new position. + * + * @param iterator The iterator handle to advance (may be NULL) + * @return true if advanced to the next row, false if `iterator` is + * NULL or if the iterator has reached the end + * + * @ingroup render + */ +GHOSTTY_API bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator); + +/** + * Get a value from the current row in a render-state row iterator. + * + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateRowData). + * Call ghostty_render_state_row_iterator_next() at least once before + * calling this function. + * + * @param iterator The iterator handle to query (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if + * `iterator` is NULL or the iterator is not positioned on a row + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_get( + GhosttyRenderStateRowIterator iterator, + GhosttyRenderStateRowData data, + void* out); + +/** + * Set an option on the current row in a render-state row iterator. + * + * The `value` pointer must point to a value of the type corresponding to the + * requested option kind (see GhosttyRenderStateRowOption). + * Call ghostty_render_state_row_iterator_next() at least once before + * calling this function. + * + * @param iterator The iterator handle to update (NULL returns GHOSTTY_INVALID_VALUE) + * @param option The option to set + * @param[in] value Pointer to the value to set (NULL returns + * GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if + * `iterator` is NULL or the iterator is not positioned on a row + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_set( + GhosttyRenderStateRowIterator iterator, + GhosttyRenderStateRowOption option, + const void* value); + +/** + * Create a new row cells instance. + * + * All fields except the allocator are left undefined until populated + * via ghostty_render_state_row_get() with + * GHOSTTY_RENDER_STATE_ROW_DATA_CELLS. + * + * You can reuse this value repeatedly with ghostty_render_state_row_get() to + * avoid allocating a new cells container for every row. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_cells On success, receives the created row cells handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_new( + const GhosttyAllocator* allocator, + GhosttyRenderStateRowCells* out_cells); + +/** + * Queryable data kinds for ghostty_render_state_row_cells_get(). + * + * @ingroup render + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID = 0, + + /** The raw cell value (GhosttyCell). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW = 1, + + /** The style for the current cell (GhosttyStyle). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE = 2, + + /** The total number of grapheme codepoints including the base codepoint + * (uint32_t). Returns 0 if the cell has no text. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3, + + /** Write grapheme codepoints into a caller-provided buffer (uint32_t*). + * The buffer must be at least graphemes_len elements. The base codepoint + * is written first, followed by any extra codepoints. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4, + + /** The resolved background color of the cell (GhosttyColorRgb). + * Flattens the three possible sources: content-tag bg_color_rgb, + * content-tag bg_color_palette (looked up in the palette), or the + * style's bg_color. Returns GHOSTTY_INVALID_VALUE if the cell has + * no background color, in which case the caller should use whatever + * default background color it wants (e.g. the terminal background). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR = 5, + + /** The resolved foreground color of the cell (GhosttyColorRgb). + * Resolves palette indices through the palette. Bold color handling + * is not applied; the caller should handle bold styling separately. + * Returns GHOSTTY_INVALID_VALUE if the cell has no explicit foreground + * color, in which case the caller should use whatever default foreground + * color it wants (e.g. the terminal foreground). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR = 6, +} GhosttyRenderStateRowCellsData; + +/** + * Move a render-state row cells iterator to the next cell. + * + * Returns true if the iterator moved successfully and cell data is + * available to read at the new position. + * + * @param cells The row cells handle to advance (may be NULL) + * @return true if advanced to the next cell, false if `cells` is + * NULL or if the iterator has reached the end + * + * @ingroup render + */ +GHOSTTY_API bool ghostty_render_state_row_cells_next(GhosttyRenderStateRowCells cells); + +/** + * Move a render-state row cells iterator to a specific column. + * + * Positions the iterator at the given x (column) index so that + * subsequent reads return data for that cell. + * + * @param cells The row cells handle to reposition (NULL returns + * GHOSTTY_INVALID_VALUE) + * @param x The zero-based column index to select + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `cells` + * is NULL or `x` is out of range + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_select( + GhosttyRenderStateRowCells cells, uint16_t x); + +/** + * Get a value from the current cell in a render-state row cells iterator. + * + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateRowCellsData). + * Call ghostty_render_state_row_cells_next() or + * ghostty_render_state_row_cells_select() at least once before + * calling this function. + * + * @param cells The row cells handle to query (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if + * `cells` is NULL or the iterator is not positioned on a cell + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_get( + GhosttyRenderStateRowCells cells, + GhosttyRenderStateRowCellsData data, + void* out); + +/** + * Free a row cells instance. + * + * @param cells The row cells handle to free (may be NULL) + * + * @ingroup render + */ +GHOSTTY_API void ghostty_render_state_row_cells_free(GhosttyRenderStateRowCells cells); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_RENDER_H */ 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/screen.h b/include/ghostty/vt/screen.h new file mode 100644 index 00000000000..89b4825fe6b --- /dev/null +++ b/include/ghostty/vt/screen.h @@ -0,0 +1,340 @@ +/** + * @file screen.h + * + * Terminal screen cell and row types. + */ + +#ifndef GHOSTTY_VT_SCREEN_H +#define GHOSTTY_VT_SCREEN_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup screen Screen + * + * Terminal screen cell and row types. + * + * These types represent the contents of a terminal screen. A GhosttyCell + * is a single grid cell and a GhosttyRow is a single row. Both are opaque + * values whose fields are accessed via ghostty_cell_get() and + * ghostty_row_get() respectively. + * + * @{ + */ + +/** + * Opaque cell value. + * + * Represents a single terminal cell. The internal layout is opaque and + * must be queried via ghostty_cell_get(). Obtain cell values from + * terminal query APIs. + * + * @ingroup screen + */ +typedef uint64_t GhosttyCell; + +/** + * Opaque row value. + * + * Represents a single terminal row. The internal layout is opaque and + * must be queried via ghostty_row_get(). Obtain row values from + * terminal query APIs. + * + * @ingroup screen + */ +typedef uint64_t GhosttyRow; + +/** + * Cell content tag. + * + * Describes what kind of content a cell holds. + * + * @ingroup screen + */ +typedef enum { + /** A single codepoint (may be zero for empty). */ + GHOSTTY_CELL_CONTENT_CODEPOINT = 0, + + /** A codepoint that is part of a multi-codepoint grapheme cluster. */ + GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME = 1, + + /** No text; background color from palette. */ + GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE = 2, + + /** No text; background color as RGB. */ + GHOSTTY_CELL_CONTENT_BG_COLOR_RGB = 3, +} GhosttyCellContentTag; + +/** + * Cell wide property. + * + * Describes the width behavior of a cell. + * + * @ingroup screen + */ +typedef enum { + /** Not a wide character, cell width 1. */ + GHOSTTY_CELL_WIDE_NARROW = 0, + + /** Wide character, cell width 2. */ + GHOSTTY_CELL_WIDE_WIDE = 1, + + /** Spacer after wide character. Do not render. */ + GHOSTTY_CELL_WIDE_SPACER_TAIL = 2, + + /** Spacer at end of soft-wrapped line for a wide character. */ + GHOSTTY_CELL_WIDE_SPACER_HEAD = 3, +} GhosttyCellWide; + +/** + * Semantic content type of a cell. + * + * Set by semantic prompt sequences (OSC 133) to distinguish between + * command output, user input, and shell prompt text. + * + * @ingroup screen + */ +typedef enum { + /** Regular output content, such as command output. */ + GHOSTTY_CELL_SEMANTIC_OUTPUT = 0, + + /** Content that is part of user input. */ + GHOSTTY_CELL_SEMANTIC_INPUT = 1, + + /** Content that is part of a shell prompt. */ + GHOSTTY_CELL_SEMANTIC_PROMPT = 2, +} GhosttyCellSemanticContent; + +/** + * Cell data types. + * + * These values specify what type of data to extract from a cell + * using `ghostty_cell_get`. + * + * @ingroup screen + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_CELL_DATA_INVALID = 0, + + /** + * The codepoint of the cell (0 if empty or bg-color-only). + * + * Output type: uint32_t * + */ + GHOSTTY_CELL_DATA_CODEPOINT = 1, + + /** + * The content tag describing what kind of content is in the cell. + * + * Output type: GhosttyCellContentTag * + */ + GHOSTTY_CELL_DATA_CONTENT_TAG = 2, + + /** + * The wide property of the cell. + * + * Output type: GhosttyCellWide * + */ + GHOSTTY_CELL_DATA_WIDE = 3, + + /** + * Whether the cell has text to render. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_HAS_TEXT = 4, + + /** + * Whether the cell has non-default styling. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_HAS_STYLING = 5, + + /** + * The style ID for the cell (for use with style lookups). + * + * Output type: uint16_t * + */ + GHOSTTY_CELL_DATA_STYLE_ID = 6, + + /** + * Whether the cell has a hyperlink. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_HAS_HYPERLINK = 7, + + /** + * Whether the cell is protected. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_PROTECTED = 8, + + /** + * The semantic content type of the cell (from OSC 133). + * + * Output type: GhosttyCellSemanticContent * + */ + GHOSTTY_CELL_DATA_SEMANTIC_CONTENT = 9, + + /** + * The palette index for the cell's background color. + * Only valid when content_tag is GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE. + * + * Output type: GhosttyColorPaletteIndex * + */ + GHOSTTY_CELL_DATA_COLOR_PALETTE = 10, + + /** + * The RGB value for the cell's background color. + * Only valid when content_tag is GHOSTTY_CELL_CONTENT_BG_COLOR_RGB. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_CELL_DATA_COLOR_RGB = 11, +} GhosttyCellData; + +/** + * Row semantic prompt state. + * + * Indicates whether any cells in a row are part of a shell prompt, + * as reported by OSC 133 sequences. + * + * @ingroup screen + */ +typedef enum { + /** No prompt cells in this row. */ + GHOSTTY_ROW_SEMANTIC_NONE = 0, + + /** Prompt cells exist and this is a primary prompt line. */ + GHOSTTY_ROW_SEMANTIC_PROMPT = 1, + + /** Prompt cells exist and this is a continuation line. */ + GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION = 2, +} GhosttyRowSemanticPrompt; + +/** + * Row data types. + * + * These values specify what type of data to extract from a row + * using `ghostty_row_get`. + * + * @ingroup screen + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_ROW_DATA_INVALID = 0, + + /** + * Whether this row is soft-wrapped. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_WRAP = 1, + + /** + * Whether this row is a continuation of a soft-wrapped row. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_WRAP_CONTINUATION = 2, + + /** + * Whether any cells in this row have grapheme clusters. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_GRAPHEME = 3, + + /** + * Whether any cells in this row have styling (may have false positives). + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_STYLED = 4, + + /** + * Whether any cells in this row have hyperlinks (may have false positives). + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_HYPERLINK = 5, + + /** + * The semantic prompt state of this row. + * + * Output type: GhosttyRowSemanticPrompt * + */ + GHOSTTY_ROW_DATA_SEMANTIC_PROMPT = 6, + + /** + * Whether this row contains a Kitty virtual placeholder. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER = 7, + + /** + * Whether this row is dirty and requires a redraw. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_DIRTY = 8, +} GhosttyRowData; + +/** + * Get data from a cell. + * + * Extracts typed data from the given cell based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid data types and output types are documented + * in the `GhosttyCellData` enum. + * + * @param cell The cell value + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * data type is invalid + * + * @ingroup screen + */ +GHOSTTY_API GhosttyResult ghostty_cell_get(GhosttyCell cell, + GhosttyCellData data, + void *out); + +/** + * Get data from a row. + * + * Extracts typed data from the given row based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid data types and output types are documented + * in the `GhosttyRowData` enum. + * + * @param row The row value + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * data type is invalid + * + * @ingroup screen + */ +GHOSTTY_API GhosttyResult ghostty_row_get(GhosttyRow row, + GhosttyRowData data, + void *out); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_SCREEN_H */ diff --git a/include/ghostty/vt/sgr.h b/include/ghostty/vt/sgr.h index 0c1afc309bd..01ea3a359e5 100644 --- a/include/ghostty/vt/sgr.h +++ b/include/ghostty/vt/sgr.h @@ -31,49 +31,14 @@ * * ## Example * - * @code{.c} - * #include - * #include - * #include - * - * int main() { - * // Create parser - * GhosttySgrParser parser; - * GhosttyResult result = ghostty_sgr_new(NULL, &parser); - * assert(result == GHOSTTY_SUCCESS); - * - * // Parse "bold, red foreground" sequence: ESC[1;31m - * uint16_t params[] = {1, 31}; - * result = ghostty_sgr_set_params(parser, params, NULL, 2); - * assert(result == GHOSTTY_SUCCESS); - * - * // Iterate through attributes - * GhosttySgrAttribute attr; - * while (ghostty_sgr_next(parser, &attr)) { - * switch (attr.tag) { - * case GHOSTTY_SGR_ATTR_BOLD: - * printf("Bold enabled\n"); - * break; - * case GHOSTTY_SGR_ATTR_FG_8: - * printf("Foreground color: %d\n", attr.value.fg_8); - * break; - * default: - * break; - * } - * } - * - * // Cleanup - * ghostty_sgr_free(parser); - * return 0; - * } - * @endcode + * @snippet c-vt-sgr/src/main.c sgr-basic * * @{ */ #include #include -#include +#include #include #include #include @@ -90,7 +55,7 @@ extern "C" { * * @ingroup sgr */ -typedef struct GhosttySgrParser* GhosttySgrParser; +typedef struct GhosttySgrParserImpl* GhosttySgrParser; /** * SGR attribute tags. @@ -109,30 +74,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; /** @@ -220,7 +184,7 @@ typedef struct { * * @ingroup sgr */ -GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, +GHOSTTY_API GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, GhosttySgrParser* parser); /** @@ -234,7 +198,7 @@ GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, * * @ingroup sgr */ -void ghostty_sgr_free(GhosttySgrParser parser); +GHOSTTY_API void ghostty_sgr_free(GhosttySgrParser parser); /** * Reset an SGR parser instance to the beginning of the parameter list. @@ -247,7 +211,7 @@ void ghostty_sgr_free(GhosttySgrParser parser); * * @ingroup sgr */ -void ghostty_sgr_reset(GhosttySgrParser parser); +GHOSTTY_API void ghostty_sgr_reset(GhosttySgrParser parser); /** * Set SGR parameters for parsing. @@ -279,7 +243,7 @@ void ghostty_sgr_reset(GhosttySgrParser parser); * * @ingroup sgr */ -GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, +GHOSTTY_API GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, const uint16_t* params, const char* separators, size_t len); @@ -297,7 +261,7 @@ GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, * * @ingroup sgr */ -bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); +GHOSTTY_API bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); /** * Get the full parameter list from an unknown SGR attribute. @@ -312,7 +276,7 @@ bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); * * @ingroup sgr */ -size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, +GHOSTTY_API size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, const uint16_t** ptr); /** @@ -328,7 +292,7 @@ size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, * * @ingroup sgr */ -size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, +GHOSTTY_API size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, const uint16_t** ptr); /** @@ -343,7 +307,7 @@ size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, * * @ingroup sgr */ -GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); +GHOSTTY_API GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); /** * Get the value from an SGR attribute. @@ -357,7 +321,7 @@ GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); * * @ingroup sgr */ -GhosttySgrAttributeValue* ghostty_sgr_attribute_value( +GHOSTTY_API GhosttySgrAttributeValue* ghostty_sgr_attribute_value( GhosttySgrAttribute* attr); #ifdef __wasm__ @@ -371,7 +335,7 @@ GhosttySgrAttributeValue* ghostty_sgr_attribute_value( * * @ingroup wasm */ -GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); +GHOSTTY_API GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); /** * Free memory for an SGR attribute (WebAssembly only). @@ -382,7 +346,7 @@ GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); * * @ingroup wasm */ -void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr); +GHOSTTY_API void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr); #endif #ifdef __cplusplus diff --git a/include/ghostty/vt/size_report.h b/include/ghostty/vt/size_report.h new file mode 100644 index 00000000000..98c67c5ed9a --- /dev/null +++ b/include/ghostty/vt/size_report.h @@ -0,0 +1,100 @@ +/** + * @file size_report.h + * + * Size report encoding - encode terminal size reports into escape sequences. + */ + +#ifndef GHOSTTY_VT_SIZE_REPORT_H +#define GHOSTTY_VT_SIZE_REPORT_H + +/** @defgroup size_report Size Report Encoding + * + * Utilities for encoding terminal size reports into escape sequences, + * supporting in-band size reports (mode 2048) and XTWINOPS responses + * (CSI 14 t, CSI 16 t, CSI 18 t). + * + * ## Basic Usage + * + * Use ghostty_size_report_encode() to encode a size report into a + * caller-provided buffer. If the buffer is too small, the function + * returns GHOSTTY_OUT_OF_SPACE and sets the required size in the + * output parameter. + * + * ## Example + * + * @snippet c-vt-size-report/src/main.c size-report-encode + * + * @{ + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Size report style. + * + * Determines the output format for the terminal size report. + */ +typedef enum { + /** In-band size report (mode 2048): ESC [ 48 ; rows ; cols ; height ; width t */ + GHOSTTY_SIZE_REPORT_MODE_2048 = 0, + /** XTWINOPS text area size in pixels: ESC [ 4 ; height ; width t */ + GHOSTTY_SIZE_REPORT_CSI_14_T = 1, + /** XTWINOPS cell size in pixels: ESC [ 6 ; height ; width t */ + GHOSTTY_SIZE_REPORT_CSI_16_T = 2, + /** XTWINOPS text area size in characters: ESC [ 8 ; rows ; cols t */ + GHOSTTY_SIZE_REPORT_CSI_18_T = 3, +} GhosttySizeReportStyle; + +/** + * Terminal size information for encoding size reports. + */ +typedef struct { + /** Terminal row count in cells. */ + uint16_t rows; + /** Terminal column count in cells. */ + uint16_t columns; + /** Width of a single terminal cell in pixels. */ + uint32_t cell_width; + /** Height of a single terminal cell in pixels. */ + uint32_t cell_height; +} GhosttySizeReportSize; + +/** + * Encode a terminal size report into an escape sequence. + * + * Encodes a size report in the format specified by @p style into the + * provided buffer. + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param style The size report format to encode + * @param size Terminal size information + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_size_report_encode( + GhosttySizeReportStyle style, + GhosttySizeReportSize size, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SIZE_REPORT_H */ diff --git a/include/ghostty/vt/style.h b/include/ghostty/vt/style.h new file mode 100644 index 00000000000..ac049560079 --- /dev/null +++ b/include/ghostty/vt/style.h @@ -0,0 +1,138 @@ +/** + * @file style.h + * + * Terminal cell style types. + */ + +#ifndef GHOSTTY_VT_STYLE_H +#define GHOSTTY_VT_STYLE_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup style Style + * + * Terminal cell style attributes. + * + * A style describes the visual attributes of a terminal cell, including + * foreground, background, and underline colors, as well as flags for + * bold, italic, underline, and other text decorations. + * + * @{ + */ + +/** + * Style identifier type. + * + * Used to look up the full style from a grid reference. + * Obtain this from a cell via GHOSTTY_CELL_DATA_STYLE_ID. + * + * @ingroup style + */ +typedef uint16_t GhosttyStyleId; + +/** + * Style color tags. + * + * These values identify the type of color in a style color. + * Use the tag to determine which field in the color value union to access. + * + * @ingroup style + */ +typedef enum { + GHOSTTY_STYLE_COLOR_NONE = 0, + GHOSTTY_STYLE_COLOR_PALETTE = 1, + GHOSTTY_STYLE_COLOR_RGB = 2, +} GhosttyStyleColorTag; + +/** + * Style color value union. + * + * Use the tag to determine which field is active. + * + * @ingroup style + */ +typedef union { + GhosttyColorPaletteIndex palette; + GhosttyColorRgb rgb; + uint64_t _padding; +} GhosttyStyleColorValue; + +/** + * Style color (tagged union). + * + * A color used in a style attribute. Can be unset (none), a palette + * index, or a direct RGB value. + * + * @ingroup style + */ +typedef struct { + GhosttyStyleColorTag tag; + GhosttyStyleColorValue value; +} GhosttyStyleColor; + +/** + * Terminal cell style. + * + * Describes the complete visual style for a terminal cell, including + * foreground, background, and underline colors, as well as text + * decoration flags. The underline field uses the same values as + * GhosttySgrUnderline. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * + * @ingroup style + */ +typedef struct { + size_t size; + GhosttyStyleColor fg_color; + GhosttyStyleColor bg_color; + GhosttyStyleColor underline_color; + bool bold; + bool italic; + bool faint; + bool blink; + bool inverse; + bool invisible; + bool strikethrough; + bool overline; + int underline; /**< One of GHOSTTY_SGR_UNDERLINE_* values */ +} GhosttyStyle; + +/** + * Get the default style. + * + * Initializes the style to the default values (no colors, no flags). + * + * @param style Pointer to the style to initialize + * + * @ingroup style + */ +GHOSTTY_API void ghostty_style_default(GhosttyStyle* style); + +/** + * Check if a style is the default style. + * + * Returns true if all colors are unset and all flags are off. + * + * @param style Pointer to the style to check + * @return true if the style is the default style + * + * @ingroup style + */ +GHOSTTY_API bool ghostty_style_is_default(const GhosttyStyle* style); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_STYLE_H */ diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h new file mode 100644 index 00000000000..b43e37cf43a --- /dev/null +++ b/include/ghostty/vt/terminal.h @@ -0,0 +1,983 @@ +/** + * @file terminal.h + * + * Complete terminal emulator state and rendering. + */ + +#ifndef GHOSTTY_VT_TERMINAL_H +#define GHOSTTY_VT_TERMINAL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#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(). + * + * ### Example: VT stream processing + * @snippet c-vt-stream/src/main.c vt-stream-init + * @snippet c-vt-stream/src/main.c vt-stream-write + * + * ## Effects + * + * By default, the terminal sequence processing with ghostty_terminal_vt_write() + * only process sequences that directly affect terminal state and + * ignores sequences that have side effect behavior or require responses. + * These sequences include things like bell characters, title changes, device + * attributes queries, and more. To handle these sequences, the embedder + * must configure "effects." + * + * Effects are callbacks that the terminal invokes in response to VT + * sequences processed during ghostty_terminal_vt_write(). They let the + * embedding application react to terminal-initiated events such as bell + * characters, title changes, device status report responses, and more. + * + * Each effect is registered with ghostty_terminal_set() using the + * corresponding `GhosttyTerminalOption` identifier. A `NULL` value + * pointer clears the callback and disables the effect. + * + * A userdata pointer can be attached via `GHOSTTY_TERMINAL_OPT_USERDATA` + * and is passed to every callback, allowing callers to route events + * back to their own application state without global variables. + * You cannot specify different userdata for different callbacks. + * + * All callbacks are invoked synchronously during + * ghostty_terminal_vt_write(). Callbacks **must not** call + * ghostty_terminal_vt_write() on the same terminal (no reentrancy). + * And callbacks must be very careful to not block for too long or perform + * expensive operations, since they are blocking further IO processing. + * + * The available effects are: + * + * | Option | Callback Type | Trigger | + * |-----------------------------------------|-----------------------------------|-------------------------------------------| + * | `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | Query responses written back to the pty | + * | `GHOSTTY_TERMINAL_OPT_BELL` | `GhosttyTerminalBellFn` | BEL character (0x07) | + * | `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` | `GhosttyTerminalTitleChangedFn` | Title change via OSC 0 / OSC 2 | + * | `GHOSTTY_TERMINAL_OPT_ENQUIRY` | `GhosttyTerminalEnquiryFn` | ENQ character (0x05) | + * | `GHOSTTY_TERMINAL_OPT_XTVERSION` | `GhosttyTerminalXtversionFn` | XTVERSION query (CSI > q) | + * | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS size query (CSI 14/16/18 t) | + * | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) | + * | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)| + * + * ### Defining a write_pty callback + * @snippet c-vt-effects/src/main.c effects-write-pty + * + * ### Defining a bell callback + * @snippet c-vt-effects/src/main.c effects-bell + * + * ### Defining a title_changed callback + * @snippet c-vt-effects/src/main.c effects-title-changed + * + * ### Registering effects and processing VT data + * @snippet c-vt-effects/src/main.c effects-register + * + * ## Color Theme + * + * The terminal maintains a set of colors used for rendering: a foreground + * color, a background color, a cursor color, and a 256-color palette. Each + * of these has two layers: a **default** value set by the embedder, and an + * **override** value that programs running in the terminal can set via OSC + * escape sequences (e.g. OSC 10/11/12 for foreground/background/cursor, + * OSC 4 for individual palette entries). + * + * ### Default Colors + * + * Use ghostty_terminal_set() with the color options to configure the + * default colors. These represent the theme or configuration chosen by + * the embedder. Passing `NULL` clears the default, leaving the color + * unset. + * + * | Option | Input Type | Description | + * |-----------------------------------------|-------------------------|--------------------------------------| + * | `GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Default foreground color | + * | `GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Default background color | + * | `GHOSTTY_TERMINAL_OPT_COLOR_CURSOR` | `GhosttyColorRgb*` | Default cursor color | + * | `GHOSTTY_TERMINAL_OPT_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Default 256-color palette | + * + * For the palette, passing `NULL` resets to the built-in default palette. + * The palette set operation preserves any per-index OSC overrides that + * programs have applied; only unmodified indices are updated. + * + * ### Reading colors + * + * Use ghostty_terminal_get() to read colors. There are two variants for + * each color: the **effective** value (which returns the OSC override if + * one is active, otherwise the default) and the **default** value (which + * ignores any OSC overrides). + * + * | Data | Output Type | Description | + * |---------------------------------------------------|-------------------------|------------------------------------------------| + * | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Effective foreground (override or default) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Effective background (override or default) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR` | `GhosttyColorRgb*` | Effective cursor (override or default) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Current palette (with any OSC overrides) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT` | `GhosttyColorRgb*` | Default foreground only (ignores OSC override) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT` | `GhosttyColorRgb*` | Default background only (ignores OSC override) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT` | `GhosttyColorRgb*` | Default cursor only (ignores OSC override) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT` | `GhosttyColorRgb[256]*` | Default palette only (ignores OSC overrides) | + * + * For foreground, background, and cursor colors, the getters return + * `GHOSTTY_NO_VALUE` if no color is configured (neither a default nor an + * OSC override). The palette getters always succeed since the palette + * always has a value (the built-in default if nothing else is set). + * + * ### Setting a color theme + * @snippet c-vt-colors/src/main.c colors-set-defaults + * + * ### Reading effective and default colors + * @snippet c-vt-colors/src/main.c colors-read + * + * ### Full example with OSC overrides + * @snippet c-vt-colors/src/main.c colors-main + * + * @{ + */ + +/** + * Opaque handle to a terminal instance. + * + * @ingroup terminal + */ +typedef struct GhosttyTerminalImpl* 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; + +/** + * Terminal screen identifier. + * + * Identifies which screen buffer is active in the terminal. + * + * @ingroup terminal + */ +typedef enum { + /** The primary (normal) screen. */ + GHOSTTY_TERMINAL_SCREEN_PRIMARY = 0, + + /** The alternate screen. */ + GHOSTTY_TERMINAL_SCREEN_ALTERNATE = 1, +} GhosttyTerminalScreen; + +/** + * Scrollbar state for the terminal viewport. + * + * Represents the scrollable area dimensions needed to render a scrollbar. + * + * @ingroup terminal + */ +typedef struct { + /** Total size of the scrollable area in rows. */ + uint64_t total; + + /** Offset into the total area that the viewport is at. */ + uint64_t offset; + + /** Length of the visible area in rows. */ + uint64_t len; +} GhosttyTerminalScrollbar; + +/** + * Callback function type for bell. + * + * Called when the terminal receives a BEL character (0x07). + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalBellFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for color scheme queries (CSI ? 996 n). + * + * Called when the terminal receives a color scheme device status report + * query. Return true and fill *out_scheme with the current color scheme, + * or return false to silently ignore the query. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param[out] out_scheme Pointer to store the current color scheme + * @return true if the color scheme was filled, false to ignore the query + * + * @ingroup terminal + */ +typedef bool (*GhosttyTerminalColorSchemeFn)(GhosttyTerminal terminal, + void* userdata, + GhosttyColorScheme* out_scheme); + +/** + * Callback function type for device attributes queries (DA1/DA2/DA3). + * + * Called when the terminal receives a device attributes query (CSI c, + * CSI > c, or CSI = c). Return true and fill *out_attrs with the + * response data, or return false to silently ignore the query. + * + * The terminal uses whichever sub-struct (primary, secondary, tertiary) + * matches the request type, but all three should be filled for simplicity. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param[out] out_attrs Pointer to store the device attributes response + * @return true if attributes were filled, false to ignore the query + * + * @ingroup terminal + */ +typedef bool (*GhosttyTerminalDeviceAttributesFn)(GhosttyTerminal terminal, + void* userdata, + GhosttyDeviceAttributes* out_attrs); + +/** + * Callback function type for enquiry (ENQ, 0x05). + * + * Called when the terminal receives an ENQ character. Return the + * response bytes as a GhosttyString. The memory must remain valid + * until the callback returns. Return a zero-length string to send + * no response. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @return The response bytes to write back to the pty + * + * @ingroup terminal + */ +typedef GhosttyString (*GhosttyTerminalEnquiryFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for size queries (XTWINOPS). + * + * Called in response to XTWINOPS size queries (CSI 14/16/18 t). + * Return true and fill *out_size with the current terminal geometry, + * or return false to silently ignore the query. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param[out] out_size Pointer to store the terminal size information + * @return true if size was filled, false to ignore the query + * + * @ingroup terminal + */ +typedef bool (*GhosttyTerminalSizeFn)(GhosttyTerminal terminal, + void* userdata, + GhosttySizeReportSize* out_size); + +/** + * Callback function type for title_changed. + * + * Called when the terminal title changes via escape sequences + * (e.g. OSC 0 or OSC 2). The new title can be queried from the + * terminal after the callback returns. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalTitleChangedFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for write_pty. + * + * Called when the terminal needs to write data back to the pty, for + * example in response to a device status report or mode query. The + * data is only valid for the duration of the call; callers must copy + * it if it needs to persist. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param data Pointer to the response bytes + * @param len Length of the response in bytes + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalWritePtyFn)(GhosttyTerminal terminal, + void* userdata, + const uint8_t* data, + size_t len); + +/** + * Callback function type for XTVERSION. + * + * Called when the terminal receives an XTVERSION query (CSI > q). + * Return the version string (e.g. "myterm 1.0") as a GhosttyString. + * The memory must remain valid until the callback returns. Return a + * zero-length string to report the default "libghostty" version. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @return The version string to report + * + * @ingroup terminal + */ +typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Terminal option identifiers. + * + * These values are used with ghostty_terminal_set() to configure + * terminal callbacks and associated state. + * + * @ingroup terminal + */ +typedef enum { + /** + * Opaque userdata pointer passed to all callbacks. + * + * Input type: void* + */ + GHOSTTY_TERMINAL_OPT_USERDATA = 0, + + /** + * Callback invoked when the terminal needs to write data back + * to the pty (e.g. in response to a DECRQM query or device + * status report). Set to NULL to ignore such sequences. + * + * Input type: GhosttyTerminalWritePtyFn + */ + GHOSTTY_TERMINAL_OPT_WRITE_PTY = 1, + + /** + * Callback invoked when the terminal receives a BEL character + * (0x07). Set to NULL to ignore bell events. + * + * Input type: GhosttyTerminalBellFn + */ + GHOSTTY_TERMINAL_OPT_BELL = 2, + + /** + * Callback invoked when the terminal receives an ENQ character + * (0x05). Set to NULL to send no response. + * + * Input type: GhosttyTerminalEnquiryFn + */ + GHOSTTY_TERMINAL_OPT_ENQUIRY = 3, + + /** + * Callback invoked when the terminal receives an XTVERSION query + * (CSI > q). Set to NULL to report the default "libghostty" string. + * + * Input type: GhosttyTerminalXtversionFn + */ + GHOSTTY_TERMINAL_OPT_XTVERSION = 4, + + /** + * Callback invoked when the terminal title changes via escape + * sequences (e.g. OSC 0 or OSC 2). Set to NULL to ignore title + * change events. + * + * Input type: GhosttyTerminalTitleChangedFn + */ + GHOSTTY_TERMINAL_OPT_TITLE_CHANGED = 5, + + /** + * Callback invoked in response to XTWINOPS size queries + * (CSI 14/16/18 t). Set to NULL to silently ignore size queries. + * + * Input type: GhosttyTerminalSizeFn + */ + GHOSTTY_TERMINAL_OPT_SIZE = 6, + + /** + * Callback invoked in response to a color scheme device status + * report query (CSI ? 996 n). Return true and fill the out pointer + * to report the current scheme, or return false to silently ignore. + * Set to NULL to ignore color scheme queries. + * + * Input type: GhosttyTerminalColorSchemeFn + */ + GHOSTTY_TERMINAL_OPT_COLOR_SCHEME = 7, + + /** + * Callback invoked in response to a device attributes query + * (CSI c, CSI > c, or CSI = c). Return true and fill the out + * pointer with response data, or return false to silently ignore. + * Set to NULL to ignore device attributes queries. + * + * Input type: GhosttyTerminalDeviceAttributesFn + */ + GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES = 8, + + /** + * Set the terminal title manually. + * + * The string data is copied into the terminal. A NULL value pointer + * clears the title (equivalent to setting an empty string). + * + * Input type: GhosttyString* + */ + GHOSTTY_TERMINAL_OPT_TITLE = 9, + + /** + * Set the terminal working directory manually. + * + * The string data is copied into the terminal. A NULL value pointer + * clears the pwd (equivalent to setting an empty string). + * + * Input type: GhosttyString* + */ + GHOSTTY_TERMINAL_OPT_PWD = 10, + + /** + * Set the default foreground color. + * + * A NULL value pointer clears the default (unset). + * + * Input type: GhosttyColorRgb* + */ + GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND = 11, + + /** + * Set the default background color. + * + * A NULL value pointer clears the default (unset). + * + * Input type: GhosttyColorRgb* + */ + GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND = 12, + + /** + * Set the default cursor color. + * + * A NULL value pointer clears the default (unset). + * + * Input type: GhosttyColorRgb* + */ + GHOSTTY_TERMINAL_OPT_COLOR_CURSOR = 13, + + /** + * Set the default 256-color palette. + * + * The value must point to an array of exactly 256 GhosttyColorRgb values. + * A NULL value pointer resets to the built-in default palette. + * + * Input type: GhosttyColorRgb[256]* + */ + GHOSTTY_TERMINAL_OPT_COLOR_PALETTE = 14, +} GhosttyTerminalOption; + +/** + * Terminal data types. + * + * These values specify what type of data to extract from a terminal + * using `ghostty_terminal_get`. + * + * @ingroup terminal + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_TERMINAL_DATA_INVALID = 0, + + /** + * Terminal width in cells. + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_COLS = 1, + + /** + * Terminal height in cells. + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_ROWS = 2, + + /** + * Cursor column position (0-indexed). + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_X = 3, + + /** + * Cursor row position within the active area (0-indexed). + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_Y = 4, + + /** + * Whether the cursor has a pending wrap (next print will soft-wrap). + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP = 5, + + /** + * The currently active screen. + * + * Output type: GhosttyTerminalScreen * + */ + GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN = 6, + + /** + * Whether the cursor is visible (DEC mode 25). + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE = 7, + + /** + * Current Kitty keyboard protocol flags. + * + * Output type: GhosttyKittyKeyFlags * (uint8_t *) + */ + GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS = 8, + + /** + * Scrollbar state for the terminal viewport. + * + * This may be expensive to calculate depending on where the viewport + * is (arbitrary pins are expensive). The caller should take care to only + * call this as needed and not too frequently. + * + * Output type: GhosttyTerminalScrollbar * + */ + GHOSTTY_TERMINAL_DATA_SCROLLBAR = 9, + + /** + * The current SGR style of the cursor. + * + * This is the style that will be applied to newly printed characters. + * + * Output type: GhosttyStyle * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_STYLE = 10, + + /** + * Whether any mouse tracking mode is active. + * + * Returns true if any of the mouse tracking modes (X10, normal, button, + * or any-event) are enabled. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING = 11, + + /** + * The terminal title as set by escape sequences (e.g. OSC 0/2). + * + * Returns a borrowed string. The pointer is valid until the next call + * to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty + * string (len=0) is returned when no title has been set. + * + * Output type: GhosttyString * + */ + GHOSTTY_TERMINAL_DATA_TITLE = 12, + + /** + * The terminal's current working directory as set by escape sequences + * (e.g. OSC 7). + * + * Returns a borrowed string. The pointer is valid until the next call + * to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty + * string (len=0) is returned when no pwd has been set. + * + * Output type: GhosttyString * + */ + GHOSTTY_TERMINAL_DATA_PWD = 13, + + /** + * The total number of rows in the active screen including scrollback. + * + * Output type: size_t * + */ + GHOSTTY_TERMINAL_DATA_TOTAL_ROWS = 14, + + /** + * The number of scrollback rows (total rows minus viewport rows). + * + * Output type: size_t * + */ + GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS = 15, + + /** + * The total width of the terminal in pixels. + * + * This is cols * cell_width_px as set by ghostty_terminal_resize(). + * + * Output type: uint32_t * + */ + GHOSTTY_TERMINAL_DATA_WIDTH_PX = 16, + + /** + * The total height of the terminal in pixels. + * + * This is rows * cell_height_px as set by ghostty_terminal_resize(). + * + * Output type: uint32_t * + */ + GHOSTTY_TERMINAL_DATA_HEIGHT_PX = 17, + + /** + * The effective foreground color (override or default). + * + * Returns GHOSTTY_NO_VALUE if no foreground color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND = 18, + + /** + * The effective background color (override or default). + * + * Returns GHOSTTY_NO_VALUE if no background color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND = 19, + + /** + * The effective cursor color (override or default). + * + * Returns GHOSTTY_NO_VALUE if no cursor color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_CURSOR = 20, + + /** + * The current 256-color palette. + * + * Output type: GhosttyColorRgb[256] * + */ + GHOSTTY_TERMINAL_DATA_COLOR_PALETTE = 21, + + /** + * The default foreground color (ignoring any OSC override). + * + * Returns GHOSTTY_NO_VALUE if no default foreground color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT = 22, + + /** + * The default background color (ignoring any OSC override). + * + * Returns GHOSTTY_NO_VALUE if no default background color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT = 23, + + /** + * The default cursor color (ignoring any OSC override). + * + * Returns GHOSTTY_NO_VALUE if no default cursor color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT = 24, + + /** + * The default 256-color palette (ignoring any OSC overrides). + * + * Output type: GhosttyColorRgb[256] * + */ + GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT = 25, +} GhosttyTerminalData; + +/** + * 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 + */ +GHOSTTY_API 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 + */ +GHOSTTY_API 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 + */ +GHOSTTY_API 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. + * + * This also updates the terminal's pixel dimensions (used for image + * protocols and size reports), disables synchronized output mode (allowed + * by the spec so that resize results are shown immediately), and sends an + * in-band size report if mode 2048 is enabled. + * + * @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) + * @param cell_width_px Width of a single cell in pixels + * @param cell_height_px Height of a single cell in pixels + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal, + uint16_t cols, + uint16_t rows, + uint32_t cell_width_px, + uint32_t cell_height_px); + +/** + * Set an option on the terminal. + * + * Configures terminal callbacks and associated state such as the + * write_pty callback and userdata pointer. The value is passed + * directly for pointer types (callbacks, userdata) or as a pointer + * to the value for non-pointer types (e.g. GhosttyString*). + * NULL clears the option to its default. + * + * Callbacks are invoked synchronously during ghostty_terminal_vt_write(). + * Callbacks must not call ghostty_terminal_vt_write() on the same + * terminal (no reentrancy). + * + * @param terminal The terminal handle (may be NULL, in which case this is a no-op) + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option), + * or NULL to clear the option + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal, + GhosttyTerminalOption option, + const void* value); + +/** + * Write VT-encoded data to the terminal for processing. + * + * Feeds raw bytes through the terminal's VT stream parser, updating + * terminal state accordingly. By default, sequences that require output + * (queries, device status reports) are silently ignored. Use + * ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_WRITE_PTY to install + * a callback that receives response data. + * + * 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 + */ +GHOSTTY_API 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 + */ +GHOSTTY_API void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal, + GhosttyTerminalScrollViewport behavior); + +/** + * Get the current value of a terminal mode. + * + * Returns the value of the mode identified by the given mode. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param mode The mode identifying the mode to query + * @param[out] out_value On success, set to true if the mode is set, false + * if it is reset + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the mode does not correspond to a known mode + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal, + GhosttyMode mode, + bool* out_value); + +/** + * Set the value of a terminal mode. + * + * Sets the mode identified by the given mode to the specified value. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param mode The mode identifying the mode to set + * @param value true to set the mode, false to reset it + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the mode does not correspond to a known mode + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, + GhosttyMode mode, + bool value); + +/** + * Get data from a terminal instance. + * + * Extracts typed data from the given terminal based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid data types and output types are documented + * in the `GhosttyTerminalData` enum. + * + * @param terminal The terminal handle (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the data type is invalid + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal, + GhosttyTerminalData data, + void *out); + +/** + * Resolve a point in the terminal grid to a grid reference. + * + * Resolves the given point (which can be in active, viewport, screen, + * or history coordinates) to a grid reference for that location. Use + * ghostty_grid_ref_cell() and ghostty_grid_ref_row() to extract the cell + * and row. + * + * Lookups using the `active` and `viewport` tags are fast. The `screen` + * and `history` tags may require traversing the full scrollback page list + * to resolve the y coordinate, so they can be expensive for large + * scrollback buffers. + * + * This function isn't meant to be used as the core of render loop. It + * isn't built to sustain the framerates needed for rendering large screens. + * Use the render state API for that. This API is instead meant for less + * strictly performance-sensitive use cases. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param point The point specifying which cell to look up + * @param[out] out_ref On success, set to the grid reference at the given point (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the point is out of bounds + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal, + GhosttyPoint point, + GhosttyGridRef *out_ref); + +/** @} */ + +#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..8f0be7760f2 --- /dev/null +++ b/include/ghostty/vt/types.h @@ -0,0 +1,121 @@ +/** + * @file types.h + * + * Common types, macros, and utilities for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_TYPES_H +#define GHOSTTY_VT_TYPES_H + +#include +#include + +// Symbol visibility for shared library builds. On Windows, functions +// are exported from the DLL when building and imported when consuming. +// On other platforms with GCC/Clang, functions are marked with default +// visibility so they remain accessible when the library is built with +// -fvisibility=hidden. For static library builds, define GHOSTTY_STATIC +// before including this header to make this a no-op. +#ifndef GHOSTTY_API +#if defined(GHOSTTY_STATIC) + #define GHOSTTY_API +#elif defined(_WIN32) || defined(_WIN64) + #ifdef GHOSTTY_BUILD_SHARED + #define GHOSTTY_API __declspec(dllexport) + #else + #define GHOSTTY_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) && __GNUC__ >= 4 + #define GHOSTTY_API __attribute__((visibility("default"))) +#else + #define GHOSTTY_API +#endif +#endif + +/** + * 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, + /** The requested value has no value */ + GHOSTTY_NO_VALUE = -4, +} GhosttyResult; + +/** + * A borrowed byte string (pointer + length). + * + * The memory is not owned by this struct. The pointer is only valid + * for the lifetime documented by the API that produces or consumes it. + */ +typedef struct { + /** Pointer to the string bytes. */ + const uint8_t* ptr; + + /** Length of the string in bytes. */ + size_t len; +} GhosttyString; + +/** + * 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) }) + +/** + * Return a pointer to a null-terminated JSON string describing the + * layout of every C API struct for the current target. + * + * This is primarily useful for language bindings that can't easily + * set C struct fields and need to do so via byte offsets. For example, + * WebAssembly modules can't share struct definitions with the host. + * + * Example (abbreviated): + * @code{.json} + * { + * "GhosttyMouseEncoderSize": { + * "size": 40, + * "align": 8, + * "fields": { + * "size": { "offset": 0, "size": 8, "type": "u64" }, + * "screen_width": { "offset": 8, "size": 4, "type": "u32" }, + * "screen_height": { "offset": 12, "size": 4, "type": "u32" }, + * "cell_width": { "offset": 16, "size": 4, "type": "u32" }, + * "cell_height": { "offset": 20, "size": 4, "type": "u32" }, + * "padding_top": { "offset": 24, "size": 4, "type": "u32" }, + * "padding_bottom": { "offset": 28, "size": 4, "type": "u32" }, + * "padding_right": { "offset": 32, "size": 4, "type": "u32" }, + * "padding_left": { "offset": 36, "size": 4, "type": "u32" } + * } + * } + * } + * @endcode + * + * The returned pointer is valid for the lifetime of the process. + * + * @return Pointer to the null-terminated JSON string. + */ +GHOSTTY_API const char *ghostty_type_json(void); + +#endif /* GHOSTTY_VT_TYPES_H */ diff --git a/include/ghostty/vt/wasm.h b/include/ghostty/vt/wasm.h index 37a8263265d..e2b63e2c601 100644 --- a/include/ghostty/vt/wasm.h +++ b/include/ghostty/vt/wasm.h @@ -11,6 +11,7 @@ #include #include +#include /** @defgroup wasm WebAssembly Utilities * @@ -74,7 +75,7 @@ * @return Pointer to allocated opaque pointer, or NULL if allocation failed * @ingroup wasm */ -void** ghostty_wasm_alloc_opaque(void); +GHOSTTY_API void** ghostty_wasm_alloc_opaque(void); /** * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque(). @@ -82,7 +83,7 @@ void** ghostty_wasm_alloc_opaque(void); * @param ptr Pointer to free, or NULL (NULL is safely ignored) * @ingroup wasm */ -void ghostty_wasm_free_opaque(void **ptr); +GHOSTTY_API void ghostty_wasm_free_opaque(void **ptr); /** * Allocate an array of uint8_t values. @@ -91,7 +92,7 @@ void ghostty_wasm_free_opaque(void **ptr); * @return Pointer to allocated array, or NULL if allocation failed * @ingroup wasm */ -uint8_t* ghostty_wasm_alloc_u8_array(size_t len); +GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8_array(size_t len); /** * Free an array allocated by ghostty_wasm_alloc_u8_array(). @@ -100,7 +101,7 @@ uint8_t* ghostty_wasm_alloc_u8_array(size_t len); * @param len Length of the array (must match the length passed to alloc) * @ingroup wasm */ -void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); +GHOSTTY_API void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); /** * Allocate an array of uint16_t values. @@ -109,7 +110,7 @@ void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); * @return Pointer to allocated array, or NULL if allocation failed * @ingroup wasm */ -uint16_t* ghostty_wasm_alloc_u16_array(size_t len); +GHOSTTY_API uint16_t* ghostty_wasm_alloc_u16_array(size_t len); /** * Free an array allocated by ghostty_wasm_alloc_u16_array(). @@ -118,7 +119,7 @@ uint16_t* ghostty_wasm_alloc_u16_array(size_t len); * @param len Length of the array (must match the length passed to alloc) * @ingroup wasm */ -void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); +GHOSTTY_API void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); /** * Allocate a single uint8_t value. @@ -126,7 +127,7 @@ void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); * @return Pointer to allocated uint8_t, or NULL if allocation failed * @ingroup wasm */ -uint8_t* ghostty_wasm_alloc_u8(void); +GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8(void); /** * Free a uint8_t allocated by ghostty_wasm_alloc_u8(). @@ -134,7 +135,7 @@ uint8_t* ghostty_wasm_alloc_u8(void); * @param ptr Pointer to free, or NULL (NULL is safely ignored) * @ingroup wasm */ -void ghostty_wasm_free_u8(uint8_t *ptr); +GHOSTTY_API void ghostty_wasm_free_u8(uint8_t *ptr); /** * Allocate a single size_t value. @@ -142,7 +143,7 @@ void ghostty_wasm_free_u8(uint8_t *ptr); * @return Pointer to allocated size_t, or NULL if allocation failed * @ingroup wasm */ -size_t* ghostty_wasm_alloc_usize(void); +GHOSTTY_API size_t* ghostty_wasm_alloc_usize(void); /** * Free a size_t allocated by ghostty_wasm_alloc_usize(). @@ -150,7 +151,7 @@ size_t* ghostty_wasm_alloc_usize(void); * @param ptr Pointer to free, or NULL (NULL is safely ignored) * @ingroup wasm */ -void ghostty_wasm_free_usize(size_t *ptr); +GHOSTTY_API void ghostty_wasm_free_usize(size_t *ptr); /** @} */ 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..68d055dd56f 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, @@ -151,6 +210,7 @@ Ghostty/Ghostty.Error.swift, Ghostty/Ghostty.Event.swift, Ghostty/Ghostty.Input.swift, + Ghostty/Ghostty.MenuShortcutManager.swift, Ghostty/Ghostty.Surface.swift, "Ghostty/NSEvent+Extension.swift", "Ghostty/Surface View/InspectorView.swift", @@ -186,12 +246,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 +260,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 +269,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 +281,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8193244A2F24E6C000A9ED8F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F02E1F047A0046BD5C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -268,6 +337,7 @@ isa = PBXGroup; children = ( A571AB1C2A206FC600248498 /* Ghostty-Info.plist */, + 8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */, A5B30538299BEAAB0047F10C /* Assets.xcassets */, A553F4122E06EB1600257779 /* Ghostty.icon */, A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */, @@ -289,6 +359,7 @@ A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */, 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */, + 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */, ); name = Products; sourceTree = ""; @@ -328,6 +399,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 +445,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 +501,9 @@ CreatedOnToolsVersion = 26.1; TestTargetID = A5B30530299BEAAA0047F10C; }; + 8193244C2F24E6C000A9ED8F = { + CreatedOnToolsVersion = 26.2; + }; A54F45F22E1F047A0046BD5C = { CreatedOnToolsVersion = 26.0; TestTargetID = A5B30530299BEAAA0047F10C; @@ -439,6 +535,7 @@ targets = ( A5B30530299BEAAA0047F10C /* Ghostty */, A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, + 8193244C2F24E6C000A9ED8F /* DockTilePlugin */, A54F45F22E1F047A0046BD5C /* GhosttyTests */, 810ACC9E2E9D3301004F8F92 /* GhosttyUITests */, ); @@ -453,6 +550,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 +573,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 +596,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 +627,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 819324492F24E6C000A9ED8F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45EF2E1F047A0046BD5C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -527,6 +663,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 +800,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 +823,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 +846,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 +855,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 +1368,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/GhosttyCommandPaletteTests.swift b/macos/GhosttyUITests/GhosttyCommandPaletteTests.swift new file mode 100644 index 00000000000..428682b4f1c --- /dev/null +++ b/macos/GhosttyUITests/GhosttyCommandPaletteTests.swift @@ -0,0 +1,81 @@ +// +// GhosttyCommandPaletteTests.swift +// Ghostty +// +// Created by Lukas on 19.03.2026. +// + +import XCTest + +final class GhosttyCommandPaletteTests: GhosttyCustomConfigCase { + override static var runsForEachTargetApplicationUIConfiguration: Bool { false } + @MainActor func testDismissingCommandPalette() async throws { + let app = try ghosttyApplication() + app.activate() + + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5), "New window should appear") + + app.menuItems["Command Palette"].firstMatch.click() + + let clearScreenButton = app.buttons + .containing(NSPredicate(format: "label CONTAINS[c] 'Clear Screen'")) + .firstMatch + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + clearScreenButton.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: -30, dy: 0)) + .click() + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after clicking outside") + + app.typeKey("p", modifierFlags: [.command, .shift]) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + app.typeKey(.escape, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after typing escape") + + app.typeKey("p", modifierFlags: [.command, .shift]) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + app.typeKey(.enter, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after submitting query") + + app.typeKey("p", modifierFlags: [.command, .shift]) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + app.typeText("Clear Screen") + app.typeKey(.enter, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after selecting a command by keyboard") + + app.typeKey("p", modifierFlags: [.command, .shift]) + app.typeKey(.delete, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + clearScreenButton.click() + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after selecting a command by mouse") + } + + @MainActor func testSelectCommandWithMouse() async throws { + let app = try ghosttyApplication() + app.activate() + + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5), "New window should appear") + + app.menuItems["Command Palette"].firstMatch.click() + + app.buttons + .containing(NSPredicate(format: "label CONTAINS[c] 'Close All Windows'")) + .firstMatch.click() + + XCTAssertTrue(app.windows.firstMatch.waitForNonExistence(timeout: 2), "All windows should be closed") + } +} + 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/GhosttyMouseStateTests.swift b/macos/GhosttyUITests/GhosttyMouseStateTests.swift new file mode 100644 index 00000000000..b8f20261710 --- /dev/null +++ b/macos/GhosttyUITests/GhosttyMouseStateTests.swift @@ -0,0 +1,87 @@ +// +// GhosttyMouseStateTests.swift +// Ghostty +// +// Created by Lukas on 19.03.2026. +// + +import XCTest + +final class GhosttyMouseStateTests: GhosttyCustomConfigCase { + override static var runsForEachTargetApplicationUIConfiguration: Bool { false } + + // https://github.com/ghostty-org/ghostty/pull/11276 + @MainActor func testSelectionFocusChange() async throws { + let app = XCUIApplication() + app.activate() + // Write dummy text to a temp file, cat it into the terminal, then clean up + let lines = (1...200).map { "Line \($0): The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit." } + let text = lines.joined(separator: "\n") + "\n" + let tmpFile = NSTemporaryDirectory() + "ghostty_test_dummy.txt" + try text.write(toFile: tmpFile, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(atPath: tmpFile) } + + app.typeText("cat \(tmpFile)\r") + app.menuItems["Command Palette"].firstMatch.click() + + let finder = XCUIApplication(bundleIdentifier: "com.apple.finder") + finder.activate() + + app.activate() + + app.buttons + .containing(NSPredicate(format: "label CONTAINS[c] 'Clear Screen'")) + .firstMatch + .click() + let surface = app.groups["Terminal pane"] + surface + .coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: 20, dy: 10)) + .click() + + surface + .coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: 20, dy: surface.frame.height * 0.5)) + .hover() + + NSPasteboard.general.clearContents() + app.typeKey("c", modifierFlags: .command) + + XCTAssertEqual(NSPasteboard.general.string(forType: .string), nil, "Moving mouse shouldn't select any texts") + } + + @MainActor func testSearchFocusState() async throws { + let app = try ghosttyApplication() + app.activate() + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5), "New window should appear") + app.typeKey("f", modifierFlags: .command) + + let textfield = app.textFields.firstMatch + XCTAssertTrue(textfield.waitForExistence(timeout: 5), "Search field should appear") + app.typeText("a") + + XCTAssertTrue(textfield.stringValue == "a", "Search text should be `a`") + + textfield.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: textfield.frame.width * 0.5, dy: 0)) + .click() + + app.typeText("b") + + XCTAssertTrue(textfield.stringValue == "ab", "Search text should be `ab`") + + // resign + app.typeKey(.escape, modifierFlags: []) + + // dismiss + app.typeKey(.escape, modifierFlags: []) + + XCTAssertTrue(textfield.waitForNonExistence(timeout: 5), "Search field should disappear") + } +} + +private extension XCUIElement { + var stringValue: String? { + (value as? String) + } +} 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..645915ae10b 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,18 @@ 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? + + @MainActor private lazy var menuShortcutManager = Ghostty.MenuShortcutManager() override init() { #if DEBUG @@ -165,14 +167,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 +197,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 +206,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 +253,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 +295,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 +314,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 +343,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 +365,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 +392,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 +403,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 +412,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 +455,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 +477,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 +504,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 +522,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 +554,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 +619,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 +634,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 +650,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 +732,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 +749,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 +768,32 @@ 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) + DispatchQueue.main.async { + self.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 +804,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 +828,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 +837,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 - } - - // 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 + 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) } - // 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 +857,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 +876,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 +884,7 @@ class AppDelegate: NSObject, } } - //MARK: - UNUserNotificationCenterDelegate + // MARK: - UNUserNotificationCenterDelegate func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -1077,36 +905,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 +931,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 +947,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 +1079,147 @@ 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. + @MainActor private func syncMenuShortcuts(_ config: Ghostty.Config) { + guard ghostty.readiness == .ready else { return } + + menuShortcutManager.reset() + + 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() + } + + @MainActor private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) { + menuShortcutManager.syncMenuShortcut(config, action: action, menuItem: menuItem) + } + + @MainActor func performGhosttyBindingMenuKeyEquivalent(with event: NSEvent) -> Bool { + menuShortcutManager.performGhosttyBindingMenuKeyEquivalent(with: event) + } +} + // MARK: Floating Windows extension AppDelegate { @@ -1284,14 +1240,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 +1272,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 +1312,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..c4ba7106cd9 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, @@ -64,7 +64,6 @@ struct CommandPaletteView: View { @State private var query = "" @State private var selectedIndex: UInt? @State private var hoveredOptionID: UUID? - @FocusState private var isTextFieldFocused: Bool // The options that we should show, taking into account any filtering from // the query. Options with matching leadingColor are ranked higher. @@ -78,7 +77,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) @@ -105,8 +104,8 @@ struct CommandPaletteView: View { } VStack(alignment: .leading, spacing: 0) { - CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in - switch (event) { + CommandPaletteQuery(query: $query) { event in + switch event { case .exit: isPresented = false @@ -128,7 +127,7 @@ struct CommandPaletteView: View { ? 0 : current + 1 - case .move(_): + case .move: // Unknown, ignore break } @@ -178,42 +177,28 @@ struct CommandPaletteView: View { .padding() .environment(\.colorScheme, scheme) .onChange(of: isPresented) { newValue in - // Reset focus when quickly showing and hiding. - // macOS will destroy this view after a while, - // so task/onAppear will not be called again. - // If you toggle it rather quickly, we reset - // it here when dismissing. - isTextFieldFocused = newValue - if !isPresented { + if !newValue { // This is optional, since most of the time // there will be a delay before the next use. // To keep behavior the same as before, we reset it. query = "" } } - .task { - // Grab focus on the first appearance. - // This happens right after onAppear, - // so we don’t need to dispatch it again. - // Fixes: https://github.com/ghostty-org/ghostty/issues/8497 - // Also fixes initial focus while animating. - 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,21 +208,20 @@ 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) { + init(query: Binding, onEvent: ((KeyboardEvent) -> Void)? = nil) { _query = query self.onEvent = onEvent - _isTextFieldFocused = isTextFieldFocused } enum KeyboardEvent { @@ -280,11 +264,22 @@ fileprivate struct CommandPaletteQuery: View { .onExitCommand { onEvent?(.exit) } .onMoveCommand { onEvent?(.move($0)) } .onSubmit { onEvent?(.submit) } + .onAppear { + // Grab focus on the first appearance. + // Debug and Release build using Xcode 26.4, + // has same issue again + // Fixes: https://github.com/ghostty-org/ghostty/issues/8497 + // SearchOverlay works magically as expected, I don't know + // why it's different here, but dispatching to next loop fixes it + DispatchQueue.main.async { + isTextFieldFocused = true + } + } } } } -fileprivate struct CommandTable: View { +private struct CommandTable: View { var options: [CommandOption] @Binding var selectedIndex: UInt? @Binding var hoveredOptionID: UUID? @@ -332,7 +327,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 +341,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 +371,7 @@ fileprivate struct CommandRow: View { ) .foregroundStyle(Color.accentColor) } - + if let symbols = option.symbols { ShortcutSymbolsView(symbols: symbols) .foregroundStyle(.secondary) @@ -406,7 +401,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..5d9d5d52701 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 } } @@ -293,9 +301,8 @@ class BaseTerminalController: NSWindowController, // Our focus state requires that this window is key and our currently // focused surface is the surface in this view. let focused: Bool = (window?.isKeyWindow ?? false) && - !commandPaletteIsShowing && - focusedSurface != nil && - surfaceView == focusedSurface! + surfaceView == focusedSurface && + surfaceView.isFirstResponder surfaceView.focusDidChange(focused) } } @@ -424,7 +431,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 +440,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 +478,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 +495,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: oldView, from: target.focusedSurface) } } - + undoManager.registerUndo( withTarget: target, expiresAfter: target.undoExpiration @@ -531,14 +538,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 +603,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 +616,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 +635,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 +656,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 +684,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 +705,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 +723,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 +736,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 +753,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 +790,7 @@ class BaseTerminalController: NSWindowController, if NSApp.mainWindow == window { surfaces = surfaces.filter { $0 != focusedSurface } } - + for surface in surfaces { surface.flagsChanged(with: event) } @@ -817,10 +824,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 +841,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 +863,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 +902,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 +914,7 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during drop: \(error)") return } - + replaceSurfaceTree( newTree, moveFocusTo: source, @@ -916,7 +922,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 +935,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 +951,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 +972,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 +986,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 +1058,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 +1067,7 @@ class BaseTerminalController: NSWindowController, } else { updateOverlayIsVisible = defaultUpdateOverlayVisibility() } - + // Always resync our appearance syncAppearance() } @@ -1109,7 +1115,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 +1123,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 +1152,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 +1208,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 +1238,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 +1286,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 +1325,6 @@ class BaseTerminalController: NSWindowController, ghostty.splitToggleZoom(surface: surface) } - @IBAction func splitMoveFocusPrevious(_ sender: Any) { splitMoveFocus(direction: .previous) } @@ -1368,7 +1397,7 @@ class BaseTerminalController: NSWindowController, @IBAction func toggleCommandPalette(_ sender: Any?) { commandPaletteIsShowing.toggle() } - + @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } @@ -1384,11 +1413,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 +1459,7 @@ extension BaseTerminalController: NSMenuItemValidation { return true } } - + // MARK: - Surface Color Scheme /// Update the surface tree's color scheme only when it actually changes. @@ -1462,3 +1491,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..ac1d2b8814d 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,20 @@ 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 + } + + if tabTitleEditor.handleRightMouseDown(event) { + return + } + + super.sendEvent(event) + } + override func close() { + tabTitleEditor.finishEditing(commit: true) NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self) super.close() } @@ -187,6 +205,7 @@ class TerminalWindow: NSWindow { override func resignKey() { super.resignKey() resetZoomTabButton.contentTintColor = .secondaryLabelColor + tabTitleEditor.finishEditing(commit: true) } override func becomeMain() { @@ -207,6 +226,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 +329,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 +381,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 +483,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 +492,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 +540,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) + + // 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) - let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen) - setFrameOrigin(frame.origin) + setFrameOrigin(safeOrigin) + return true } private func hideWindowButtons() { @@ -544,7 +578,7 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } - + // MARK: Config struct DerivedConfig { @@ -553,7 +587,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 +596,7 @@ class TerminalWindow: NSWindow { self.backgroundOpacity = 1 self.macosWindowButtons = .visible self.backgroundBlur = .disabled - self.macosTitlebarStyle = "transparent" + self.macosTitlebarStyle = .default self.windowCornerRadius = 16 } @@ -578,7 +612,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 +766,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 +796,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..2bf3bd42f08 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,47 +58,15 @@ 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 - } - - /// On our Tahoe titlebar tabs, we need to fix up right click events because they don't work - /// naturally due to whatever mess we made. - override func sendEvent(_ event: NSEvent) { - guard viewModel.hasTabBar else { - super.sendEvent(event) - return - } - let isRightClick = - event.type == .rightMouseDown || - (event.type == .otherMouseDown && event.buttonNumber == 2) || - (event.type == .leftMouseDown && event.modifierFlags.contains(.control)) - guard isRightClick else { - super.sendEvent(event) - return - } - - guard let tabBarView else { - super.sendEvent(event) - return - } - - let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil) - guard tabBarView.bounds.contains(locationInTabBar) else { - super.sendEvent(event) - return - } - - tabBarView.rightMouseDown(with: event) + viewModel.isMainWindow = false } - // This is called by macOS for native tabbing in order to add the tab bar. We hook into // this, detect the tab bar being added, and override its behavior. override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { @@ -107,7 +75,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 +84,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 +157,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 +167,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 +250,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 +295,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..743ebfa2fe1 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 +77,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 +96,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 +191,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 +199,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 +207,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 +216,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 +239,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 +249,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 +259,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 +274,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 +340,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 +350,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 +358,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 +382,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 +392,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 +401,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 +413,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 +427,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 +455,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 +463,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 +496,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 +518,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 +539,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 +555,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 +565,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 +591,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 +610,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 +621,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 +634,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 +647,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 +658,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 +668,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 +676,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 +701,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 +712,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 +729,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 +826,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 +848,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 +864,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 +905,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.MenuShortcutManager.swift b/macos/Sources/Ghostty/Ghostty.MenuShortcutManager.swift new file mode 100644 index 00000000000..e97f71a6289 --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.MenuShortcutManager.swift @@ -0,0 +1,124 @@ +import AppKit + +extension Ghostty { + /// The manager that's responsible for updating shortcuts of Ghostty's app menu + @MainActor + class MenuShortcutManager { + + /// 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] = [:] + + /// Reset our shortcut index since we're about to rebuild all menu bindings. + func reset() { + menuItemsByShortcut.removeAll(keepingCapacity: true) + } + + /// Syncs a single menu shortcut for the given action. The action string is the same + /// action string used for the Ghostty configuration. + func syncMenuShortcut(_ config: Ghostty.Config, action: String?, menuItem: NSMenuItem?) { + guard let menu = menuItem else { return } + + guard let action, 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( + // We don't want to check missing `shift` for Ghostty configured shortcuts, + // because we know it's there when it needs to be + keyEquivalent: keyEquivalent.lowercased(), + 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 + } + } +} + +extension Ghostty.MenuShortcutManager { + /// Hashable key for a menu shortcut match, normalized for quick lookup. + struct MenuShortcutKey: Hashable { + private static let shortcutModifiers: NSEvent.ModifierFlags = [.shift, .control, .option, .command] + + let keyEquivalent: String + let modifiersRawValue: UInt + + init?(keyEquivalent: String, modifiers: NSEvent.ModifierFlags) { + let normalized = keyEquivalent.lowercased() + guard !normalized.isEmpty else { return nil } + var mods = modifiers.intersection(Self.shortcutModifiers) + if + keyEquivalent.lowercased() != keyEquivalent.uppercased(), + normalized.uppercased() == keyEquivalent { + // If key equivalent is case sensitive and + // it's originally uppercased, then we need to add `shift` to the modifiers + mods.insert(.shift) + } + self.keyEquivalent = normalized + self.modifiersRawValue = mods.rawValue + } + + init?(event: NSEvent) { + guard let keyEquivalent = event.charactersIgnoringModifiers else { return nil } + self.init(keyEquivalent: keyEquivalent, modifiers: event.modifierFlags) + } + } +} 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..8f4fb01cff2 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 @@ -628,6 +652,14 @@ extension Ghostty { } private func localEventLeftMouseDown(_ event: NSEvent) -> NSEvent? { + let isCommandPaletteVisible = (event.window?.windowController as? BaseTerminalController)? + .commandPaletteIsShowing == true + guard !isCommandPaletteVisible else { + // We don't want to process events that + // are supposed to be handled by CommandPaletteView + return event + } + // We only want to process events that are on this window. guard let window, event.window != nil, @@ -635,14 +667,28 @@ extension Ghostty { // The clicked location in this window should be this view. 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 } - - // If we're already focused we do nothing - guard !focused else { return event } + // We should use window to perform hitTest here, + // because there could be some other overlays on top, like search bar + guard window.contentView?.hitTest(location) == self 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 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 +702,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 +717,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 +768,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 +813,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { focusDidChange(true) } + if result { focusDidChange(true) } return result } @@ -776,7 +822,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 +893,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 +926,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 +948,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 +963,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 +984,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 +1005,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 +1022,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 +1050,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 +1080,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 +1106,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 +1119,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 +1151,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 +1166,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 +1243,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 +1262,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 +1275,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 +1299,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 +1324,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 +1363,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 +1382,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 +1448,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 +1459,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 +1542,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 +1550,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 +1566,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 +1574,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 +1582,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 +1590,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 +1598,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 +1606,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 +1614,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 +1622,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 +1630,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 +1666,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 +1674,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 +1684,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 +1718,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 +1892,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 +1911,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 +1930,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 +1953,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 +1974,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 +2005,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 +2112,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 +2194,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..6d055e5d4d7 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -14,6 +14,11 @@ extension NSView { return false } + + /// Returns true if this view is currently the first responder + var isFirstResponder: Bool { + window?.firstResponder === self + } } // MARK: Screenshot @@ -52,10 +57,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 +70,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 +132,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 +156,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..3e04d73c165 --- /dev/null +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -0,0 +1,438 @@ +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 { + guard event.type == .rightMouseDown else { return false } + 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..fa219953716 --- /dev/null +++ b/macos/Tests/Ghostty/ConfigTests.swift @@ -0,0 +1,223 @@ +import Testing +@testable import Ghostty +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 reloadConfig() throws { + let config = try TemporaryConfig("background-opacity = 0.5") + #expect(config.backgroundOpacity == 0.5) + + try config.reload("background-opacity = 0.7") + #expect(config.backgroundOpacity == 0.7) + } + + @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) + } +} diff --git a/macos/Tests/Ghostty/MenuShortcutManagerTests.swift b/macos/Tests/Ghostty/MenuShortcutManagerTests.swift new file mode 100644 index 00000000000..ab8806b9b16 --- /dev/null +++ b/macos/Tests/Ghostty/MenuShortcutManagerTests.swift @@ -0,0 +1,50 @@ +import AppKit +import Foundation +import Testing +@testable import Ghostty + +struct MenuShortcutManagerTests { + @Test(.bug("https://github.com/ghostty-org/ghostty/issues/779", id: 779)) + func unbindShouldDiscardDefault() async throws { + let config = try TemporaryConfig("keybind = super+d=unbind") + + let item = NSMenuItem(title: "Split Right", action: #selector(BaseTerminalController.splitRight(_:)), keyEquivalent: "d") + item.keyEquivalentModifierMask = .command + let manager = await Ghostty.MenuShortcutManager() + await manager.reset() + await manager.syncMenuShortcut(config, action: "new_split:right", menuItem: item) + + #expect(item.keyEquivalent.isEmpty) + #expect(item.keyEquivalentModifierMask.isEmpty) + + try config.reload("") + + await manager.reset() + await manager.syncMenuShortcut(config, action: "new_split:right", menuItem: item) + + #expect(item.keyEquivalent == "d") + #expect(item.keyEquivalentModifierMask == .command) + } + + @Test(.bug("https://github.com/ghostty-org/ghostty/issues/11396", id: 11396)) + func overrideDefault() async throws { + let config = try TemporaryConfig("keybind=super+h=goto_split:left") + + let hideItem = NSMenuItem(title: "Hide Ghostty", action: "hide:", keyEquivalent: "h") + hideItem.keyEquivalentModifierMask = .command + + let goToLeftItem = NSMenuItem(title: "Select Split Left", action: "splitMoveFocusLeft:", keyEquivalent: "") + + let manager = await Ghostty.MenuShortcutManager() + await manager.reset() + + await manager.syncMenuShortcut(config, action: nil, menuItem: hideItem) + await manager.syncMenuShortcut(config, action: "goto_split:left", menuItem: goToLeftItem) + + #expect(hideItem.keyEquivalent.isEmpty) + #expect(hideItem.keyEquivalentModifierMask.isEmpty) + + #expect(goToLeftItem.keyEquivalent == "h") + #expect(goToLeftItem.keyEquivalentModifierMask == .command) + } +} diff --git a/macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift b/macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift new file mode 100644 index 00000000000..5fb984a2f39 --- /dev/null +++ b/macos/Tests/Ghostty/NormalizedMenuShortcutKeyTests.swift @@ -0,0 +1,93 @@ +import AppKit +import Testing +@testable import Ghostty + +@Suite +struct NormalizedMenuShortcutKeyTests { + typealias Key = Ghostty.MenuShortcutManager.MenuShortcutKey + + // MARK: - Init from keyEquivalent + modifiers + + @Test func returnsNilForEmptyKeyEquivalent() { + let key = Key(keyEquivalent: "", modifiers: .command) + #expect(key == nil) + } + + @Test func lowercasesKeyEquivalent() { + let key = Key(keyEquivalent: "A", modifiers: .command) + #expect(key?.keyEquivalent == "a") + } + + @Test func stripsNonShortcutModifiers() { + // .capsLock and .function should be stripped + let key = Key(keyEquivalent: "c", modifiers: [.command, .capsLock, .function]) + let expected = Key(keyEquivalent: "c", modifiers: .command) + #expect(key == expected) + } + + @Test func preservesShortcutModifiers() { + let key = Key(keyEquivalent: "c", modifiers: [.shift, .control, .option, .command]) + let allMods: NSEvent.ModifierFlags = [.shift, .control, .option, .command] + #expect(key?.modifiersRawValue == allMods.rawValue) + } + + @Test func uppercaseLetterInsertsShift() { + // "A" is uppercase and case-sensitive, so .shift should be added + let key = Key(keyEquivalent: "A", modifiers: .command) + let expected = NSEvent.ModifierFlags([.command, .shift]).rawValue + #expect(key?.modifiersRawValue == expected) + } + + @Test func lowercaseLetterDoesNotInsertShift() { + let key = Key(keyEquivalent: "a", modifiers: .command) + let expected = NSEvent.ModifierFlags.command.rawValue + #expect(key?.modifiersRawValue == expected) + } + + @Test func nonCaseSensitiveCharacterDoesNotInsertShift() { + // "1" is not case-sensitive (uppercased == lowercased is false for digits, + // but "1".uppercased() == "1".lowercased() == "1" so isCaseSensitive is false) + let key = Key(keyEquivalent: "1", modifiers: .command) + let expected = NSEvent.ModifierFlags.command.rawValue + #expect(key?.modifiersRawValue == expected) + } + + // MARK: - Equality / Hashing + + @Test func sameKeyAndModsAreEqual() { + let a = Key(keyEquivalent: "c", modifiers: .command) + let b = Key(keyEquivalent: "c", modifiers: .command) + #expect(a == b) + } + + @Test func uppercaseAndLowercaseWithShiftAreEqual() { + // "C" with .command should equal "c" with [.command, .shift] + // because the uppercase init auto-inserts .shift + let fromUpper = Key(keyEquivalent: "C", modifiers: .command) + let fromLowerWithShift = Key(keyEquivalent: "c", modifiers: [.command, .shift]) + #expect(fromUpper == fromLowerWithShift) + } + + @Test func differentKeysAreNotEqual() { + let a = Key(keyEquivalent: "a", modifiers: .command) + let b = Key(keyEquivalent: "b", modifiers: .command) + #expect(a != b) + } + + @Test func differentModifiersAreNotEqual() { + let a = Key(keyEquivalent: "c", modifiers: .command) + let b = Key(keyEquivalent: "c", modifiers: .option) + #expect(a != b) + } + + @Test func canBeUsedAsDictionaryKey() { + let key = Key(keyEquivalent: "c", modifiers: .command)! + var dict: [Key: String] = [:] + dict[key] = "copy" + #expect(dict[key] == "copy") + + // Same key created separately should find the same entry + let key2 = Key(keyEquivalent: "c", modifiers: .command)! + #expect(dict[key2] == "copy") + } +} 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/Helpers/TemporaryConfig.swift b/macos/Tests/Helpers/TemporaryConfig.swift new file mode 100644 index 00000000000..f3e18dc5327 --- /dev/null +++ b/macos/Tests/Helpers/TemporaryConfig.swift @@ -0,0 +1,45 @@ +import Foundation +@testable import Ghostty +@testable import GhosttyKit + +/// Create a temporary config file and delete it when this is deallocated +class TemporaryConfig: Ghostty.Config { + enum Error: Swift.Error { + case failedToLoad + } + + 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)) + } + + func reload(_ newConfigText: String?, finalize: Bool = true) throws { + if let newConfigText { + try newConfigText.write(to: temporaryFile, atomically: true, encoding: .utf8) + } + guard let cfg = Self.loadConfig(at: temporaryFile.path(), finalize: finalize) else { + throw Error.failedToLoad + } + clone(config: cfg) + } + + 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/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..df08fc2048d 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -8,6 +8,7 @@ appstream, flatpak-builder, gdb, + cmake, #, glxinfo # unused ncurses, nodejs, @@ -66,6 +67,7 @@ poop, typos, shellcheck, + swiftlint, uv, wayland, wayland-scanner, @@ -90,6 +92,7 @@ in packages = [ # For builds + cmake doxygen jq llvmPackages_latest.llvm @@ -198,6 +201,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..01a5879d6ae 100644 --- a/pkg/dcimgui/build.zig +++ b/pkg/dcimgui/build.zig @@ -26,7 +26,14 @@ pub fn build(b: *std.Build) !void { .linkage = .static, }); lib.linkLibC(); - lib.linkLibCpp(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } b.installArtifact(lib); // Zig module @@ -56,6 +63,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/freetype/build.zig b/pkg/freetype/build.zig index ecb22cb6c40..b85310a5b1c 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -84,11 +84,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu "-DFT_CONFIG_OPTION_SYSTEM_ZLIB=1", - "-DHAVE_UNISTD_H", - "-DHAVE_FCNTL_H", - "-fno-sanitize=undefined", }); + if (target.result.os.tag != .windows) { + try flags.appendSlice(b.allocator, &.{ + "-DHAVE_UNISTD_H", + "-DHAVE_FCNTL_H", + }); + } if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index d4f74b7ee80..cd949e357b1 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -52,7 +52,7 @@ pub const Face = struct { /// Select a given charmap by its encoding tag (as listed in freetype.h). pub fn selectCharmap(self: Face, encoding: Encoding) Error!void { - return intToError(c.FT_Select_Charmap(self.handle, @intFromEnum(encoding))); + return intToError(c.FT_Select_Charmap(self.handle, @intCast(@intFromEnum(encoding)))); } /// Call FT_Request_Size to request the nominal size (in points). @@ -99,7 +99,7 @@ pub const Face = struct { pub fn renderGlyph(self: Face, render_mode: RenderMode) Error!void { return intToError(c.FT_Render_Glyph( self.handle.*.glyph, - @intFromEnum(render_mode), + @intCast(@intFromEnum(render_mode)), )); } diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index c41e052177a..1dc82a6e304 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -51,7 +51,14 @@ fn buildGlslang( .linkage = .static, }); lib.linkLibC(); - lib.linkLibCpp(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } if (upstream_) |upstream| lib.addIncludePath(upstream.path("")); lib.addIncludePath(b.path("override")); if (target.result.os.tag.isDarwin()) { @@ -65,6 +72,10 @@ fn buildGlslang( "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined", }); + // MSVC requires explicit std specification otherwise C++17 features + // like std::variant, std::filesystem, and inline variables are + // guarded behind _HAS_CXX17. + try flags.append(b.allocator, "-std=c++17"); if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); diff --git a/pkg/gtk4-layer-shell/src/main.zig b/pkg/gtk4-layer-shell/src/main.zig index f7848ea947f..a1531323135 100644 --- a/pkg/gtk4-layer-shell/src/main.zig +++ b/pkg/gtk4-layer-shell/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); const c = @cImport({ @cInclude("gtk4-layer-shell.h"); }); +const gdk = @import("gdk"); const gtk = @import("gtk"); pub const ShellLayer = enum(c_uint) { @@ -61,6 +62,10 @@ pub fn setKeyboardMode(window: *gtk.Window, mode: KeyboardMode) void { c.gtk_layer_set_keyboard_mode(@ptrCast(window), @intFromEnum(mode)); } +pub fn setMonitor(window: *gtk.Window, monitor: ?*gdk.Monitor) void { + c.gtk_layer_set_monitor(@ptrCast(window), @ptrCast(monitor)); +} + pub fn setNamespace(window: *gtk.Window, name: [:0]const u8) void { c.gtk_layer_set_namespace(@ptrCast(window), name.ptr); } diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index 8696c020346..6d8f3be70ad 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -103,7 +103,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .linkage = .static, }); lib.linkLibC(); - lib.linkLibCpp(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } if (target.result.os.tag.isDarwin()) { try apple_sdk.addPaths(b, lib); diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 3715baf4a54..49656b93e68 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -20,7 +20,15 @@ pub fn build(b: *std.Build) !void { }), .linkage = .static, }); - lib.linkLibCpp(); + lib.linkLibC(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } if (upstream_) |upstream| { lib.addIncludePath(upstream.path("")); module.addIncludePath(upstream.path("")); @@ -31,6 +39,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/build.zig b/pkg/oniguruma/build.zig index ea39b481457..efc013b4344 100644 --- a/pkg/oniguruma/build.zig +++ b/pkg/oniguruma/build.zig @@ -68,6 +68,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .linkage = .static, }); const t = target.result; + const is_windows = t.os.tag == .windows; lib.linkLibC(); if (target.result.os.tag.isDarwin()) { @@ -86,13 +87,13 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .PACKAGE_VERSION = "6.9.9", .VERSION = "6.9.9", .HAVE_ALLOCA = true, - .HAVE_ALLOCA_H = true, - .USE_CRNL_AS_LINE_TERMINATOR = false, + .HAVE_ALLOCA_H = !is_windows, + .USE_CRNL_AS_LINE_TERMINATOR = is_windows, .HAVE_STDINT_H = true, - .HAVE_SYS_TIMES_H = true, - .HAVE_SYS_TIME_H = true, + .HAVE_SYS_TIMES_H = !is_windows, + .HAVE_SYS_TIME_H = !is_windows, .HAVE_SYS_TYPES_H = true, - .HAVE_UNISTD_H = true, + .HAVE_UNISTD_H = !is_windows, .HAVE_INTTYPES_H = true, .SIZEOF_INT = t.cTypeByteSize(.int), .SIZEOF_LONG = t.cTypeByteSize(.long), 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..e132507a1a4 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -12,7 +12,15 @@ pub fn build(b: *std.Build) !void { }), .linkage = .static, }); - lib.linkLibCpp(); + lib.linkLibC(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } lib.addIncludePath(b.path("vendor")); if (target.result.os.tag.isDarwin()) { @@ -20,6 +28,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/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index f85e74adf83..72ce61eb60f 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -58,7 +58,14 @@ fn buildSpirvCross( .linkage = .static, }); lib.linkLibC(); - lib.linkLibCpp(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); try apple_sdk.addPaths(b, lib); diff --git a/pkg/utfcpp/build.zig b/pkg/utfcpp/build.zig index e06813b8353..15c652c147b 100644 --- a/pkg/utfcpp/build.zig +++ b/pkg/utfcpp/build.zig @@ -12,13 +12,26 @@ pub fn build(b: *std.Build) !void { }), .linkage = .static, }); - lib.linkLibCpp(); + lib.linkLibC(); + // On MSVC, we must not use linkLibCpp because Zig unconditionally + // passes -nostdinc++ and then adds its bundled libc++/libc++abi + // include paths, which conflict with MSVC's own C++ runtime headers. + // The MSVC SDK include directories (added via linkLibC) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (target.result.abi != .msvc) { + lib.linkLibCpp(); + } if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); 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/pkg/zlib/build.zig b/pkg/zlib/build.zig index 246ab1bcbad..6bde60ec790 100644 --- a/pkg/zlib/build.zig +++ b/pkg/zlib/build.zig @@ -32,8 +32,16 @@ pub fn build(b: *std.Build) !void { "-DHAVE_SYS_TYPES_H", "-DHAVE_STDINT_H", "-DHAVE_STDDEF_H", - "-DZ_HAVE_UNISTD_H", }); + if (target.result.os.tag != .windows) { + try flags.append(b.allocator, "-DZ_HAVE_UNISTD_H"); + } + if (target.result.abi == .msvc) { + try flags.appendSlice(b.allocator, &.{ + "-D_CRT_SECURE_NO_DEPRECATE", + "-D_CRT_NONSTDC_NO_DEPRECATE", + }); + } lib.addCSourceFiles(.{ .root = upstream.path(""), .files = srcs, 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..1549aaf347f --- /dev/null +++ b/po/id.po @@ -0,0 +1,355 @@ +# 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" +"Plural-Forms: nplurals=2; plural=(n != 1);\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..bbcadd8b400 --- /dev/null +++ b/po/lt.po @@ -0,0 +1,355 @@ +# 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" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"(n%100<10 || n%100>=20) ? 1 : 2);\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..4a48df5d7da --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,346 @@ +# 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" +"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/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..a26103911bb --- /dev/null +++ b/po/zh_TW.po @@ -0,0 +1,344 @@ +# 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" +"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/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..7fed946c0c2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -36,6 +36,7 @@ const App = @import("App.zig"); const internal_os = @import("os/main.zig"); const inspectorpkg = @import("inspector/main.zig"); const SurfaceMouse = @import("surface_mouse.zig"); +const ProcessInfo = @import("pty.zig").ProcessInfo; const log = std.log.scoped(.surface); @@ -46,8 +47,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 +313,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 +325,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 +391,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 +537,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 +608,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 +640,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 +1181,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(.{ @@ -1758,6 +1765,13 @@ pub fn updateConfig( termio_config_ptr.* = try termio.Termio.DerivedConfig.init(self.alloc, config); errdefer termio_config_ptr.deinit(); + // Always use the surface's own conditional state for the termio config. + // When changeConditionalState returns null (no theme-conditional config + // rules), the config's conditional_state inherits from the app level + // which may have a stale theme. This ensures DECRPM mode 2031 reports + // the correct color scheme for this surface. + termio_config_ptr.conditional_state = self.config_conditional_state; + _ = self.renderer_thread.mailbox.push(renderer_message, .{ .forever = {} }); self.queueIo(.{ .change_config = .{ @@ -2010,6 +2024,26 @@ 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: { + 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.*; + } + } + break :pin screen.pages.pin(.{ .viewport = .{} }) orelse return false; + }; + try screen.select(terminal.Selection.init(pin, pin, false)); + 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 +2466,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 +2481,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 +2500,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 +2823,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 +3015,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 +3556,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 +3564,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 +3579,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 +3614,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 +3623,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 +3633,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); + 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, }, - - .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); - }, - - .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 +3875,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 +4148,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 +4612,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. @@ -4956,14 +4840,14 @@ fn mouseSelection( break :ebs drag_pin.before(click_pin); }; - // Whether or not the the click pin cell + // Whether or not the click pin cell // should be included in the selection. const include_click_cell = if (end_before_start) click_x_frac >= threshold_point else click_x_frac < threshold_point; - // Whether or not the the drag pin cell + // Whether or not the drag pin cell // should be included in the selection. const include_drag_cell = if (end_before_start) drag_x_frac < threshold_point @@ -5063,7 +4947,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 +5354,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 @@ -6476,6 +6380,13 @@ fn testMouseSelectionIsNull( ); } +/// Get information about the process(es) running within the surface. Returns +/// `null` if there was an error getting the information or the information is +/// not available on a particular platform. +pub fn getProcessInfo(self: *Surface, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { + return self.io.getProcessInfo(info); +} + test "Surface: selection logic" { // We disable format to make these easier to // read by pairing sets of coordinates per line. diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1d9ef633c7d..862425535cc 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. @@ -335,6 +340,9 @@ pub const Action = union(Key) { /// otherwise the terminal-set title. copy_title_to_clipboard, + /// Set the tab title override for the target's tab. + set_tab_title: SetTitle, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -401,6 +409,11 @@ pub const Action = union(Key) { search_selected, readonly, copy_title_to_clipboard, + set_tab_title, + + 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..efd0fbd6f1f 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,26 @@ pub const CAPI = struct { return surface.core_surface.hasSelection(); } + /// Start a selection anchored at the cursor position. + 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 current selection. Returns true if a selection was cleared. + export fn ghostty_surface_clear_selection(surface: *Surface) bool { + surface.core_surface.renderer_state.mutex.lock(); + defer surface.core_surface.renderer_state.mutex.unlock(); + + const screen: *terminal.Screen = surface.core_surface.io.terminal.screens.active; + if (screen.selection == null) return false; + screen.clearSelection(); + screen.dirty.selection = true; + return true; + } + /// 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.zig b/src/apprt/gtk.zig index 36a9290fbc2..eb33c4e4db5 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -13,4 +13,5 @@ test { @import("std").testing.refAllDecls(@This()); _ = @import("gtk/ext.zig"); _ = @import("gtk/key.zig"); + _ = @import("gtk/portal.zig"); } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 6c731033946..39c13c19d42 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -22,16 +22,10 @@ const log = std.log.scoped(.gtk); pub const must_draw_from_app_thread = true; /// GTK application ID -pub const application_id = switch (builtin.mode) { - .Debug, .ReleaseSafe => "com.mitchellh.ghostty-debug", - .ReleaseFast, .ReleaseSmall => "com.mitchellh.ghostty", -}; +pub const application_id = @import("build/info.zig").application_id; /// GTK object path -pub const object_path = switch (builtin.mode) { - .Debug, .ReleaseSafe => "/com/mitchellh/ghostty_debug", - .ReleaseFast, .ReleaseSmall => "/com/mitchellh/ghostty", -}; +pub const object_path = @import("build/info.zig").object_path; /// The GObject Application instance app: *Application, 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/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index bcece4caabf..c50ea8cd5ca 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -7,9 +7,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -/// Prefix/appid for the gresource file. -pub const prefix = "/com/mitchellh/ghostty"; -pub const app_id = "com.mitchellh.ghostty"; +const build_info = @import("info.zig"); /// The path to the Blueprint files. The folder structure is expected to be /// `{version}/{name}.blp` where `version` is the major and minor @@ -112,7 +110,7 @@ pub fn blueprint(comptime bp: Blueprint) [:0]const u8 { std.mem.eql(u8, candidate.name, bp.name)) { return std.fmt.comptimePrint("{s}/ui/{d}.{d}/{s}.ui", .{ - prefix, + build_info.resource_path, candidate.major, candidate.minor, candidate.name, @@ -173,7 +171,7 @@ fn genIcons(writer: *std.Io.Writer) !void { try writer.print( \\ \\ - , .{prefix}); + , .{build_info.resource_path}); const cwd = std.fs.cwd(); inline for (icon_sizes) |size| { @@ -186,7 +184,7 @@ fn genIcons(writer: *std.Io.Writer) !void { \\ {s} \\ , - .{ alias, app_id, source }, + .{ alias, build_info.base_application_id, source }, ); } @@ -199,7 +197,7 @@ fn genIcons(writer: *std.Io.Writer) !void { \\ {s} \\ , - .{ alias, app_id, source }, + .{ alias, build_info.base_application_id, source }, ); } } @@ -215,7 +213,7 @@ fn genRoot(writer: *std.Io.Writer) !void { try writer.print( \\ \\ - , .{prefix}); + , .{build_info.resource_path}); const cwd = std.fs.cwd(); inline for (css) |name| { @@ -249,7 +247,7 @@ fn genUi( try writer.print( \\ \\ - , .{prefix}); + , .{build_info.resource_path}); for (files.items) |ui_file| { for (blueprints) |bp| { diff --git a/src/apprt/gtk/build/info.zig b/src/apprt/gtk/build/info.zig new file mode 100644 index 00000000000..fc6478d8103 --- /dev/null +++ b/src/apprt/gtk/build/info.zig @@ -0,0 +1,18 @@ +const builtin = @import("builtin"); + +/// Base application ID +pub const base_application_id = "com.mitchellh.ghostty"; + +/// GTK application ID +pub const application_id = switch (builtin.mode) { + .Debug, .ReleaseSafe => base_application_id ++ "-debug", + .ReleaseFast, .ReleaseSmall => base_application_id, +}; + +pub const resource_path = "/com/mitchellh/ghostty"; + +/// GTK object path +pub const object_path = switch (builtin.mode) { + .Debug, .ReleaseSafe => resource_path ++ "_debug", + .ReleaseFast, .ReleaseSmall => resource_path, +}; diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index c24352c180e..fa9b71eb7d1 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -9,6 +9,7 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); const build_config = @import("../../../build_config.zig"); +const build_info = @import("../build/info.zig"); const state = &@import("../../../global.zig").state; const i18n = @import("../../../os/main.zig").i18n; const apprt = @import("../../../apprt.zig"); @@ -22,6 +23,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"); @@ -39,6 +41,7 @@ const Tab = @import("tab.zig").Tab; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog; const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts; +const OpenURI = @import("../portal.zig").OpenURI; const log = std.log.scoped(.gtk_ghostty_application); @@ -213,6 +216,8 @@ pub const Application = extern struct { /// not exist in Ghostty's environment variable. saved_language: ?[:0]const u8 = null, + open_uri: OpenURI = undefined, + pub var offset: c_int = 0; }; @@ -326,7 +331,7 @@ pub const Application = extern struct { } } - break :app_id ApprtApp.application_id; + break :app_id build_info.application_id; }; const display: *gdk.Display = gdk.Display.getDefault() orelse { @@ -349,7 +354,7 @@ pub const Application = extern struct { log.warn("error initializing windowing protocol err={}", .{err}); break :wp .{ .none = .{} }; }; - errdefer wp.deinit(alloc); + errdefer wp.deinit(); log.debug("windowing protocol={s}", .{@tagName(wp)}); // Create our GTK Application which encapsulates our process. @@ -380,7 +385,7 @@ pub const Application = extern struct { // Force the resource path to a known value so it doesn't depend // on the app id (which changes between debug/release and can be // user-configured) and force it to load in compiled resources. - .resource_base_path = "/com/mitchellh/ghostty", + .resource_base_path = build_info.resource_path, }); // Setup our private state. More setup is done in the init @@ -396,6 +401,7 @@ pub const Application = extern struct { .custom_css_providers = .empty, .global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}), .saved_language = saved_language, + .open_uri = .init(rt_app), }; // Signals @@ -430,7 +436,8 @@ pub const Application = extern struct { const alloc = self.allocator(); const priv: *Private = self.private(); priv.config.unref(); - priv.winproto.deinit(alloc); + priv.winproto.deinit(); + priv.open_uri.deinit(); priv.global_shortcuts.unref(); if (priv.saved_language) |language| alloc.free(language); if (gdk.Display.getDefault()) |display| { @@ -709,6 +716,7 @@ pub const Application = extern struct { .app => null, .surface => |v| v, }, + .none, ), .open_config => return Action.openConfig(self), @@ -738,6 +746,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), @@ -802,6 +811,11 @@ pub const Application = extern struct { return &self.private().winproto; } + /// Returns the open URI portal implementation. + pub fn openUri(self: *Self) *OpenURI { + return &self.private().open_uri; + } + /// This will get called when there are no more open surfaces. fn startQuitTimer(self: *Self) void { const priv = self.private(); @@ -1285,6 +1299,11 @@ pub const Application = extern struct { // Set ourselves as the default application. gio.Application.setDefault(self.as(gio.Application)); + // The D-Bus connection is only valid after GApplication startup. + self.openUri().setDbusConnection( + self.as(gio.Application).getDbusConnection(), + ); + // Setup our event loop self.startupXev(); @@ -1669,17 +1688,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 +1720,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 +1733,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( @@ -1802,6 +1888,17 @@ pub const Application = extern struct { gobject.Object.virtual_methods.finalize.implement(class, &finalize); } }; + + pub fn openUrlFallback(self: *Application, kind: apprt.action.OpenUrl.Kind, url: []const u8) void { + // Fallback to the minimal cross-platform way of opening a URL. + // This is always a safe fallback and enables for example Windows + // to open URLs (GTK on Windows via WSL is a thing). + internal_os.open( + self.allocator(), + kind, + url, + ) catch |err| log.warn("unable to open url: {}", .{err}); + } }; /// All apprt action handlers @@ -2151,6 +2248,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 +2263,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 +2302,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)); @@ -2205,16 +2343,20 @@ const Action = struct { self: *Application, value: apprt.action.OpenUrl, ) void { - // TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html + if (std.mem.startsWith(u8, value.url, "/")) { + self.openUrlFallback(value.kind, value.url); + return; + } + if (std.mem.startsWith(u8, value.url, "file://")) { + self.openUrlFallback(value.kind, value.url); + return; + } - // Fallback to the minimal cross-platform way of opening a URL. - // This is always a safe fallback and enables for example Windows - // to open URLs (GTK on Windows via WSL is a thing). - internal_os.open( - self.allocator(), - value.kind, - value.url, - ) catch |err| log.warn("unable to open url: {}", .{err}); + self.openUri().start(value) catch |err| { + log.err("unable to open uri err={}", .{err}); + self.openUrlFallback(value.kind, value.url); + return; + }; } pub fn pwd( @@ -2435,6 +2577,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 +2660,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..6101e82e807 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -353,7 +353,7 @@ pub const CommandPalette = extern struct { // Regular command - emit trigger signal const action = cmd.getAction() orelse return; - // Signal that an an action has been selected. Signals are synchronous + // Signal that an action has been selected. Signals are synchronous // so we shouldn't need to worry about cloning the action. signals.trigger.impl.emit( self, @@ -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..f1e1c39078a 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"); @@ -34,6 +35,7 @@ const TitleDialog = @import("title_dialog.zig").TitleDialog; const Window = @import("window.zig").Window; const InspectorWindow = @import("inspector_window.zig").InspectorWindow; const i18n = @import("../../../os/i18n.zig"); +const media = @import("../media.zig"); const log = std.log.scoped(.gtk_ghostty_surface); @@ -693,6 +695,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 +706,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 +824,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 +1014,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 +1886,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 +1900,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 +1932,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 +2049,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(); @@ -2365,34 +2458,8 @@ pub const Surface = extern struct { 1.0, ); - assert(std.fs.path.isAbsolute(path)); - const media_file = gtk.MediaFile.newForFilename(path); - - // If the audio file is marked as required, we'll emit an error if - // there was a problem playing it. Otherwise there will be silence. - if (required) { - _ = gobject.Object.signals.notify.connect( - media_file, - ?*anyopaque, - mediaFileError, - null, - .{ .detail = "error" }, - ); - } - - // Watch for the "ended" signal so that we can clean up after - // ourselves. - _ = gobject.Object.signals.notify.connect( - media_file, - ?*anyopaque, - mediaFileEnded, - null, - .{ .detail = "ended" }, - ); - - const media_stream = media_file.as(gtk.MediaStream); - media_stream.setVolume(volume); - media_stream.play(); + const media_file = media.fromFilename(path) orelse break :audio; + media.playMediaFile(media_file, volume, required); } } @@ -2684,13 +2751,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 +2821,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; } @@ -3212,10 +3292,13 @@ pub const Surface = extern struct { // Store our cached size const priv = self.private(); - priv.size = .{ + + const new_size: apprt.SurfaceSize = .{ .width = @intCast(width), .height = @intCast(height), }; + const changed = !priv.size.eql(&new_size); + priv.size = new_size; // If our surface is realize, we send callbacks. if (priv.core_surface) |surface| { @@ -3225,12 +3308,13 @@ pub const Surface = extern struct { log.warn("error in content scale callback err={}", .{err}); }; - surface.sizeCallback(priv.size) catch |err| { - log.warn("error in size callback err={}", .{err}); - }; - - // Setup our resize overlay if configured - self.resizeOverlaySchedule(); + if (changed) { + surface.sizeCallback(new_size) catch |err| { + log.warn("error in size callback err={}", .{err}); + }; + // Setup our resize overlay if configured + self.resizeOverlaySchedule(); + } return; } @@ -3247,7 +3331,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 +3364,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( @@ -3361,35 +3460,6 @@ pub const Surface = extern struct { right.setVisible(0); } - fn mediaFileError( - media_file: *gtk.MediaFile, - _: *gobject.ParamSpec, - _: ?*anyopaque, - ) callconv(.c) void { - const path = path: { - const file = media_file.getFile() orelse break :path null; - break :path file.getPath(); - }; - defer if (path) |p| glib.free(p); - - const media_stream = media_file.as(gtk.MediaStream); - const err = media_stream.getError() orelse return; - log.warn("error playing bell from {s}: {s} {d} {s}", .{ - path orelse "<>", - glib.quarkToString(err.f_domain), - err.f_code, - err.f_message orelse "", - }); - } - - fn mediaFileEnded( - media_file: *gtk.MediaFile, - _: *gobject.ParamSpec, - _: ?*anyopaque, - ) callconv(.c) void { - media_file.unref(); - } - fn titleDialogSet( _: *TitleDialog, title_ptr: [*:0]const u8, diff --git a/src/apprt/gtk/class/surface_scrolled_window.zig b/src/apprt/gtk/class/surface_scrolled_window.zig index 488fdb3f4e6..dd4f17d1163 100644 --- a/src/apprt/gtk/class/surface_scrolled_window.zig +++ b/src/apprt/gtk/class/surface_scrolled_window.zig @@ -2,6 +2,7 @@ const std = @import("std"); const adw = @import("adw"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const gtk_version = @import("../gtk_version.zig"); const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; @@ -59,11 +60,29 @@ pub const SurfaceScrolledWindow = extern struct { config: ?*Config = null, config_binding: ?*gobject.Binding = null, surface: ?*Surface = null, + scrolled_window: *gtk.ScrolledWindow, pub var offset: c_int = 0; }; fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + if (gtk_version.runtimeUntil(4, 20, 1)) self.disableKineticScroll(); + } + + fn disableKineticScroll(self: *Self) void { + // Until gtk 4.20.1 trackpads have kinetic scrolling behavior regardless + // of `Gtk.ScrolledWindow.kinetic_scrolling`. As a workaround, disable + // EventControllerScroll.kinetic + const controllers = self.private().scrolled_window.as(gtk.Widget).observeControllers(); + defer controllers.unref(); + var i: c_uint = 0; + while (controllers.getObject(i)) |obj| : (i += 1) { + defer obj.unref(); + const controller = gobject.ext.cast(gtk.EventControllerScroll, obj) orelse continue; + var flags = controller.getFlags(); + flags.kinetic = false; + controller.setFlags(flags); + } } fn dispose(self: *Self) callconv(.c) void { @@ -189,6 +208,7 @@ pub const SurfaceScrolledWindow = extern struct { // Bindings class.bindTemplateCallback("scrollbar_policy", &closureScrollbarPolicy); class.bindTemplateCallback("notify_surface", &propSurface); + class.bindTemplateChildPrivate("scrolled_window", .{}); // Properties gobject.ext.registerProperties(class, &.{ 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..bf2a2fe7c82 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); } @@ -1011,21 +1071,6 @@ pub const Window = extern struct { self.syncAppearance(); } - fn propGdkSurfaceHeight( - _: *gdk.Surface, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - // X11 needs to fix blurring on resize, but winproto implementations - // could do anything. - self.private().winproto.resizeEvent() catch |err| { - log.warn( - "winproto resize event failed error={}", - .{err}, - ); - }; - } - fn propIsActive( _: *gtk.Window, _: *gobject.ParamSpec, @@ -1051,7 +1096,7 @@ pub const Window = extern struct { }; } - fn propGdkSurfaceWidth( + fn propGdkSurfaceDims( _: *gdk.Surface, _: *gobject.ParamSpec, self: *Self, @@ -1190,7 +1235,7 @@ pub const Window = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); priv.tab_bindings.unref(); - priv.winproto.deinit(Application.default().allocator()); + priv.winproto.deinit(); gobject.Object.virtual_methods.finalize.call( Class.parent, @@ -1222,14 +1267,14 @@ pub const Window = extern struct { _ = gobject.Object.signals.notify.connect( gdk_surface, *Self, - propGdkSurfaceWidth, + propGdkSurfaceDims, self, .{ .detail = "width" }, ); _ = gobject.Object.signals.notify.connect( gdk_surface, *Self, - propGdkSurfaceHeight, + propGdkSurfaceDims, self, .{ .detail = "height" }, ); @@ -1248,7 +1293,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 +1844,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/media.zig b/src/apprt/gtk/media.zig new file mode 100644 index 00000000000..1015c933f0e --- /dev/null +++ b/src/apprt/gtk/media.zig @@ -0,0 +1,102 @@ +const std = @import("std"); +const assert = @import("../../quirks.zig").inlineAssert; + +const log = std.log.scoped(.gtk_media); + +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +pub fn fromFilename(path: [:0]const u8) ?*gtk.MediaFile { + assert(std.fs.path.isAbsolute(path)); + std.fs.accessAbsolute(path, .{ .mode = .read_only }) catch |err| { + log.warn("unable to access {s}: {t}", .{ path, err }); + return null; + }; + return gtk.MediaFile.newForFilename(path); +} + +pub fn fromResource(path: [:0]const u8) ?*gtk.MediaFile { + assert(std.fs.path.isAbsolute(path)); + var gerr: ?*glib.Error = null; + + const found = gio.resourcesGetInfo(path, .{}, null, null, &gerr); + if (gerr) |err| { + defer err.free(); + log.warn( + "failed to find resource {s}: {s} {d} {s}", + .{ + path, + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "(no message)", + }, + ); + return null; + } + + if (found == 0) { + log.warn("failed to find resource {s}", .{path}); + return null; + } + + return gtk.MediaFile.newForResource(path); +} + +pub fn playMediaFile(media_file: *gtk.MediaFile, volume: f64, required: bool) void { + // If the audio file is marked as required, we'll emit an error if + // there was a problem playing it. Otherwise there will be silence. + if (required) { + _ = gobject.Object.signals.notify.connect( + media_file, + ?*anyopaque, + mediaFileError, + null, + .{ .detail = "error" }, + ); + } + + // Watch for the "ended" signal so that we can clean up after + // ourselves. + _ = gobject.Object.signals.notify.connect( + media_file, + ?*anyopaque, + mediaFileEnded, + null, + .{ .detail = "ended" }, + ); + + const media_stream = media_file.as(gtk.MediaStream); + media_stream.setVolume(volume); + media_stream.play(); +} + +fn mediaFileError( + media_file: *gtk.MediaFile, + _: *gobject.ParamSpec, + _: ?*anyopaque, +) callconv(.c) void { + const path = path: { + const file = media_file.getFile() orelse break :path null; + break :path file.getPath(); + }; + defer if (path) |p| glib.free(p); + + const media_stream = media_file.as(gtk.MediaStream); + const err = media_stream.getError() orelse return; + log.warn("error playing sound from {s}: {s} {d} {s}", .{ + path orelse "<>", + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "", + }); +} + +fn mediaFileEnded( + media_file: *gtk.MediaFile, + _: *gobject.ParamSpec, + _: ?*anyopaque, +) callconv(.c) void { + media_file.unref(); +} diff --git a/src/apprt/gtk/portal.zig b/src/apprt/gtk/portal.zig new file mode 100644 index 00000000000..3e51042d608 --- /dev/null +++ b/src/apprt/gtk/portal.zig @@ -0,0 +1,105 @@ +const std = @import("std"); + +const gio = @import("gio"); + +const Allocator = std.mem.Allocator; + +pub const OpenURI = @import("portal/OpenURI.zig"); +pub const token_hex_len = @sizeOf(usize) * 2; +pub const TokenBuffer = [token_hex_len + 1]u8; +const token_format = std.fmt.comptimePrint("{{x:0>{}}}", .{token_hex_len}); + +/// Generate a token suitable for use in requests to the XDG Desktop Portal +pub fn generateToken() usize { + return std.crypto.random.int(usize); +} + +/// Format a request token consistently for use in portal object paths and payloads. +pub fn formatToken(buf: *TokenBuffer, token: usize) [:0]const u8 { + return std.fmt.bufPrintZ(buf, token_format, .{token}) catch unreachable; +} + +/// Get the XDG portal request path for the current Ghostty instance. +/// +/// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html +/// for the protocol of the Request interface. +pub fn getRequestPath(alloc: Allocator, dbus: *gio.DBusConnection, token: usize) (Allocator.Error || error{NoDBusUniqueName})![:0]const u8 { + // Get the unique name from D-Bus and strip the leading `:` + const unique_name = std.mem.span( + dbus.getUniqueName() orelse { + return error.NoDBusUniqueName; + }, + )[1..]; + + return buildRequestPath(alloc, unique_name, token); +} + +/// Build the XDG portal request path for given unique name and token. +fn buildRequestPath(alloc: Allocator, unique_name: []const u8, token: usize) Allocator.Error![:0]const u8 { + var token_buf: TokenBuffer = undefined; + const token_string = formatToken(&token_buf, token); + + const object_path = try std.mem.joinZ( + alloc, + "/", + &.{ + "/org/freedesktop/portal/desktop/request", + unique_name, + token_string, + }, + ); + + // Sanitize the unique name by replacing every `.` with `_`. In effect, this + // will turn a unique name like `1.192` into `1_192`. + // This sounds arbitrary, but it's part of the Request protocol. + _ = std.mem.replaceScalar(u8, object_path, '.', '_'); + + return object_path; +} + +/// Try and parse the token out of a request path. +pub fn parseRequestPathToken(request_path: []const u8) ?usize { + const index = std.mem.lastIndexOfScalar(u8, request_path, '/') orelse return null; + const token = request_path[index + 1 ..]; + return std.fmt.parseUnsigned(usize, token, 16) catch return null; +} + +test "formatToken pads to fixed width" { + const testing = std.testing; + + var token_buf: TokenBuffer = undefined; + const token = formatToken(&token_buf, 0x42); + + try testing.expectEqual(@as(usize, token_hex_len), token.len); + try testing.expectEqualStrings("0000000000000042", token); +} + +test "buildRequestPath" { + const testing = std.testing; + + const path = try buildRequestPath(testing.allocator, "1.42", 0x75af01a79c6fea34); + try testing.expectEqualStrings( + "/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34", + path, + ); + testing.allocator.free(path); +} + +test "buildRequestPath pads token" { + const testing = std.testing; + const path = try buildRequestPath(testing.allocator, "1.42", 0x42); + + try testing.expectEqualStrings( + "/org/freedesktop/portal/desktop/request/1_42/0000000000000042", + path, + ); + testing.allocator.free(path); +} + +test "parseRequestPathToken" { + const testing = std.testing; + + try testing.expectEqual(0x75af01a79c6fea34, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34").?); + try testing.expectEqual(null, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fGa34")); + try testing.expectEqual(null, parseRequestPathToken("75af01a79c6fea34")); +} diff --git a/src/apprt/gtk/portal/OpenURI.zig b/src/apprt/gtk/portal/OpenURI.zig new file mode 100644 index 00000000000..97aa013e55a --- /dev/null +++ b/src/apprt/gtk/portal/OpenURI.zig @@ -0,0 +1,565 @@ +//! Use DBus to call the XDG Desktop Portal to open an URI. +//! See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html#org-freedesktop-portal-openuri-openuri +const OpenURI = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); + +const App = @import("../App.zig"); +const portal = @import("../portal.zig"); +const apprt = @import("../../../apprt.zig"); + +const log = std.log.scoped(.openuri); + +/// The GTK app that we "belong" to. +app: *App, + +/// Connection to the D-Bus session bus that we'll use for all of our messaging. +dbus: ?*gio.DBusConnection = null, + +/// Mutex to protect modification of the entries map or the cleanup timer. +mutex: std.Thread.Mutex = .{}, + +/// Map to store data about any in-flight calls to the portal. +entries: std.AutoArrayHashMapUnmanaged(usize, *Entry) = .empty, + +/// Used to manage a timer to clean up any orphan entries in the map. +cleanup_timer: ?c_uint = null, + +/// Set to false during shutdown so callbacks stop touching internal state. +alive: bool = true, + +const RequestData = struct { + open_uri: *OpenURI, + token: usize, + kind: apprt.action.OpenUrl.Kind, + uri: [:0]const u8, + request_path: [:0]const u8, + + pub fn init( + alloc: Allocator, + open_uri: *OpenURI, + token: usize, + kind: apprt.action.OpenUrl.Kind, + uri: []const u8, + request_path: []const u8, + ) Allocator.Error!*RequestData { + const uri_copy = try alloc.dupeZ(u8, uri); + errdefer alloc.free(uri_copy); + + const request_path_copy = try alloc.dupeZ(u8, request_path); + errdefer alloc.free(request_path_copy); + + const data = try alloc.create(RequestData); + errdefer alloc.destroy(data); + + data.* = .{ + .open_uri = open_uri, + .token = token, + .kind = kind, + .uri = uri_copy, + .request_path = request_path_copy, + }; + + return data; + } + + pub fn deinit(self: *const RequestData, alloc: Allocator) void { + alloc.free(self.uri); + alloc.free(self.request_path); + } +}; + +/// Data about any in-flight calls to the portal. +pub const Entry = struct { + /// When the request started. + start: std.time.Instant, + /// A token used by the portal to identify requests and responses. The + /// actual format of the token does not really matter as long as it can be + /// used as part of a D-Bus object path. `usize` was chosen since it's easy + /// to hash and to generate random tokens. + token: usize, + /// The "kind" of URI. Unused here, but we may need to pass it on to the + /// fallback URL opener if the D-Bus method fails. + kind: apprt.action.OpenUrl.Kind, + /// A copy of the URI that we are opening. We need our own copy since the + /// method calls are asynchronous and the original may have been freed by + /// the time we need it. + uri: [:0]const u8, + /// Used to manage a subscription to a D-Bus signal, which is how the XDG + /// Portal reports results of the method call. + subscription: ?c_uint = null, + + pub fn deinit(self: *const Entry, alloc: Allocator) void { + alloc.free(self.uri); + } +}; + +pub const Errors = error{ + /// Could not get a D-Bus connection + DBusConnectionRequired, + /// The D-Bus connection did not have a unique name. This _should_ be + /// impossible, but is handled for safety's sake. + NoDBusUniqueName, + /// The system was unable to give us the time. + TimerUnavailable, +}; + +pub fn init(app: *App) OpenURI { + return .{ + .app = app, + }; +} + +pub fn setDbusConnection(self: *OpenURI, dbus: ?*gio.DBusConnection) void { + self.dbus = dbus; +} + +pub fn deinit(self: *OpenURI) void { + const alloc = self.app.app.allocator(); + + self.mutex.lock(); + defer self.mutex.unlock(); + + if (!self.alive) return; + self.alive = false; + + self.stopCleanupTimer(); + + for (self.entries.entries.items(.value)) |entry| { + self.unsubscribeFromResponse(entry); + destroyEntry(alloc, entry); + } + + self.entries.deinit(alloc); + self.entries = .empty; + self.dbus = null; +} + +/// Send the D-Bus method call to the XDG Desktop portal. The result of the +/// method call will be reported asynchronously. +pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || Errors)!void { + const alloc = self.app.app.allocator(); + const dbus = self.dbus orelse return error.DBusConnectionRequired; + + const token = portal.generateToken(); + const request_path = try portal.getRequestPath(alloc, dbus, token); + defer alloc.free(request_path); + + const request = try RequestData.init(alloc, self, token, value.kind, value.url, request_path); + errdefer { + request.deinit(alloc); + alloc.destroy(request); + } + + self.mutex.lock(); + defer self.mutex.unlock(); + + // Create an entry that is used to track the results of the D-Bus method + // call. + const entry = entry: { + const entry = try alloc.create(Entry); + errdefer alloc.destroy(entry); + entry.* = .{ + .start = std.time.Instant.now() catch return error.TimerUnavailable, + .token = token, + .kind = value.kind, + .uri = try alloc.dupeZ(u8, value.url), + }; + errdefer entry.deinit(alloc); + try self.entries.putNoClobber(alloc, token, entry); + break :entry entry; + }; + + errdefer { + _ = self.entries.swapRemove(token); + destroyEntry(alloc, entry); + } + + self.startCleanupTimer(); + self.subscribeToResponse(entry, dbus, request_path.ptr); + self.sendRequest(entry, dbus, request); +} + +/// Subscribe to the D-Bus signal that will contain the results of our method +/// call to the portal. This must be called with the mutex locked. +fn subscribeToResponse( + self: *OpenURI, + entry: *Entry, + dbus: *gio.DBusConnection, + request_path: [*:0]const u8, +) void { + assert(!self.mutex.tryLock()); + + if (entry.subscription != null) return; + + entry.subscription = dbus.signalSubscribe( + null, + "org.freedesktop.portal.Request", + "Response", + request_path, + null, + .{}, + responseReceived, + self, + null, + ); +} + +/// Unsubscribe to the D-Bus signal that contains the result of the method call. +/// This will prevent a response from being processed multiple times. This must +/// be called when the mutex is locked. +fn unsubscribeFromResponse(self: *OpenURI, entry: *Entry) void { + assert(!self.mutex.tryLock()); + + // Unsubscribe from the response signal + if (entry.subscription) |subscription| { + const dbus = self.dbus orelse { + entry.subscription = null; + log.warn("unable to unsubscribe open uri response without dbus connection", .{}); + return; + }; + dbus.signalUnsubscribe(subscription); + entry.subscription = null; + } +} + +fn destroyEntry(alloc: Allocator, entry: *Entry) void { + entry.deinit(alloc); + alloc.destroy(entry); +} + +fn failRequest(self: *OpenURI, token: usize) ?*Entry { + self.mutex.lock(); + defer self.mutex.unlock(); + + if (!self.alive) return null; + + const entry = (self.entries.fetchSwapRemove(token) orelse return null).value; + self.unsubscribeFromResponse(entry); + return entry; +} + +fn failRequestAndFallback(self: *OpenURI, request: *const RequestData) void { + const alloc = self.app.app.allocator(); + const entry = self.failRequest(request.token) orelse return; + defer destroyEntry(alloc, entry); + + self.app.app.openUrlFallback(request.kind, request.uri); +} + +/// Send the D-Bus method call to the portal. The mutex must be locked when this +/// is called. +fn sendRequest( + self: *OpenURI, + entry: *Entry, + dbus: *gio.DBusConnection, + request: *RequestData, +) void { + assert(!self.mutex.tryLock()); + + const payload = payload: { + const builder_type = glib.VariantType.new("(ssa{sv})"); + defer builder_type.free(); + + // Initialize our builder to build up our parameters + var builder: glib.VariantBuilder = undefined; + builder.init(builder_type); + + // parent window - empty string means we have no window + builder.add("s", ""); + + // URI to open + builder.add("s", entry.uri.ptr); + + // Options + { + const options = glib.VariantType.new("a{sv}"); + defer options.free(); + + builder.open(options); + defer builder.close(); + + { + const option = glib.VariantType.new("{sv}"); + defer option.free(); + + builder.open(option); + defer builder.close(); + + builder.add("s", "handle_token"); + + var token_buf: portal.TokenBuffer = undefined; + const token = portal.formatToken(&token_buf, entry.token); + + const handle_token = glib.Variant.newString(token.ptr); + builder.add("v", handle_token); + } + { + const option = glib.VariantType.new("{sv}"); + defer option.free(); + + builder.open(option); + defer builder.close(); + + builder.add("s", "writable"); + + const writable = glib.Variant.newBoolean(@intFromBool(false)); + builder.add("v", writable); + } + { + const option = glib.VariantType.new("{sv}"); + defer option.free(); + + builder.open(option); + defer builder.close(); + + builder.add("s", "ask"); + + const ask = glib.Variant.newBoolean(@intFromBool(false)); + builder.add("v", ask); + } + } + + break :payload builder.end(); + }; + + // We're expecting an object path back from the method call. + const reply_type = glib.VariantType.new("(o)"); + defer reply_type.free(); + + dbus.call( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.OpenURI", + "OpenURI", + payload, + reply_type, + .{}, + -1, + null, + requestCallback, + request, + ); +} + +/// Process the result of the original method call. Receiving this result does +/// not indicate that the that the method call succeeded but it may contain an +/// error message that is useful to log for debugging purposes. +fn requestCallback( + source: ?*gobject.Object, + result: *gio.AsyncResult, + ud: ?*anyopaque, +) callconv(.c) void { + const request: *RequestData = @ptrCast(@alignCast(ud orelse return)); + const self = request.open_uri; + const alloc = self.app.app.allocator(); + defer { + request.deinit(alloc); + alloc.destroy(request); + } + + const dbus = gobject.ext.cast(gio.DBusConnection, source orelse { + log.err("Open URI request finished without a D-Bus source object", .{}); + self.failRequestAndFallback(request); + return; + }) orelse { + log.err("Open URI request finished with an unexpected source object", .{}); + self.failRequestAndFallback(request); + return; + }; + + var err_: ?*glib.Error = null; + defer if (err_) |err| err.free(); + + const reply_ = dbus.callFinish(result, &err_); + + if (err_) |err| { + log.err("Open URI request failed={s} ({})", .{ + err.f_message orelse "(unknown)", + err.f_code, + }); + self.failRequestAndFallback(request); + return; + } + + const reply = reply_ orelse { + log.err("D-Bus method call returned a null value!", .{}); + self.failRequestAndFallback(request); + return; + }; + defer reply.unref(); + + const reply_type = glib.VariantType.new("(o)"); + defer reply_type.free(); + + if (reply.isOfType(reply_type) == 0) { + log.warn("Reply from D-Bus method call does not contain an object path!", .{}); + self.failRequestAndFallback(request); + return; + } + + var object_path: [*:0]const u8 = undefined; + reply.get("(&o)", &object_path); + + const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse { + log.warn("Unable to parse token from the object path {s}", .{object_path}); + self.failRequestAndFallback(request); + return; + }; + + if (token != request.token) { + log.warn("Open URI request returned mismatched token expected={x} actual={x}", .{ + request.token, + token, + }); + self.failRequestAndFallback(request); + return; + } + + self.mutex.lock(); + defer self.mutex.unlock(); + + if (!self.alive) return; + + const entry = self.entries.get(token) orelse return; + if (std.mem.eql(u8, request.request_path, std.mem.span(object_path))) return; + + log.debug("updating open uri request path old={s} new={s}", .{ + request.request_path, + object_path, + }); + self.unsubscribeFromResponse(entry); + self.subscribeToResponse(entry, dbus, object_path); +} + +/// Handle the response signal from the portal. This should contain the actual +/// results of the method call (success or failure). +fn responseReceived( + _: *gio.DBusConnection, + _: ?[*:0]const u8, + object_path: [*:0]const u8, + _: [*:0]const u8, + _: [*:0]const u8, + params: *glib.Variant, + ud: ?*anyopaque, +) callconv(.c) void { + const self: *OpenURI = @ptrCast(@alignCast(ud orelse { + log.err("OpenURI response received with null userdata", .{}); + return; + })); + + const alloc = self.app.app.allocator(); + + const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse { + log.warn("invalid object path: {s}", .{std.mem.span(object_path)}); + return; + }; + + self.mutex.lock(); + defer self.mutex.unlock(); + + if (!self.alive) return; + + const entry = (self.entries.fetchSwapRemove(token) orelse { + log.warn("no entry for token {x}", .{token}); + return; + }).value; + + defer destroyEntry(alloc, entry); + + self.unsubscribeFromResponse(entry); + + var response: u32 = 0; + var results: ?*glib.Variant = null; + defer if (results) |variant| variant.unref(); + params.get("(u@a{sv})", &response, &results); + + switch (response) { + 0 => { + log.debug("open uri successful", .{}); + }, + 1 => { + log.debug("open uri request was cancelled by the user", .{}); + }, + 2 => { + log.warn("open uri request ended unexpectedly", .{}); + self.app.app.openUrlFallback(entry.kind, entry.uri); + }, + else => { + log.err("unrecognized response code={}", .{response}); + self.app.app.openUrlFallback(entry.kind, entry.uri); + }, + } +} + +/// Wait this number of seconds and then clean up any orphaned entries. +const cleanup_timeout = 30; + +/// If there is an active cleanup timer, cancel it. This must be called with the +/// mutex locked +fn stopCleanupTimer(self: *OpenURI) void { + assert(!self.mutex.tryLock()); + + if (self.cleanup_timer) |timer| { + if (glib.Source.remove(timer) == 0) { + log.warn("unable to remove cleanup timer source={d}", .{timer}); + } + self.cleanup_timer = null; + } +} + +/// Start a timer to clean up any entries that have not received a timely +/// response. If there is already a timer it will be stopped and replaced with a +/// new one. This must be called with the mutex locked. +fn startCleanupTimer(self: *OpenURI) void { + assert(!self.mutex.tryLock()); + + self.stopCleanupTimer(); + self.cleanup_timer = glib.timeoutAddSeconds(cleanup_timeout + 1, cleanup, self); +} + +/// The cleanup timer is used to free up any entries that may have failed +/// to get a response in a timely manner. +fn cleanup(ud: ?*anyopaque) callconv(.c) c_int { + const self: *OpenURI = @ptrCast(@alignCast(ud orelse { + log.warn("cleanup called with null userdata", .{}); + return @intFromBool(glib.SOURCE_REMOVE); + })); + + const alloc = self.app.app.allocator(); + + self.mutex.lock(); + defer self.mutex.unlock(); + + self.cleanup_timer = null; + if (!self.alive) return @intFromBool(glib.SOURCE_REMOVE); + + const now = std.time.Instant.now() catch { + // `now()` should never fail, but if it does, don't crash, just return. + // This might cause a small memory leak in rare circumstances but it + // should get cleaned up the next time a URL is clicked. + return @intFromBool(glib.SOURCE_REMOVE); + }; + + loop: while (true) { + for (self.entries.entries.items(.value)) |entry| { + if (now.since(entry.start) > cleanup_timeout * std.time.ns_per_s) { + log.warn("open uri request timed out token={x}", .{entry.token}); + self.unsubscribeFromResponse(entry); + _ = self.entries.swapRemove(entry.token); + self.app.app.openUrlFallback(entry.kind, entry.uri); + destroyEntry(alloc, entry); + continue :loop; + } + } + break :loop; + } + + return @intFromBool(glib.SOURCE_REMOVE); +} diff --git a/src/apprt/gtk/ui/1.2/resize-overlay.blp b/src/apprt/gtk/ui/1.2/resize-overlay.blp index 5c4a94a8fd8..a05986b4a49 100644 --- a/src/apprt/gtk/ui/1.2/resize-overlay.blp +++ b/src/apprt/gtk/ui/1.2/resize-overlay.blp @@ -4,6 +4,10 @@ using Adw 1; // type in zig-gobject. template $GhosttyResizeOverlay: Adw.Bin { visible: false; + can-focus: false; + can-target: false; + focusable: false; + focus-on-click: false; duration: 750; first-delay: 250; overlay-halign: center; @@ -16,10 +20,12 @@ template $GhosttyResizeOverlay: Adw.Bin { "resize-overlay", ] + can-focus: false; + can-target: false; focusable: false; focus-on-click: false; - justify: center; selectable: false; + justify: center; halign: bind template.overlay-halign; valign: bind template.overlay-valign; } 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/surface-scrolled-window.blp b/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp index 722c4427b3f..75582a89ff1 100644 --- a/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp +++ b/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp @@ -4,7 +4,7 @@ using Adw 1; template $GhostttySurfaceScrolledWindow: Adw.Bin { notify::surface => $notify_surface(); - Gtk.ScrolledWindow { + Gtk.ScrolledWindow scrolled_window { hscrollbar-policy: never; vscrollbar-policy: bind $scrollbar_policy(template.config) as ; } 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/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index 3c1da2b2185..d409d6c2688 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -46,9 +46,9 @@ pub const App = union(Protocol) { return .{ .none = .{} }; } - pub fn deinit(self: *App, alloc: Allocator) void { + pub fn deinit(self: *App) void { switch (self.*) { - inline else => |*v| v.deinit(alloc), + inline else => |*v| v.deinit(), } } @@ -117,9 +117,9 @@ pub const Window = union(Protocol) { }; } - pub fn deinit(self: *Window, alloc: Allocator) void { + pub fn deinit(self: *Window) void { switch (self.*) { - inline else => |*v| v.deinit(alloc), + inline else => |*v| v.deinit(), } } diff --git a/src/apprt/gtk/winproto/BlurRegion.zig b/src/apprt/gtk/winproto/BlurRegion.zig new file mode 100644 index 00000000000..f3041dae087 --- /dev/null +++ b/src/apprt/gtk/winproto/BlurRegion.zig @@ -0,0 +1,187 @@ +const BlurRegion = @This(); +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const gobject = @import("gobject"); +const gdk = @import("gdk"); +const gtk = @import("gtk"); + +const Window = @import("../winproto.zig").Window; +const ApprtWindow = @import("../class/window.zig").Window; + +slices: std.ArrayList(Slice), + +/// A rectangular slice of the blur region. +// Marked `extern` since we want to be able to use this in X11 directly. +pub const Slice = extern struct { + x: Pos, + y: Pos, + width: Pos, + height: Pos, +}; + +// X11 compatibility. Ideally this should just be an `i32` like Wayland, +// but XLib sucks +const Pos = c_long; + +pub const empty: BlurRegion = .{ + .slices = .empty, +}; + +pub fn deinit(self: *BlurRegion, alloc: Allocator) void { + self.slices.deinit(alloc); + self.slices = .empty; +} + +// Calculate the blur regions for a window. +// +// Since we have rounded corners by default, we need to carve out the +// pixels on each corner to avoid the "korners bug". +// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134) +pub fn calcForWindow( + alloc: Allocator, + window: *ApprtWindow, + csd: bool, + to_device_coordinates: bool, +) Allocator.Error!BlurRegion { + const native = window.as(gtk.Native); + const surface = native.getSurface() orelse return .empty; + + var slices: std.ArrayList(Slice) = .empty; + errdefer slices.deinit(alloc); + + // Calculate the primary blur region + // (the one that covers most of the screen). + // It's easier to do this inside a vector since we have to scale + // everything by the scale factor anyways. + + // NOTE(pluiedev): CSDs are a f--king mistake. + // Please, GNOME, stop this nonsense of making a window ~30% bigger + // internally than how they really are just for your shadows and + // rounded corners and all that fluff. Please. I beg of you. + const x: Pos, const y: Pos = off: { + var x: f64 = 0; + var y: f64 = 0; + native.getSurfaceTransform(&x, &y); + // Slightly inset the corners if we're using CSDs + if (csd) { + x += 1; + y += 1; + } + break :off .{ @intFromFloat(x), @intFromFloat(y) }; + }; + + var width = @as(Pos, surface.getWidth()); + var height = @as(Pos, surface.getHeight()); + + // Trim off the offsets. Be careful not to get negative. + width -= x * 2; + height -= y * 2; + if (width <= 0 or height <= 0) return .empty; + + // Empirically determined. + const are_corners_rounded = rounded: { + // This cast should always succeed as all of our windows + // should be toplevel. If this fails, something very strange + // is going on. + const toplevel = gobject.ext.cast( + gdk.Toplevel, + surface, + ) orelse break :rounded false; + + const state = toplevel.getState(); + if (state.fullscreen or state.maximized or state.tiled) + break :rounded false; + + break :rounded csd; + }; + + const new_slices = try approxRoundedRect( + alloc, + x, + y, + width, + height, + // See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html#window-radius + if (are_corners_rounded) 15 else 0, + ); + + if (to_device_coordinates) { + // Transform surface coordinates to device coordinates. + const sf = surface.getScaleFactor(); + for (new_slices.items) |*s| { + s.x *= sf; + s.y *= sf; + s.width *= sf; + s.height *= sf; + } + } + + return .{ .slices = new_slices }; +} + +/// Whether two sets of blur regions are equal. +pub fn eql(self: BlurRegion, other: BlurRegion) bool { + if (self.slices.items.len != other.slices.items.len) return false; + for (self.slices.items, other.slices.items) |this, that| { + if (!std.meta.eql(this, that)) return false; + } + return true; +} + +/// Approximate a rounded rectangle with many smaller rectangles. +fn approxRoundedRect( + alloc: Allocator, + x: Pos, + y: Pos, + width: Pos, + height: Pos, + radius: Pos, +) Allocator.Error!std.ArrayList(Slice) { + const r_f: f32 = @floatFromInt(radius); + + var slices: std.ArrayList(Slice) = .empty; + errdefer slices.deinit(alloc); + + // Add the central rectangle + try slices.append(alloc, .{ + .x = x, + .y = y + radius, + .width = width, + .height = height - 2 * radius, + }); + + // Add the corner rows. This is honestly quite cursed. + var row: Pos = 0; + while (row < radius) : (row += 1) { + // y distance from this row to the center corner circle + const dy = @as(f32, @floatFromInt(radius - row)) - 0.5; + + // x distance - as given by the definition of a circle + const dx = @sqrt(r_f * r_f - dy * dy); + + // How much each row should be offset, rounded to an integer + const row_x: Pos = @intFromFloat(r_f - @round(dx + 0.5)); + + // Remove the offset from both ends + const row_w = width - 2 * row_x; + + // Top slice + try slices.append(alloc, .{ + .x = x + row_x, + .y = y + row, + .width = row_w, + .height = 1, + }); + + // Bottom slice + try slices.append(alloc, .{ + .x = x + row_x, + .y = y + height - 1 - row, + .width = row_w, + .height = 1, + }); + } + + return slices; +} diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index ed69736f81b..950ee0f373a 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -19,9 +19,8 @@ pub const App = struct { return null; } - pub fn deinit(self: *App, alloc: Allocator) void { + pub fn deinit(self: *App) void { _ = self; - _ = alloc; } pub fn eventMods( @@ -47,9 +46,8 @@ pub const Window = struct { return .{}; } - pub fn deinit(self: Window, alloc: Allocator) void { + pub fn deinit(self: *Window) void { _ = self; - _ = alloc; } pub fn updateConfigEvent( diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index ec02fbee536..12c7fb8a218 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -7,49 +7,26 @@ const gdk_wayland = @import("gdk_wayland"); const gobject = @import("gobject"); const gtk = @import("gtk"); const layer_shell = @import("gtk4-layer-shell"); + const wayland = @import("wayland"); +const wl = wayland.client.wl; +const ext = wayland.client.ext; +const kde = wayland.client.kde; +const org = wayland.client.org; +const xdg = wayland.client.xdg; const Config = @import("../../../config.zig").Config; +const Globals = @import("wayland/Globals.zig"); const input = @import("../../../input.zig"); const ApprtWindow = @import("../class/window.zig").Window; - -const wl = wayland.client.wl; -const org = wayland.client.org; -const xdg = wayland.client.xdg; +const BlurRegion = @import("BlurRegion.zig"); const log = std.log.scoped(.winproto_wayland); /// Wayland state that contains application-wide Wayland objects (e.g. wl_display). pub const App = struct { display: *wl.Display, - context: *Context, - - const Context = struct { - kde_blur_manager: ?*org.KdeKwinBlurManager = null, - - // FIXME: replace with `zxdg_decoration_v1` once GTK merges - // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 - kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null, - - kde_slide_manager: ?*org.KdeKwinSlideManager = null, - - default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, - - xdg_activation: ?*xdg.ActivationV1 = null, - - /// Whether the xdg_wm_dialog_v1 protocol is present. - /// - /// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user - /// creates a quick terminal, and we need to ensure this fails - /// gracefully if this situation occurs. - /// - /// FIXME: This is a temporary workaround - we should remove this when - /// all of our supported distros drop support for affected old - /// gtk4-layer-shell versions. - /// - /// See https://github.com/wmww/gtk4-layer-shell/issues/50 - xdg_wm_dialog_present: bool = false, - }; + globals: *Globals, pub fn init( alloc: Allocator, @@ -69,34 +46,17 @@ pub const App = struct { gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay, )); - // Create our context for our callbacks so we have a stable pointer. - // Note: at the time of writing this comment, we don't really need - // a stable pointer, but it's too scary that we'd need one in the future - // and not have it and corrupt memory or something so let's just do it. - const context = try alloc.create(Context); - errdefer alloc.destroy(context); - context.* = .{}; - - // Get our display registry so we can get all the available interfaces - // and bind to what we need. - const registry = try display.getRegistry(); - registry.setListener(*Context, registryListener, context); - if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; - - // Do another round-trip to get the default decoration mode - if (context.kde_decoration_manager) |deco_manager| { - deco_manager.setListener(*Context, decoManagerListener, context); - if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; - } + const globals: *Globals = try .init(alloc, display); + errdefer globals.deinit(); return .{ .display = display, - .context = context, + .globals = globals, }; } - pub fn deinit(self: *App, alloc: Allocator) void { - alloc.destroy(self.context); + pub fn deinit(self: *App) void { + self.globals.deinit(); } pub fn eventMods( @@ -108,118 +68,23 @@ pub const App = struct { } pub fn supportsQuickTerminal(self: App) bool { + _ = self; if (!layer_shell.isSupported()) { log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{}); return false; } - if (self.context.xdg_wm_dialog_present and - layer_shell.getLibraryVersion().order(.{ - .major = 1, - .minor = 0, - .patch = 4, - }) == .lt) - { - log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{}); - return false; - } - return true; } - pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void { + pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void { const window = apprt_window.as(gtk.Window); layer_shell.initForWindow(window); - } - fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type { - // Globals should be optional pointers - const T = switch (@typeInfo(field.type)) { - .optional => |o| switch (@typeInfo(o.child)) { - .pointer => |v| v.child, - else => return null, - }, - else => return null, - }; - - // Only process Wayland interfaces - if (!@hasDecl(T, "interface")) return null; - return T; - } - - fn registryListener( - registry: *wl.Registry, - event: wl.Registry.Event, - context: *Context, - ) void { - const ctx_fields = @typeInfo(Context).@"struct".fields; - - switch (event) { - .global => |v| { - log.debug("found global {s}", .{v.interface}); - - // We don't actually do anything with this other than checking - // for its existence, so we process this separately. - if (std.mem.orderZ( - u8, - v.interface, - "xdg_wm_dialog_v1", - ) == .eq) { - context.xdg_wm_dialog_present = true; - return; - } - - inline for (ctx_fields) |field| { - const T = getInterfaceType(field) orelse continue; - - if (std.mem.orderZ( - u8, - v.interface, - T.interface.name, - ) == .eq) { - log.debug("matched {}", .{T}); - - @field(context, field.name) = registry.bind( - v.name, - T, - T.generated_version, - ) catch |err| { - log.warn( - "error binding interface {s} error={}", - .{ v.interface, err }, - ); - return; - }; - } - } - }, - - // This should be a rare occurrence, but in case a global - // is suddenly no longer available, we destroy and unset it - // as the protocol mandates. - .global_remove => |v| remove: { - inline for (ctx_fields) |field| { - if (getInterfaceType(field) == null) continue; - const global = @field(context, field.name) orelse break :remove; - if (global.getId() == v.name) { - global.destroy(); - @field(context, field.name) = null; - } - } - }, - } - } - - fn decoManagerListener( - _: *org.KdeKwinServerDecorationManager, - event: org.KdeKwinServerDecorationManager.Event, - context: *Context, - ) void { - switch (event) { - .default_mode => |mode| { - context.default_deco_mode = @enumFromInt(mode.mode); - }, - } + // Set target monitor based on config (null lets compositor decide) + const monitor = resolveQuickTerminalMonitor(self.globals, apprt_window); + defer if (monitor) |v| v.unref(); + layer_shell.setMonitor(window, monitor); } }; @@ -231,10 +96,10 @@ pub const Window = struct { surface: *wl.Surface, /// The context from the app where we can load our Wayland interfaces. - app_context: *App.Context, + globals: *Globals, - /// A token that, when present, indicates that the window is blurred. - blur_token: ?*org.KdeKwinBlur = null, + /// Object that controls background effects like background blur. + bg_effect: ?*ext.BackgroundEffectSurfaceV1 = null, /// Object that controls the decoration mode (client/server/auto) /// of the window. @@ -248,6 +113,8 @@ pub const Window = struct { /// requesting attention from the user. activation_token: ?*xdg.ActivationTokenV1 = null, + blur_region: BlurRegion = .empty, + pub fn init( alloc: Allocator, app: *App, @@ -272,7 +139,7 @@ pub const Window = struct { // Get our decoration object so we can control the // CSD vs SSD status of this surface. const deco: ?*org.KdeKwinServerDecoration = deco: { - const mgr = app.context.kde_decoration_manager orelse + const mgr = app.globals.get(.kde_decoration_manager) orelse break :deco null; const deco: *org.KdeKwinServerDecoration = mgr.create( @@ -285,6 +152,20 @@ pub const Window = struct { break :deco deco; }; + const bg_effect: ?*ext.BackgroundEffectSurfaceV1 = bg: { + const mgr = app.globals.get(.ext_background_effect) orelse + break :bg null; + + const bg_effect: *ext.BackgroundEffectSurfaceV1 = mgr.getBackgroundEffect( + wl_surface, + ) catch |err| { + log.warn("could not create background effect object={}", .{err}); + break :bg null; + }; + + break :bg bg_effect; + }; + if (apprt_window.isQuickTerminal()) { _ = gdk.Surface.signals.enter_monitor.connect( gdk_surface, @@ -298,26 +179,31 @@ pub const Window = struct { return .{ .apprt_window = apprt_window, .surface = wl_surface, - .app_context = app.context, + .globals = app.globals, .decoration = deco, + .bg_effect = bg_effect, }; } - pub fn deinit(self: Window, alloc: Allocator) void { - _ = alloc; - if (self.blur_token) |blur| blur.release(); + pub fn deinit(self: *Window) void { + self.blur_region.deinit(self.globals.alloc); + if (self.bg_effect) |bg| bg.destroy(); if (self.decoration) |deco| deco.release(); if (self.slide) |slide| slide.release(); } - pub fn resizeEvent(_: *Window) !void {} + pub fn resizeEvent(self: *Window) !void { + self.syncBlur() catch |err| { + log.err("failed to sync blur={}", .{err}); + }; + } pub fn syncAppearance(self: *Window) !void { self.syncBlur() catch |err| { log.err("failed to sync blur={}", .{err}); }; self.syncDecoration() catch |err| { - log.err("failed to sync blur={}", .{err}); + log.err("failed to sync decoration={}", .{err}); }; if (self.apprt_window.isQuickTerminal()) { @@ -333,7 +219,7 @@ pub const Window = struct { // If we support SSDs, then we should *not* enable CSDs if we prefer SSDs. // However, if we do not support SSDs (e.g. GNOME) then we should enable // CSDs even if the user prefers SSDs. - .Server => if (self.app_context.kde_decoration_manager) |_| false else true, + .Server => if (self.globals.get(.kde_decoration_manager)) |_| false else true, .None => false, else => unreachable, }; @@ -345,7 +231,7 @@ pub const Window = struct { } pub fn setUrgent(self: *Window, urgent: bool) !void { - const activation = self.app_context.xdg_activation orelse return; + const activation = self.globals.get(.xdg_activation) orelse return; // If there already is a token, destroy and unset it if (self.activation_token) |token| token.destroy(); @@ -361,28 +247,47 @@ pub const Window = struct { /// Update the blur state of the window. fn syncBlur(self: *Window) !void { - const manager = self.app_context.kde_blur_manager orelse return; + const compositor = self.globals.get(.compositor) orelse return; + const bg = self.bg_effect orelse return; + if (!self.globals.state.bg_effect_capabilities.blur) return; + const config = if (self.apprt_window.getConfig()) |v| v.get() else return; const blur = config.@"background-blur"; - if (self.blur_token) |tok| { - // Only release token when transitioning from blurred -> not blurred - if (!blur.enabled()) { - manager.unset(self.surface); - tok.release(); - self.blur_token = null; - } - } else { - // Only acquire token when transitioning from not blurred -> blurred - if (blur.enabled()) { - const tok = try manager.create(self.surface); - tok.commit(); - self.blur_token = tok; - } + if (!blur.enabled()) { + self.blur_region.deinit(self.globals.alloc); + bg.setBlurRegion(null); + return; + } + + var region: BlurRegion = try .calcForWindow( + self.globals.alloc, + self.apprt_window, + self.clientSideDecorationEnabled(), + false, + ); + errdefer region.deinit(self.globals.alloc); + + if (region.eql(self.blur_region)) { + // Region didn't change. Don't do anything. + region.deinit(self.globals.alloc); + return; } + + const wl_region = try compositor.createRegion(); + errdefer if (wl_region) |r| r.destroy(); + for (region.slices.items) |s| wl_region.add( + @intCast(s.x), + @intCast(s.y), + @intCast(s.width), + @intCast(s.height), + ); + + bg.setBlurRegion(wl_region); + self.blur_region = region; } fn syncDecoration(self: *Window) !void { @@ -395,7 +300,7 @@ pub const Window = struct { fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode { return switch (self.apprt_window.getWindowDecoration()) { - .auto => self.app_context.default_deco_mode orelse .Client, + .auto => self.globals.state.default_deco_mode orelse .Client, .client => .Client, .server => .Server, .none => .None, @@ -417,6 +322,12 @@ pub const Window = struct { }); layer_shell.setNamespace(window, config.@"gtk-quick-terminal-namespace"); + // Re-resolve the target monitor on every sync so that config reloads + // and primary-output changes take effect without recreating the window. + const target_monitor = resolveQuickTerminalMonitor(self.globals, self.apprt_window); + defer if (target_monitor) |v| v.unref(); + layer_shell.setMonitor(window, target_monitor); + layer_shell.setKeyboardMode( window, switch (config.@"quick-terminal-keyboard-interactivity") { @@ -457,7 +368,7 @@ pub const Window = struct { if (self.slide) |slide| slide.release(); self.slide = if (anchored_edge) |anchored| slide: { - const mgr = self.app_context.kde_slide_manager orelse break :slide null; + const mgr = self.globals.get(.kde_slide_manager) orelse break :slide null; const slide = mgr.create(self.surface) catch |err| { log.warn("could not create slide object={}", .{err}); @@ -486,8 +397,17 @@ pub const Window = struct { const window = apprt_window.as(gtk.Window); const config = if (apprt_window.getConfig()) |v| v.get() else return; + const resolved_monitor = resolveQuickTerminalMonitor( + apprt_window.winproto().wayland.globals, + apprt_window, + ); + defer if (resolved_monitor) |v| v.unref(); + + // Use the configured monitor for sizing if not in mouse mode. + const size_monitor = resolved_monitor orelse monitor; + var monitor_size: gdk.Rectangle = undefined; - monitor.getGeometry(&monitor_size); + size_monitor.getGeometry(&monitor_size); const dims = config.@"quick-terminal-size".calculate( config.@"quick-terminal-position", @@ -505,7 +425,7 @@ pub const Window = struct { event: xdg.ActivationTokenV1.Event, self: *Window, ) void { - const activation = self.app_context.xdg_activation orelse return; + const activation = self.globals.get(.xdg_activation) orelse return; const current_token = self.activation_token orelse return; if (token.getId() != current_token.getId()) { @@ -522,3 +442,45 @@ pub const Window = struct { } } }; + +/// Resolve the quick-terminal-screen config to a specific monitor. +/// Returns null to let the compositor decide (used for .mouse mode). +/// Caller owns the returned ref and must unref it. +fn resolveQuickTerminalMonitor( + globals: *Globals, + apprt_window: *ApprtWindow, +) ?*gdk.Monitor { + const config = if (apprt_window.getConfig()) |v| v.get() else return null; + + switch (config.@"quick-terminal-screen") { + .mouse => return null, + .main, .@"macos-menu-bar" => {}, + } + + const display = apprt_window.as(gtk.Widget).getDisplay(); + const monitors = display.getMonitors(); + + // Try to find the monitor matching the primary output name. + if (globals.state.primary_output_name) |stored_name| { + var i: u32 = 0; + while (monitors.getObject(i)) |item| : (i += 1) { + const monitor = gobject.ext.cast(gdk.Monitor, item) orelse { + item.unref(); + continue; + }; + if (monitor.getConnector()) |connector_z| { + if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) { + return monitor; + } + } + monitor.unref(); + } + } + + // Fall back to the first monitor in the list. + const first = monitors.getObject(0) orelse return null; + return gobject.ext.cast(gdk.Monitor, first) orelse { + first.unref(); + return null; + }; +} diff --git a/src/apprt/gtk/winproto/wayland/Globals.zig b/src/apprt/gtk/winproto/wayland/Globals.zig new file mode 100644 index 00000000000..83052cbebc7 --- /dev/null +++ b/src/apprt/gtk/winproto/wayland/Globals.zig @@ -0,0 +1,252 @@ +const Globals = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const wayland = @import("wayland"); +const wl = wayland.client.wl; +const ext = wayland.client.ext; +const kde = wayland.client.kde; +const org = wayland.client.org; +const xdg = wayland.client.xdg; + +const log = std.log.scoped(.winproto_wayland_globals); + +alloc: Allocator, +state: State, +map: std.EnumMap(Tag, Binding), + +/// Used in the initial roundtrip to determine whether more +/// roundtrips are required to fetch the initial state. +needs_roundtrip: bool = false, + +const Binding = struct { + // All globals can be casted into a wl.Proxy object. + proxy: *wl.Proxy, + name: u32, +}; + +pub const Tag = enum { + compositor, + ext_background_effect, + kde_decoration_manager, + kde_slide_manager, + kde_output_order, + xdg_activation, + + fn Type(comptime self: Tag) type { + return switch (self) { + .compositor => wl.Compositor, + .ext_background_effect => ext.BackgroundEffectManagerV1, + .kde_decoration_manager => org.KdeKwinServerDecorationManager, + .kde_slide_manager => org.KdeKwinSlideManager, + .kde_output_order => kde.OutputOrderV1, + .xdg_activation => xdg.ActivationV1, + }; + } +}; + +pub const State = struct { + /// Connector name of the primary output (e.g., "DP-1") as reported + /// by kde_output_order_v1. The first output in each priority list + /// is the primary. + primary_output_name: ?[:0]const u8 = null, + + /// Tracks the output order event cycle. Set to true after a `done` + /// event so the next `output` event is captured as the new primary. + /// Initialized to true so the first event after binding is captured. + output_order_done: bool = true, + + default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, + + bg_effect_capabilities: ext.BackgroundEffectManagerV1.Capability = .{}, + + /// Reset cached state derived from kde_output_order_v1. + fn resetOutputOrder(self: *State, alloc: Allocator) void { + if (self.primary_output_name) |name| alloc.free(name); + self.primary_output_name = null; + self.output_order_done = true; + } +}; + +pub fn init(alloc: Allocator, display: *wl.Display) !*Globals { + // We need to allocate here since the listener + // expects a stable memory address. + const self = try alloc.create(Globals); + self.* = .{ + .alloc = alloc, + .state = .{}, + .map = .{}, + }; + + const registry = try display.getRegistry(); + registry.setListener(*Globals, registryListener, self); + if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; + + // Do another roundtrip to process events emitted by globals we bound + // during registry discovery (e.g. default decoration mode, output + // order). Listeners are installed at bind time in registryListener. + if (self.needs_roundtrip) { + if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; + } + return self; +} + +pub fn deinit(self: *Globals) void { + if (self.state.primary_output_name) |name| self.alloc.free(name); + self.alloc.destroy(self); +} + +pub fn get(self: *const Globals, comptime tag: Tag) ?*tag.Type() { + const binding = self.map.get(tag) orelse return null; + return @ptrCast(binding.proxy); +} + +fn onGlobalAttached(self: *Globals, comptime tag: Tag) void { + // Install listeners immediately at bind time. This + // keeps listener setup and object lifetime in one + // place and also supports globals that appear later. + switch (tag) { + .ext_background_effect => { + const v = self.get(tag) orelse return; + v.setListener(*Globals, bgEffectListener, self); + self.needs_roundtrip = true; + }, + .kde_decoration_manager => { + const v = self.get(tag) orelse return; + v.setListener(*Globals, decoManagerListener, self); + self.needs_roundtrip = true; + }, + .kde_output_order => { + const v = self.get(tag) orelse return; + v.setListener(*Globals, outputOrderListener, self); + self.needs_roundtrip = true; + }, + else => {}, + } +} + +fn onGlobalRemoved(self: *Globals, tag: Tag) void { + switch (tag) { + .kde_output_order => self.state.resetOutputOrder(self.alloc), + else => {}, + } +} + +fn registryListener( + registry: *wl.Registry, + event: wl.Registry.Event, + self: *Globals, +) void { + switch (event) { + .global => |v| { + log.debug("found global {s}", .{v.interface}); + inline for (comptime std.meta.tags(Tag)) |tag| { + const T = tag.Type(); + if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) { + log.debug("matched {}", .{T}); + + const new_proxy = registry.bind( + v.name, + T, + T.generated_version, + ) catch |err| { + log.warn( + "error binding interface {s} error={}", + .{ v.interface, err }, + ); + return; + }; + + // If this global was already bound, + // then we also need to destroy the old binding. + if (self.map.get(tag)) |old| { + self.onGlobalRemoved(tag); + old.proxy.destroy(); + } + + self.map.put(tag, .{ + .proxy = @ptrCast(new_proxy), + .name = v.name, + }); + self.onGlobalAttached(tag); + } + } + }, + + // This should be a rare occurrence, but in case a global + // is suddenly no longer available, we destroy and unset it + // as the protocol mandates. + .global_remove => |v| { + var it = self.map.iterator(); + while (it.next()) |kv| { + if (kv.value.name != v.name) continue; + self.onGlobalRemoved(kv.key); + kv.value.proxy.destroy(); + self.map.remove(kv.key); + } + }, + } +} + +fn bgEffectListener( + _: *ext.BackgroundEffectManagerV1, + event: ext.BackgroundEffectManagerV1.Event, + self: *Globals, +) void { + switch (event) { + .capabilities => |cap| { + self.state.bg_effect_capabilities = cap.flags; + }, + } +} + +fn decoManagerListener( + _: *org.KdeKwinServerDecorationManager, + event: org.KdeKwinServerDecorationManager.Event, + self: *Globals, +) void { + switch (event) { + .default_mode => |mode| { + self.state.default_deco_mode = @enumFromInt(mode.mode); + }, + } +} + +fn outputOrderListener( + _: *kde.OutputOrderV1, + event: kde.OutputOrderV1.Event, + self: *Globals, +) void { + switch (event) { + .output => |v| { + // Only the first output event after a `done` is the new primary. + if (!self.state.output_order_done) return; + self.state.output_order_done = false; + + const name = std.mem.sliceTo(v.output_name, 0); + if (self.state.primary_output_name) |old| self.alloc.free(old); + + if (name.len == 0) { + self.state.primary_output_name = null; + log.warn("ignoring empty primary output name from kde_output_order_v1", .{}); + } else { + self.state.primary_output_name = self.alloc.dupeZ(u8, name) catch |err| { + self.state.primary_output_name = null; + log.warn("failed to allocate primary output name: {}", .{err}); + return; + }; + log.debug("primary output: {s}", .{name}); + } + }, + .done => { + if (self.state.output_order_done) { + // No output arrived since the previous done. Treat this as + // an empty update and drop any stale cached primary. + self.state.resetOutputOrder(self.alloc); + return; + } + self.state.output_order_done = true; + }, + } +} diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 1e73c613908..8109959dafb 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -19,6 +19,7 @@ pub const c = @cImport({ const input = @import("../../../input.zig"); const Config = @import("../../../config.zig").Config; const ApprtWindow = @import("../class/window.zig").Window; +const BlurRegion = @import("BlurRegion.zig"); const log = std.log.scoped(.gtk_x11); @@ -48,7 +49,7 @@ pub const App = struct { else "ghostty"; - // Set the X11 window class property (WM_CLASS) if are are on an X11 + // Set the X11 window class property (WM_CLASS) if we are on an X11 // display. // // Note that we also set the program name here using g_set_prgname. @@ -106,9 +107,8 @@ pub const App = struct { }; } - pub fn deinit(self: *App, alloc: Allocator) void { + pub fn deinit(self: *App) void { _ = self; - _ = alloc; } /// Checks for an immediate pending XKB state update event, and returns the @@ -170,13 +170,13 @@ pub const Window = struct { app: *App, apprt_window: *ApprtWindow, x11_surface: *gdk_x11.X11Surface, + alloc: Allocator, - blur_region: Region = .{}, + blur_region: BlurRegion = .empty, // Cache last applied values to avoid redundant X11 property updates. // Redundant property updates seem to cause some visual glitches // with some window managers: https://github.com/ghostty-org/ghostty/pull/8075 - last_applied_blur_region: ?Region = null, last_applied_decoration_hints: ?MotifWMHints = null, pub fn init( @@ -184,8 +184,6 @@ pub const Window = struct { app: *App, apprt_window: *ApprtWindow, ) !Window { - _ = alloc; - const surface = apprt_window.as(gtk.Native).getSurface() orelse return error.NotX11Surface; @@ -196,49 +194,31 @@ pub const Window = struct { return .{ .app = app, + .alloc = alloc, .apprt_window = apprt_window, .x11_surface = x11_surface, }; } - pub fn deinit(self: Window, alloc: Allocator) void { - _ = self; - _ = alloc; + pub fn deinit(self: *Window) void { + self.blur_region.deinit(self.alloc); } pub fn resizeEvent(self: *Window) !void { // The blur region must update with window resizes - try self.syncBlur(); + self.syncBlur() catch |err| { + log.err("failed to sync blur={}", .{err}); + }; } pub fn syncAppearance(self: *Window) !void { // The user could have toggled between CSDs and SSDs, // therefore we need to recalculate the blur region offset. - self.blur_region = blur: { - // NOTE(pluiedev): CSDs are a f--king mistake. - // Please, GNOME, stop this nonsense of making a window ~30% bigger - // internally than how they really are just for your shadows and - // rounded corners and all that fluff. Please. I beg of you. - var x: f64 = 0; - var y: f64 = 0; - - self.apprt_window.as(gtk.Native).getSurfaceTransform(&x, &y); - - // Transform surface coordinates to device coordinates. - const scale: f64 = @floatFromInt(self.apprt_window.as(gtk.Widget).getScaleFactor()); - x *= scale; - y *= scale; - - break :blur .{ - .x = @intFromFloat(x), - .y = @intFromFloat(y), - }; - }; self.syncBlur() catch |err| { - log.err("failed to synchronize blur={}", .{err}); + log.err("failed to sync blur={}", .{err}); }; self.syncDecorations() catch |err| { - log.err("failed to synchronize decorations={}", .{err}); + log.err("failed to sync decorations={}", .{err}); }; } @@ -250,53 +230,49 @@ pub const Window = struct { } fn syncBlur(self: *Window) !void { - // FIXME: This doesn't currently factor in rounded corners on Adwaita, - // which means that the blur region will grow slightly outside of the - // window borders. Unfortunately, actually calculating the rounded - // region can be quite complex without having access to existing APIs - // (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134) - // and I think it's not really noticeable enough to justify the effort. - // (Wayland also has this visual artifact anyway...) - - const gtk_widget = self.apprt_window.as(gtk.Widget); const config = if (self.apprt_window.getConfig()) |v| v.get() else return; // When blur is disabled, remove the property if it was previously set const blur = config.@"background-blur"; - if (!blur.enabled()) { - if (self.last_applied_blur_region != null) { - try self.deleteProperty(self.app.atoms.kde_blur); - self.last_applied_blur_region = null; - } - return; - } - - // Transform surface coordinates to device coordinates. - const scale = gtk_widget.getScaleFactor(); - self.blur_region.width = gtk_widget.getWidth() * scale; - self.blur_region.height = gtk_widget.getHeight() * scale; + var region: BlurRegion = if (blur.enabled()) + try .calcForWindow( + self.alloc, + self.apprt_window, + self.clientSideDecorationEnabled(), + true, + ) + else + .empty; + errdefer region.deinit(self.alloc); // Only update X11 properties when the blur region actually changes - if (self.last_applied_blur_region) |last| { - if (std.meta.eql(self.blur_region, last)) return; + if (region.eql(self.blur_region)) { + region.deinit(self.alloc); + return; } - log.debug("set blur={}, window xid={}, region={}", .{ - blur, - self.x11_surface.getXid(), - self.blur_region, - }); + if (region.slices.items.len > 0) { + log.debug("set blur={}, window xid={}, region={}", .{ + blur, + self.x11_surface.getXid(), + region, + }); + + try self.changeProperty( + BlurRegion.Slice, + self.app.atoms.kde_blur, + c.XA_CARDINAL, + ._32, + .{ .mode = .replace }, + region.slices.items, + ); + } else { + try self.deleteProperty(self.app.atoms.kde_blur); + } - try self.changeProperty( - Region, - self.app.atoms.kde_blur, - c.XA_CARDINAL, - ._32, - .{ .mode = .replace }, - &self.blur_region, - ); - self.last_applied_blur_region = self.blur_region; + self.blur_region.deinit(self.alloc); + self.blur_region = region; } fn syncDecorations(self: *Window) !void { @@ -336,7 +312,7 @@ pub const Window = struct { self.app.atoms.motif_wm_hints, ._32, .{ .mode = .replace }, - &hints, + &.{hints}, ); self.last_applied_decoration_hints = hints; } @@ -411,9 +387,11 @@ pub const Window = struct { options: struct { mode: PropertyChangeMode, }, - value: *T, + values: []const T, ) X11Error!void { - const data: format.bufferType() = @ptrCast(value); + const data: format.bufferType() = @ptrCast(@constCast(values)); + // The number of "words" that each element `T` occupies. + const words_per_elem = @divExact(@sizeOf(T), @sizeOf(format.elemType())); const status = c.XChangeProperty( @ptrCast(@alignCast(self.app.display)), @@ -423,7 +401,7 @@ pub const Window = struct { @intFromEnum(format), @intFromEnum(options.mode), data, - @divExact(@sizeOf(T), @sizeOf(format.elemType())), + @intCast(words_per_elem * values.len), ); // For some godforsaken reason Xlib alternates between @@ -499,13 +477,6 @@ const PropertyFormat = enum(c_int) { } }; -const Region = extern struct { - x: c_long = 0, - y: c_long = 0, - width: c_long = 0, - height: c_long = 0, -}; - // See Xm/MwmUtil.h, packaged with the Motif Window Manager const MotifWMHints = extern struct { flags: packed struct(c_ulong) { 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/structs.zig b/src/apprt/structs.zig index bf14b65a9c4..2c37dbd5ee3 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -12,6 +12,10 @@ pub const ContentScale = struct { pub const SurfaceSize = struct { width: u32, height: u32, + + pub fn eql(self: *const SurfaceSize, other: *const SurfaceSize) bool { + return self.width == other.width and self.height == other.height; + } }; /// The position of the cursor in pixels. 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/Benchmark.zig b/src/benchmark/Benchmark.zig index 0ca1544147b..41c3695d40e 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -131,12 +131,17 @@ pub const VTable = struct { }; test Benchmark { - // This test fails on FreeBSD so skip: + // This test fails on FreeBSD and Windows so skip: // // /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test) // try testing.expect(result.duration > 0); // ^ - if (builtin.os.tag == .freebsd) return error.SkipZigTest; + switch (builtin.os.tag) { + .freebsd, + .windows, + => return error.SkipZigTest, + else => {}, + } const testing = std.testing; const Simple = struct { 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/Config.zig b/src/build/Config.zig index 3a8a4e0c746..88968aab7fe 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -51,6 +51,7 @@ emit_bench: bool = false, emit_docs: bool = false, emit_exe: bool = false, emit_helpgen: bool = false, +emit_lib_vt: bool = false, emit_macos_app: bool = false, emit_terminfo: bool = false, emit_termcap: bool = false, @@ -60,6 +61,10 @@ emit_xcframework: bool = false, emit_webdata: bool = false, emit_unicode_table_gen: bool = false, +/// True when Ghostty is being built as a dependency of another project +/// rather than as the root project. +is_dep: bool = false, + /// Environmental properties env: std.process.EnvMap, @@ -78,6 +83,19 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { result = genericMacOSTarget(b, result.query.cpu_arch); } + // On Windows, default to the MSVC ABI so that produced COFF + // objects (including compiler_rt) are compatible with the MSVC + // linker. Zig defaults to the GNU ABI which produces objects + // with invalid COMDAT sections that MSVC rejects (LNK1143). + // Only override when no explicit ABI was requested. + if (result.result.os.tag == .windows and + result.query.abi == null) + { + var query = result.query; + query.abi = .msvc; + result = b.resolveTargetQuery(query); + } + // If we have no minimum OS version, we set the default based on // our tag. Not all tags have a minimum so this may be null. if (result.query.os_version_min == null) { @@ -87,6 +105,10 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { break :target result; }; + // Detect if Ghostty is a dependency of another project. + // dep_prefix is non-empty when this build is running as a dependency. + const is_dep = b.dep_prefix.len > 0; + // This is set to true when we're building a system package. For now // this is trivially detected using the "system_package_mode" bool // but we may want to make this more sophisticated in the future. @@ -109,6 +131,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { .optimize = optimize, .target = target, .wasm_target = wasm_target, + .is_dep = is_dep, .env = env, }; @@ -220,9 +243,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { const app_version = try std.SemanticVersion.parse(appVersion); // Is ghostty a dependency? If so, skip git detection. - // @src().file won't resolve from b.build_root unless ghostty - // is the project being built. - b.build_root.handle.access(@src().file, .{}) catch break :version .{ + if (is_dep) break :version .{ .major = app_version.major, .minor = app_version.minor, .patch = app_version.patch, @@ -314,11 +335,17 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { //--------------------------------------------------------------- // Artifacts to Emit + config.emit_lib_vt = b.option( + bool, + "emit-lib-vt", + "Set defaults for a libghostty-vt-only build (disables xcframework, macOS app, and docs).", + ) orelse false; + config.emit_exe = b.option( bool, "emit-exe", "Build and install main executables with 'build'", - ) orelse true; + ) orelse !config.emit_lib_vt; config.emit_test_exe = b.option( bool, @@ -352,7 +379,8 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { // If we are emitting any other artifacts then we default to false. if (config.emit_bench or config.emit_test_exe or - config.emit_helpgen) break :emit_docs false; + config.emit_helpgen or + config.emit_lib_vt) break :emit_docs false; // We always emit docs in system package mode. if (system_package) break :emit_docs true; @@ -401,7 +429,8 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { bool, "emit-xcframework", "Build and install the xcframework for the macOS library.", - ) orelse builtin.target.os.tag.isDarwin() and + ) orelse !config.emit_lib_vt and + builtin.target.os.tag.isDarwin() and target.result.os.tag == .macos and config.app_runtime == .none and (!config.emit_bench and @@ -412,7 +441,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { bool, "emit-macos-app", "Build and install the macOS app bundle.", - ) orelse config.emit_xcframework; + ) orelse !config.emit_lib_vt and config.emit_xcframework; //--------------------------------------------------------------- // System Packages @@ -516,6 +545,7 @@ pub fn terminalOptions(self: *const Config) TerminalBuildOptions { .simd = self.simd, .oniguruma = true, .c_abi = false, + .version = self.version, .slow_runtime_safety = switch (self.optimize) { .Debug => true, .ReleaseSafe, diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index 600aa4883cc..448047f4b73 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -18,17 +18,23 @@ archive_step: *std.Build.Step, check_step: *std.Build.Step, pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { + // The name prefix used for all paths in the archive. + const name = if (cfg.emit_lib_vt) "libghostty-vt" else "ghostty"; + // Get the resources we're going to inject into the source tarball. + // lib-vt doesn't need GTK resources or frame data. const alloc = b.allocator; var resources: std.ArrayListUnmanaged(Resource) = .empty; - { - const gtk = SharedDeps.gtkNgDistResources(b); - try resources.append(alloc, gtk.resources_c); - try resources.append(alloc, gtk.resources_h); - } - { - const framedata = GhosttyFrameData.distResources(b); - try resources.append(alloc, framedata.framedata); + if (!cfg.emit_lib_vt) { + { + const gtk = SharedDeps.gtkNgDistResources(b); + try resources.append(alloc, gtk.resources_c); + try resources.append(alloc, gtk.resources_h); + } + { + const framedata = GhosttyFrameData.distResources(b); + try resources.append(alloc, framedata.framedata); + } } // git archive to create the final tarball. "git archive" is the @@ -46,8 +52,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { const version = b.addWriteFiles().add("VERSION", b.fmt("{f}", .{cfg.version})); // --add-file uses the most recent --prefix to determine the path // in the archive to copy the file (the directory only). - git_archive.addArg(b.fmt("--prefix=ghostty-{f}/", .{ - cfg.version, + git_archive.addArg(b.fmt("--prefix={s}-{f}/", .{ + name, cfg.version, })); git_archive.addPrefixedFileArg("--add-file=", version); } @@ -65,8 +71,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // --add-file uses the most recent --prefix to determine the path // in the archive to copy the file (the directory only). - git_archive.addArg(b.fmt("--prefix=ghostty-{f}/{s}/", .{ - cfg.version, + git_archive.addArg(b.fmt("--prefix={s}-{f}/{s}/", .{ + name, cfg.version, std.fs.path.dirname(resource.dist).?, })); git_archive.addPrefixedFileArg("--add-file=", copied); @@ -77,19 +83,28 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // This is important. Standard source tarballs extract into // a directory named `project-version`. This is expected by // standard tooling such as debhelper and rpmbuild. - b.fmt("--prefix=ghostty-{f}/", .{cfg.version}), + b.fmt("--prefix={s}-{f}/", .{ name, cfg.version }), "-o", }); const output = git_archive.addOutputFileArg(b.fmt( - "ghostty-{f}.tar.gz", - .{cfg.version}, + "{s}-{f}.tar.gz", + .{ name, cfg.version }, )); git_archive.addArg("HEAD"); + // When building for lib-vt only, exclude large directories that + // are not needed to build libghostty-vt. This significantly reduces + // the size of the resulting archive. + if (cfg.emit_lib_vt) { + for (lib_vt_excludes) |exclude| { + git_archive.addArg(b.fmt(":(exclude){s}", .{exclude})); + } + } + // The install step to put the dist into the build directory. const install = b.addInstallFile( output, - b.fmt("dist/ghostty-{f}.tar.gz", .{cfg.version}), + b.fmt("dist/{s}-{f}.tar.gz", .{ name, cfg.version }), ); // The check step to ensure the archive works. @@ -100,8 +115,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // This is the root Ghostty source dir of the extracted source tarball. // i.e. this is way `build.zig` is. const extract_dir = check - .addOutputDirectoryArg("ghostty") - .path(b, b.fmt("ghostty-{f}", .{cfg.version})); + .addOutputDirectoryArg(name) + .path(b, b.fmt("{s}-{f}", .{ name, cfg.version })); // Check that tests pass within the extracted directory. This isn't // a fully hermetic test because we're sharing the Zig cache. In @@ -109,7 +124,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // in the interest of speed we don't do that for now and hope other // CI catches any issues. const check_test = step: { - const step = b.addSystemCommand(&.{ "zig", "build", "test" }); + // For lib-vt, we run the lib-vt tests instead of the full test suite. + const check_cmd = if (cfg.emit_lib_vt) + &[_][]const u8{ "zig", "build", "test-lib-vt", "-Demit-lib-vt=true" } + else + &[_][]const u8{ "zig", "build", "test" }; + const step = b.addSystemCommand(check_cmd); step.setCwd(extract_dir); // Must be set so that Zig knows that this command doesn't @@ -133,6 +153,23 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { check_test.step.dependOn(&check_path.step); } + // For lib-vt, also verify the CMake build works from the tarball. + if (cfg.emit_lib_vt) { + const cmake_build_dir = extract_dir.path(b, "cmake-build"); + const cmake_configure = b.addSystemCommand(&.{ "cmake", "-B" }); + cmake_configure.addDirectoryArg(cmake_build_dir); + cmake_configure.setCwd(extract_dir); + cmake_configure.expectExitCode(0); + cmake_configure.step.dependOn(&check.step); + + const cmake_build = b.addSystemCommand(&.{ "cmake", "--build" }); + cmake_build.addDirectoryArg(cmake_build_dir); + cmake_build.expectExitCode(0); + cmake_build.step.dependOn(&cmake_configure.step); + + check_test.step.dependOn(&cmake_build.step); + } + return .{ .archive = output, .install_step = &install.step, @@ -141,6 +178,36 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { }; } +/// Paths to exclude from the dist archive when building for lib-vt only. +/// These are large files and directories that are not needed to build or +/// test libghostty-vt, specified as git pathspec exclude patterns. +const lib_vt_excludes = &[_][]const u8{ + // App and platform resources + "images", + "macos", + "dist/doxygen", + "dist/linux", + "dist/macos", + "dist/windows", + "flatpak", + "snap", + "po", + "example", + + // Test corpus (lib-vt tests use embedded testdata within src/terminal/) + "test", + + // Large binary assets + "src/font/res", + "src/crash/testdata", + "pkg/wuffs/src/too_big.jpg", + "pkg/wuffs/src/too_big.png", + "pkg/breakpad/vendor", + + // Vendored libraries not used by lib-vt + "vendor", +}; + /// A dist resource is a resource that is built and distributed as part /// of the source tarball with Ghostty. These aren't committed to the Git /// repository but are built as part of the `zig build dist` command. diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index 3e63b602630..caa564bf008 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -29,8 +29,9 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty // Set PIE if requested if (cfg.pie) exe.pie = true; - // Add the shared dependencies - _ = try deps.add(exe); + // Add the shared dependencies. When building only lib-vt we skip + // heavy deps so cross-compilation doesn't pull in GTK, etc. + if (!cfg.emit_lib_vt) _ = try deps.add(exe); // Check for possible issues try checkNixShell(exe, cfg); diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index 2ac383544ef..4e15fbbf486 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -39,6 +39,12 @@ pub fn initStatic( lib.bundle_compiler_rt = true; lib.bundle_ubsan_rt = true; + if (deps.config.target.result.os.tag == .windows) { + // Zig's ubsan emits /exclude-symbols linker directives that + // are incompatible with the MSVC linker (LNK4229). + lib.bundle_ubsan_rt = false; + } + // Add our dependencies. Get the list of all static deps so we can // build a combined archive if necessary. var lib_list = try deps.add(lib); @@ -88,6 +94,44 @@ pub fn initShared( }); _ = try deps.add(lib); + // On Windows with MSVC, building a DLL requires the full CRT library + // chain. linkLibC() (called via deps.add) provides msvcrt.lib, but + // that references symbols in vcruntime.lib and ucrt.lib. Zig's library + // search paths include the MSVC lib dir and the Windows SDK 'um' dir, + // but not the SDK 'ucrt' dir where ucrt.lib lives. + if (deps.config.target.result.os.tag == .windows and + deps.config.target.result.abi == .msvc) + { + // The CRT initialization code in msvcrt.lib calls __vcrt_initialize + // and __acrt_initialize, which are in the static CRT libraries. + lib.linkSystemLibrary("libvcruntime"); + + // ucrt.lib is in the Windows SDK 'ucrt' dir. Detect the SDK + // installation and add the UCRT library path. + const arch = deps.config.target.result.cpu.arch; + const sdk = std.zig.WindowsSdk.find(b.allocator, arch) catch null; + if (sdk) |s| { + if (s.windows10sdk) |w10| { + const arch_str: []const u8 = switch (arch) { + .x86_64 => "x64", + .x86 => "x86", + .aarch64 => "arm64", + else => "x64", + }; + const ucrt_lib_path = std.fmt.allocPrint( + b.allocator, + "{s}\\Lib\\{s}\\ucrt\\{s}", + .{ w10.path, w10.version, arch_str }, + ) catch null; + + if (ucrt_lib_path) |path| { + lib.addLibraryPath(.{ .cwd_relative = path }); + } + } + } + lib.linkSystemLibrary("libucrt"); + } + // Get our debug symbols const dsymutil: ?std.Build.LazyPath = dsymutil: { if (!deps.config.target.result.os.tag.isDarwin()) { diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index aae8ace191a..408f1ebc83c 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"); @@ -11,11 +12,22 @@ step: *std.Build.Step, /// The artifact result artifact: *std.Build.Step.InstallArtifact, +/// The kind of library +kind: Kind, + /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, pkg_config: ?std.Build.LazyPath, +/// The kind of library being built. This is similar to LinkMode but +/// also includes wasm which is an executable, not a library. +const Kind = enum { + wasm, + shared, + static, +}; + pub fn initWasm( b: *std.Build, zig: *const GhosttyZig, @@ -38,20 +50,40 @@ pub fn initWasm( return .{ .step = &exe.step, .artifact = b.addInstallArtifact(exe, .{}), + .kind = .wasm, .output = exe.getEmittedBin(), .dsym = null, .pkg_config = null, }; } +pub fn initStatic( + b: *std.Build, + zig: *const GhosttyZig, +) !GhosttyLibVt { + return initLib(b, zig, .static); +} + pub fn initShared( b: *std.Build, zig: *const GhosttyZig, ) !GhosttyLibVt { + return initLib(b, zig, .dynamic); +} + +fn initLib( + b: *std.Build, + zig: *const GhosttyZig, + linkage: std.builtin.LinkMode, +) !GhosttyLibVt { + const kind: Kind = switch (linkage) { + .static => .static, + .dynamic => .shared, + }; const target = zig.vt.resolved_target.?; const lib = b.addLibrary(.{ - .name = "ghostty-vt", - .linkage = .dynamic, + .name = if (kind == .static) "ghostty-vt-static" else "ghostty-vt", + .linkage = linkage, .root_module = zig.vt_c, .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, }); @@ -61,11 +93,49 @@ pub fn initShared( .{ .include_extensions = &.{".h"} }, ); - // Get our debug symbols + if (kind == .static) { + // These must be bundled since we're compiling into a static lib. + // Otherwise, you get undefined symbol errors. This could cause + // problems if you're linking multiple static Zig libraries but + // we'll cross that bridge when we get to it. + lib.bundle_compiler_rt = true; + lib.bundle_ubsan_rt = true; + + // Enable PIC so the static library can be linked into PIE + // executables, which is the default on most Linux distributions. + lib.root_module.pic = true; + } + + if (target.result.os.tag == .windows) { + // Zig's ubsan emits /exclude-symbols linker directives that + // are incompatible with the MSVC linker (LNK4229). + lib.bundle_ubsan_rt = false; + } + + 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 (only for shared libs; static libs aren't linked) const dsymutil: ?std.Build.LazyPath = dsymutil: { - if (!target.result.os.tag.isDarwin()) { - break :dsymutil null; - } + if (kind != .shared) break :dsymutil null; + if (!target.result.os.tag.isDarwin()) break :dsymutil null; const dsymutil = RunStep.create(b, "dsymutil"); dsymutil.addArgs(&.{"dsymutil"}); @@ -95,6 +165,7 @@ pub fn initShared( return .{ .step = &lib.step, .artifact = b.addInstallArtifact(lib, .{}), + .kind = kind, .output = lib.getEmittedBin(), .dsym = dsymutil, .pkg_config = pc, 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/GhosttyZig.zig b/src/build/GhosttyZig.zig index e63120e7467..aabc00d46f2 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -64,8 +64,12 @@ fn initVt( .optimize = cfg.optimize, // SIMD require libc/libcpp (both) but otherwise we don't care. + // On MSVC, we must not use linkLibCpp because Zig passes + // -nostdinc++ and adds its bundled libc++/libc++abi headers + // which conflict with MSVC's C++ runtime. The MSVC SDK dirs + // added via link_libc contain both C and C++ headers. .link_libc = if (cfg.simd) true else null, - .link_libcpp = if (cfg.simd) true else null, + .link_libcpp = if (cfg.simd and cfg.target.result.abi != .msvc) true else null, }); vt.addOptions("build_options", general_options); vt_options.add(b, vt); 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/LibtoolStep.zig b/src/build/LibtoolStep.zig index d2b5149275f..856a33aa853 100644 --- a/src/build/LibtoolStep.zig +++ b/src/build/LibtoolStep.zig @@ -33,7 +33,15 @@ pub fn create(b: *std.Build, opts: Options) *LibtoolStep { const run_step = RunStep.create(b, b.fmt("libtool {s}", .{opts.name})); run_step.addArgs(&.{ "libtool", "-static", "-o" }); const output = run_step.addOutputFileArg(opts.out_name); - for (opts.sources) |source| run_step.addFileArg(source); + for (opts.sources, 0..) |source, i| { + run_step.addFileArg(normalizeArchive( + b, + opts.name, + opts.out_name, + i, + source, + )); + } self.* = .{ .step = &run_step.step, @@ -42,3 +50,29 @@ pub fn create(b: *std.Build, opts: Options) *LibtoolStep { return self; } + +fn normalizeArchive( + b: *std.Build, + step_name: []const u8, + out_name: []const u8, + index: usize, + source: LazyPath, +) LazyPath { + // Newer Xcode libtool can drop 64-bit archive members if the input + // archive layout doesn't match what it expects. ranlib rewrites the + // archive without flattening members through the filesystem, so we + // normalize each source archive first. This is a Zig/toolchain + // interoperability workaround, not a Ghostty archive format change. + const run_step = RunStep.create( + b, + b.fmt("ranlib {s} #{d}", .{ step_name, index }), + ); + run_step.addArgs(&.{ + "/bin/sh", + "-c", + "/bin/cp \"$1\" \"$2\" && /usr/bin/ranlib \"$2\"", + "_", + }); + run_step.addFileArg(source); + return run_step.addOutputFileArg(b.fmt("{d}-{s}", .{ index, out_name })); +} diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0ca43e78d6f..aa63c08248c 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -121,7 +121,7 @@ pub fn add( // We don't support cross-compiling to Darwin but due to the way // lazy dependencies work with Zig, we call this function. So we just // bail. The build will fail but the build would've failed anyways. - // And this lets other non-platform-specific targets like `lib-vt` + // And this lets other non-platform-specific targets like `-Demit-lib-vt` // cross-compile properly. if (!builtin.target.os.tag.isDarwin() and self.config.target.result.os.tag.isDarwin()) @@ -135,6 +135,51 @@ pub fn add( // Every exe needs the terminal options self.config.terminalOptions().add(b, step.root_module); + // C imports for locale constants and functions + { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/os/locale.c"), + .target = target, + .optimize = optimize, + }); + if (target.result.os.tag.isDarwin()) { + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = &target.result, + .verbose = false, + }); + c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); + } + step.root_module.addImport("locale-c", c.createModule()); + } + + // C imports needed to manage/create PTYs + switch (target.result.os.tag) { + .freebsd, + .linux, + .macos, + => { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/pty.c"), + .target = target, + .optimize = optimize, + }); + switch (target.result.os.tag) { + .macos => { + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = &target.result, + .verbose = false, + }); + c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); + }, + else => {}, + } + step.root_module.addImport("pty-c", c.createModule()); + }, + else => {}, + } + // Freetype. We always include this even if our font backend doesn't // use it because Dear Imgui uses Freetype. _ = b.systemIntegrationOption("freetype", .{}); // Shows it in help @@ -372,8 +417,15 @@ pub fn add( step.addIncludePath(b.path("src/apprt/gtk")); } - // libcpp is required for various dependencies - step.linkLibCpp(); + // libcpp is required for various dependencies. On MSVC, we must + // not use linkLibCpp because Zig unconditionally passes -nostdinc++ + // and then adds its bundled libc++/libc++abi include paths, which + // conflict with MSVC's own C++ runtime headers. The MSVC SDK + // include directories (already added via linkLibC above) contain + // both C and C++ headers, so linkLibCpp is not needed. + if (step.rootModuleTarget().abi != .msvc) { + step.linkLibCpp(); + } // We always require the system SDK so that our system headers are available. // This makes things like `os/log.h` available for cross-compiling. @@ -477,7 +529,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")); @@ -624,9 +678,6 @@ fn addGtkNg( .wayland_protocols = wayland_protocols_dep.path(""), }); - scanner.addCustomProtocol( - plasma_wayland_protocols_dep.path("src/protocols/blur.xml"), - ); // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"), @@ -634,13 +685,18 @@ fn addGtkNg( scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/slide.xml"), ); + scanner.addCustomProtocol( + plasma_wayland_protocols_dep.path("src/protocols/kde-output-order-v1.xml"), + ); scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml"); + scanner.addSystemProtocol("staging/ext-background-effect/ext-background-effect-v1.xml"); scanner.generate("wl_compositor", 1); - scanner.generate("org_kde_kwin_blur_manager", 1); scanner.generate("org_kde_kwin_server_decoration_manager", 1); scanner.generate("org_kde_kwin_slide_manager", 1); + scanner.generate("kde_output_order_v1", 1); scanner.generate("xdg_activation_v1", 1); + scanner.generate("ext_background_effect_manager_v1", 1); step.root_module.addImport("wayland", b.createModule(.{ .root_source_file = scanner.result, @@ -655,10 +711,10 @@ fn addGtkNg( .optimize = optimize, })) |gtk4_layer_shell| { const layer_shell_module = gtk4_layer_shell.module("gtk4-layer-shell"); - if (gobject_) |gobject| layer_shell_module.addImport( - "gtk", - gobject.module("gtk4"), - ); + if (gobject_) |gobject| { + layer_shell_module.addImport("gtk", gobject.module("gtk4")); + layer_shell_module.addImport("gdk", gobject.module("gdk4")); + } step.root_module.addImport( "gtk4-layer-shell", layer_shell_module, @@ -752,12 +808,35 @@ pub fn addSimd( const HWY_AVX3_DL: c_int = 1 << 7; const HWY_AVX3: c_int = 1 << 8; + var flags: std.ArrayListUnmanaged([]const u8) = .empty; + // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 // To workaround this we just disable AVX512 support completely. // The performance difference between AVX2 and AVX512 is not // significant for our use case and AVX512 is very rare on consumer // hardware anyways. const HWY_DISABLED_TARGETS: c_int = HWY_AVX10_2 | HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3; + if (target.result.cpu.arch == .x86_64) try flags.append( + b.allocator, + b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}), + ); + + // MSVC requires explicit std specification otherwise these + // are guarded, at least on Windows 2025. Doing it unconditionally + // doesn't cause any issues on other platforms and ensures we get + // C++17 support on MSVC. + try flags.append( + b.allocator, + "-std=c++17", + ); + + // Disable ubsan for MSVC to avoid undefined references to + // __ubsan_handle_* symbols that require a runtime we don't link + // and bundle. Hopefully we can fix this one day since ubsan is nice! + if (target.result.abi == .msvc) try flags.appendSlice(b.allocator, &.{ + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", + }); m.addCSourceFiles(.{ .files = &.{ @@ -766,9 +845,7 @@ pub fn addSimd( "src/simd/index_of.cpp", "src/simd/vt.cpp", }, - .flags = if (target.result.cpu.arch == .x86_64) &.{ - b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}), - } else &.{}, + .flags = flags.items, }); } } diff --git a/src/build/framegen/main.c b/src/build/framegen/main.c index 647768006ce..2139b15ddfa 100644 --- a/src/build/framegen/main.c +++ b/src/build/framegen/main.c @@ -8,15 +8,16 @@ #define SEPARATOR '\x01' #define CHUNK_SIZE 16384 +#define MAX_FRAMES 1024 +#define PATH_SEP '/' -static int filter_frames(const struct dirent *entry) { - const char *name = entry->d_name; +static int is_frame_file(const char *name) { size_t len = strlen(name); return len > 4 && strcmp(name + len - 4, ".txt") == 0; } -static int compare_frames(const struct dirent **a, const struct dirent **b) { - return strcmp((*a)->d_name, (*b)->d_name); +static int compare_names(const void *a, const void *b) { + return strcmp(*(const char **)a, *(const char **)b); } static char *read_file(const char *path, size_t *out_size) { @@ -54,25 +55,47 @@ int main(int argc, char **argv) { const char *frames_dir = argv[1]; const char *output_file = argv[2]; - struct dirent **namelist; - int n = scandir(frames_dir, &namelist, filter_frames, compare_frames); - if (n < 0) { + // Use opendir/readdir instead of scandir for Windows compatibility + DIR *dir = opendir(frames_dir); + if (!dir) { fprintf(stderr, "Failed to scan directory %s: %s\n", frames_dir, strerror(errno)); return 1; } + char *names[MAX_FRAMES]; + int n = 0; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (!is_frame_file(entry->d_name)) continue; + if (n >= MAX_FRAMES) { + fprintf(stderr, "Too many frame files (max %d)\n", MAX_FRAMES); + closedir(dir); + return 1; + } + names[n] = strdup(entry->d_name); + if (!names[n]) { + fprintf(stderr, "Failed to allocate memory\n"); + closedir(dir); + return 1; + } + n++; + } + closedir(dir); + if (n == 0) { fprintf(stderr, "No frame files found in %s\n", frames_dir); return 1; } + qsort(names, n, sizeof(char *), compare_names); + size_t total_size = 0; char **frame_contents = calloc(n, sizeof(char*)); size_t *frame_sizes = calloc(n, sizeof(size_t)); for (int i = 0; i < n; i++) { char path[4096]; - snprintf(path, sizeof(path), "%s/%s", frames_dir, namelist[i]->d_name); + snprintf(path, sizeof(path), "%s%c%s", frames_dir, PATH_SEP, names[i]); frame_contents[i] = read_file(path, &frame_sizes[i]); if (!frame_contents[i]) { 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/CommaSplitter.zig b/src/cli/CommaSplitter.zig index 3168c1ffaa8..d5093992d74 100644 --- a/src/cli/CommaSplitter.zig +++ b/src/cli/CommaSplitter.zig @@ -13,8 +13,18 @@ //! //! Quotes and escapes are not stripped or decoded, that must be handled as a //! separate step! +//! +//! On Windows, backslash is only treated as an escape character inside quoted +//! strings. Outside quotes, backslash is a literal character (path separator). const CommaSplitter = @This(); +const builtin = @import("builtin"); + +/// Whether backslash acts as an escape character outside quoted strings. +/// On Windows, backslash is the path separator so it is always literal +/// outside quotes. +const escape_outside_quotes = builtin.os.tag != .windows; + pub const Error = error{ UnclosedQuote, UnfinishedEscape, @@ -77,8 +87,11 @@ pub fn next(self: *CommaSplitter) Error!?[]const u8 { }, '\\' => { self.index += 1; - last = .normal; - continue :loop .escape; + if (comptime escape_outside_quotes) { + last = .normal; + continue :loop .escape; + } + continue :loop .normal; }, else => { self.index += 1; @@ -273,6 +286,7 @@ test "splitter 8" { } test "splitter 9" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -281,6 +295,7 @@ test "splitter 9" { } test "splitter 10" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -289,6 +304,7 @@ test "splitter 10" { } test "splitter 11" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -297,6 +313,7 @@ test "splitter 11" { } test "splitter 12" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -305,6 +322,7 @@ test "splitter 12" { } test "splitter 13" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -313,6 +331,7 @@ test "splitter 13" { } test "splitter 14" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -330,6 +349,7 @@ test "splitter 15" { } test "splitter 16" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -338,6 +358,7 @@ test "splitter 16" { } test "splitter 17" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -346,6 +367,7 @@ test "splitter 17" { } test "splitter 18" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -415,6 +437,7 @@ test "splitter 24" { } test "splitter 25" { + if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing; @@ -422,3 +445,39 @@ test "splitter 25" { try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectError(error.IllegalEscape, s.next()); } + +// Windows-specific tests: backslash is literal outside quotes. + +test "splitter: windows paths" { + if (comptime escape_outside_quotes) return error.SkipZigTest; + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("light:C:\\Users\\foo\\theme,dark:C:\\Users\\bar\\theme"); + try testing.expectEqualStrings("light:C:\\Users\\foo\\theme", (try s.next()).?); + try testing.expectEqualStrings("dark:C:\\Users\\bar\\theme", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter: backslash literal outside quotes on windows" { + if (comptime escape_outside_quotes) return error.SkipZigTest; + const std = @import("std"); + const testing = std.testing; + + // Backslash followed by characters that would be escapes on Unix + // are treated as literal on Windows outside quotes. + var s: CommaSplitter = .init("\\n\\r\\t"); + try testing.expectEqualStrings("\\n\\r\\t", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter: backslash still escapes inside quotes on windows" { + if (comptime escape_outside_quotes) return error.SkipZigTest; + const std = @import("std"); + const testing = std.testing; + + // Inside quotes, backslash escapes work on all platforms. + var s: CommaSplitter = .init("\"hello\\nworld\""); + try testing.expectEqualStrings("\"hello\\nworld\"", (try s.next()).?); + try testing.expect(null == try s.next()); +} diff --git a/src/cli/Pager.zig b/src/cli/Pager.zig new file mode 100644 index 00000000000..247c998e10e --- /dev/null +++ b/src/cli/Pager.zig @@ -0,0 +1,93 @@ +//! A pager wraps output to an external pager program (like `less`) when +//! stdout is a TTY. The pager command is resolved as: +//! +//! `$GHOSTTY_PAGER` > `$PAGER` > `less` +//! +//! Setting either env var to an empty string disables paging. +//! If stdout is not a TTY, writes go directly to stdout. +const Pager = @This(); +const std = @import("std"); +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); + +/// The pager child process, if one was spawned. +child: ?std.process.Child = null, + +/// The buffered file writer used for both the pager pipe and direct +/// stdout paths. +file_writer: std.fs.File.Writer = undefined, + +/// Initialize the pager. If stdout is a TTY, this spawns the pager +/// process. Otherwise, output goes directly to stdout. +pub fn init(alloc: Allocator) Pager { + return .{ .child = initPager(alloc) }; +} + +/// Writes to the pager process if available; otherwise, stdout. +pub fn writer(self: *Pager, buffer: []u8) *std.Io.Writer { + if (self.child) |child| { + self.file_writer = child.stdin.?.writer(buffer); + } else { + self.file_writer = std.fs.File.stdout().writer(buffer); + } + return &self.file_writer.interface; +} + +/// Deinitialize the pager. Waits for the spawned process to exit. +pub fn deinit(self: *Pager) void { + if (self.child) |*child| { + // Flush any remaining buffered data, close the pipe so the + // pager sees EOF, then wait for it to exit. + self.file_writer.interface.flush() catch {}; + if (child.stdin) |stdin| { + stdin.close(); + child.stdin = null; + } + _ = child.wait() catch {}; + } + + self.* = undefined; +} + +fn initPager(alloc: Allocator) ?std.process.Child { + const stdout_file: std.fs.File = .stdout(); + if (!stdout_file.isTty()) return null; + + // Resolve the pager command: $GHOSTTY_PAGER > $PAGER > `less`. + // An empty value for either env var disables paging. + const ghostty_var = internal_os.getenv(alloc, "GHOSTTY_PAGER") catch null; + defer if (ghostty_var) |v| v.deinit(alloc); + const pager_var = internal_os.getenv(alloc, "PAGER") catch null; + defer if (pager_var) |v| v.deinit(alloc); + + const cmd: ?[]const u8 = cmd: { + if (ghostty_var) |v| break :cmd if (v.value.len > 0) v.value else null; + if (pager_var) |v| break :cmd if (v.value.len > 0) v.value else null; + break :cmd "less"; + }; + + if (cmd == null) return null; + + var child: std.process.Child = .init(&.{cmd.?}, alloc); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + child.spawn() catch return null; + return child; +} + +test "pager: non-tty" { + var pager: Pager = .init(std.testing.allocator); + defer pager.deinit(); + try std.testing.expect(pager.child == null); +} + +test "pager: default writer" { + var pager: Pager = .{}; + defer pager.deinit(); + try std.testing.expect(pager.child == null); + var buf: [4096]u8 = undefined; + const w = pager.writer(&buf); + try w.writeAll("hello"); +} diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index 056aecc0d44..c08651a06cf 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -136,19 +136,31 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { return 1; } + const command = command: { + var buffer: std.io.Writer.Allocating = .init(alloc); + defer buffer.deinit(); + const writer = &buffer.writer; + try writer.writeAll(editor); + try writer.writeByte(' '); + { + var sh: internal_os.ShellEscapeWriter = .init(writer); + try sh.writer.writeAll(path); + try sh.writer.flush(); + } + try writer.flush(); + break :command try buffer.toOwnedSliceSentinel(0); + }; + defer alloc.free(command); + // We require libc because we want to use std.c.environ for envp // and not have to build that ourselves. We can remove this // limitation later but Ghostty already heavily requires libc // so this is not a big deal. comptime assert(builtin.link_libc); - const editorZ = try alloc.dupeZ(u8, editor); - defer alloc.free(editorZ); - const pathZ = try alloc.dupeZ(u8, path); - defer alloc.free(pathZ); const err = std.posix.execvpeZ( - editorZ, - &.{ editorZ, pathZ }, + "/bin/sh", + &.{ "/bin/sh", "-c", command }, std.c.environ, ); diff --git a/src/cli/explain_config.zig b/src/cli/explain_config.zig new file mode 100644 index 00000000000..4f034afef6b --- /dev/null +++ b/src/cli/explain_config.zig @@ -0,0 +1,151 @@ +const std = @import("std"); +const args = @import("args.zig"); +const Allocator = std.mem.Allocator; +const Action = @import("ghostty.zig").Action; +const help_strings = @import("help_strings"); +const Config = @import("../config/Config.zig"); +const ConfigKey = @import("../config/key.zig").Key; +const KeybindAction = @import("../input/Binding.zig").Action; +const Pager = @import("Pager.zig"); + +pub const Options = struct { + /// The config option to explain. For example: + /// + /// ghostty +explain-config --option=font-size + option: ?[]const u8 = null, + + /// The keybind action to explain. For example: + /// + /// ghostty +explain-config --keybind=copy_to_clipboard + keybind: ?[]const u8 = null, + + pub fn deinit(self: Options) void { + _ = self; + } + + /// Enables `-h` and `--help` to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// The `explain-config` command prints the documentation for a single +/// Ghostty configuration option or keybind action. +/// +/// Examples: +/// +/// ghostty +explain-config font-size +/// ghostty +explain-config copy_to_clipboard +/// ghostty +explain-config --option=font-size +/// ghostty +explain-config --keybind=copy_to_clipboard +/// +/// Flags: +/// +/// * `--option`: The name of the configuration option to explain. +/// * `--keybind`: The name of the keybind action to explain. +/// * `--no-pager`: Disable automatic paging of output. +pub fn run(alloc: Allocator) !u8 { + var option_name: ?[]const u8 = null; + var keybind_name: ?[]const u8 = null; + var positional: ?[]const u8 = null; + var no_pager: bool = false; + + var iter = try args.argsIterator(alloc); + defer iter.deinit(); + defer if (option_name) |s| alloc.free(s); + defer if (keybind_name) |s| alloc.free(s); + defer if (positional) |s| alloc.free(s); + + while (iter.next()) |arg| { + if (std.mem.startsWith(u8, arg, "--option=")) { + option_name = try alloc.dupe(u8, arg["--option=".len..]); + } else if (std.mem.startsWith(u8, arg, "--keybind=")) { + keybind_name = try alloc.dupe(u8, arg["--keybind=".len..]); + } else if (std.mem.eql(u8, arg, "--no-pager")) { + no_pager = true; + } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + return Action.help_error; + } else if (!std.mem.startsWith(u8, arg, "-")) { + positional = try alloc.dupe(u8, arg); + } + } + + // Resolve what to look up. Explicit flags go directly to their + // respective lookup. A bare positional argument tries config + // options first, then keybind actions as a fallback. + const name = keybind_name orelse option_name orelse positional orelse { + var stderr: std.fs.File = .stderr(); + var buffer: [4096]u8 = undefined; + var stderr_writer = stderr.writer(&buffer); + try stderr_writer.interface.writeAll("Usage: ghostty +explain-config