diff --git a/.github/scripts/request_review.py b/.github/scripts/request_review.py new file mode 100644 index 000000000..1a53e82e4 --- /dev/null +++ b/.github/scripts/request_review.py @@ -0,0 +1,115 @@ +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "githubkit", +# ] +# /// + +import asyncio +import os +import re +from itertools import chain + +from githubkit import GitHub + +ORG_NAME = "ghostty-org" +REPO_NAME = "ghostty" +ALLOWED_PARENT_TEAM = "localization" +LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}") + +gh = GitHub(os.environ["GITHUB_TOKEN"]) + + +async def fetch_and_parse_codeowners() -> dict[str, str]: + content = ( + await gh.rest.repos.async_get_content( + ORG_NAME, + REPO_NAME, + "CODEOWNERS", + headers={"Accept": "application/vnd.github.raw+json"}, + ) + ).text + + codeowners: dict[str, str] = {} + for line in content.splitlines(): + if not line or line.lstrip().startswith("#"): + continue + # This assumes that all entries only list one owner + # and that this owner is a team (ghostty-org/foobar) + path, owner = line.split() + codeowners[path.lstrip("/")] = owner.removeprefix(f"@{ORG_NAME}/") + return codeowners + + +async def get_team_members(team_name: str) -> list[str]: + team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data + if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM: + members = ( + await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) + ).parsed_data + return [m.login for m in members] + return [] + + +async def get_changed_files(pr_number: int) -> list[str]: + diff_entries = ( + await gh.rest.pulls.async_list_files( + ORG_NAME, + REPO_NAME, + pr_number, + per_page=3000, + headers={"Accept": "application/vnd.github+json"}, + ) + ).parsed_data + return [d.filename for d in diff_entries] + + +async def request_review(pr_number: int, pr_author: str, *users: str) -> None: + await asyncio.gather( + *( + gh.rest.pulls.async_request_reviewers( + ORG_NAME, + REPO_NAME, + pr_number, + headers={"Accept": "application/vnd.github+json"}, + data={"reviewers": [user]}, + ) + for user in users + if user != pr_author + ) + ) + + +def is_localization_team(team_name: str) -> bool: + return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None + + +async def main() -> None: + pr_number = int(os.environ["PR_NUMBER"]) + changed_files = await get_changed_files(pr_number) + pr_author = ( + await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) + ).parsed_data.user.login + localization_codewners = { + path: owner + for path, owner in (await fetch_and_parse_codeowners()).items() + if is_localization_team(owner) + } + + found_owners = set[str]() + for file in changed_files: + for path, owner in localization_codewners.items(): + if file.startswith(path): + break + else: + continue + found_owners.add(owner) + + member_lists = await asyncio.gather( + *(get_team_members(owner) for owner in found_owners) + ) + await request_review(pr_number, pr_author, *chain.from_iterable(member_lists)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml new file mode 100644 index 000000000..9abe0b5e2 --- /dev/null +++ b/.github/workflows/review.yml @@ -0,0 +1,37 @@ +name: Request Review + +on: + pull_request: + types: + - opened + - synchronize + +env: + PY_COLORS: 1 + +jobs: + review: + runs-on: namespace-profile-ghostty-xsm + steps: + - uses: actions/checkout@v4 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@v1.2.0 + with: + path: | + /nix + /zig + + - uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v15 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Request Localization Review + env: + GITHUB_TOKEN: ${{ secrets.GH_REVIEW_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: nix develop -c uv run .github/scripts/request_review.py diff --git a/CODEOWNERS b/CODEOWNERS index b76c7b3da..fa3d73fd3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -158,9 +158,18 @@ # 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/fr_FR.UTF-8.po @ghostty-org/fr_FR +/po/id_ID.UTF-8.po @ghostty-org/id_ID +/po/ja_JP.UTF-8.po @ghostty-org/ja_JP /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 diff --git a/build.zig.zon b/build.zig.zon index 3cd2eed7f..086e19dd8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ghostty, - .version = "1.1.3", + .version = "1.1.4", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, .dependencies = .{ @@ -14,8 +14,8 @@ }, .vaxis = .{ // rockorager/libvaxis - .url = "git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447", - .hash = "vaxis-0.1.0-BWNV_MHyCAA0rNbPTr50Z44PyEdNP9zQSnHcXBXoo3Ti", + .url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", + .hash = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn", .lazy = true, }, .z2d = .{ @@ -48,8 +48,8 @@ }, .zf = .{ // natecraddock/zf - .url = "https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz", - .hash = "zf-0.10.3-OIRy8bKIAADhjqtdjVaDfONRuI7RVl5gMbhCoOwiBWV5", + .url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", + .hash = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9", .lazy = true, }, .gobject = .{ @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", - .hash = "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz", + .hash = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index a4b8f923b..d43bf3d56 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6": { + "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", - "hash": "sha256-nOkH31MQQd2PPdjVpRxBxNQWfR9Exg6nRF/KHgSz3cM=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz", + "hash": "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", @@ -104,10 +104,10 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "vaxis-0.1.0-BWNV_MHyCAA0rNbPTr50Z44PyEdNP9zQSnHcXBXoo3Ti": { + "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { "name": "vaxis", - "url": "git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447", - "hash": "sha256-iONEySjPeD0WYJ93fw5mxT+0pVfUO/m6008J/LXjQkA=" + "url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", + "hash": "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY=" }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "name": "wayland", @@ -129,10 +129,10 @@ "url": "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz", "hash": "sha256-PEKVSUZ6teRbDyhFPWSiuBSe40pgr0kVRivIY8Cn8HQ=" }, - "zf-0.10.3-OIRy8bKIAADhjqtdjVaDfONRuI7RVl5gMbhCoOwiBWV5": { + "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "name": "zf", - "url": "https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz", - "hash": "sha256-xVva07TAYlVv4E4PKe2wUj86a6Ky2YC30YBgtbvNKvw=" + "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", + "hash": "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I=" }, "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": { "name": "zg", @@ -154,10 +154,10 @@ "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", "hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk=" }, - "zigimg-0.1.0-lly-O4heEADSRxoTwJwrD3TBfUob9052sIgb9SL8Iz-A": { + "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj": { "name": "zigimg", - "url": "git+https://github.com/TUSF/zigimg#0ce4eca3560d5553b13263d6b6bb72e146dd43d0", - "hash": "sha256-Rr+mAfbLOoaxHOwCug+0cWCmW9gDhjhnaO2J/Oik9HI=" + "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d", + "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=" }, "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": { "name": "ziglyph", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index f98709d65..1dc56da50 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6"; + name = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz"; - hash = "sha256-nOkH31MQQd2PPdjVpRxBxNQWfR9Exg6nRF/KHgSz3cM="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz"; + hash = "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU="; }; } { @@ -250,11 +250,11 @@ in }; } { - name = "vaxis-0.1.0-BWNV_MHyCAA0rNbPTr50Z44PyEdNP9zQSnHcXBXoo3Ti"; + name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; path = fetchZigArtifact { name = "vaxis"; - url = "git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447"; - hash = "sha256-iONEySjPeD0WYJ93fw5mxT+0pVfUO/m6008J/LXjQkA="; + url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; + hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; }; } { @@ -290,11 +290,11 @@ in }; } { - name = "zf-0.10.3-OIRy8bKIAADhjqtdjVaDfONRuI7RVl5gMbhCoOwiBWV5"; + name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz"; - hash = "sha256-xVva07TAYlVv4E4PKe2wUj86a6Ky2YC30YBgtbvNKvw="; + url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; + hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; }; } { @@ -330,11 +330,11 @@ in }; } { - name = "zigimg-0.1.0-lly-O4heEADSRxoTwJwrD3TBfUob9052sIgb9SL8Iz-A"; + name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; path = fetchZigArtifact { name = "zigimg"; - url = "git+https://github.com/TUSF/zigimg#0ce4eca3560d5553b13263d6b6bb72e146dd43d0"; - hash = "sha256-Rr+mAfbLOoaxHOwCug+0cWCmW9gDhjhnaO2J/Oik9HI="; + url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; + hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index b0ea16fa3..b9bdc50d2 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,6 +1,6 @@ git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc -git+https://github.com/TUSF/zigimg#0ce4eca3560d5553b13263d6b6bb72e146dd43d0 -git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447 +git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d +git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23 https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz @@ -27,8 +27,8 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz -https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz +https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz diff --git a/flake.nix b/flake.nix index c8e53d7e9..d4c6aa6ca 100644 --- a/flake.nix +++ b/flake.nix @@ -51,6 +51,7 @@ devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix { zig = zig.packages.${system}."0.14.0"; wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {}; + uv = pkgs-unstable.uv; # remove once blueprint-compiler 0.16.0 is in the stable nixpkgs blueprint-compiler = pkgs-unstable.blueprint-compiler; zon2nix = zon2nix; diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ddc459c5b..f54eb6539 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -91,6 +91,12 @@ class TerminalController: BaseTerminalController { name: Ghostty.Notification.didEqualizeSplits, object: nil ) + center.addObserver( + self, + selector: #selector(onCloseWindow), + name: .ghosttyCloseWindow, + object: nil + ) } required init?(coder: NSCoder) { @@ -842,6 +848,12 @@ class TerminalController: BaseTerminalController { closeTab(self) } + @objc private func onCloseWindow(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree?.contains(view: target) ?? false else { return } + closeWindow(self) + } + @objc private func onResetWindowSize(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard surfaceTree?.contains(view: target) ?? false else { return } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 88f8d1dc9..ddb954e04 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -107,7 +107,7 @@ extension Ghostty { deinit { // This will force the didSet callbacks to run which free. self.app = nil - + #if os(macOS) NotificationCenter.default.removeObserver(self) #endif @@ -451,6 +451,9 @@ extension Ghostty { case GHOSTTY_ACTION_CLOSE_TAB: closeTab(app, target: target) + case GHOSTTY_ACTION_CLOSE_WINDOW: + closeWindow(app, target: target) + case GHOSTTY_ACTION_TOGGLE_FULLSCREEN: toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen) @@ -686,6 +689,26 @@ extension Ghostty { } } + private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("close window does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + + NotificationCenter.default.post( + name: .ghosttyCloseWindow, + object: surfaceView + ) + + default: + assertionFailure() + } + } + private static func toggleFullscreen( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index ca37002b0..cda4b557e 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -248,6 +248,9 @@ extension Notification.Name { /// Close tab static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") + /// 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") } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index e4c9072f3..c6a3d7629 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -201,7 +201,14 @@ extension Ghostty { self.eventMonitor = NSEvent.addLocalMonitorForEvents( matching: [ // We need keyUp because command+key events don't trigger keyUp. - .keyUp + .keyUp, + + // We need leftMouseDown to determine if we should focus ourselves + // when the app/window isn't in focus. We do this instead of + // "acceptsFirstMouse" because that forces us to also handle the + // event and encode the event to the pty which we want to avoid. + // (Issue 2595) + .leftMouseDown, ] ) { [weak self] event in self?.localEventHandler(event) } @@ -450,11 +457,40 @@ extension Ghostty { case .keyUp: localEventKeyUp(event) + case .leftMouseDown: + localEventLeftMouseDown(event) + default: event } } + private func localEventLeftMouseDown(_ event: NSEvent) -> NSEvent? { + // We only want to process events that are on this window. + guard let window, + event.window != nil, + window == event.window else { return event } + + // 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 } + + // Make ourselves the first responder + window.makeFirstResponder(self) + + // We have to keep processing the event so that AppKit can properly + // focus the window and dispatch events. If you return nil here then + // nobody gets a windowDidBecomeKey event and so on. + return event + } + private func localEventKeyUp(_ event: NSEvent) -> NSEvent? { // We only care about events with "command" because all others will // trigger the normal responder chain. @@ -620,14 +656,6 @@ extension Ghostty { ghostty_surface_draw(surface); } - override func acceptsFirstMouse(for event: NSEvent?) -> Bool { - // "Override this method in a subclass to allow instances to respond to - // click-through. This allows the user to click on a view in an inactive - // window, activating the view with one click, instead of clicking first - // to make the window active and then clicking the view." - return true - } - override func mouseDown(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) @@ -884,6 +912,11 @@ extension Ghostty { nil } + // If we are in a keyDown then we don't need to redispatch a command-modded + // key event (see docs for this field) so reset this to nil because + // `interpretKeyEvents` may dispach it. + self.lastPerformKeyEvent = nil + self.interpretKeyEvents([translationEvent]) // If our keyboard changed from this we just assume an input method @@ -922,6 +955,34 @@ extension Ghostty { _ = keyAction(GHOSTTY_ACTION_RELEASE, event: event) } + /// Records the timestamp of the last event to performKeyEquivalent that we need to save. + /// We currently save all commands with command or control set. + /// + /// For command+key inputs, the AppKit input stack calls performKeyEquivalent to give us a chance + /// to handle them first. If we return "false" then it goes through the standard AppKit responder chain. + /// For an NSTextInputClient, that may redirect some commands _before_ our keyDown gets called. + /// Concretely: Command+Period will do: performKeyEquivalent, doCommand ("cancel:"). In doCommand, + /// we need to know that we actually want to handle that in keyDown, so we send it back through the + /// event dispatch system and use this timestamp as an identity to know to actually send it to keyDown. + /// + /// Why not send it to keyDown always? Because if the user rebinds a command to something we + /// actually handle then we do want the standard response chain to handle the key input. Unfortunately, + /// we can't know what a command is bound to at a system level until we let it flow through the system. + /// That's the crux of the problem. + /// + /// So, we have to send it back through if we didn't handle it. + /// + /// The next part of the problem is comparing NSEvent identity seems pretty nasty. I couldn't + /// find a good way to do it. I originally stored a weak ref and did identity comparison but that + /// doesn't work and for reasons I couldn't figure out the value gets mangled (fields don't match + /// before/after the assignment). I suspect it has something to do with the fact an NSEvent is wrapping + /// a lower level event pointer and its just not surviving the Swift runtime somehow. I don't know. + /// + /// The best thing I could find was to store the event timestamp which has decent granularity + /// and compare that. To further complicate things, some events are synthetic and have a zero + /// timestamp so we have to protect against that. Fun! + var lastPerformKeyEvent: TimeInterval? + /// Special case handling for some control keys override func performKeyEquivalent(with event: NSEvent) -> Bool { switch (event.type) { @@ -975,15 +1036,42 @@ extension Ghostty { equivalent = "\r" - case ".": - if (!event.modifierFlags.contains(.command)) { + default: + // It looks like some part of AppKit sometimes generates synthetic NSEvents + // with a zero timestamp. We never process these at this point. Concretely, + // this happens for me when pressing Cmd+period with default bindings. This + // binds to "cancel" which goes through AppKit to produce a synthetic "escape". + // + // Question: should we be ignoring all synthetic events? Should we be finding + // synthetic escape and ignoring it? I feel like Cmd+period could map to a + // escape binding by accident, but it hasn't happened yet... + if event.timestamp == 0 { return false } - equivalent = "." + // All of this logic here re: lastCommandEvent is to workaround some + // nasty behavior. See the docs for lastCommandEvent for more info. - default: - // Ignore other events + // 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)) { + // Reset since we got a non-command event. + lastPerformKeyEvent = nil + return false + } + + // If we have a prior command binding and the timestamp matches exactly + // then we pass it through to keyDown for encoding. + if let lastPerformKeyEvent { + self.lastPerformKeyEvent = nil + if lastPerformKeyEvent == event.timestamp { + equivalent = event.characters ?? "" + break + } + } + + lastPerformKeyEvent = event.timestamp return false } @@ -1480,9 +1568,19 @@ extension Ghostty.SurfaceView: NSTextInputClient { } } + /// This function needs to exist for two reasons: + /// 1. Prevents an audible NSBeep for unimplemented actions. + /// 2. Allows us to properly encode super+key input events that we don't handle override func doCommand(by selector: Selector) { - // This currently just prevents NSBeep from interpretKeyEvents but in the future - // we may want to make some of this work. + // If we are being processed by performKeyEquivalent with a command binding, + // we send it back through the event system so it can be encoded. + if let lastPerformKeyEvent, + let current = NSApp.currentEvent, + lastPerformKeyEvent == current.timestamp + { + NSApp.sendEvent(current) + return + } print("SEL: \(selector)") } diff --git a/nix/devShell.nix b/nix/devShell.nix index 6949744d0..5b69f882b 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -57,6 +57,7 @@ pandoc, hyperfine, typos, + uv, wayland, wayland-scanner, wayland-protocols, @@ -109,6 +110,9 @@ in # Localization gettext + # CI + uv + # We need these GTK-related deps on all platform so we can build # dist tarballs. blueprint-compiler diff --git a/nix/package.nix b/nix/package.nix index 46bf18122..9368b2cde 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -39,7 +39,7 @@ in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; - version = "1.1.3"; + version = "1.1.4"; # 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/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po new file mode 100644 index 000000000..5cbb7efd5 --- /dev/null +++ b/po/ca_ES.UTF-8.po @@ -0,0 +1,269 @@ +# Catalan 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. +# Francesc Arpi , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-20 08:07+0100\n" +"Last-Translator: Francesc Arpi \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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Canvia el títol del terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Cancel·la" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "D'acord" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Errors de configuració" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"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.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignora" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Carrega la configuració" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Copia" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Enganxa" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Neteja" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Reinicia" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Divideix" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Canvia el títol…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Divideix cap amunt" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Divideix cap avall" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Divideix a l'esquerra" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Divideix a la dreta" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Pestanya" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Nova pestanya" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Tanca la pestanya" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Finestra" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Nova finestra" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Tanca la finestra" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configuració" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Obre la configuració" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspector de terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Sobre Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Surt" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Autoritza l'accés al porta-retalls" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Permet" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Avís: Enganxament potencialment insegur" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiat al porta-retalls" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Tanca" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Surt de Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Tanca la finestra?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Tanca la pestanya?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Tanca la divisió?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Totes les sessions del terminal es tancaran." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Totes les sessions del terminal en aquesta finestra es tancaran." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Totes les sessions del terminal en aquesta pestanya es tancaran." + +#: src/apprt/gtk/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Mostra les pestanyes obertes" + +#: src/apprt/gtk/Window.zig:295 +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/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "S'ha tornat a carregar la configuració" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Desenvolupadors de Ghostty" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po new file mode 100644 index 000000000..339ff54c4 --- /dev/null +++ b/po/es_BO.UTF-8.po @@ -0,0 +1,267 @@ +# 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. +# Miguel Peredo , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" +"PO-Revision-Date: 2025-03-28 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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Cambiar el título de la terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Dejar en blanco para restaurar el título predeterminado." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Aceptar" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Errores de configuración" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"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.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Recargar configuración" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Pegar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Limpiar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Cambiar título…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Dividir arriba" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Dividir abajo" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Dividir a la izquierda" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Dividir a la derecha" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Nueva pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Cerrar pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Nueva ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Cerrar ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configuración" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Abrir configuración" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspector de la terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Acerca de Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Salir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acceso al portapapeles" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Advertencia: Pegado potencialmente inseguro" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de la terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Cerrar" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "¿Salir de Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "¿Cerrar ventana?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "¿Cerrar pestaña?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "¿Cerrar división?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Todas las sesiones de terminal serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:97 +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/CloseDialog.zig:98 +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/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/Window.zig:295 +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/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po new file mode 100644 index 000000000..fc5bfd054 --- /dev/null +++ b/po/fr_FR.UTF-8.po @@ -0,0 +1,268 @@ +# French 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. +# Kirwiisp , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-22 09:31+0100\n" +"Last-Translator: Kirwiisp \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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Changer le nom du terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Laisser vide pour restaurer le titre par défaut." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Annuler" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Erreurs de configuration" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"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.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignorer" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Recharger la configuration" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Copier" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Coller" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Tout effacer" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Réinitialiser" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Créer panneau" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Changer le titre…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Panneau en haut" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Panneau en bas" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Panneau à gauche" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Panneau à droite" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Onglet" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Nouvel onglet" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Fermer onglet" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Fenêtre" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Nouvelle fenêtre" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Fermer la fenêtre" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Config" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Ouvrir la configuration" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspecteur de terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "À propos de Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Quitter" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Autoriser l'accès au presse-papiers" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Refuser" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Autoriser" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Attention: Collage potentiellement dangereux" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspecteur" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copié dans le presse-papiers" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Fermer" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Quitter Ghostty ?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Fermer la fenêtre ?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Fermer l'onglet ?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Fermer le panneau ?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Toutes les sessions vont être arrêtées." + +#: src/apprt/gtk/CloseDialog.zig:97 +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/CloseDialog.zig:98 +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/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Menu principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Voir les onglets ouverts" + +#: src/apprt/gtk/Window.zig:295 +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/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Recharger la configuration" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Les développeurs de Ghostty" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po new file mode 100644 index 000000000..c8b89a89e --- /dev/null +++ b/po/id_ID.UTF-8.po @@ -0,0 +1,266 @@ +# Indonesian 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. +# Satrio Bayu Aji , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-20 15:19+0700\n" +"Last-Translator: Satrio Bayu Aji \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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Ubah judul terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Biarkan kosong untuk mengembalikan judul bawaan." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Batal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Kesalahan konfigurasi" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"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.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Abaikan" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Muat ulang konfigurasi" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Salin" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Tempel" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Hapus" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Atur ulang" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Belah" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Ubah judul…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Belah atas" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Belah bawah" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Belah kiri" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Belah kanan" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Tab" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Tab baru" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Tutup tab" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Jendela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Jendela baru" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Tutup jendela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Konfigurasi" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Buka konfigurasi" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspektur terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Tentang Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Keluar" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Mengesahkan akses papan klip" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Menyangkal" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Izinkan" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Peringatan: Tempelan yang berpotensi tidak aman" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspektur terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Disalin ke papan klip" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Tutup" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Keluar dari Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Tutup jendela?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Tutup tab?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Tutup belahan?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Semua sesi terminal akan diakhiri." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Semua sesi terminal di jendela ini akan diakhiri." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Semua sesi terminal di tab ini akan diakhiri." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Menu utama" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Lihat tab terbuka" + +#: src/apprt/gtk/Window.zig:295 +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/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Memuat ulang konfigurasi" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Pengembang Ghostty" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po new file mode 100644 index 000000000..7a4ee6929 --- /dev/null +++ b/po/ja_JP.UTF-8.po @@ -0,0 +1,268 @@ +# Japanese translations for com.mitchellh.ghostty package +# com.mitchellh.ghostty パッケージに対する英訳. +# Copyright (C) 2025 Mitchell Hashimoto +# 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: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-21 00:08+0900\n" +"Last-Translator: Lon Sagisawa \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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "ターミナルのタイトルを変更する" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "空白にした場合、デフォルトのタイトルを使用します。" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "キャンセル" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "設定エラー" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"設定ファイルにエラーがあります。以下のエラーを確認し、" +"設定ファイルの再読み込みをするか、無視してください。" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "無視" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "設定ファイルの再読み込み" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "コピー" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "貼り付け" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "クリア" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "リセット" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "分割" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "タイトルを変更…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "上に分割" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "下に分割" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "左に分割" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "右に分割" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "タブ" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "新しいタブ" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "タブを閉じる" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "ウィンドウ" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "新しいウィンドウ" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "ウィンドウを閉じる" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "設定" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "設定ファイルを開く" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "端末インスペクター" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Ghostty について" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "終了" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "クリップボードへのアクセスを承認" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"アプリケーションがクリップボードを読み取ろうとしています。" +"現在のクリップボードの内容は以下の通りです。" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "拒否" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "許可" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"アプリケーションがクリップボードに書き込もうとしています。" +"現在のクリップボードの内容は以下の通りです。" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "警告: 危険な可能性のあるペースト" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"このテキストには実行可能なコマンドが含まれており、" +"ターミナルに貼り付けるのは危険な可能性があります。" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: 端末インスペクター" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "クリップボードにコピーしました" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "閉じる" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Ghostty を終了しますか?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "ウィンドウを閉じますか?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "タブを閉じますか?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "分割ウィンドウを閉じますか?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "すべてのターミナルセッションが終了されます。" + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "ウィンドウ内のすべてのターミナルセッションが終了されます。" + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "タブ内のすべてのターミナルセッションが終了されます。" + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "分割ウィンドウ内のすべてのターミナルセッションが終了されます。" + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "メインメニュー" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "開いているすべてのタブを表示" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "設定を再読み込みしました" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty 開発者" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po new file mode 100644 index 000000000..6ebea478b --- /dev/null +++ b/po/nl_NL.UTF-8.po @@ -0,0 +1,268 @@ +# Dutch 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. +# Nico Geesink , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-24 15:00+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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Titel van de terminal wijzigen" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Laat leeg om de standaard titel te herstellen." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Annuleren" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Configuratiefouten" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " +"en herlaad je configuratie of negeer deze fouten." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Negeer" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Herlaad configuratie" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Kopiëren" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Plakken" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Leegmaken" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Herstellen" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Splitsen" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Wijzig titel…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Splits naar boven" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Splits naar beneden" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Splits naar links" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Splits naar rechts" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Tabblad" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Nieuw tabblad" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Sluit tabblad" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Venster" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Nieuw venster" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Sluit venster" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configuratie" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Open configuratie" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Terminal inspecteur" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Over Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Afsluiten" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Verleen toegang tot klembord" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Weigeren" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Toestaan" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Waarschuwing: mogelijk onveilige plakactie" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: terminal inspecteur" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Gekopieerd naar klembord" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Afsluiten" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Wil je Ghostty afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Wil je dit venster afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Wil je dit tabblad afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Wil je deze splitsing afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Alle terminalsessies zullen worden beëindigd." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Alle terminalsessies binnen dit venster zullen worden beëindigd." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Alle processen die nu draaien in deze splitsing zullen worden beëindigd." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Hoofdmenu" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Open tabbladen bekijken" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan normaal." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "De configuratie is herladen" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty ontwikkelaars" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po new file mode 100644 index 000000000..f9fadce66 --- /dev/null +++ b/po/pt_BR.UTF-8.po @@ -0,0 +1,269 @@ +# Portuguese translations for com.mitchellh.ghostty package +# Traduções em português brasileiro para o pacote com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Gustavo Peres , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" +"PO-Revision-Date: 2025-03-28 11:04-0300\n" +"Last-Translator: Gustavo Peres \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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Mudar título do Terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Deixe em branco para restaurar o título original." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Erros de configuração" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"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.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Recarregar configuração" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Colar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Limpar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Mudar título…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Dividir para cima" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Dividir para baixo" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Dividir à esquerda" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Dividir à direita" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Aba" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Nova aba" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Fechar aba" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Janela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Nova janela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Fechar janela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configurar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Abrir configuração" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspetor de terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Sobre o Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Sair" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acesso à área de transferência" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Negar" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Aviso: Conteúdo potencialmente inseguro" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspetor de terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado para a área de transferência" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Fechar" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Fechar Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Fechar janela?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Fechar aba?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Fechar divisão?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Todas as sessões de terminal serão finalizadas." + +#: src/apprt/gtk/CloseDialog.zig:97 +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/CloseDialog.zig:98 +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/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "O processo atual rodando nessa divisão será finalizado." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Menu Principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Visualizar abas abertas" + +#: src/apprt/gtk/Window.zig:295 +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/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Configuração recarregada" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Desenvolvedores Ghostty" diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po new file mode 100644 index 000000000..a3c21a246 --- /dev/null +++ b/po/ru_RU.UTF-8.po @@ -0,0 +1,270 @@ +# Russian translations for com.mitchellh.ghostty package +# Русские переводы для пакета com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# blackzeshi , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-24 00:01+0500\n" +"Last-Translator: blackzeshi \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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Изменить заголовок терминала" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Оставьте пустым, чтобы восстановить исходный заголовок." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Отмена" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "ОК" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Ошибки конфигурации" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Конфигурация содержит ошибки. Проверьте их ниже, а затем" +"либо перезагрузите конфигурацию, либо проигнорируйте ошибки." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Игнорировать" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Обновить конфигурацию" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Копировать" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Вставить" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Очистить" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Сброс" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Сплит" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Изменить заголовок…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Сплит вверх" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Сплит вниз" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Сплит влево" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Сплит вправо" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Вкладка" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Новая вкладка" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Закрыть вкладку" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Окно" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Новое окно" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Закрыть окно" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Конфигурация" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Открыть конфигурационный файл" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Инспектор терминала" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "О Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Выход" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Разрешить доступ к буферу обмена" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Приложение пытается прочитать данные из буфера обмена. Эти данные " +"отображены ниже." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Отклонить" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Разрешить" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Приложение пытается записать данные в буфер обмена. Эти данные " +"показаны ниже." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Внимание! Вставляемые данные могут нанести вред вашей системе" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Вставка этого текста в терминал может быть опасной. Это выглядит " +"как команды, которые могут быть исполнены." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: инспектор терминала" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопировано в буфер обмена" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Закрыть" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Закрыть Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Закрыть окно?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Закрыть вкладку?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Закрыть сплит-режим?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Все сессии терминала будут остановлены." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Все сессии терминала в этом окне будут остановлены." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Все сессии терминала в этой вкладке будут остановлены." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Процесс, работающий в этой сплит-области, будет остановлен." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Главное меню" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Просмотреть открытые вкладки" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на производительность." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Конфигурация была обновлена" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Разработчики Ghostty" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po new file mode 100644 index 000000000..cee17a6a1 --- /dev/null +++ b/po/tr_TR.UTF-8.po @@ -0,0 +1,270 @@ +# Turkish 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. +# Emir SARI , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" +"PO-Revision-Date: 2025-03-24 22:01+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" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Uçbirim Başlığını Değiştir" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "İptal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Tamam" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Yapılandırma Hataları" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"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.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Yok Say" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "Yapılandırmayı Yeniden Yükle" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Kopyala" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Yapıştır" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Temizle" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Sıfırla" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Böl" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Başlığı Değiştir…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Yukarı Doğru Böl" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Aşağı Doğru Böl" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Sola Doğru Böl" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Sağa Doğru Böl" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Sekme" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "Yeni Sekme" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Sekmeyi Kapat" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Pencere" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Yeni Pencere" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Pencereyi Kapat" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Yapılandırma" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Yapılandırmayı Aç" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Uçbirim Denetçisi" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Ghostty Hakkında" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Çık" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Pano Erişimine İzin Ver" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +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/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Reddet" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "İzin Ver" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +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/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Uyarı: Tehlikeli Olabilecek Yapıştırma" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Uçbirim Denetçisi" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Kapat" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Ghostty’den Çık?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Pencereyi Kapat?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Sekmeyi Kapat?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Bölmeyi Kapat?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/CloseDialog.zig:97 +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/CloseDialog.zig:98 +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/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Ana Menü" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Açık Sekmeleri Görüntüle" + +#: src/apprt/gtk/Window.zig:295 +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/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Yapılandırma yeniden yüklendi" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty Geliştiricileri" diff --git a/src/Command.zig b/src/Command.zig index a810b16ce..e17c1b370 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -33,14 +33,17 @@ const EnvMap = std.process.EnvMap; const PreExecFn = fn (*Command) void; -/// Path to the command to run. This must be an absolute path. This -/// library does not do PATH lookup. -path: []const u8, +/// Path to the command to run. This doesn't have to be an absolute path, +/// because use exec functions that search the PATH, if necessary. +/// +/// This field is null-terminated to avoid a copy for the sake of +/// adding a null terminator since POSIX systems are so common. +path: [:0]const u8, /// Command-line arguments. It is the responsibility of the caller to set /// args[0] to the command. If args is empty then args[0] will automatically /// be set to equal path. -args: []const []const u8, +args: []const [:0]const u8, /// Environment variables for the child process. If this is null, inherits /// the environment variables from this process. These are the exact @@ -129,9 +132,8 @@ pub fn start(self: *Command, alloc: Allocator) !void { fn startPosix(self: *Command, arena: Allocator) !void { // Null-terminate all our arguments - const pathZ = try arena.dupeZ(u8, self.path); - const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null); - for (self.args, 0..) |arg, i| argsZ[i] = (try arena.dupeZ(u8, arg)).ptr; + const argsZ = try arena.allocSentinel(?[*:0]const u8, self.args.len, null); + for (self.args, 0..) |arg, i| argsZ[i] = arg.ptr; // Determine our env vars const envp = if (self.env) |env_map| @@ -184,7 +186,9 @@ fn startPosix(self: *Command, arena: Allocator) !void { if (self.pre_exec) |f| f(self); // Finally, replace our process. - _ = posix.execveZ(pathZ, argsZ, envp) catch null; + // Note: we must use the "p"-variant of exec here because we + // do not guarantee our command is looked up already in the path. + _ = posix.execvpeZ(self.path, argsZ, envp) catch null; // If we are executing this code, the exec failed. In that scenario, // we return a very specific error that can be detected to determine diff --git a/src/Surface.zig b/src/Surface.zig index 46fa476f7..89031a1b5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -518,7 +518,7 @@ pub fn init( }; // The command we're going to execute - const command: ?[]const u8 = if (app.first) + const command: ?configpkg.Command = if (app.first) config.@"initial-command" orelse config.command else config.command; @@ -650,21 +650,19 @@ pub fn init( // title to the command being executed. This allows window managers // to set custom styling based on the command being executed. const v = command orelse break :xdg; - if (v.len > 0) { - const title = alloc.dupeZ(u8, v) catch |err| { - log.warn( - "error copying command for title, title will not be set err={}", - .{err}, - ); - break :xdg; - }; - defer alloc.free(title); - _ = try rt_app.performAction( - .{ .surface = self }, - .set_title, - .{ .title = title }, + const title = v.string(alloc) catch |err| { + log.warn( + "error copying command for title, title will not be set err={}", + .{err}, ); - } + break :xdg; + }; + defer alloc.free(title); + _ = try rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = title }, + ); } // We are no longer the first surface diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 2ddbee524..30cb2fa5e 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -311,7 +311,7 @@ pub const Action = union(Key) { break :cvalue @Type(.{ .@"union" = .{ .layout = .@"extern", - .tag_type = Key, + .tag_type = null, .fields = &union_fields, .decls = &.{}, } }); @@ -323,6 +323,13 @@ pub const Action = union(Key) { value: CValue, }; + comptime { + // For ABI compatibility, we expect that this is our union size. + // At the time of writing, we don't promise ABI compatibility + // so we can change this but I want to be aware of it. + assert(@sizeOf(CValue) == 16); + } + /// Returns the value type for the given key. pub fn Value(comptime key: Key) type { inline for (@typeInfo(Action).@"union".fields) |field| { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 9ae00ab8e..50b54435d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -636,6 +636,11 @@ pub const Surface = struct { /// The command to run in the new surface. If this is set then /// the "wait-after-command" option is also automatically set to true, /// since this is used for scripting. + /// + /// This command always run in a shell (e.g. via `/bin/sh -c`), + /// despite Ghostty allowing directly executed commands via config. + /// This is a legacy thing and we should probably change it in the + /// future once we have a concrete use case. command: [*:0]const u8 = "", }; @@ -696,7 +701,7 @@ pub const Surface = struct { // If we have a command from the options then we set it. const cmd = std.mem.sliceTo(opts.command, 0); if (cmd.len > 0) { - config.command = cmd; + config.command = .{ .shell = cmd }; config.@"wait-after-command" = true; } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b4bebe8ee..ddee49459 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -314,8 +314,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .prefer_dark; }, .system => .prefer_light, - .dark => .prefer_dark, - .light => .force_dark, + .dark => .force_dark, + .light => .force_light, }, ); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2866c5ede..5fcb0d42b 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -281,6 +281,15 @@ pub fn init(self: *Window, app: *App) !void { .detail = "is-active", }, ); + _ = gobject.Object.signals.notify.connect( + self.window, + *Window, + gtkWindowUpdateScaleFactor, + self, + .{ + .detail = "scale-factor", + }, + ); // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we // need to stick the headerbar into the content box. @@ -473,11 +482,13 @@ pub fn syncAppearance(self: *Window) !void { if (self.isQuickTerminal()) break :visible false; // Unconditionally disable the header bar when fullscreened. - if (self.config.fullscreen) break :visible false; + if (self.window.as(gtk.Window).isFullscreen() != 0) + break :visible false; // *Conditionally* disable the header bar when maximized, // and gtk-titlebar-hide-when-maximized is set - if (self.config.maximize and self.config.gtk_titlebar_hide_when_maximized) + if (self.window.as(gtk.Window).isMaximized() != 0 and + self.config.gtk_titlebar_hide_when_maximized) break :visible false; break :visible self.config.gtk_titlebar; @@ -672,7 +683,7 @@ pub fn toggleTabOverview(self: *Window) void { /// Toggle the maximized state for this window. pub fn toggleMaximize(self: *Window) void { - if (self.config.maximize) { + if (self.window.as(gtk.Window).isMaximized() != 0) { self.window.as(gtk.Window).unmaximize(); } else { self.window.as(gtk.Window).maximize(); @@ -683,7 +694,7 @@ pub fn toggleMaximize(self: *Window) void { /// Toggle fullscreen for this window. pub fn toggleFullscreen(self: *Window) void { - if (self.config.fullscreen) { + if (self.window.as(gtk.Window).isFullscreen() != 0) { self.window.as(gtk.Window).unfullscreen(); } else { self.window.as(gtk.Window).fullscreen(); @@ -754,7 +765,6 @@ fn gtkWindowNotifyMaximized( _: *gobject.ParamSpec, self: *Window, ) callconv(.c) void { - self.config.maximize = self.window.as(gtk.Window).isMaximized() != 0; self.syncAppearance() catch |err| { log.err("failed to sync appearance={}", .{err}); }; @@ -765,7 +775,6 @@ fn gtkWindowNotifyFullscreened( _: *gobject.ParamSpec, self: *Window, ) callconv(.c) void { - self.config.fullscreen = self.window.as(gtk.Window).isFullscreen() != 0; self.syncAppearance() catch |err| { log.err("failed to sync appearance={}", .{err}); }; @@ -784,6 +793,24 @@ fn gtkWindowNotifyIsActive( } } +fn gtkWindowUpdateScaleFactor( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Window, +) callconv(.c) void { + // On some platforms (namely X11) we need to refresh our appearance when + // the scale factor changes. In theory this could be more fine-grained as + // a full refresh could be expensive, but a) this *should* be rare, and + // b) quite noticeable visual bugs would occur if this is not present. + self.winproto.syncAppearance() catch |err| { + log.err( + "failed to sync appearance after scale factor has been updated={}", + .{err}, + ); + return; + }; +} + // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab // sends an undefined value. fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 6d6950f74..c2b6bf416 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -219,13 +219,12 @@ pub const Window = struct { pub fn resizeEvent(self: *Window) !void { // The blur region must update with window resizes - const gtk_widget = self.gtk_window.as(gtk.Widget); - self.blur_region.width = gtk_widget.getWidth(); - self.blur_region.height = gtk_widget.getHeight(); try self.syncBlur(); } 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 @@ -236,6 +235,11 @@ pub const Window = struct { self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y); + // Transform surface coordinates to device coordinates. + const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor()); + x *= scale; + y *= scale; + break :blur .{ .x = @intFromFloat(x), .y = @intFromFloat(y), @@ -265,6 +269,13 @@ pub const Window = struct { // and I think it's not really noticeable enough to justify the effort. // (Wayland also has this visual artifact anyway...) + const gtk_widget = self.gtk_window.as(gtk.Widget); + + // Transform surface coordinates to device coordinates. + const scale = self.gtk_window.as(gtk.Widget).getScaleFactor(); + self.blur_region.width = gtk_widget.getWidth() * scale; + self.blur_region.height = gtk_widget.getHeight() * scale; + const blur = self.config.background_blur; log.debug("set blur={}, window xid={}, region={}", .{ blur, diff --git a/src/build/Config.zig b/src/build/Config.zig index 48456734a..8974e1f0c 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig"); /// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. /// Until then this MUST match build.zig.zon and should always be the /// _next_ version to release. -const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 3 }; +const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 4 }; /// Standard build configuration options. optimize: std.builtin.OptimizeMode, diff --git a/src/config.zig b/src/config.zig index a06e19872..fb7359b3e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -14,6 +14,7 @@ pub const formatEntry = formatter.formatEntry; // Field types pub const ClipboardAccess = Config.ClipboardAccess; +pub const Command = Config.Command; pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const CopyOnSelect = Config.CopyOnSelect; pub const CustomShaderAnimation = Config.CustomShaderAnimation; diff --git a/src/config/Config.zig b/src/config/Config.zig index ecdcee7fc..a0d9275e9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -22,7 +22,6 @@ const inputpkg = @import("../input.zig"); const terminal = @import("../terminal/main.zig"); const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); -const Command = @import("../Command.zig"); const conditional = @import("conditional.zig"); const Conditional = conditional.Conditional; @@ -34,6 +33,7 @@ const KeyValue = @import("key.zig").Value; const ErrorList = @import("ErrorList.zig"); const MetricModifier = fontpkg.Metrics.Modifier; const help_strings = @import("help_strings"); +pub const Command = @import("command.zig").Command; const RepeatableStringMap = @import("RepeatableStringMap.zig"); pub const Path = @import("path.zig").Path; pub const RepeatablePath = @import("path.zig").RepeatablePath; @@ -691,8 +691,17 @@ palette: Palette = .{}, /// * `passwd` entry (user information) /// /// This can contain additional arguments to run the command with. If additional -/// arguments are provided, the command will be executed using `/bin/sh -c`. -/// Ghostty does not do any shell command parsing. +/// arguments are provided, the command will be executed using `/bin/sh -c` +/// to offload shell argument expansion. +/// +/// To avoid shell expansion altogether, prefix the command with `direct:`, +/// e.g. `direct:nvim foo`. This will avoid the roundtrip to `/bin/sh` but will +/// also not support any shell parsing such as arguments with spaces, filepaths +/// with `~`, globs, etc. +/// +/// You can also explicitly prefix the command with `shell:` to always +/// wrap the command in a shell. This can be used to ensure our heuristics +/// to choose the right mode are not used in case they are wrong. /// /// This command will be used for all new terminal surfaces, i.e. new windows, /// tabs, etc. If you want to run a command only for the first terminal surface @@ -702,7 +711,7 @@ palette: Palette = .{}, /// arguments. For example, `ghostty -e fish --with --custom --args`. /// This flag sets the `initial-command` configuration, see that for more /// information. -command: ?[]const u8 = null, +command: ?Command = null, /// This is the same as "command", but only applies to the first terminal /// surface created when Ghostty starts. Subsequent terminal surfaces will use @@ -718,6 +727,10 @@ command: ?[]const u8 = null, /// fish --with --custom --args`. The `-e` flag automatically forces some /// other behaviors as well: /// +/// * Disables shell expansion since the input is expected to already +/// be shell-expanded by the upstream (e.g. the shell used to type in +/// the `ghostty -e` command). +/// /// * `gtk-single-instance=false` - This ensures that a new instance is /// launched and the CLI args are respected. /// @@ -735,7 +748,7 @@ command: ?[]const u8 = null, /// name your binary appropriately or source the shell integration script /// manually. /// -@"initial-command": ?[]const u8 = null, +@"initial-command": ?Command = null, /// Extra environment variables to pass to commands launched in a terminal /// surface. The format is `env=KEY=VALUE`. @@ -826,7 +839,7 @@ env: RepeatableStringMap = .{}, link: RepeatableLink = .{}, /// Enable URL matching. URLs are matched on hover with control (Linux) or -/// super (macOS) pressed and open using the default system application for +/// command (macOS) pressed and open using the default system application for /// the linked URL. /// /// The URL matcher is always lowest priority of any configured links (see @@ -2564,21 +2577,17 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { // Next, take all remaining args and use that to build up // a command to execute. - var command = std.ArrayList(u8).init(arena_alloc); - errdefer command.deinit(); + var builder = std.ArrayList([:0]const u8).init(arena_alloc); + errdefer builder.deinit(); for (args) |arg_raw| { const arg = std.mem.sliceTo(arg_raw, 0); - try self._replay_steps.append( - arena_alloc, - .{ .arg = try arena_alloc.dupe(u8, arg) }, - ); - - try command.appendSlice(arg); - try command.append(' '); + const copy = try arena_alloc.dupeZ(u8, arg); + try self._replay_steps.append(arena_alloc, .{ .arg = copy }); + try builder.append(copy); } self.@"_xdg-terminal-exec" = true; - self.@"initial-command" = command.items[0 .. command.items.len - 1]; + self.@"initial-command" = .{ .direct = try builder.toOwnedSlice() }; return; } } @@ -3023,7 +3032,7 @@ pub fn finalize(self: *Config) !void { // We don't do this in flatpak because SHELL in Flatpak is always // set to /bin/sh. if (self.command) |cmd| - log.info("shell src=config value={s}", .{cmd}) + log.info("shell src=config value={}", .{cmd}) else shell_env: { // Flatpak always gets its shell from outside the sandbox if (internal_os.isFlatpak()) break :shell_env; @@ -3035,7 +3044,9 @@ pub fn finalize(self: *Config) !void { if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| { log.info("default shell source=env value={s}", .{value}); - self.command = value; + + const copy = try alloc.dupeZ(u8, value); + self.command = .{ .shell = copy }; // If we don't need the working directory, then we can exit now. if (!wd_home) break :command; @@ -3046,7 +3057,7 @@ pub fn finalize(self: *Config) !void { .windows => { if (self.command == null) { log.warn("no default shell found, will default to using cmd", .{}); - self.command = "cmd.exe"; + self.command = .{ .shell = "cmd.exe" }; } if (wd_home) { @@ -3063,7 +3074,7 @@ pub fn finalize(self: *Config) !void { if (self.command == null) { if (pw.shell) |sh| { log.info("default shell src=passwd value={s}", .{sh}); - self.command = sh; + self.command = .{ .shell = sh }; } } @@ -3145,13 +3156,13 @@ pub fn parseManuallyHook( // Build up the command. We don't clean this up because we take // ownership in our allocator. - var command = std.ArrayList(u8).init(alloc); + var command: std.ArrayList([:0]const u8) = .init(alloc); errdefer command.deinit(); while (iter.next()) |param| { - try self._replay_steps.append(alloc, .{ .arg = try alloc.dupe(u8, param) }); - try command.appendSlice(param); - try command.append(' '); + const copy = try alloc.dupeZ(u8, param); + try self._replay_steps.append(alloc, .{ .arg = copy }); + try command.append(copy); } if (command.items.len == 0) { @@ -3167,9 +3178,8 @@ pub fn parseManuallyHook( return false; } - self.@"initial-command" = command.items[0 .. command.items.len - 1]; - // See "command" docs for the implied configurations and why. + self.@"initial-command" = .{ .direct = command.items }; self.@"gtk-single-instance" = .false; self.@"quit-after-last-window-closed" = true; self.@"quit-after-last-window-closed-delay" = null; @@ -3184,7 +3194,7 @@ pub fn parseManuallyHook( // Keep track of our input args for replay try self._replay_steps.append( alloc, - .{ .arg = try alloc.dupe(u8, arg) }, + .{ .arg = try alloc.dupeZ(u8, arg) }, ); // If we didn't find a special case, continue parsing normally @@ -3377,6 +3387,16 @@ fn equalField(comptime T: type, old: T, new: T) bool { [:0]const u8, => return std.mem.eql(u8, old, new), + []const [:0]const u8, + => { + if (old.len != new.len) return false; + for (old, new) |a, b| { + if (!std.mem.eql(u8, a, b)) return false; + } + + return true; + }, + else => {}, } @@ -3412,6 +3432,8 @@ fn equalField(comptime T: type, old: T, new: T) bool { }, .@"union" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + const tag_type = info.tag_type.?; const old_tag = std.meta.activeTag(old); const new_tag = std.meta.activeTag(new); @@ -3441,7 +3463,7 @@ fn equalField(comptime T: type, old: T, new: T) bool { const Replay = struct { const Step = union(enum) { /// An argument to parse as if it came from the CLI or file. - arg: []const u8, + arg: [:0]const u8, /// A base path to expand relative paths against. expand: []const u8, @@ -3481,7 +3503,7 @@ const Replay = struct { return switch (self) { .@"-e" => self, .diagnostic => |v| .{ .diagnostic = try v.clone(alloc) }, - .arg => |v| .{ .arg = try alloc.dupe(u8, v) }, + .arg => |v| .{ .arg = try alloc.dupeZ(u8, v) }, .expand => |v| .{ .expand = try alloc.dupe(u8, v) }, .conditional_arg => |v| conditional: { var conds = try alloc.alloc(Conditional, v.conditions.len); @@ -6620,7 +6642,11 @@ test "parse e: command only" { var it: TestIterator = .{ .data = &.{"foo"} }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("foo", cfg.@"initial-command".?); + + const cmd = cfg.@"initial-command".?; + try testing.expect(cmd == .direct); + try testing.expectEqual(cmd.direct.len, 1); + try testing.expectEqualStrings(cmd.direct[0], "foo"); } test "parse e: command and args" { @@ -6631,7 +6657,13 @@ test "parse e: command and args" { var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("echo foo bar baz", cfg.@"initial-command".?); + + const cmd = cfg.@"initial-command".?; + try testing.expect(cmd == .direct); + try testing.expectEqual(cmd.direct.len, 3); + try testing.expectEqualStrings(cmd.direct[0], "echo"); + try testing.expectEqualStrings(cmd.direct[1], "foo"); + try testing.expectEqualStrings(cmd.direct[2], "bar baz"); } test "clone default" { diff --git a/src/config/command.zig b/src/config/command.zig new file mode 100644 index 000000000..9efeb199e --- /dev/null +++ b/src/config/command.zig @@ -0,0 +1,322 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const formatterpkg = @import("formatter.zig"); + +/// A command to execute (argv0 and args). +/// +/// A command is specified as a simple string such as "nvim a b c". +/// By default, we expect the downstream to do some sort of shell expansion +/// on this string. +/// +/// If a command is already expanded and the user does NOT want to do +/// shell expansion (because this usually requires a round trip into +/// /bin/sh or equivalent), specify a `direct:`-prefix. e.g. +/// `direct:nvim a b c`. +/// +/// The whitespace before or around the prefix is ignored. For example, +/// ` direct:nvim a b c` and `direct: nvim a b c` are equivalent. +/// +/// If the command is not absolute, it'll be looked up via the PATH. +/// For the shell-expansion case, we let the shell do this. For the +/// direct case, we do this directly. +pub const Command = union(enum) { + const Self = @This(); + + /// Execute a command directly, e.g. via `exec`. The format here + /// is already structured to be ready to passed directly to `exec` + /// with index zero being the command to execute. + /// + /// Index zero is not guaranteed to be an absolute path, and may require + /// PATH lookup. It is up to the downstream to do this, usually via + /// delegation to something like `execvp`. + direct: []const [:0]const u8, + + /// Execute a command via shell expansion. This provides the command + /// as a single string that is expected to be expanded in some way + /// (up to the downstream). Usually `/bin/sh -c`. + shell: [:0]const u8, + + pub fn parseCLI( + self: *Self, + alloc: Allocator, + input_: ?[]const u8, + ) !void { + // Input is required. Whitespace on the edges isn't needed. + // Commands must be non-empty. + const input = input_ orelse return error.ValueRequired; + const trimmed = std.mem.trim(u8, input, " "); + if (trimmed.len == 0) return error.ValueRequired; + + // If we have a `:` then we MIGHT have a prefix to specify what + // tag we should use. + const tag: std.meta.Tag(Self), const str: []const u8 = tag: { + if (std.mem.indexOfScalar(u8, trimmed, ':')) |idx| { + const prefix = trimmed[0..idx]; + if (std.mem.eql(u8, prefix, "direct")) { + break :tag .{ .direct, trimmed[idx + 1 ..] }; + } else if (std.mem.eql(u8, prefix, "shell")) { + break :tag .{ .shell, trimmed[idx + 1 ..] }; + } + } + + break :tag .{ .shell, trimmed }; + }; + + switch (tag) { + .shell => { + // We have a shell command, so we can just dupe it. + const copy = try alloc.dupeZ(u8, std.mem.trim(u8, str, " ")); + self.* = .{ .shell = copy }; + }, + + .direct => { + // We're not shell expanding, so the arguments are naively + // split on spaces. + var builder: std.ArrayListUnmanaged([:0]const u8) = .empty; + var args = std.mem.splitScalar( + u8, + std.mem.trim(u8, str, " "), + ' ', + ); + while (args.next()) |arg| { + const copy = try alloc.dupeZ(u8, arg); + try builder.append(alloc, copy); + } + + self.* = .{ .direct = try builder.toOwnedSlice(alloc) }; + }, + } + } + + /// Creates a command as a single string, joining arguments as + /// necessary with spaces. Its not guaranteed that this is a valid + /// command; it is only meant to be human readable. + pub fn string( + self: *const Self, + alloc: Allocator, + ) Allocator.Error![:0]const u8 { + return switch (self.*) { + .shell => |v| try alloc.dupeZ(u8, v), + .direct => |v| try std.mem.joinZ(alloc, " ", v), + }; + } + + /// Get an iterator over the arguments array. This may allocate + /// depending on the active tag of the command. + /// + /// For direct commands, this is very cheap and just iterates over + /// the array. There is no allocation. + /// + /// For shell commands, this will use Zig's ArgIteratorGeneral as + /// a best effort shell string parser. This is not guaranteed to be + /// 100% accurate, but it works for common cases. This requires allocation. + pub fn argIterator( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!ArgIterator { + return switch (self.*) { + .direct => |v| .{ .direct = .{ .args = v } }, + .shell => |v| .{ .shell = try .init(alloc, v) }, + }; + } + + /// Iterates over each argument in the command. + pub const ArgIterator = union(enum) { + shell: std.process.ArgIteratorGeneral(.{}), + direct: struct { + i: usize = 0, + args: []const [:0]const u8, + }, + + /// Return the next argument. This may or may not be a copy + /// depending on the active tag. If you want to ensure that every + /// argument is a copy, use the `clone` method first. + pub fn next(self: *ArgIterator) ?[:0]const u8 { + return switch (self.*) { + .shell => |*v| v.next(), + .direct => |*v| { + if (v.i >= v.args.len) return null; + defer v.i += 1; + return v.args[v.i]; + }, + }; + } + + pub fn deinit(self: *ArgIterator) void { + switch (self.*) { + .shell => |*v| v.deinit(), + .direct => {}, + } + } + }; + + pub fn clone( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!Self { + return switch (self.*) { + .shell => |v| .{ .shell = try alloc.dupeZ(u8, v) }, + .direct => |v| direct: { + const copy = try alloc.alloc([:0]const u8, v.len); + for (v, 0..) |arg, i| copy[i] = try alloc.dupeZ(u8, arg); + break :direct .{ .direct = copy }; + }, + }; + } + + pub fn formatEntry(self: Self, formatter: anytype) !void { + switch (self) { + .shell => |v| try formatter.formatEntry([]const u8, v), + + .direct => |v| { + var buf: [4096]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + writer.writeAll("direct:") catch return error.OutOfMemory; + for (v) |arg| { + writer.writeAll(arg) catch return error.OutOfMemory; + writer.writeByte(' ') catch return error.OutOfMemory; + } + + const written = fbs.getWritten(); + try formatter.formatEntry( + []const u8, + written[0..@intCast(written.len - 1)], + ); + }, + } + } + + test "Command: parseCLI errors" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null)); + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, "")); + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, " ")); + } + + test "Command: parseCLI shell expanded" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try v.parseCLI(alloc, "echo hello"); + try testing.expect(v == .shell); + try testing.expectEqualStrings(v.shell, "echo hello"); + + // Spaces are stripped + try v.parseCLI(alloc, " echo hello "); + try testing.expect(v == .shell); + try testing.expectEqualStrings(v.shell, "echo hello"); + } + + test "Command: parseCLI direct" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try v.parseCLI(alloc, "direct:echo hello"); + try testing.expect(v == .direct); + try testing.expectEqual(v.direct.len, 2); + try testing.expectEqualStrings(v.direct[0], "echo"); + try testing.expectEqualStrings(v.direct[1], "hello"); + + // Spaces around the prefix + try v.parseCLI(alloc, " direct: echo hello"); + try testing.expect(v == .direct); + try testing.expectEqual(v.direct.len, 2); + try testing.expectEqualStrings(v.direct[0], "echo"); + try testing.expectEqualStrings(v.direct[1], "hello"); + } + + test "Command: argIterator shell" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .shell = "echo hello world" }; + var it = try v.argIterator(alloc); + defer it.deinit(); + + try testing.expectEqualStrings(it.next().?, "echo"); + try testing.expectEqualStrings(it.next().?, "hello"); + try testing.expectEqualStrings(it.next().?, "world"); + try testing.expect(it.next() == null); + } + + test "Command: argIterator direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .direct = &.{ "echo", "hello world" } }; + var it = try v.argIterator(alloc); + defer it.deinit(); + + try testing.expectEqualStrings(it.next().?, "echo"); + try testing.expectEqualStrings(it.next().?, "hello world"); + try testing.expect(it.next() == null); + } + + test "Command: string shell" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .shell = "echo hello world" }; + const str = try v.string(alloc); + defer alloc.free(str); + try testing.expectEqualStrings(str, "echo hello world"); + } + + test "Command: string direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .direct = &.{ "echo", "hello world" } }; + const str = try v.string(alloc); + defer alloc.free(str); + try testing.expectEqualStrings(str, "echo hello world"); + } + + test "Command: formatConfig shell" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + var v: Self = undefined; + try v.parseCLI(alloc, "echo hello"); + try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.items); + } + + test "Command: formatConfig direct" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + var v: Self = undefined; + try v.parseCLI(alloc, "direct: echo hello"); + try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.items); + } +}; + +test { + _ = Command; +} diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 65fd43153..475f5e705 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -23,13 +23,28 @@ const log = std.log.scoped(.i18n); /// /// 3. Most preferred locale for a language without a country code. /// +/// Note for "most common" locales, this is subjective and based on +/// the perceived userbase of Ghostty, which may not be representative +/// of general populations or global language distribution. Also note +/// that ordering may be weird when we first merge a new locale since +/// we don't have a good way to determine this. We can always reorder +/// with some data. pub const locales = [_][:0]const u8{ - "de_DE.UTF-8", "zh_CN.UTF-8", + "de_DE.UTF-8", + "fr_FR.UTF-8", + "ja_JP.UTF-8", + "nl_NL.UTF-8", "nb_NO.UTF-8", + "ru_RU.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", "mk_MK.UTF-8", + "tr_TR.UTF-8", + "id_ID.UTF-8", + "es_BO.UTF-8", + "pt_BR.UTF-8", + "ca_ES.UTF-8", }; /// Set for faster membership lookup of locales. @@ -108,6 +123,9 @@ pub fn canonicalizeLocale( buf: []u8, locale: []const u8, ) error{NoSpaceLeft}![:0]const u8 { + // Fix zh locales for macOS + if (fixZhLocale(locale)) |fixed| return fixed; + // Buffer must be 16 or at least as long as the locale and null term if (buf.len < @max(16, locale.len + 1)) return error.NoSpaceLeft; @@ -126,6 +144,30 @@ pub fn canonicalizeLocale( return buf[0..slice.len :0]; } +/// Handles some zh locales canonicalization because internal libintl +/// canonicalization function doesn't handle correctly in these cases. +fn fixZhLocale(locale: []const u8) ?[:0]const u8 { + var it = std.mem.splitScalar(u8, locale, '-'); + const name = it.next() orelse return null; + if (!std.mem.eql(u8, name, "zh")) return null; + + const script = it.next() orelse return null; + const region = it.next() orelse return null; + + if (std.mem.eql(u8, script, "Hans")) { + if (std.mem.eql(u8, region, "SG")) return "zh_SG"; + return "zh_CN"; + } + + if (std.mem.eql(u8, script, "Hant")) { + if (std.mem.eql(u8, region, "MO")) return "zh_MO"; + if (std.mem.eql(u8, region, "HK")) return "zh_HK"; + return "zh_TW"; + } + + return null; +} + /// This can be called at any point a compile-time-known locale is /// available. This will use comptime to verify the locale is supported. pub fn staticLocale(comptime v: [*:0]const u8) [*:0]const u8 { @@ -160,6 +202,12 @@ test "canonicalizeLocale darwin" { try testing.expectEqualStrings("zh_CN", try canonicalizeLocale(&buf, "zh-Hans")); try testing.expectEqualStrings("zh_TW", try canonicalizeLocale(&buf, "zh-Hant")); + try testing.expectEqualStrings("zh_CN", try canonicalizeLocale(&buf, "zh-Hans-CN")); + try testing.expectEqualStrings("zh_SG", try canonicalizeLocale(&buf, "zh-Hans-SG")); + try testing.expectEqualStrings("zh_TW", try canonicalizeLocale(&buf, "zh-Hant-TW")); + try testing.expectEqualStrings("zh_HK", try canonicalizeLocale(&buf, "zh-Hant-HK")); + try testing.expectEqualStrings("zh_MO", try canonicalizeLocale(&buf, "zh-Hant-MO")); + // This is just an edge case I want to make sure we're aware of: // canonicalizeLocale does not handle encodings and will turn them into // underscores. We should parse them out before calling this function. diff --git a/src/os/passwd.zig b/src/os/passwd.zig index c12214ee4..e9bbff066 100644 --- a/src/os/passwd.zig +++ b/src/os/passwd.zig @@ -25,9 +25,9 @@ const c = if (builtin.os.tag != .windows) @cImport({ // Entry that is retrieved from the passwd API. This only contains the fields // we care about. pub const Entry = struct { - shell: ?[]const u8 = null, - home: ?[]const u8 = null, - name: ?[]const u8 = null, + shell: ?[:0]const u8 = null, + home: ?[:0]const u8 = null, + name: ?[:0]const u8 = null, }; /// Get the passwd entry for the currently executing user. @@ -117,30 +117,27 @@ pub fn get(alloc: Allocator) !Entry { // Shell and home are the last two entries var it = std.mem.splitBackwardsScalar(u8, std.mem.trimRight(u8, output, " \r\n"), ':'); - result.shell = it.next() orelse null; - result.home = it.next() orelse null; + result.shell = if (it.next()) |v| try alloc.dupeZ(u8, v) else null; + result.home = if (it.next()) |v| try alloc.dupeZ(u8, v) else null; return result; } if (pw.pw_shell) |ptr| { const source = std.mem.sliceTo(ptr, 0); - const sh = try alloc.alloc(u8, source.len); - @memcpy(sh, source); - result.shell = sh; + const value = try alloc.dupeZ(u8, source); + result.shell = value; } if (pw.pw_dir) |ptr| { const source = std.mem.sliceTo(ptr, 0); - const dir = try alloc.alloc(u8, source.len); - @memcpy(dir, source); - result.home = dir; + const value = try alloc.dupeZ(u8, source); + result.home = value; } if (pw.pw_name) |ptr| { const source = std.mem.sliceTo(ptr, 0); - const name = try alloc.alloc(u8, source.len); - @memcpy(name, source); - result.name = name; + const value = try alloc.dupeZ(u8, source); + result.name = value; } return result; diff --git a/src/os/shell.zig b/src/os/shell.zig index 23648a82a..d4d682d3e 100644 --- a/src/os/shell.zig +++ b/src/os/shell.zig @@ -23,6 +23,8 @@ pub fn ShellEscapeWriter(comptime T: type) type { '?', ' ', '|', + '(', + ')', => &[_]u8{ '\\', byte }, else => &[_]u8{byte}, }; @@ -93,3 +95,12 @@ test "shell escape 6" { try writer.writeAll("a\"c"); try testing.expectEqualStrings("a\\\"c", fmt.getWritten()); } + +test "shell escape 7" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("a(1)"); + try testing.expectEqualStrings("a\\(1\\)", fmt.getWritten()); +} diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index e24ddcb1e..e80ead9ad 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -425,11 +425,19 @@ vertex CellTextVertexOut cell_text_vertex( // If we're constrained then we need to scale the glyph. if (in.mode == MODE_TEXT_CONSTRAINED) { float max_width = uniforms.cell_size.x * in.constraint_width; + // If this glyph is wider than the constraint width, + // fit it to the width and remove its horizontal offset. if (size.x > max_width) { float new_y = size.y * (max_width / size.x); offset.y += (size.y - new_y) / 2; + offset.x = 0; size.y = new_y; size.x = max_width; + } else if (max_width - size.x > offset.x) { + // However, if it does fit in the constraint width, make + // sure the offset is small enough to not push it over the + // right edge of the constraint width. + offset.x = max_width - size.x; } } @@ -499,7 +507,11 @@ fragment float4 cell_text_fragment( constexpr sampler textureSampler( coord::pixel, address::clamp_to_edge, - filter::nearest + // TODO(qwerasd): This can be changed back to filter::nearest when + // we move the constraint logic out of the GPU code + // which should once again guarantee pixel perfect + // sizing. + filter::linear ); switch (in.mode) { diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 7fae435a3..0cfd41663 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -70,7 +70,7 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then fi # Sudo -if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" && -n "$TERMINFO" ]]; then +if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved. # # This approach supports wrapping a `sudo` alias, but the alias definition @@ -124,13 +124,13 @@ function __ghostty_precmd() { fi # Cursor - if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then PS1=$PS1'\[\e[5 q\]' PS0=$PS0'\[\e[0 q\]' fi # Title (working directory) - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then PS1=$PS1'\[\e]2;\w\a\]' fi fi @@ -161,7 +161,7 @@ function __ghostty_preexec() { PS2="$_GHOSTTY_SAVE_PS2" # Title (current command) - if [[ -n $cmd && "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then + if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}" fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 08fe42f3f..a6d052a72 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -36,6 +36,8 @@ } { + use str + # helper used by `mark-*` functions fn set-prompt-state {|new| set-env __ghostty_prompt_state $new } @@ -73,7 +75,8 @@ } fn report-pwd { - printf "\e]7;file://%s%s\a" (hostname) (pwd) + use platform + printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd } fn sudo-with-terminfo {|@args| @@ -104,20 +107,18 @@ set edit:after-readline = (conj $edit:after-readline $mark-output-start~) set edit:after-command = (conj $edit:after-command $mark-output-end~) - var no-title = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE) - var no-cursor = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR) - var no-sudo = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO) + var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] - if (not $no-title) { + if (has-value $features title) { set after-chdir = (conj $after-chdir {|_| report-pwd }) } - if (not $no-cursor) { + if (has-value $features cursor) { fn beam { printf "\e[5 q" } fn block { printf "\e[0 q" } set edit:before-readline = (conj $edit:before-readline $beam~) set edit:after-readline = (conj $edit:after-readline {|_| block }) } - if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { + if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { edit:add-var sudo~ $sudo-with-terminfo~ } } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index cd4f56105..e7c264e1f 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -49,10 +49,9 @@ status --is-interactive || ghostty_exit function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" functions -e __ghostty_setup - # Check if we are setting cursors - set --local no_cursor "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" + set --local features (string split , $GHOSTTY_SHELL_FEATURES) - if test -z $no_cursor + if contains cursor $features # Change the cursor to a beam on prompt. function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" echo -en "\e[5 q" @@ -62,13 +61,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # Check if we are setting sudo - set --local no_sudo "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" - # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias - if test -z $no_sudo - and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") + if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") # Wrap `sudo` command to ensure Ghostty terminfo is preserved function sudo -d "Wrap sudo to preserve terminfo" set --function sudo_has_sudoedit_flags "no" @@ -125,7 +120,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set --global fish_handle_reflow 1 # Initial calls for first prompt - if test -z $no_cursor + if contains cursor $features __ghostty_set_cursor_beam end __ghostty_mark_prompt_start diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 9eebe1a30..c1329683e 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -194,7 +194,7 @@ _ghostty_deferred_init() { _ghostty_report_pwd" _ghostty_report_pwd - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then # Enable terminal title changes. functions[_ghostty_precmd]+=" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'" @@ -202,7 +202,7 @@ _ghostty_deferred_init() { builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'" fi - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then # Enable cursor shape changes depending on the current keymap. # This implementation leaks blinking block cursor into external commands # executed from zle. For example, users of fzf-based widgets may find @@ -221,7 +221,7 @@ _ghostty_deferred_init() { fi # Sudo - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" ]] && [[ -n "$TERMINFO" ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved sudo() { builtin local sudo_has_sudoedit_flags="no" diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index 840949d74..61ba33a4d 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -98,6 +98,12 @@ pub const Parser = struct { self.state = .control_value; }, + // This can be encountered if we have a sequence with no + // control data, only payload data (i.e. "\x1b_G;"). + // + // Kitty treats this as valid so we do as well. + ';' => self.state = .data, + else => try self.accumulateValue(c, .control_key_ignore), }, @@ -1053,6 +1059,21 @@ test "delete command" { try testing.expectEqual(@as(u32, 4), dv.y); } +test "no control data" { + const testing = std.testing; + const alloc = testing.allocator; + var p = Parser.init(alloc); + defer p.deinit(); + + const input = ";QUFBQQ"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .transmit); + try testing.expectEqualStrings("AAAA", command.data); +} + test "ignore unknown keys (long)" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 61b501258..abe49a47b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -24,6 +24,7 @@ const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool; const ptypkg = @import("../pty.zig"); const Pty = ptypkg.Pty; const EnvMap = std.process.EnvMap; +const PasswdEntry = internal_os.passwd.Entry; const windows = internal_os.windows; const log = std.log.scoped(.io_exec); @@ -725,7 +726,7 @@ pub const ThreadData = struct { }; pub const Config = struct { - command: ?[]const u8 = null, + command: ?configpkg.Command = null, env: EnvMap, env_override: configpkg.RepeatableStringMap = .{}, shell_integration: configpkg.Config.ShellIntegration = .detect, @@ -746,7 +747,7 @@ const Subprocess = struct { arena: std.heap.ArenaAllocator, cwd: ?[]const u8, env: ?EnvMap, - args: [][]const u8, + args: []const [:0]const u8, grid_size: renderer.GridSize, screen_size: renderer.ScreenSize, pty: ?Pty = null, @@ -892,18 +893,29 @@ const Subprocess = struct { env.remove("VTE_VERSION"); // Setup our shell integration, if we can. - const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: { - const default_shell_command = cfg.command orelse switch (builtin.os.tag) { - .windows => "cmd.exe", - else => "sh", - }; + const shell_command: configpkg.Command = shell: { + const default_shell_command: configpkg.Command = + cfg.command orelse .{ .shell = switch (builtin.os.tag) { + .windows => "cmd.exe", + else => "sh", + } }; const force: ?shell_integration.Shell = switch (cfg.shell_integration) { .none => { - // Even if shell integration is none, we still want to set up the feature env vars - try shell_integration.setupFeatures(&env, cfg.shell_integration_features); - break :shell .{ null, default_shell_command }; + // Even if shell integration is none, we still want to + // set up the feature env vars + try shell_integration.setupFeatures( + &env, + cfg.shell_integration_features, + ); + + // This is a source of confusion for users despite being + // opt-in since it results in some Ghostty features not + // working. We always want to log it. + log.info("shell integration disabled by configuration", .{}); + break :shell default_shell_command; }, + .detect => null, .bash => .bash, .elvish => .elvish, @@ -911,9 +923,9 @@ const Subprocess = struct { .zsh => .zsh, }; - const dir = cfg.resources_dir orelse break :shell .{ - null, - default_shell_command, + const dir = cfg.resources_dir orelse { + log.warn("no resources dir set, shell integration disabled", .{}); + break :shell default_shell_command; }; const integration = try shell_integration.setup( @@ -923,19 +935,18 @@ const Subprocess = struct { &env, force, cfg.shell_integration_features, - ) orelse break :shell .{ null, default_shell_command }; + ) orelse { + log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); + break :shell default_shell_command; + }; - break :shell .{ integration.shell, integration.command }; - }; - - if (integrated_shell) |shell| { log.info( "shell integration automatically injected shell={}", - .{shell}, + .{integration.shell}, ); - } else if (cfg.shell_integration != .none) { - log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); - } + + break :shell integration.command; + }; // Add the environment variables that override any others. { @@ -947,134 +958,29 @@ const Subprocess = struct { } // Build our args list - const args = args: { - const cap = 9; // the most we'll ever use - var args = try std.ArrayList([]const u8).initCapacity(alloc, cap); - defer args.deinit(); + const args: []const [:0]const u8 = execCommand( + alloc, + shell_command, + internal_os.passwd, + ) catch |err| switch (err) { + // If we fail to allocate space for the command we want to + // execute, we'd still like to try to run something so + // Ghostty can launch (and maybe the user can debug this further). + // Realistically, if you're getting OOM, I think other stuff is + // about to crash, but we can try. + error.OutOfMemory => oom: { + log.warn("failed to allocate space for command args, falling back to basic shell", .{}); - // If we're on macOS, we have to use `login(1)` to get all of - // the proper environment variables set, a login shell, and proper - // hushlogin behavior. - if (comptime builtin.target.os.tag.isDarwin()) darwin: { - const passwd = internal_os.passwd.get(alloc) catch |err| { - log.warn("failed to read passwd, not using a login shell err={}", .{err}); - break :darwin; + // The comptime here is important to ensure the full slice + // is put into the binary data and not the stack. + break :oom comptime switch (builtin.os.tag) { + .windows => &.{"cmd.exe"}, + else => &.{"/bin/sh"}, }; + }, - const username = passwd.name orelse { - log.warn("failed to get username, not using a login shell", .{}); - break :darwin; - }; - - const hush = if (passwd.home) |home| hush: { - var dir = std.fs.openDirAbsolute(home, .{}) catch |err| { - log.warn( - "failed to open home dir, not checking for hushlogin err={}", - .{err}, - ); - break :hush false; - }; - defer dir.close(); - - break :hush if (dir.access(".hushlogin", .{})) true else |_| false; - } else false; - - const cmd = try std.fmt.allocPrint( - alloc, - "exec -l {s}", - .{shell_command}, - ); - - // The reason for executing login this way is unclear. This - // comment will attempt to explain but prepare for a truly - // unhinged reality. - // - // The first major issue is that on macOS, a lot of users - // put shell configurations in ~/.bash_profile instead of - // ~/.bashrc (or equivalent for another shell). This file is only - // loaded for a login shell so macOS users expect all their terminals - // to be login shells. No other platform behaves this way and its - // totally braindead but somehow the entire dev community on - // macOS has cargo culted their way to this reality so we have to - // do it... - // - // To get a login shell, you COULD just prepend argv0 with a `-` - // but that doesn't fully work because `getlogin()` C API will - // return the wrong value, SHELL won't be set, and various - // other login behaviors that macOS users expect. - // - // The proper way is to use `login(1)`. But login(1) forces - // the working directory to change to the home directory, - // which we may not want. If we specify "-l" then we can avoid - // this behavior but now the shell isn't a login shell. - // - // There is another issue: `login(1)` on macOS 14.3 and earlier - // checked for ".hushlogin" in the working directory. This means - // that if we specify "-l" then we won't get hushlogin honored - // if its in the home directory (which is standard). To get - // around this, we check for hushlogin ourselves and if present - // specify the "-q" flag to login(1). - // - // So to get all the behaviors we want, we specify "-l" but - // execute "bash" (which is built-in to macOS). We then use - // the bash builtin "exec" to replace the process with a login - // shell ("-l" on exec) with the command we really want. - // - // We use "bash" instead of other shells that ship with macOS - // because as of macOS Sonoma, we found with a microbenchmark - // that bash can `exec` into the desired command ~2x faster - // than zsh. - // - // To figure out a lot of this logic I read the login.c - // source code in the OSS distribution Apple provides for - // macOS. - // - // Awesome. - try args.append("/usr/bin/login"); - if (hush) try args.append("-q"); - try args.append("-flp"); - - // We execute bash with "--noprofile --norc" so that it doesn't - // load startup files so that (1) our shell integration doesn't - // break and (2) user configuration doesn't mess this process - // up. - try args.append(username); - try args.append("/bin/bash"); - try args.append("--noprofile"); - try args.append("--norc"); - try args.append("-c"); - try args.append(cmd); - break :args try args.toOwnedSlice(); - } - - if (comptime builtin.os.tag == .windows) { - // We run our shell wrapped in `cmd.exe` so that we don't have - // to parse the command line ourselves if it has arguments. - - // Note we don't free any of the memory below since it is - // allocated in the arena. - const windir = try std.process.getEnvVarOwned(alloc, "WINDIR"); - const cmd = try std.fs.path.join(alloc, &[_][]const u8{ - windir, - "System32", - "cmd.exe", - }); - - try args.append(cmd); - try args.append("/C"); - } else { - // We run our shell wrapped in `/bin/sh` so that we don't have - // to parse the command line ourselves if it has arguments. - // Additionally, some environments (NixOS, I found) use /bin/sh - // to setup some environment variables that are important to - // have set. - try args.append("/bin/sh"); - if (internal_os.isFlatpak()) try args.append("-l"); - try args.append("-c"); - } - - try args.append(shell_command); - break :args try args.toOwnedSlice(); + // This logs on its own, this is a bad error. + error.SystemError => return err, }; // We have to copy the cwd because there is no guarantee that @@ -1562,3 +1468,320 @@ pub const ReadThread = struct { } } }; + +/// Builds the argv array for the process we should exec for the +/// configured command. This isn't as straightforward as it seems since +/// we deal with shell-wrapping, macOS login shells, etc. +/// +/// The passwdpkg comptime argument is expected to have a single function +/// `get(Allocator)` that returns a passwd entry. This is used by macOS +/// to determine the username and home directory for the login shell. +/// It is unused on other platforms. +/// +/// Memory ownership: +/// +/// The allocator should be an arena, since the returned value may or +/// may not be allocated and args may or may not be allocated (or copied). +/// Pointers in the return value may point to pointers in the command +/// struct. +fn execCommand( + alloc: Allocator, + command: configpkg.Command, + comptime passwdpkg: type, +) (Allocator.Error || error{SystemError})![]const [:0]const u8 { + // If we're on macOS, we have to use `login(1)` to get all of + // the proper environment variables set, a login shell, and proper + // hushlogin behavior. + if (comptime builtin.target.os.tag.isDarwin()) darwin: { + const passwd = passwdpkg.get(alloc) catch |err| { + log.warn("failed to read passwd, not using a login shell err={}", .{err}); + break :darwin; + }; + + const username = passwd.name orelse { + log.warn("failed to get username, not using a login shell", .{}); + break :darwin; + }; + + const hush = if (passwd.home) |home| hush: { + var dir = std.fs.openDirAbsolute(home, .{}) catch |err| { + log.warn( + "failed to open home dir, not checking for hushlogin err={}", + .{err}, + ); + break :hush false; + }; + defer dir.close(); + + break :hush if (dir.access(".hushlogin", .{})) true else |_| false; + } else false; + + // If we made it this far we're going to start building + // the actual command. + var args: std.ArrayList([:0]const u8) = try .initCapacity( + alloc, + + // This capacity is chosen based on what we'd need to + // execute a shell command (very common). We can/will + // grow if necessary for a longer command (uncommon). + 9, + ); + defer args.deinit(); + + // The reason for executing login this way is unclear. This + // comment will attempt to explain but prepare for a truly + // unhinged reality. + // + // The first major issue is that on macOS, a lot of users + // put shell configurations in ~/.bash_profile instead of + // ~/.bashrc (or equivalent for another shell). This file is only + // loaded for a login shell so macOS users expect all their terminals + // to be login shells. No other platform behaves this way and its + // totally braindead but somehow the entire dev community on + // macOS has cargo culted their way to this reality so we have to + // do it... + // + // To get a login shell, you COULD just prepend argv0 with a `-` + // but that doesn't fully work because `getlogin()` C API will + // return the wrong value, SHELL won't be set, and various + // other login behaviors that macOS users expect. + // + // The proper way is to use `login(1)`. But login(1) forces + // the working directory to change to the home directory, + // which we may not want. If we specify "-l" then we can avoid + // this behavior but now the shell isn't a login shell. + // + // There is another issue: `login(1)` on macOS 14.3 and earlier + // checked for ".hushlogin" in the working directory. This means + // that if we specify "-l" then we won't get hushlogin honored + // if its in the home directory (which is standard). To get + // around this, we check for hushlogin ourselves and if present + // specify the "-q" flag to login(1). + // + // So to get all the behaviors we want, we specify "-l" but + // execute "bash" (which is built-in to macOS). We then use + // the bash builtin "exec" to replace the process with a login + // shell ("-l" on exec) with the command we really want. + // + // We use "bash" instead of other shells that ship with macOS + // because as of macOS Sonoma, we found with a microbenchmark + // that bash can `exec` into the desired command ~2x faster + // than zsh. + // + // To figure out a lot of this logic I read the login.c + // source code in the OSS distribution Apple provides for + // macOS. + // + // Awesome. + try args.append("/usr/bin/login"); + if (hush) try args.append("-q"); + try args.append("-flp"); + try args.append(username); + + switch (command) { + // Direct args can be passed directly to login, since + // login uses execvp we don't need to worry about PATH + // searching. + .direct => |v| try args.appendSlice(v), + + .shell => |v| { + // Use "exec" to replace the bash process with + // our intended command so we don't have a parent + // process hanging around. + const cmd = try std.fmt.allocPrintZ( + alloc, + "exec -l {s}", + .{v}, + ); + + // We execute bash with "--noprofile --norc" so that it doesn't + // load startup files so that (1) our shell integration doesn't + // break and (2) user configuration doesn't mess this process + // up. + try args.append("/bin/bash"); + try args.append("--noprofile"); + try args.append("--norc"); + try args.append("-c"); + try args.append(cmd); + }, + } + + return try args.toOwnedSlice(); + } + + return switch (command) { + .direct => |v| v, + + .shell => |v| shell: { + var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); + defer args.deinit(); + + if (comptime builtin.os.tag == .windows) { + // We run our shell wrapped in `cmd.exe` so that we don't have + // to parse the command line ourselves if it has arguments. + + // Note we don't free any of the memory below since it is + // allocated in the arena. + const windir = std.process.getEnvVarOwned( + alloc, + "WINDIR", + ) catch |err| { + log.warn("failed to get WINDIR, cannot run shell command err={}", .{err}); + return error.SystemError; + }; + const cmd = try std.fs.path.joinZ(alloc, &[_][]const u8{ + windir, + "System32", + "cmd.exe", + }); + + try args.append(cmd); + try args.append("/C"); + } else { + // We run our shell wrapped in `/bin/sh` so that we don't have + // to parse the command line ourselves if it has arguments. + // Additionally, some environments (NixOS, I found) use /bin/sh + // to setup some environment variables that are important to + // have set. + try args.append("/bin/sh"); + if (internal_os.isFlatpak()) try args.append("-l"); + try args.append("-c"); + } + + try args.append(v); + break :shell try args.toOwnedSlice(); + }, + }; +} + +test "execCommand darwin: shell command" { + if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand(alloc, .{ .shell = "foo bar baz" }, struct { + fn get(_: Allocator) !PasswdEntry { + return .{ + .name = "testuser", + }; + } + }); + + try testing.expectEqual(8, result.len); + try testing.expectEqualStrings(result[0], "/usr/bin/login"); + try testing.expectEqualStrings(result[1], "-flp"); + try testing.expectEqualStrings(result[2], "testuser"); + try testing.expectEqualStrings(result[3], "/bin/bash"); + try testing.expectEqualStrings(result[4], "--noprofile"); + try testing.expectEqualStrings(result[5], "--norc"); + try testing.expectEqualStrings(result[6], "-c"); + try testing.expectEqualStrings(result[7], "exec -l foo bar baz"); +} + +test "execCommand darwin: direct command" { + if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand(alloc, .{ .direct = &.{ + "foo", + "bar baz", + } }, struct { + fn get(_: Allocator) !PasswdEntry { + return .{ + .name = "testuser", + }; + } + }); + + try testing.expectEqual(5, result.len); + try testing.expectEqualStrings(result[0], "/usr/bin/login"); + try testing.expectEqualStrings(result[1], "-flp"); + try testing.expectEqualStrings(result[2], "testuser"); + try testing.expectEqualStrings(result[3], "foo"); + try testing.expectEqualStrings(result[4], "bar baz"); +} + +test "execCommand: shell command, empty passwd" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand( + alloc, + .{ .shell = "foo bar baz" }, + struct { + fn get(_: Allocator) !PasswdEntry { + // Empty passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return .{}; + } + }, + ); + + try testing.expectEqual(3, result.len); + try testing.expectEqualStrings(result[0], "/bin/sh"); + try testing.expectEqualStrings(result[1], "-c"); + try testing.expectEqualStrings(result[2], "foo bar baz"); +} + +test "execCommand: shell command, error passwd" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand( + alloc, + .{ .shell = "foo bar baz" }, + struct { + fn get(_: Allocator) !PasswdEntry { + // Failed passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return error.Fail; + } + }, + ); + + try testing.expectEqual(3, result.len); + try testing.expectEqualStrings(result[0], "/bin/sh"); + try testing.expectEqualStrings(result[1], "-c"); + try testing.expectEqualStrings(result[2], "foo bar baz"); +} + +test "execCommand: direct command, error passwd" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand(alloc, .{ + .direct = &.{ + "foo", + "bar baz", + }, + }, struct { + fn get(_: Allocator) !PasswdEntry { + // Failed passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return error.Fail; + } + }); + + try testing.expectEqual(2, result.len); + try testing.expectEqualStrings(result[0], "foo"); + try testing.expectEqualStrings(result[1], "bar baz"); +} diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 4bbf0a3b5..2cf809694 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -27,10 +27,10 @@ pub const ShellIntegration = struct { /// bash in particular it may be different. /// /// The memory is allocated in the arena given to setup. - command: []const u8, + command: config.Command, }; -/// Setup the command execution environment for automatic +/// Set up the command execution environment for automatic /// integrated shell integration and return a ShellIntegration /// struct describing the integration. If integration fails /// (shell type couldn't be detected, etc.), this will return null. @@ -41,7 +41,7 @@ pub const ShellIntegration = struct { pub fn setup( alloc_arena: Allocator, resource_dir: []const u8, - command: []const u8, + command: config.Command, env: *EnvMap, force_shell: ?Shell, features: config.ShellIntegrationFeatures, @@ -51,14 +51,24 @@ pub fn setup( .elvish => "elvish", .fish => "fish", .zsh => "zsh", - } else exe: { - // The command can include arguments. Look for the first space - // and use the basename of the first part as the command's exe. - const idx = std.mem.indexOfScalar(u8, command, ' ') orelse command.len; - break :exe std.fs.path.basename(command[0..idx]); + } else switch (command) { + .direct => |v| std.fs.path.basename(v[0]), + .shell => |v| exe: { + // Shell strings can include spaces so we want to only + // look up to the space if it exists. No shell that we integrate + // has spaces. + const idx = std.mem.indexOfScalar(u8, v, ' ') orelse v.len; + break :exe std.fs.path.basename(v[0..idx]); + }, }; - const result = try setupShell(alloc_arena, resource_dir, command, env, exe); + const result = try setupShell( + alloc_arena, + resource_dir, + command, + env, + exe, + ); // Setup our feature env vars try setupFeatures(env, features); @@ -69,7 +79,7 @@ pub fn setup( fn setupShell( alloc_arena: Allocator, resource_dir: []const u8, - command: []const u8, + command: config.Command, env: *EnvMap, exe: []const u8, ) !?ShellIntegration { @@ -83,7 +93,10 @@ fn setupShell( // we're using Apple's Bash because /bin is non-writable // on modern macOS due to System Integrity Protection. if (comptime builtin.target.os.tag.isDarwin()) { - if (std.mem.eql(u8, "/bin/bash", command)) { + if (std.mem.eql(u8, "/bin/bash", switch (command) { + .direct => |v| v[0], + .shell => |v| v, + })) { return null; } } @@ -104,7 +117,7 @@ fn setupShell( try setupXdgDataDirs(alloc_arena, resource_dir, env); return .{ .shell = .elvish, - .command = try alloc_arena.dupe(u8, command), + .command = try command.clone(alloc_arena), }; } @@ -112,7 +125,7 @@ fn setupShell( try setupXdgDataDirs(alloc_arena, resource_dir, env); return .{ .shell = .fish, - .command = try alloc_arena.dupe(u8, command), + .command = try command.clone(alloc_arena), }; } @@ -120,7 +133,7 @@ fn setupShell( try setupZsh(resource_dir, env); return .{ .shell = .zsh, - .command = try alloc_arena.dupe(u8, command), + .command = try command.clone(alloc_arena), }; } @@ -139,20 +152,41 @@ test "force shell" { inline for (@typeInfo(Shell).@"enum".fields) |field| { const shell = @field(Shell, field.name); - const result = try setup(alloc, ".", "sh", &env, shell, .{}); + const result = try setup( + alloc, + ".", + .{ .shell = "sh" }, + &env, + shell, + .{}, + ); try testing.expectEqual(shell, result.?.shell); } } -/// Setup shell integration feature environment variables without -/// performing full shell integration setup. +/// Set up the shell integration features environment variable. pub fn setupFeatures( env: *EnvMap, features: config.ShellIntegrationFeatures, ) !void { - if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1"); - if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1"); - if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1"); + const fields = @typeInfo(@TypeOf(features)).@"struct".fields; + const capacity: usize = capacity: { + comptime var n: usize = fields.len - 1; // commas + inline for (fields) |field| n += field.name.len; + break :capacity n; + }; + var buffer = try std.BoundedArray(u8, capacity).init(0); + + inline for (fields) |field| { + if (@field(features, field.name)) { + if (buffer.len > 0) try buffer.append(','); + try buffer.appendSlice(field.name); + } + } + + if (buffer.len > 0) { + try env.put("GHOSTTY_SHELL_FEATURES", buffer.slice()); + } } test "setup features" { @@ -162,15 +196,13 @@ test "setup features" { defer arena.deinit(); const alloc = arena.allocator(); - // Test: all features enabled (no environment variables should be set) + // Test: all features enabled { var env = EnvMap.init(alloc); defer env.deinit(); try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true }); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") == null); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE") == null); + try testing.expectEqualStrings("cursor,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); } // Test: all features disabled @@ -179,9 +211,7 @@ test "setup features" { defer env.deinit(); try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false }); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").?); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?); + try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } // Test: mixed features @@ -190,9 +220,7 @@ test "setup features" { defer env.deinit(); try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false }); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?); + try testing.expectEqualStrings("sudo", env.get("GHOSTTY_SHELL_FEATURES").?); } } @@ -207,25 +235,21 @@ test "setup features" { /// enables the integration or null if integration failed. fn setupBash( alloc: Allocator, - command: []const u8, + command: config.Command, resource_dir: []const u8, env: *EnvMap, -) !?[]const u8 { - // Accumulates the arguments that will form the final shell command line. - // We can build this list on the stack because we're just temporarily - // referencing other slices, but we can fall back to heap in extreme cases. - var args_alloc = std.heap.stackFallback(1024, alloc); - var args = try std.ArrayList([]const u8).initCapacity(args_alloc.get(), 2); +) !?config.Command { + var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 2); defer args.deinit(); // Iterator that yields each argument in the original command line. // This will allocate once proportionate to the command line length. - var iter = try std.process.ArgIteratorGeneral(.{}).init(alloc, command); + var iter = try command.argIterator(alloc); defer iter.deinit(); // Start accumulating arguments with the executable and `--posix` mode flag. if (iter.next()) |exe| { - try args.append(exe); + try args.append(try alloc.dupeZ(u8, exe)); } else return null; try args.append("--posix"); @@ -259,17 +283,17 @@ fn setupBash( if (std.mem.indexOfScalar(u8, arg, 'c') != null) { return null; } - try args.append(arg); + try args.append(try alloc.dupeZ(u8, arg)); } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) { // All remaining arguments should be passed directly to the shell // command. We shouldn't perform any further option processing. - try args.append(arg); + try args.append(try alloc.dupeZ(u8, arg)); while (iter.next()) |remaining_arg| { - try args.append(remaining_arg); + try args.append(try alloc.dupeZ(u8, remaining_arg)); } break; } else { - try args.append(arg); + try args.append(try alloc.dupeZ(u8, arg)); } } try env.put("GHOSTTY_BASH_INJECT", inject.slice()); @@ -302,30 +326,36 @@ fn setupBash( ); try env.put("ENV", integ_dir); - // Join the accumulated arguments to form the final command string. - return try std.mem.join(alloc, " ", args.items); + // Since we built up a command line, we don't need to wrap it in + // ANOTHER shell anymore and can do a direct command. + return .{ .direct = try args.toOwnedSlice() }; } test "bash" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash", ".", &env); - defer if (command) |c| alloc.free(c); + const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - try testing.expectEqualStrings("bash --posix", command.?); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?); } test "bash: unsupported options" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); - const cmdlines = [_][]const u8{ + const cmdlines = [_][:0]const u8{ "bash --posix", "bash --rcfile script.sh --posix", "bash --init-file script.sh --posix", @@ -337,7 +367,7 @@ test "bash: unsupported options" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(try setupBash(alloc, cmdline, ".", &env) == null); + try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, ".", &env) == null); try testing.expect(env.get("GHOSTTY_BASH_INJECT") == null); try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); @@ -346,17 +376,20 @@ test "bash: unsupported options" { test "bash: inject flags" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); // bash --norc { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash --norc", ".", &env); - defer if (command) |c| alloc.free(c); + const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env); - try testing.expectEqualStrings("bash --posix", command.?); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?); } @@ -365,52 +398,55 @@ test "bash: inject flags" { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash --noprofile", ".", &env); - defer if (command) |c| alloc.free(c); + const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env); - try testing.expectEqualStrings("bash --posix", command.?); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?); } } test "bash: rcfile" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var env = EnvMap.init(alloc); defer env.deinit(); // bash --rcfile { - const command = try setupBash(alloc, "bash --rcfile profile.sh", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix", command.?); + const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } // bash --init-file { - const command = try setupBash(alloc, "bash --init-file profile.sh", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix", command.?); + const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } } test "bash: HISTFILE" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); // HISTFILE unset { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash", ".", &env); - defer if (command) |c| alloc.free(c); - + _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); try testing.expect(std.mem.endsWith(u8, env.get("HISTFILE").?, ".bash_history")); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE").?); } @@ -422,9 +458,7 @@ test "bash: HISTFILE" { try env.put("HISTFILE", "my_history"); - const command = try setupBash(alloc, "bash", ".", &env); - defer if (command) |c| alloc.free(c); - + _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); try testing.expectEqualStrings("my_history", env.get("HISTFILE").?); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); } @@ -432,25 +466,35 @@ test "bash: HISTFILE" { test "bash: additional arguments" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var env = EnvMap.init(alloc); defer env.deinit(); // "-" argument separator { - const command = try setupBash(alloc, "bash - --arg file1 file2", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?); + const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env); + try testing.expectEqual(6, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); + try testing.expectEqualStrings("-", command.?.direct[2]); + try testing.expectEqualStrings("--arg", command.?.direct[3]); + try testing.expectEqualStrings("file1", command.?.direct[4]); + try testing.expectEqualStrings("file2", command.?.direct[5]); } // "--" argument separator { - const command = try setupBash(alloc, "bash -- --arg file1 file2", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?); + const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env); + try testing.expectEqual(6, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); + try testing.expectEqualStrings("--", command.?.direct[2]); + try testing.expectEqualStrings("--arg", command.?.direct[3]); + try testing.expectEqualStrings("file1", command.?.direct[4]); + try testing.expectEqualStrings("file2", command.?.direct[5]); } }