Merge branch 'main' into mk-localization

This commit is contained in:
Andrej Daskalov
2025-04-11 21:47:07 +02:00
committed by GitHub
45 changed files with 3867 additions and 391 deletions

115
.github/scripts/request_review.py vendored Normal file
View File

@ -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())

37
.github/workflows/review.yml vendored Normal file
View File

@ -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

View File

@ -158,9 +158,18 @@
# Localization # Localization
/po/README_TRANSLATORS.md @ghostty-org/localization /po/README_TRANSLATORS.md @ghostty-org/localization
/po/com.mitchellh.ghostty.pot @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/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/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/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/uk_UA.UTF-8.po @ghostty-org/uk_UA
/po/zh_CN.UTF-8.po @ghostty-org/zh_CN /po/zh_CN.UTF-8.po @ghostty-org/zh_CN

View File

@ -1,6 +1,6 @@
.{ .{
.name = .ghostty, .name = .ghostty,
.version = "1.1.3", .version = "1.1.4",
.paths = .{""}, .paths = .{""},
.fingerprint = 0x64407a2a0b4147e5, .fingerprint = 0x64407a2a0b4147e5,
.dependencies = .{ .dependencies = .{
@ -14,8 +14,8 @@
}, },
.vaxis = .{ .vaxis = .{
// rockorager/libvaxis // rockorager/libvaxis
.url = "git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447", .url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
.hash = "vaxis-0.1.0-BWNV_MHyCAA0rNbPTr50Z44PyEdNP9zQSnHcXBXoo3Ti", .hash = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn",
.lazy = true, .lazy = true,
}, },
.z2d = .{ .z2d = .{
@ -48,8 +48,8 @@
}, },
.zf = .{ .zf = .{
// natecraddock/zf // natecraddock/zf
.url = "https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz", .url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
.hash = "zf-0.10.3-OIRy8bKIAADhjqtdjVaDfONRuI7RVl5gMbhCoOwiBWV5", .hash = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9",
.lazy = true, .lazy = true,
}, },
.gobject = .{ .gobject = .{
@ -103,8 +103,8 @@
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz",
.hash = "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6", .hash = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3",
.lazy = true, .lazy = true,
}, },
}, },

24
build.zig.zon.json generated
View File

@ -54,10 +54,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
}, },
"N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6": { "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3": {
"name": "iterm2_themes", "name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz",
"hash": "sha256-nOkH31MQQd2PPdjVpRxBxNQWfR9Exg6nRF/KHgSz3cM=" "hash": "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU="
}, },
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
"name": "libpng", "name": "libpng",
@ -104,10 +104,10 @@
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
"hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
}, },
"vaxis-0.1.0-BWNV_MHyCAA0rNbPTr50Z44PyEdNP9zQSnHcXBXoo3Ti": { "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": {
"name": "vaxis", "name": "vaxis",
"url": "git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447", "url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
"hash": "sha256-iONEySjPeD0WYJ93fw5mxT+0pVfUO/m6008J/LXjQkA=" "hash": "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="
}, },
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
"name": "wayland", "name": "wayland",
@ -129,10 +129,10 @@
"url": "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz", "url": "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz",
"hash": "sha256-PEKVSUZ6teRbDyhFPWSiuBSe40pgr0kVRivIY8Cn8HQ=" "hash": "sha256-PEKVSUZ6teRbDyhFPWSiuBSe40pgr0kVRivIY8Cn8HQ="
}, },
"zf-0.10.3-OIRy8bKIAADhjqtdjVaDfONRuI7RVl5gMbhCoOwiBWV5": { "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf", "name": "zf",
"url": "https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz", "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
"hash": "sha256-xVva07TAYlVv4E4PKe2wUj86a6Ky2YC30YBgtbvNKvw=" "hash": "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="
}, },
"zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": { "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": {
"name": "zg", "name": "zg",
@ -154,10 +154,10 @@
"url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
"hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk=" "hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="
}, },
"zigimg-0.1.0-lly-O4heEADSRxoTwJwrD3TBfUob9052sIgb9SL8Iz-A": { "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj": {
"name": "zigimg", "name": "zigimg",
"url": "git+https://github.com/TUSF/zigimg#0ce4eca3560d5553b13263d6b6bb72e146dd43d0", "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d",
"hash": "sha256-Rr+mAfbLOoaxHOwCug+0cWCmW9gDhjhnaO2J/Oik9HI=" "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="
}, },
"ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": { "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": {
"name": "ziglyph", "name": "ziglyph",

24
build.zig.zon.nix generated
View File

@ -170,11 +170,11 @@ in
}; };
} }
{ {
name = "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6"; name = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "iterm2_themes"; name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz"; url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz";
hash = "sha256-nOkH31MQQd2PPdjVpRxBxNQWfR9Exg6nRF/KHgSz3cM="; 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 { path = fetchZigArtifact {
name = "vaxis"; name = "vaxis";
url = "git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447"; url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23";
hash = "sha256-iONEySjPeD0WYJ93fw5mxT+0pVfUO/m6008J/LXjQkA="; 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 { path = fetchZigArtifact {
name = "zf"; name = "zf";
url = "https://github.com/natecraddock/zf/archive/1039cf75447a8d5b8d481fedb914fe848d246276.tar.gz"; url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz";
hash = "sha256-xVva07TAYlVv4E4PKe2wUj86a6Ky2YC30YBgtbvNKvw="; 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 { path = fetchZigArtifact {
name = "zigimg"; name = "zigimg";
url = "git+https://github.com/TUSF/zigimg#0ce4eca3560d5553b13263d6b6bb72e146dd43d0"; url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d";
hash = "sha256-Rr+mAfbLOoaxHOwCug+0cWCmW9gDhjhnaO2J/Oik9HI="; hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=";
}; };
} }
{ {

8
build.zig.zon.txt generated
View File

@ -1,6 +1,6 @@
git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc
git+https://github.com/TUSF/zigimg#0ce4eca3560d5553b13263d6b6bb72e146dd43d0 git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d
git+https://github.com/rockorager/libvaxis#4182b7fa42f27cf14a71dbdb54cfd82c5c6e3447 git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23
https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz
https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
https://deps.files.ghostty.org/fontconfig-2.14.2.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://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.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/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/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.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 https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz

View File

@ -51,6 +51,7 @@
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix { devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
zig = zig.packages.${system}."0.14.0"; zig = zig.packages.${system}."0.14.0";
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {}; wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
uv = pkgs-unstable.uv;
# remove once blueprint-compiler 0.16.0 is in the stable nixpkgs # remove once blueprint-compiler 0.16.0 is in the stable nixpkgs
blueprint-compiler = pkgs-unstable.blueprint-compiler; blueprint-compiler = pkgs-unstable.blueprint-compiler;
zon2nix = zon2nix; zon2nix = zon2nix;

View File

@ -91,6 +91,12 @@ class TerminalController: BaseTerminalController {
name: Ghostty.Notification.didEqualizeSplits, name: Ghostty.Notification.didEqualizeSplits,
object: nil object: nil
) )
center.addObserver(
self,
selector: #selector(onCloseWindow),
name: .ghosttyCloseWindow,
object: nil
)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -842,6 +848,12 @@ class TerminalController: BaseTerminalController {
closeTab(self) 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) { @objc private func onResetWindowSize(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree?.contains(view: target) ?? false else { return } guard surfaceTree?.contains(view: target) ?? false else { return }

View File

@ -451,6 +451,9 @@ extension Ghostty {
case GHOSTTY_ACTION_CLOSE_TAB: case GHOSTTY_ACTION_CLOSE_TAB:
closeTab(app, target: target) closeTab(app, target: target)
case GHOSTTY_ACTION_CLOSE_WINDOW:
closeWindow(app, target: target)
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN: case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
toggleFullscreen(app, target: target, mode: action.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( private static func toggleFullscreen(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,

View File

@ -248,6 +248,9 @@ extension Notification.Name {
/// Close tab /// Close tab
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") 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. /// Resize the window to a default size.
static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize") static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize")
} }

View File

@ -201,7 +201,14 @@ extension Ghostty {
self.eventMonitor = NSEvent.addLocalMonitorForEvents( self.eventMonitor = NSEvent.addLocalMonitorForEvents(
matching: [ matching: [
// We need keyUp because command+key events don't trigger keyUp. // 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) } ) { [weak self] event in self?.localEventHandler(event) }
@ -450,11 +457,40 @@ extension Ghostty {
case .keyUp: case .keyUp:
localEventKeyUp(event) localEventKeyUp(event)
case .leftMouseDown:
localEventLeftMouseDown(event)
default: default:
event 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? { private func localEventKeyUp(_ event: NSEvent) -> NSEvent? {
// We only care about events with "command" because all others will // We only care about events with "command" because all others will
// trigger the normal responder chain. // trigger the normal responder chain.
@ -620,14 +656,6 @@ extension Ghostty {
ghostty_surface_draw(surface); 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) { override func mouseDown(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
@ -884,6 +912,11 @@ extension Ghostty {
nil 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]) self.interpretKeyEvents([translationEvent])
// If our keyboard changed from this we just assume an input method // If our keyboard changed from this we just assume an input method
@ -922,6 +955,34 @@ extension Ghostty {
_ = keyAction(GHOSTTY_ACTION_RELEASE, event: event) _ = 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 /// Special case handling for some control keys
override func performKeyEquivalent(with event: NSEvent) -> Bool { override func performKeyEquivalent(with event: NSEvent) -> Bool {
switch (event.type) { switch (event.type) {
@ -975,15 +1036,42 @@ extension Ghostty {
equivalent = "\r" equivalent = "\r"
case ".": default:
if (!event.modifierFlags.contains(.command)) { // 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 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 all other non-command events. This lets the event continue
// Ignore other events // 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 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) { override func doCommand(by selector: Selector) {
// This currently just prevents NSBeep from interpretKeyEvents but in the future // If we are being processed by performKeyEquivalent with a command binding,
// we may want to make some of this work. // 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)") print("SEL: \(selector)")
} }

View File

@ -57,6 +57,7 @@
pandoc, pandoc,
hyperfine, hyperfine,
typos, typos,
uv,
wayland, wayland,
wayland-scanner, wayland-scanner,
wayland-protocols, wayland-protocols,
@ -109,6 +110,9 @@ in
# Localization # Localization
gettext gettext
# CI
uv
# We need these GTK-related deps on all platform so we can build # We need these GTK-related deps on all platform so we can build
# dist tarballs. # dist tarballs.
blueprint-compiler blueprint-compiler

View File

@ -39,7 +39,7 @@
in in
stdenv.mkDerivation (finalAttrs: { stdenv.mkDerivation (finalAttrs: {
pname = "ghostty"; 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 # 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 # thus we only provide the source that is needed for the build

269
po/ca_ES.UTF-8.po Normal file
View File

@ -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 <francesc.arpi@gmail.com>, 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 <francesc.arpi@gmail.com>\n"
"Language-Team: \n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: 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"

267
po/es_BO.UTF-8.po Normal file
View File

@ -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 <miguelp@quientienemail.com>, 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 <miguelp@quientienemail.com>\n"
"Language-Team: Spanish <es@tp.org.es>\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"

268
po/fr_FR.UTF-8.po Normal file
View File

@ -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 <swiip__@hotmail.com>, 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 <swiip__@hotmail.com>\n"
"Language-Team: French <traduc@traduc.org>\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"

266
po/id_ID.UTF-8.po Normal file
View File

@ -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 <halosatrio@gmail.com>, 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 <halosatrio@gmail.com>\n"
"Language-Team: Indonesian <translation-team-id@lists.sourceforge.net>\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"

268
po/ja_JP.UTF-8.po Normal file
View File

@ -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 <lon@sagisawa.me>, 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 <lon@sagisawa.me>\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 開発者"

268
po/nl_NL.UTF-8.po Normal file
View File

@ -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 <geesinknico@gmail.com>, 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 <geesinknico@gmail.com>\n"
"Language-Team: Dutch <vertaling@vrijschrift.org>\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"

269
po/pt_BR.UTF-8.po Normal file
View File

@ -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 <gsodevel@gmail.com>, 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 <gsodevel@gmail.com>\n"
"Language-Team: Brazilian Portuguese <ldpbr-"
"translation@lists.sourceforge.net>\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"

270
po/ru_RU.UTF-8.po Normal file
View File

@ -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 <sergey_zhuzhgov@mail.ru>, 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 <sergey_zhuzhgov@mail.ru>\n"
"Language-Team: Russian <gnu@d07.ru>\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"

270
po/tr_TR.UTF-8.po Normal file
View File

@ -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 <emir_sari@icloud.com>, 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 <emir_sari@icloud.com>\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 "Ghosttyden Çı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 ""
"⚠️ Ghosttynin 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"

View File

@ -33,14 +33,17 @@ const EnvMap = std.process.EnvMap;
const PreExecFn = fn (*Command) void; const PreExecFn = fn (*Command) void;
/// Path to the command to run. This must be an absolute path. This /// Path to the command to run. This doesn't have to be an absolute path,
/// library does not do PATH lookup. /// because use exec functions that search the PATH, if necessary.
path: []const u8, ///
/// 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 /// 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 /// args[0] to the command. If args is empty then args[0] will automatically
/// be set to equal path. /// 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 /// Environment variables for the child process. If this is null, inherits
/// the environment variables from this process. These are the exact /// 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 { fn startPosix(self: *Command, arena: Allocator) !void {
// Null-terminate all our arguments // Null-terminate all our arguments
const pathZ = try arena.dupeZ(u8, self.path); const argsZ = try arena.allocSentinel(?[*:0]const u8, self.args.len, null);
const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null); for (self.args, 0..) |arg, i| argsZ[i] = arg.ptr;
for (self.args, 0..) |arg, i| argsZ[i] = (try arena.dupeZ(u8, arg)).ptr;
// Determine our env vars // Determine our env vars
const envp = if (self.env) |env_map| 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); if (self.pre_exec) |f| f(self);
// Finally, replace our process. // 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, // If we are executing this code, the exec failed. In that scenario,
// we return a very specific error that can be detected to determine // we return a very specific error that can be detected to determine

View File

@ -518,7 +518,7 @@ pub fn init(
}; };
// The command we're going to execute // 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 config.@"initial-command" orelse config.command
else else
config.command; config.command;
@ -650,8 +650,7 @@ pub fn init(
// title to the command being executed. This allows window managers // title to the command being executed. This allows window managers
// to set custom styling based on the command being executed. // to set custom styling based on the command being executed.
const v = command orelse break :xdg; const v = command orelse break :xdg;
if (v.len > 0) { const title = v.string(alloc) catch |err| {
const title = alloc.dupeZ(u8, v) catch |err| {
log.warn( log.warn(
"error copying command for title, title will not be set err={}", "error copying command for title, title will not be set err={}",
.{err}, .{err},
@ -665,7 +664,6 @@ pub fn init(
.{ .title = title }, .{ .title = title },
); );
} }
}
// We are no longer the first surface // We are no longer the first surface
app.first = false; app.first = false;

View File

@ -311,7 +311,7 @@ pub const Action = union(Key) {
break :cvalue @Type(.{ .@"union" = .{ break :cvalue @Type(.{ .@"union" = .{
.layout = .@"extern", .layout = .@"extern",
.tag_type = Key, .tag_type = null,
.fields = &union_fields, .fields = &union_fields,
.decls = &.{}, .decls = &.{},
} }); } });
@ -323,6 +323,13 @@ pub const Action = union(Key) {
value: CValue, 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. /// Returns the value type for the given key.
pub fn Value(comptime key: Key) type { pub fn Value(comptime key: Key) type {
inline for (@typeInfo(Action).@"union".fields) |field| { inline for (@typeInfo(Action).@"union".fields) |field| {

View File

@ -636,6 +636,11 @@ pub const Surface = struct {
/// The command to run in the new surface. If this is set then /// The command to run in the new surface. If this is set then
/// the "wait-after-command" option is also automatically set to true, /// the "wait-after-command" option is also automatically set to true,
/// since this is used for scripting. /// 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 = "", command: [*:0]const u8 = "",
}; };
@ -696,7 +701,7 @@ pub const Surface = struct {
// If we have a command from the options then we set it. // If we have a command from the options then we set it.
const cmd = std.mem.sliceTo(opts.command, 0); const cmd = std.mem.sliceTo(opts.command, 0);
if (cmd.len > 0) { if (cmd.len > 0) {
config.command = cmd; config.command = .{ .shell = cmd };
config.@"wait-after-command" = true; config.@"wait-after-command" = true;
} }

View File

@ -314,8 +314,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
.prefer_dark; .prefer_dark;
}, },
.system => .prefer_light, .system => .prefer_light,
.dark => .prefer_dark, .dark => .force_dark,
.light => .force_dark, .light => .force_light,
}, },
); );

View File

@ -281,6 +281,15 @@ pub fn init(self: *Window, app: *App) !void {
.detail = "is-active", .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 // 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. // 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; if (self.isQuickTerminal()) break :visible false;
// Unconditionally disable the header bar when fullscreened. // 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, // *Conditionally* disable the header bar when maximized,
// and gtk-titlebar-hide-when-maximized is set // 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 false;
break :visible self.config.gtk_titlebar; break :visible self.config.gtk_titlebar;
@ -672,7 +683,7 @@ pub fn toggleTabOverview(self: *Window) void {
/// Toggle the maximized state for this window. /// Toggle the maximized state for this window.
pub fn toggleMaximize(self: *Window) void { pub fn toggleMaximize(self: *Window) void {
if (self.config.maximize) { if (self.window.as(gtk.Window).isMaximized() != 0) {
self.window.as(gtk.Window).unmaximize(); self.window.as(gtk.Window).unmaximize();
} else { } else {
self.window.as(gtk.Window).maximize(); self.window.as(gtk.Window).maximize();
@ -683,7 +694,7 @@ pub fn toggleMaximize(self: *Window) void {
/// Toggle fullscreen for this window. /// Toggle fullscreen for this window.
pub fn toggleFullscreen(self: *Window) void { pub fn toggleFullscreen(self: *Window) void {
if (self.config.fullscreen) { if (self.window.as(gtk.Window).isFullscreen() != 0) {
self.window.as(gtk.Window).unfullscreen(); self.window.as(gtk.Window).unfullscreen();
} else { } else {
self.window.as(gtk.Window).fullscreen(); self.window.as(gtk.Window).fullscreen();
@ -754,7 +765,6 @@ fn gtkWindowNotifyMaximized(
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
self: *Window, self: *Window,
) callconv(.c) void { ) callconv(.c) void {
self.config.maximize = self.window.as(gtk.Window).isMaximized() != 0;
self.syncAppearance() catch |err| { self.syncAppearance() catch |err| {
log.err("failed to sync appearance={}", .{err}); log.err("failed to sync appearance={}", .{err});
}; };
@ -765,7 +775,6 @@ fn gtkWindowNotifyFullscreened(
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
self: *Window, self: *Window,
) callconv(.c) void { ) callconv(.c) void {
self.config.fullscreen = self.window.as(gtk.Window).isFullscreen() != 0;
self.syncAppearance() catch |err| { self.syncAppearance() catch |err| {
log.err("failed to sync appearance={}", .{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 // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
// sends an undefined value. // sends an undefined value.
fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {

View File

@ -219,13 +219,12 @@ pub const Window = struct {
pub fn resizeEvent(self: *Window) !void { pub fn resizeEvent(self: *Window) !void {
// The blur region must update with window resizes // 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(); try self.syncBlur();
} }
pub fn syncAppearance(self: *Window) !void { 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: { self.blur_region = blur: {
// NOTE(pluiedev): CSDs are a f--king mistake. // NOTE(pluiedev): CSDs are a f--king mistake.
// Please, GNOME, stop this nonsense of making a window ~30% bigger // 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); 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 .{ break :blur .{
.x = @intFromFloat(x), .x = @intFromFloat(x),
.y = @intFromFloat(y), .y = @intFromFloat(y),
@ -265,6 +269,13 @@ pub const Window = struct {
// and I think it's not really noticeable enough to justify the effort. // and I think it's not really noticeable enough to justify the effort.
// (Wayland also has this visual artifact anyway...) // (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; const blur = self.config.background_blur;
log.debug("set blur={}, window xid={}, region={}", .{ log.debug("set blur={}, window xid={}, region={}", .{
blur, blur,

View File

@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. /// 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 /// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release. /// _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. /// Standard build configuration options.
optimize: std.builtin.OptimizeMode, optimize: std.builtin.OptimizeMode,

View File

@ -14,6 +14,7 @@ pub const formatEntry = formatter.formatEntry;
// Field types // Field types
pub const ClipboardAccess = Config.ClipboardAccess; pub const ClipboardAccess = Config.ClipboardAccess;
pub const Command = Config.Command;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect; pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const CustomShaderAnimation = Config.CustomShaderAnimation;

View File

@ -22,7 +22,6 @@ const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
const internal_os = @import("../os/main.zig"); const internal_os = @import("../os/main.zig");
const cli = @import("../cli.zig"); const cli = @import("../cli.zig");
const Command = @import("../Command.zig");
const conditional = @import("conditional.zig"); const conditional = @import("conditional.zig");
const Conditional = conditional.Conditional; const Conditional = conditional.Conditional;
@ -34,6 +33,7 @@ const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig"); const ErrorList = @import("ErrorList.zig");
const MetricModifier = fontpkg.Metrics.Modifier; const MetricModifier = fontpkg.Metrics.Modifier;
const help_strings = @import("help_strings"); const help_strings = @import("help_strings");
pub const Command = @import("command.zig").Command;
const RepeatableStringMap = @import("RepeatableStringMap.zig"); const RepeatableStringMap = @import("RepeatableStringMap.zig");
pub const Path = @import("path.zig").Path; pub const Path = @import("path.zig").Path;
pub const RepeatablePath = @import("path.zig").RepeatablePath; pub const RepeatablePath = @import("path.zig").RepeatablePath;
@ -691,8 +691,17 @@ palette: Palette = .{},
/// * `passwd` entry (user information) /// * `passwd` entry (user information)
/// ///
/// This can contain additional arguments to run the command with. If additional /// This can contain additional arguments to run the command with. If additional
/// arguments are provided, the command will be executed using `/bin/sh -c`. /// arguments are provided, the command will be executed using `/bin/sh -c`
/// Ghostty does not do any shell command parsing. /// 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, /// 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 /// 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`. /// arguments. For example, `ghostty -e fish --with --custom --args`.
/// This flag sets the `initial-command` configuration, see that for more /// This flag sets the `initial-command` configuration, see that for more
/// information. /// information.
command: ?[]const u8 = null, command: ?Command = null,
/// This is the same as "command", but only applies to the first terminal /// This is the same as "command", but only applies to the first terminal
/// surface created when Ghostty starts. Subsequent terminal surfaces will use /// 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 /// fish --with --custom --args`. The `-e` flag automatically forces some
/// other behaviors as well: /// 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 /// * `gtk-single-instance=false` - This ensures that a new instance is
/// launched and the CLI args are respected. /// 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 /// name your binary appropriately or source the shell integration script
/// manually. /// manually.
/// ///
@"initial-command": ?[]const u8 = null, @"initial-command": ?Command = null,
/// Extra environment variables to pass to commands launched in a terminal /// Extra environment variables to pass to commands launched in a terminal
/// surface. The format is `env=KEY=VALUE`. /// surface. The format is `env=KEY=VALUE`.
@ -826,7 +839,7 @@ env: RepeatableStringMap = .{},
link: RepeatableLink = .{}, link: RepeatableLink = .{},
/// Enable URL matching. URLs are matched on hover with control (Linux) or /// 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 linked URL.
/// ///
/// The URL matcher is always lowest priority of any configured links (see /// 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 // Next, take all remaining args and use that to build up
// a command to execute. // a command to execute.
var command = std.ArrayList(u8).init(arena_alloc); var builder = std.ArrayList([:0]const u8).init(arena_alloc);
errdefer command.deinit(); errdefer builder.deinit();
for (args) |arg_raw| { for (args) |arg_raw| {
const arg = std.mem.sliceTo(arg_raw, 0); const arg = std.mem.sliceTo(arg_raw, 0);
try self._replay_steps.append( const copy = try arena_alloc.dupeZ(u8, arg);
arena_alloc, try self._replay_steps.append(arena_alloc, .{ .arg = copy });
.{ .arg = try arena_alloc.dupe(u8, arg) }, try builder.append(copy);
);
try command.appendSlice(arg);
try command.append(' ');
} }
self.@"_xdg-terminal-exec" = true; self.@"_xdg-terminal-exec" = true;
self.@"initial-command" = command.items[0 .. command.items.len - 1]; self.@"initial-command" = .{ .direct = try builder.toOwnedSlice() };
return; return;
} }
} }
@ -3023,7 +3032,7 @@ pub fn finalize(self: *Config) !void {
// We don't do this in flatpak because SHELL in Flatpak is always // We don't do this in flatpak because SHELL in Flatpak is always
// set to /bin/sh. // set to /bin/sh.
if (self.command) |cmd| if (self.command) |cmd|
log.info("shell src=config value={s}", .{cmd}) log.info("shell src=config value={}", .{cmd})
else shell_env: { else shell_env: {
// Flatpak always gets its shell from outside the sandbox // Flatpak always gets its shell from outside the sandbox
if (internal_os.isFlatpak()) break :shell_env; if (internal_os.isFlatpak()) break :shell_env;
@ -3035,7 +3044,9 @@ pub fn finalize(self: *Config) !void {
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| { if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
log.info("default shell source=env value={s}", .{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 we don't need the working directory, then we can exit now.
if (!wd_home) break :command; if (!wd_home) break :command;
@ -3046,7 +3057,7 @@ pub fn finalize(self: *Config) !void {
.windows => { .windows => {
if (self.command == null) { if (self.command == null) {
log.warn("no default shell found, will default to using cmd", .{}); log.warn("no default shell found, will default to using cmd", .{});
self.command = "cmd.exe"; self.command = .{ .shell = "cmd.exe" };
} }
if (wd_home) { if (wd_home) {
@ -3063,7 +3074,7 @@ pub fn finalize(self: *Config) !void {
if (self.command == null) { if (self.command == null) {
if (pw.shell) |sh| { if (pw.shell) |sh| {
log.info("default shell src=passwd value={s}", .{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 // Build up the command. We don't clean this up because we take
// ownership in our allocator. // ownership in our allocator.
var command = std.ArrayList(u8).init(alloc); var command: std.ArrayList([:0]const u8) = .init(alloc);
errdefer command.deinit(); errdefer command.deinit();
while (iter.next()) |param| { while (iter.next()) |param| {
try self._replay_steps.append(alloc, .{ .arg = try alloc.dupe(u8, param) }); const copy = try alloc.dupeZ(u8, param);
try command.appendSlice(param); try self._replay_steps.append(alloc, .{ .arg = copy });
try command.append(' '); try command.append(copy);
} }
if (command.items.len == 0) { if (command.items.len == 0) {
@ -3167,9 +3178,8 @@ pub fn parseManuallyHook(
return false; return false;
} }
self.@"initial-command" = command.items[0 .. command.items.len - 1];
// See "command" docs for the implied configurations and why. // See "command" docs for the implied configurations and why.
self.@"initial-command" = .{ .direct = command.items };
self.@"gtk-single-instance" = .false; self.@"gtk-single-instance" = .false;
self.@"quit-after-last-window-closed" = true; self.@"quit-after-last-window-closed" = true;
self.@"quit-after-last-window-closed-delay" = null; self.@"quit-after-last-window-closed-delay" = null;
@ -3184,7 +3194,7 @@ pub fn parseManuallyHook(
// Keep track of our input args for replay // Keep track of our input args for replay
try self._replay_steps.append( try self._replay_steps.append(
alloc, alloc,
.{ .arg = try alloc.dupe(u8, arg) }, .{ .arg = try alloc.dupeZ(u8, arg) },
); );
// If we didn't find a special case, continue parsing normally // 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, [:0]const u8,
=> return std.mem.eql(u8, old, new), => 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 => {}, else => {},
} }
@ -3412,6 +3432,8 @@ fn equalField(comptime T: type, old: T, new: T) bool {
}, },
.@"union" => |info| { .@"union" => |info| {
if (@hasDecl(T, "equal")) return old.equal(new);
const tag_type = info.tag_type.?; const tag_type = info.tag_type.?;
const old_tag = std.meta.activeTag(old); const old_tag = std.meta.activeTag(old);
const new_tag = std.meta.activeTag(new); 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 Replay = struct {
const Step = union(enum) { const Step = union(enum) {
/// An argument to parse as if it came from the CLI or file. /// 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. /// A base path to expand relative paths against.
expand: []const u8, expand: []const u8,
@ -3481,7 +3503,7 @@ const Replay = struct {
return switch (self) { return switch (self) {
.@"-e" => self, .@"-e" => self,
.diagnostic => |v| .{ .diagnostic = try v.clone(alloc) }, .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) }, .expand => |v| .{ .expand = try alloc.dupe(u8, v) },
.conditional_arg => |v| conditional: { .conditional_arg => |v| conditional: {
var conds = try alloc.alloc(Conditional, v.conditions.len); var conds = try alloc.alloc(Conditional, v.conditions.len);
@ -6620,7 +6642,11 @@ test "parse e: command only" {
var it: TestIterator = .{ .data = &.{"foo"} }; var it: TestIterator = .{ .data = &.{"foo"} };
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); 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" { test "parse e: command and args" {
@ -6631,7 +6657,13 @@ test "parse e: command and args" {
var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } };
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); 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" { test "clone default" {

322
src/config/command.zig Normal file
View File

@ -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;
}

View File

@ -23,13 +23,28 @@ const log = std.log.scoped(.i18n);
/// ///
/// 3. Most preferred locale for a language without a country code. /// 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{ pub const locales = [_][:0]const u8{
"de_DE.UTF-8",
"zh_CN.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", "nb_NO.UTF-8",
"ru_RU.UTF-8",
"uk_UA.UTF-8", "uk_UA.UTF-8",
"pl_PL.UTF-8", "pl_PL.UTF-8",
"mk_MK.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. /// Set for faster membership lookup of locales.
@ -108,6 +123,9 @@ pub fn canonicalizeLocale(
buf: []u8, buf: []u8,
locale: []const u8, locale: []const u8,
) error{NoSpaceLeft}![:0]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 // 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; if (buf.len < @max(16, locale.len + 1)) return error.NoSpaceLeft;
@ -126,6 +144,30 @@ pub fn canonicalizeLocale(
return buf[0..slice.len :0]; 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 /// This can be called at any point a compile-time-known locale is
/// available. This will use comptime to verify the locale is supported. /// available. This will use comptime to verify the locale is supported.
pub fn staticLocale(comptime v: [*:0]const u8) [*:0]const u8 { 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_CN", try canonicalizeLocale(&buf, "zh-Hans"));
try testing.expectEqualStrings("zh_TW", try canonicalizeLocale(&buf, "zh-Hant")); 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: // 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 // canonicalizeLocale does not handle encodings and will turn them into
// underscores. We should parse them out before calling this function. // underscores. We should parse them out before calling this function.

View File

@ -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 // Entry that is retrieved from the passwd API. This only contains the fields
// we care about. // we care about.
pub const Entry = struct { pub const Entry = struct {
shell: ?[]const u8 = null, shell: ?[:0]const u8 = null,
home: ?[]const u8 = null, home: ?[:0]const u8 = null,
name: ?[]const u8 = null, name: ?[:0]const u8 = null,
}; };
/// Get the passwd entry for the currently executing user. /// 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 // Shell and home are the last two entries
var it = std.mem.splitBackwardsScalar(u8, std.mem.trimRight(u8, output, " \r\n"), ':'); var it = std.mem.splitBackwardsScalar(u8, std.mem.trimRight(u8, output, " \r\n"), ':');
result.shell = it.next() orelse null; result.shell = if (it.next()) |v| try alloc.dupeZ(u8, v) else null;
result.home = it.next() orelse null; result.home = if (it.next()) |v| try alloc.dupeZ(u8, v) else null;
return result; return result;
} }
if (pw.pw_shell) |ptr| { if (pw.pw_shell) |ptr| {
const source = std.mem.sliceTo(ptr, 0); const source = std.mem.sliceTo(ptr, 0);
const sh = try alloc.alloc(u8, source.len); const value = try alloc.dupeZ(u8, source);
@memcpy(sh, source); result.shell = value;
result.shell = sh;
} }
if (pw.pw_dir) |ptr| { if (pw.pw_dir) |ptr| {
const source = std.mem.sliceTo(ptr, 0); const source = std.mem.sliceTo(ptr, 0);
const dir = try alloc.alloc(u8, source.len); const value = try alloc.dupeZ(u8, source);
@memcpy(dir, source); result.home = value;
result.home = dir;
} }
if (pw.pw_name) |ptr| { if (pw.pw_name) |ptr| {
const source = std.mem.sliceTo(ptr, 0); const source = std.mem.sliceTo(ptr, 0);
const name = try alloc.alloc(u8, source.len); const value = try alloc.dupeZ(u8, source);
@memcpy(name, source); result.name = value;
result.name = name;
} }
return result; return result;

View File

@ -23,6 +23,8 @@ pub fn ShellEscapeWriter(comptime T: type) type {
'?', '?',
' ', ' ',
'|', '|',
'(',
')',
=> &[_]u8{ '\\', byte }, => &[_]u8{ '\\', byte },
else => &[_]u8{byte}, else => &[_]u8{byte},
}; };
@ -93,3 +95,12 @@ test "shell escape 6" {
try writer.writeAll("a\"c"); try writer.writeAll("a\"c");
try testing.expectEqualStrings("a\\\"c", fmt.getWritten()); 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());
}

View File

@ -425,11 +425,19 @@ vertex CellTextVertexOut cell_text_vertex(
// If we're constrained then we need to scale the glyph. // If we're constrained then we need to scale the glyph.
if (in.mode == MODE_TEXT_CONSTRAINED) { if (in.mode == MODE_TEXT_CONSTRAINED) {
float max_width = uniforms.cell_size.x * in.constraint_width; 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) { if (size.x > max_width) {
float new_y = size.y * (max_width / size.x); float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2; offset.y += (size.y - new_y) / 2;
offset.x = 0;
size.y = new_y; size.y = new_y;
size.x = max_width; 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( constexpr sampler textureSampler(
coord::pixel, coord::pixel,
address::clamp_to_edge, 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) { switch (in.mode) {

View File

@ -70,7 +70,7 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
fi fi
# Sudo # 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. # Wrap `sudo` command to ensure Ghostty terminfo is preserved.
# #
# This approach supports wrapping a `sudo` alias, but the alias definition # This approach supports wrapping a `sudo` alias, but the alias definition
@ -124,13 +124,13 @@ function __ghostty_precmd() {
fi fi
# Cursor # Cursor
if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
PS1=$PS1'\[\e[5 q\]' PS1=$PS1'\[\e[5 q\]'
PS0=$PS0'\[\e[0 q\]' PS0=$PS0'\[\e[0 q\]'
fi fi
# Title (working directory) # Title (working directory)
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
PS1=$PS1'\[\e]2;\w\a\]' PS1=$PS1'\[\e]2;\w\a\]'
fi fi
fi fi
@ -161,7 +161,7 @@ function __ghostty_preexec() {
PS2="$_GHOSTTY_SAVE_PS2" PS2="$_GHOSTTY_SAVE_PS2"
# Title (current command) # 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:]]}" builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}"
fi fi

View File

@ -36,6 +36,8 @@
} }
{ {
use str
# helper used by `mark-*` functions # helper used by `mark-*` functions
fn set-prompt-state {|new| set-env __ghostty_prompt_state $new } fn set-prompt-state {|new| set-env __ghostty_prompt_state $new }
@ -73,7 +75,8 @@
} }
fn report-pwd { 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| fn sudo-with-terminfo {|@args|
@ -104,20 +107,18 @@
set edit:after-readline = (conj $edit:after-readline $mark-output-start~) set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
set edit:after-command = (conj $edit:after-command $mark-output-end~) set edit:after-command = (conj $edit:after-command $mark-output-end~)
var no-title = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE) var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
var no-cursor = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR)
var no-sudo = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO)
if (not $no-title) { if (has-value $features title) {
set after-chdir = (conj $after-chdir {|_| report-pwd }) set after-chdir = (conj $after-chdir {|_| report-pwd })
} }
if (not $no-cursor) { if (has-value $features cursor) {
fn beam { printf "\e[5 q" } fn beam { printf "\e[5 q" }
fn block { printf "\e[0 q" } fn block { printf "\e[0 q" }
set edit:before-readline = (conj $edit:before-readline $beam~) set edit:before-readline = (conj $edit:before-readline $beam~)
set edit:after-readline = (conj $edit:after-readline {|_| block }) 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~ edit:add-var sudo~ $sudo-with-terminfo~
} }
} }

View File

@ -49,10 +49,9 @@ status --is-interactive || ghostty_exit
function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
functions -e __ghostty_setup functions -e __ghostty_setup
# Check if we are setting cursors set --local features (string split , $GHOSTTY_SHELL_FEATURES)
set --local no_cursor "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR"
if test -z $no_cursor if contains cursor $features
# Change the cursor to a beam on prompt. # Change the cursor to a beam on prompt.
function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape"
echo -en "\e[5 q" echo -en "\e[5 q"
@ -62,13 +61,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
end end
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 # When using sudo shell integration feature, ensure $TERMINFO is set
# and `sudo` is not already a function or alias # and `sudo` is not already a function or alias
if test -z $no_sudo if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x")
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 # Wrap `sudo` command to ensure Ghostty terminfo is preserved
function sudo -d "Wrap sudo to preserve terminfo" function sudo -d "Wrap sudo to preserve terminfo"
set --function sudo_has_sudoedit_flags "no" 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 set --global fish_handle_reflow 1
# Initial calls for first prompt # Initial calls for first prompt
if test -z $no_cursor if contains cursor $features
__ghostty_set_cursor_beam __ghostty_set_cursor_beam
end end
__ghostty_mark_prompt_start __ghostty_mark_prompt_start

View File

@ -194,7 +194,7 @@ _ghostty_deferred_init() {
_ghostty_report_pwd" _ghostty_report_pwd"
_ghostty_report_pwd _ghostty_report_pwd
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
# Enable terminal title changes. # Enable terminal title changes.
functions[_ghostty_precmd]+=" functions[_ghostty_precmd]+="
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'" 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'" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'"
fi fi
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; then if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
# Enable cursor shape changes depending on the current keymap. # Enable cursor shape changes depending on the current keymap.
# This implementation leaks blinking block cursor into external commands # This implementation leaks blinking block cursor into external commands
# executed from zle. For example, users of fzf-based widgets may find # executed from zle. For example, users of fzf-based widgets may find
@ -221,7 +221,7 @@ _ghostty_deferred_init() {
fi fi
# Sudo # 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 # Wrap `sudo` command to ensure Ghostty terminfo is preserved
sudo() { sudo() {
builtin local sudo_has_sudoedit_flags="no" builtin local sudo_has_sudoedit_flags="no"

View File

@ -98,6 +98,12 @@ pub const Parser = struct {
self.state = .control_value; self.state = .control_value;
}, },
// This can be encountered if we have a sequence with no
// control data, only payload data (i.e. "\x1b_G;<data>").
//
// Kitty treats this as valid so we do as well.
';' => self.state = .data,
else => try self.accumulateValue(c, .control_key_ignore), else => try self.accumulateValue(c, .control_key_ignore),
}, },
@ -1053,6 +1059,21 @@ test "delete command" {
try testing.expectEqual(@as(u32, 4), dv.y); 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)" { test "ignore unknown keys (long)" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;

View File

@ -24,6 +24,7 @@ const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool;
const ptypkg = @import("../pty.zig"); const ptypkg = @import("../pty.zig");
const Pty = ptypkg.Pty; const Pty = ptypkg.Pty;
const EnvMap = std.process.EnvMap; const EnvMap = std.process.EnvMap;
const PasswdEntry = internal_os.passwd.Entry;
const windows = internal_os.windows; const windows = internal_os.windows;
const log = std.log.scoped(.io_exec); const log = std.log.scoped(.io_exec);
@ -725,7 +726,7 @@ pub const ThreadData = struct {
}; };
pub const Config = struct { pub const Config = struct {
command: ?[]const u8 = null, command: ?configpkg.Command = null,
env: EnvMap, env: EnvMap,
env_override: configpkg.RepeatableStringMap = .{}, env_override: configpkg.RepeatableStringMap = .{},
shell_integration: configpkg.Config.ShellIntegration = .detect, shell_integration: configpkg.Config.ShellIntegration = .detect,
@ -746,7 +747,7 @@ const Subprocess = struct {
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
cwd: ?[]const u8, cwd: ?[]const u8,
env: ?EnvMap, env: ?EnvMap,
args: [][]const u8, args: []const [:0]const u8,
grid_size: renderer.GridSize, grid_size: renderer.GridSize,
screen_size: renderer.ScreenSize, screen_size: renderer.ScreenSize,
pty: ?Pty = null, pty: ?Pty = null,
@ -892,18 +893,29 @@ const Subprocess = struct {
env.remove("VTE_VERSION"); env.remove("VTE_VERSION");
// Setup our shell integration, if we can. // Setup our shell integration, if we can.
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: { const shell_command: configpkg.Command = shell: {
const default_shell_command = cfg.command orelse switch (builtin.os.tag) { const default_shell_command: configpkg.Command =
cfg.command orelse .{ .shell = switch (builtin.os.tag) {
.windows => "cmd.exe", .windows => "cmd.exe",
else => "sh", else => "sh",
}; } };
const force: ?shell_integration.Shell = switch (cfg.shell_integration) { const force: ?shell_integration.Shell = switch (cfg.shell_integration) {
.none => { .none => {
// Even if shell integration is none, we still want to set up the feature env vars // Even if shell integration is none, we still want to
try shell_integration.setupFeatures(&env, cfg.shell_integration_features); // set up the feature env vars
break :shell .{ null, default_shell_command }; 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, .detect => null,
.bash => .bash, .bash => .bash,
.elvish => .elvish, .elvish => .elvish,
@ -911,9 +923,9 @@ const Subprocess = struct {
.zsh => .zsh, .zsh => .zsh,
}; };
const dir = cfg.resources_dir orelse break :shell .{ const dir = cfg.resources_dir orelse {
null, log.warn("no resources dir set, shell integration disabled", .{});
default_shell_command, break :shell default_shell_command;
}; };
const integration = try shell_integration.setup( const integration = try shell_integration.setup(
@ -923,19 +935,18 @@ const Subprocess = struct {
&env, &env,
force, force,
cfg.shell_integration_features, 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 .{ integration.shell, integration.command }; break :shell default_shell_command;
}; };
if (integrated_shell) |shell| {
log.info( log.info(
"shell integration automatically injected shell={}", "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. // Add the environment variables that override any others.
{ {
@ -947,134 +958,29 @@ const Subprocess = struct {
} }
// Build our args list // Build our args list
const args = args: { const args: []const [:0]const u8 = execCommand(
const cap = 9; // the most we'll ever use
var args = try std.ArrayList([]const u8).initCapacity(alloc, cap);
defer args.deinit();
// 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;
};
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, alloc,
"exec -l {s}", shell_command,
.{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", .{});
// The reason for executing login this way is unclear. This // The comptime here is important to ensure the full slice
// comment will attempt to explain but prepare for a truly // is put into the binary data and not the stack.
// unhinged reality. break :oom comptime switch (builtin.os.tag) {
// .windows => &.{"cmd.exe"},
// The first major issue is that on macOS, a lot of users else => &.{"/bin/sh"},
// 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 // This logs on its own, this is a bad error.
// load startup files so that (1) our shell integration doesn't error.SystemError => return err,
// 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();
}; };
// We have to copy the cwd because there is no guarantee that // 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");
}

View File

@ -27,10 +27,10 @@ pub const ShellIntegration = struct {
/// bash in particular it may be different. /// bash in particular it may be different.
/// ///
/// The memory is allocated in the arena given to setup. /// 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 /// integrated shell integration and return a ShellIntegration
/// struct describing the integration. If integration fails /// struct describing the integration. If integration fails
/// (shell type couldn't be detected, etc.), this will return null. /// (shell type couldn't be detected, etc.), this will return null.
@ -41,7 +41,7 @@ pub const ShellIntegration = struct {
pub fn setup( pub fn setup(
alloc_arena: Allocator, alloc_arena: Allocator,
resource_dir: []const u8, resource_dir: []const u8,
command: []const u8, command: config.Command,
env: *EnvMap, env: *EnvMap,
force_shell: ?Shell, force_shell: ?Shell,
features: config.ShellIntegrationFeatures, features: config.ShellIntegrationFeatures,
@ -51,14 +51,24 @@ pub fn setup(
.elvish => "elvish", .elvish => "elvish",
.fish => "fish", .fish => "fish",
.zsh => "zsh", .zsh => "zsh",
} else exe: { } else switch (command) {
// The command can include arguments. Look for the first space .direct => |v| std.fs.path.basename(v[0]),
// and use the basename of the first part as the command's exe. .shell => |v| exe: {
const idx = std.mem.indexOfScalar(u8, command, ' ') orelse command.len; // Shell strings can include spaces so we want to only
break :exe std.fs.path.basename(command[0..idx]); // 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 // Setup our feature env vars
try setupFeatures(env, features); try setupFeatures(env, features);
@ -69,7 +79,7 @@ pub fn setup(
fn setupShell( fn setupShell(
alloc_arena: Allocator, alloc_arena: Allocator,
resource_dir: []const u8, resource_dir: []const u8,
command: []const u8, command: config.Command,
env: *EnvMap, env: *EnvMap,
exe: []const u8, exe: []const u8,
) !?ShellIntegration { ) !?ShellIntegration {
@ -83,7 +93,10 @@ fn setupShell(
// we're using Apple's Bash because /bin is non-writable // we're using Apple's Bash because /bin is non-writable
// on modern macOS due to System Integrity Protection. // on modern macOS due to System Integrity Protection.
if (comptime builtin.target.os.tag.isDarwin()) { 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; return null;
} }
} }
@ -104,7 +117,7 @@ fn setupShell(
try setupXdgDataDirs(alloc_arena, resource_dir, env); try setupXdgDataDirs(alloc_arena, resource_dir, env);
return .{ return .{
.shell = .elvish, .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); try setupXdgDataDirs(alloc_arena, resource_dir, env);
return .{ return .{
.shell = .fish, .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); try setupZsh(resource_dir, env);
return .{ return .{
.shell = .zsh, .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| { inline for (@typeInfo(Shell).@"enum".fields) |field| {
const shell = @field(Shell, field.name); 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); try testing.expectEqual(shell, result.?.shell);
} }
} }
/// Setup shell integration feature environment variables without /// Set up the shell integration features environment variable.
/// performing full shell integration setup.
pub fn setupFeatures( pub fn setupFeatures(
env: *EnvMap, env: *EnvMap,
features: config.ShellIntegrationFeatures, features: config.ShellIntegrationFeatures,
) !void { ) !void {
if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1"); const fields = @typeInfo(@TypeOf(features)).@"struct".fields;
if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1"); const capacity: usize = capacity: {
if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1"); 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" { test "setup features" {
@ -162,15 +196,13 @@ test "setup features" {
defer arena.deinit(); defer arena.deinit();
const alloc = arena.allocator(); const alloc = arena.allocator();
// Test: all features enabled (no environment variables should be set) // Test: all features enabled
{ {
var env = EnvMap.init(alloc); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true }); try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true });
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") == null); try testing.expectEqualStrings("cursor,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?);
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE") == null);
} }
// Test: all features disabled // Test: all features disabled
@ -179,9 +211,7 @@ test "setup features" {
defer env.deinit(); defer env.deinit();
try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false }); try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false });
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?); try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null);
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").?);
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
} }
// Test: mixed features // Test: mixed features
@ -190,9 +220,7 @@ test "setup features" {
defer env.deinit(); defer env.deinit();
try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false }); try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false });
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?); try testing.expectEqualStrings("sudo", env.get("GHOSTTY_SHELL_FEATURES").?);
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
} }
} }
@ -207,25 +235,21 @@ test "setup features" {
/// enables the integration or null if integration failed. /// enables the integration or null if integration failed.
fn setupBash( fn setupBash(
alloc: Allocator, alloc: Allocator,
command: []const u8, command: config.Command,
resource_dir: []const u8, resource_dir: []const u8,
env: *EnvMap, env: *EnvMap,
) !?[]const u8 { ) !?config.Command {
// Accumulates the arguments that will form the final shell command line. var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 2);
// 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);
defer args.deinit(); defer args.deinit();
// Iterator that yields each argument in the original command line. // Iterator that yields each argument in the original command line.
// This will allocate once proportionate to the command line length. // 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(); defer iter.deinit();
// Start accumulating arguments with the executable and `--posix` mode flag. // Start accumulating arguments with the executable and `--posix` mode flag.
if (iter.next()) |exe| { if (iter.next()) |exe| {
try args.append(exe); try args.append(try alloc.dupeZ(u8, exe));
} else return null; } else return null;
try args.append("--posix"); try args.append("--posix");
@ -259,17 +283,17 @@ fn setupBash(
if (std.mem.indexOfScalar(u8, arg, 'c') != null) { if (std.mem.indexOfScalar(u8, arg, 'c') != null) {
return 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, "--")) { } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) {
// All remaining arguments should be passed directly to the shell // All remaining arguments should be passed directly to the shell
// command. We shouldn't perform any further option processing. // 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| { while (iter.next()) |remaining_arg| {
try args.append(remaining_arg); try args.append(try alloc.dupeZ(u8, remaining_arg));
} }
break; break;
} else { } else {
try args.append(arg); try args.append(try alloc.dupeZ(u8, arg));
} }
} }
try env.put("GHOSTTY_BASH_INJECT", inject.slice()); try env.put("GHOSTTY_BASH_INJECT", inject.slice());
@ -302,30 +326,36 @@ fn setupBash(
); );
try env.put("ENV", integ_dir); try env.put("ENV", integ_dir);
// Join the accumulated arguments to form the final command string. // Since we built up a command line, we don't need to wrap it in
return try std.mem.join(alloc, " ", args.items); // ANOTHER shell anymore and can do a direct command.
return .{ .direct = try args.toOwnedSlice() };
} }
test "bash" { test "bash" {
const testing = std.testing; 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); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
const command = try setupBash(alloc, "bash", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
defer if (command) |c| alloc.free(c);
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("./shell-integration/bash/ghostty.bash", env.get("ENV").?);
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?);
} }
test "bash: unsupported options" { test "bash: unsupported options" {
const testing = std.testing; 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 --posix",
"bash --rcfile script.sh --posix", "bash --rcfile script.sh --posix",
"bash --init-file script.sh --posix", "bash --init-file script.sh --posix",
@ -337,7 +367,7 @@ test "bash: unsupported options" {
var env = EnvMap.init(alloc); var env = EnvMap.init(alloc);
defer env.deinit(); 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_INJECT") == null);
try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null); try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null);
try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null);
@ -346,17 +376,20 @@ test "bash: unsupported options" {
test "bash: inject flags" { test "bash: inject flags" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
// bash --norc // bash --norc
{ {
var env = EnvMap.init(alloc); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
const command = try setupBash(alloc, "bash --norc", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env);
defer if (command) |c| alloc.free(c);
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").?); try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?);
} }
@ -365,52 +398,55 @@ test "bash: inject flags" {
var env = EnvMap.init(alloc); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
const command = try setupBash(alloc, "bash --noprofile", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env);
defer if (command) |c| alloc.free(c);
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").?); try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?);
} }
} }
test "bash: rcfile" { test "bash: rcfile" {
const testing = std.testing; 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); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
// bash --rcfile // bash --rcfile
{ {
const command = try setupBash(alloc, "bash --rcfile profile.sh", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env);
defer if (command) |c| alloc.free(c); try testing.expectEqual(2, command.?.direct.len);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("bash --posix", command.?); try testing.expectEqualStrings("--posix", command.?.direct[1]);
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
} }
// bash --init-file // bash --init-file
{ {
const command = try setupBash(alloc, "bash --init-file profile.sh", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env);
defer if (command) |c| alloc.free(c); try testing.expectEqual(2, command.?.direct.len);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("bash --posix", command.?); try testing.expectEqualStrings("--posix", command.?.direct[1]);
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
} }
} }
test "bash: HISTFILE" { test "bash: HISTFILE" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
// HISTFILE unset // HISTFILE unset
{ {
var env = EnvMap.init(alloc); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
const command = try setupBash(alloc, "bash", ".", &env); _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
defer if (command) |c| alloc.free(c);
try testing.expect(std.mem.endsWith(u8, env.get("HISTFILE").?, ".bash_history")); try testing.expect(std.mem.endsWith(u8, env.get("HISTFILE").?, ".bash_history"));
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE").?); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE").?);
} }
@ -422,9 +458,7 @@ test "bash: HISTFILE" {
try env.put("HISTFILE", "my_history"); try env.put("HISTFILE", "my_history");
const command = try setupBash(alloc, "bash", ".", &env); _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
defer if (command) |c| alloc.free(c);
try testing.expectEqualStrings("my_history", env.get("HISTFILE").?); try testing.expectEqualStrings("my_history", env.get("HISTFILE").?);
try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null);
} }
@ -432,25 +466,35 @@ test "bash: HISTFILE" {
test "bash: additional arguments" { test "bash: additional arguments" {
const testing = std.testing; 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); var env = EnvMap.init(alloc);
defer env.deinit(); defer env.deinit();
// "-" argument separator // "-" argument separator
{ {
const command = try setupBash(alloc, "bash - --arg file1 file2", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env);
defer if (command) |c| alloc.free(c); try testing.expectEqual(6, command.?.direct.len);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?); 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 // "--" argument separator
{ {
const command = try setupBash(alloc, "bash -- --arg file1 file2", ".", &env); const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env);
defer if (command) |c| alloc.free(c); try testing.expectEqual(6, command.?.direct.len);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?); 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]);
} }
} }