Merge remote-tracking branch 'upstream/main' into jacob/uucode

pull/8757/head
Jacob Sandlund 2025-09-17 02:34:06 -04:00
commit ae21e2c8cf
41 changed files with 1781 additions and 1329 deletions

View File

@ -39,7 +39,7 @@ jobs:
echo "Version is valid: ${{ github.event.inputs.version }}"
- name: Exract the Version
- name: Extract the Version
id: extract_version
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
@ -100,8 +100,10 @@ jobs:
- name: Create Tarball
run: |
rm -rf zig-out/dist
nix develop -c zig build distcheck
cp zig-out/dist/ghostty-${GHOSTTY_VERSION}.tar.gz .
cp zig-out/dist/ghostty-${GHOSTTY_VERSION}.tar.gz ghostty-source.tar.gz
- name: Sign Tarball
run: |
@ -117,6 +119,8 @@ jobs:
path: |-
ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz
ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz.minisig
ghostty-source.tar.gz
ghostty-source.tar.gz.minisig
build-macos:
needs: [setup]
@ -139,14 +143,14 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.4.app
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Xcode Version
run: xcodebuild -version
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.7.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -311,7 +315,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.7.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -243,7 +243,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.7.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -481,7 +481,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.7.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -666,7 +666,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.7.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -273,6 +273,7 @@ jobs:
ghostty-source.tar.gz
trigger-snap:
if: github.event_name != 'pull_request'
runs-on: namespace-profile-ghostty-xsm
needs: build-dist
steps:

View File

@ -39,10 +39,14 @@ jobs:
- name: Run zig fetch
id: zig_fetch
env:
GH_TOKEN: ${{ github.token }}
run: |
UPSTREAM_REV="$(curl "https://api.github.com/repos/mbadolato/iTerm2-Color-Schemes/commits/master" | jq -r '.sha')"
nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/$UPSTREAM_REV.tar.gz"
echo "upstream_rev=$UPSTREAM_REV" >> "$GITHUB_OUTPUT"
# Get the latest release from iTerm2-Color-Schemes
RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest)
TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name')
nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
- name: Update zig cache hash
run: |
@ -71,5 +75,5 @@ jobs:
build.zig.zon.json
flatpak/zig-packages.json
body: |
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.zig_fetch.outputs.tag_name }}
labels: dependencies

View File

@ -183,6 +183,7 @@
/po/ga_IE.UTF-8.po @ghostty-org/ga_IE
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR
/po/he_IL.UTF-8.po @ghostty-org/he_IL
/po/it_IT.UTF-8.po @ghostty-org/it_IT
# Packaging - Snap
/snap/ @ghostty-org/snap

View File

@ -150,7 +150,7 @@ hash in CI, and builds will fail if it drifts.
To update it, you can run the following in the repository root:
```
./nix/build-support/check-zig-cache-hash.sh --update
./nix/build-support/check-zig-cache.sh --update
```
This will write out the `nix/zigCacheHash.nix` file with the updated hash

View File

@ -1,6 +1,6 @@
.{
.name = .ghostty,
.version = "1.1.4",
.version = "1.2.1",
.paths = .{""},
.fingerprint = 0x64407a2a0b4147e5,
.dependencies = .{
@ -20,8 +20,8 @@
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz",
.hash = "z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr",
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
.hash = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP",
.lazy = true,
},
.zig_objc = .{
@ -111,8 +111,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz",
.hash = "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b",
.url = "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz",
.hash = "N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B",
.lazy = true,
},
},

12
build.zig.zon.json generated
View File

@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b": {
"N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz",
"hash": "sha256-3vPlDDjv6BCLyro1YytzPtF0FfBH20skYuA9laDWhac="
"url": "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz",
"hash": "sha256-6rKNFpaUvSbvNZ0/+u0h4I/RRaV5V7xIPQ9y7eNVbCA="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",
@ -134,10 +134,10 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
},
"z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr": {
"z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP": {
"name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz",
"hash": "sha256-0yR5Yc5MxOJBV1cv4LOWBwWkZYcGU53qFZd40TlZPcg="
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
"hash": "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0="
},
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf",

12
build.zig.zon.nix generated
View File

@ -163,11 +163,11 @@ in
};
}
{
name = "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b";
name = "N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz";
hash = "sha256-3vPlDDjv6BCLyro1YytzPtF0FfBH20skYuA9laDWhac=";
url = "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz";
hash = "sha256-6rKNFpaUvSbvNZ0/+u0h4I/RRaV5V7xIPQ9y7eNVbCA=";
};
}
{
@ -299,11 +299,11 @@ in
};
}
{
name = "z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr";
name = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP";
path = fetchZigArtifact {
name = "z2d";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz";
hash = "sha256-0yR5Yc5MxOJBV1cv4LOWBwWkZYcGU53qFZd40TlZPcg=";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz";
hash = "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0=";
};
}
{

4
build.zig.zon.txt generated
View File

@ -8,6 +8,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
https://deps.files.ghostty.org/gettext-0.24.tar.gz
https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
@ -28,8 +29,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz

View File

@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz",
"dest": "vendor/p/N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b",
"sha256": "def3e50c38efe8108bcaba35632b733ed17415f047db4b2462e03d95a0d685a7"
"url": "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz",
"dest": "vendor/p/N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B",
"sha256": "eab28d169694bd26ef359d3ffaed21e08fd145a57957bc483d0f72ede3556c20"
},
{
"type": "archive",
@ -163,9 +163,9 @@
},
{
"type": "archive",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz",
"dest": "vendor/p/z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr",
"sha256": "d3247961ce4cc4e24157572fe0b3960705a4658706539dea159778d139593dc8"
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
"dest": "vendor/p/z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP",
"sha256": "d036c3292600d5e8e1571fd66ce9304e00f9ecf35115c9d1be2a8187cc693d9d"
},
{
"type": "archive",

View File

@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "df074165274afaa39539c05d57b0832620775b11",
"version" : "2.7.1"
"revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d",
"version" : "2.7.3"
}
}
],

View File

@ -147,6 +147,16 @@ class AppDelegate: NSObject,
// Disable the automatic full screen menu item because we handle
// it manually.
"NSFullScreenMenuItemEverywhere": false,
// On macOS 26 RC1, the autofill heuristic controller causes unusable levels
// of slowdowns and CPU usage in the terminal window under certain [unknown]
// conditions. We don't know exactly why/how. This disables the full heuristic
// controller.
//
// Practically, this means things like SMS autofill don't work, but that is
// a desirable behavior to NOT have happen for a terminal, so this is a win.
// Manual autofill via the `Edit => AutoFill` menu item still work as expected.
"NSAutoFillHeuristicControllerEnabled": false,
])
}

View File

@ -123,7 +123,8 @@ struct NewTerminalIntent: AppIntent {
if let view = controller.newSplit(
at: parent,
direction: location.splitDirection!
direction: location.splitDirection!,
baseConfig: config
) {
return .result(value: TerminalEntity(view))
}

View File

@ -165,7 +165,12 @@ fileprivate struct CommandPaletteQuery: View {
.textFieldStyle(.plain)
.focused($isTextFieldFocused)
.onAppear {
isTextFieldFocused = true
// We want to grab focus on appearance. We have to do this after a tick
// on macOS Tahoe otherwise this doesn't work. See:
// https://github.com/ghostty-org/ghostty/issues/8497
DispatchQueue.main.async {
isTextFieldFocused = true
}
}
.onChange(of: isTextFieldFocused) { focused in
if !focused {

View File

@ -1219,6 +1219,23 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// Get our target window
let targetWindow = tabbedWindows[finalIndex]
// Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs.
// I believe this is due to messed up constraints for our hacky tab bar. I'd like to
// find a better workaround. For now, this improves things dramatically.
//
// Reproduction: titlebar tabs, create two tabs, "move tab left"
if #available(macOS 26, *) {
if window is TitlebarTabsTahoeTerminalWindow {
tabGroup.removeWindow(selectedWindow)
targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
DispatchQueue.main.async {
selectedWindow.makeKey()
}
return
}
}
// Begin a group of window operations to minimize visual updates
NSAnimationContext.beginGrouping()

View File

@ -156,7 +156,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
accessoryView.needsLayout = true
// Setup an observer for the NSTabBar frame. When system appearance changes or
// other events occur, the tab bar can temporarily become zero-sized. When this
// other events occur, the tab bar can resize and clear our constraints. When this
// happens, we need to remove our custom constraints and re-apply them once the
// tab bar has proper dimensions again to avoid constraint conflicts.
tabBar.postsFrameChangedNotifications = true
@ -167,9 +167,6 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
) { [weak self] _ in
guard let self else { return }
// Check if either width or height is zero
guard tabBar.frame.size.width == 0 || tabBar.frame.size.height == 0 else { return }
// Remove the observer so we can call setup again.
self.tabBarObserver = nil

View File

@ -80,16 +80,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
// window background but with opacity. The window background is set using the
// "preferred background color" property.
//
// As an inverse, if we don't have transparency, we don't bother with this because
// the window background will be set to the correct color so we can just hide the
// titlebar completely and we're good to go.
if !isOpaque {
if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") {
titlebarView.wantsLayer = true
titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor
}
// Even if we aren't transparent, we still set this because this becomes the
// color of the titlebar in native fullscreen view.
if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") {
titlebarView.wantsLayer = true
titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor
}
// In all cases, we have to hide the background view since this has multiple subviews
// that force a background color.
titlebarBackgroundView?.isHidden = true

View File

@ -509,13 +509,14 @@ extension Ghostty {
// Make the text field the first responder so it gets focus
alert.window.initialFirstResponder = textField
let response = alert.runModal()
// Check if the user clicked "OK"
if response == .alertFirstButtonReturn {
let completionHandler: (NSApplication.ModalResponse) -> Void = { [weak self] response in
guard let self else { return }
// Check if the user clicked "OK"
guard response == .alertFirstButtonReturn else { return }
// Get the input text
let newTitle = textField.stringValue
if newTitle.isEmpty {
// Empty means that user wants the title to be set automatically
// We also need to reload the config for the "title" property to be
@ -529,6 +530,16 @@ extension Ghostty {
title = newTitle
}
}
// We prefer to run our alert in a sheet modal if we have a window.
if let window {
alert.beginSheetModal(for: window, completionHandler: completionHandler)
} else {
// On macOS 26 RC, this codepath results in the "OK" button not being
// visible. The above codepath should be taken most times but I'm just
// noting this as something I noticed consistently.
completionHandler(alert.runModal())
}
}
func setTitle(_ title: String) {

View File

@ -34,7 +34,7 @@ help() {
echo "To fix, please (manually) re-run the script from the repository root,"
echo "commit, and submit a PR with the update:"
echo ""
echo " ./nix/build-support/check-zig-cache-hash.sh --update"
echo " ./nix/build-support/check-zig-cache.sh --update"
echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json"
echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json\""
echo ""

View File

@ -40,7 +40,7 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "1.1.4";
version = "1.2.1";
# 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

320
po/it_IT.UTF-8.po Normal file
View File

@ -0,0 +1,320 @@
# Italian translations for com.mitchellh.ghostty package
# Traduzioni italiane per il pacchetto com.mitchellh.ghostty.
# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors"
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Giacomo Bettini <giaco.bettini@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-09-06 19:40+0200\n"
"Last-Translator: Giacomo Bettini <giaco.bettini@gmail.com>\n"
"Language-Team: Italian <tp@lists.linux.it>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "Cambia il titolo del terminale"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Lasciare vuoto per ripristinare il titolo predefinito."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
#: src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "Annulla"
#: 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
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Errori di configurazione"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/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 ""
"Sono stati trovati uno o più errori di configurazione. Controlla gli errori seguenti, "
"poi ricarica la tua configurazione o ignora quegli errori."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/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:100
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
msgid "Reload Configuration"
msgstr "Ricarica configurazione"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
#: 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 "Dividi in alto"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
#: 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 "Dividi in basso"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
#: 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 "Dividi a sinistra"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
#: 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 "Dividi a destra"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Esegui un comando…"
#: 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 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
msgid "Paste"
msgstr "Incolla"
#: 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 "Pulisci"
#: 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 "Reimposta"
#: 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 "Divisione"
#: 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 "Cambia titolo…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr "Scheda"
#: 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:265
msgid "New Tab"
msgstr "Nuova scheda"
#: 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 "Chiudi scheda"
#: 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 "Nuova 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 "Chiudi finestra"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Configurazione"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Open Configuration"
msgstr "Apri configurazione"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Riquadro comandi"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
msgstr "Ispettore del terminale"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1038
msgid "About Ghostty"
msgstr "Informazioni su Ghostty"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112
msgid "Quit"
msgstr "Chiudi"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "Consenti accesso agli Appunti"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Un'applicazione sta cercando di leggere dagli Appunti. Il contenuto "
"attuale degli Appunti è mostrato di seguito."
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "Non consentire"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr "Consenti"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Ricorda scelta per questa divisione"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Ricarica la configurazione per visualizzare nuovamente questo messaggio"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Un'applicazione sta cercando di scrivere negli Appunti. Il contenuto "
"attuale degli Appunti è mostrato di seguito."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr "Attenzione: Incolla potenzialmente pericoloso"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Incollare questo testo nel terminale potrebbe essere pericoloso poiché "
"sembra contenere comandi che potrebbero venire eseguiti."
#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531
msgid "Close"
msgstr "Chiudi"
#: src/apprt/gtk/CloseDialog.zig:87
msgid "Quit Ghostty?"
msgstr "Chiudere Ghostty?"
#: src/apprt/gtk/CloseDialog.zig:88
msgid "Close Window?"
msgstr "Chiudere la finestra?"
#: src/apprt/gtk/CloseDialog.zig:89
msgid "Close Tab?"
msgstr "Chiudere la scheda?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "Chiudere la divisione?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
msgstr "Tutte le sessioni del terminale saranno terminate."
#: src/apprt/gtk/CloseDialog.zig:97
msgid "All terminal sessions in this window will be terminated."
msgstr "Tutte le sessioni del terminale in questa finestra saranno terminate."
#: src/apprt/gtk/CloseDialog.zig:98
msgid "All terminal sessions in this tab will be terminated."
msgstr "Tutte le sessioni del terminale in questa scheda saranno terminate."
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr "Il processo attualmente in esecuzione in questa divisione sarà terminato."
#: src/apprt/gtk/Surface.zig:1266
msgid "Copied to clipboard"
msgstr "Copiato negli Appunti"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Appunti svuotati"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Comando riuscito"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Comando fallito"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
msgstr "Menù principale"
#: src/apprt/gtk/Window.zig:239
msgid "View Open Tabs"
msgstr "Vedi schede aperte"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Nuova divisione"
#: src/apprt/gtk/Window.zig:329
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
"⚠️ Stai usando una build di debug di Ghostty! Le prestazioni saranno ridotte."
#: src/apprt/gtk/Window.zig:775
msgid "Reloaded the configuration"
msgstr "Configurazione ricaricata"
#: src/apprt/gtk/Window.zig:1019
msgid "Ghostty Developers"
msgstr "Sviluppatori di Ghostty"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Ispettore del terminale"

View File

@ -9,7 +9,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-03-21 00:08+0900\n"
"PO-Revision-Date: 2025-09-01 14:43+0900\n"
"Last-Translator: Lon Sagisawa <lon@sagisawa.me>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
@ -88,7 +88,7 @@ msgstr "右に分割"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
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
@ -161,7 +161,7 @@ msgstr "設定ファイルを開く"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "コマンドパレット"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@ -209,12 +209,12 @@ msgstr "許可"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "この分割ウィンドウに対して設定を記憶する"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr ""
msgstr "このプロンプトを再び表示するには設定を再読み込みしてください"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@ -279,15 +279,15 @@ msgstr "クリップボードにコピーしました"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "クリップボードを空にしました"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "コマンド実行成功"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "コマンド実行失敗"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@ -299,7 +299,7 @@ msgstr "開いているすべてのタブを表示"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "新しい分割"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@ -4,14 +4,15 @@
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Gustavo Peres <gsodevel@gmail.com>, 2025.
# Guilherme Tiscoski <github@guilhermetiscoski.com>, 2025.
# Nilton Perim Neto <niltonperimneto@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-25 11:46-0500\n"
"Last-Translator: Guilherme Tiscoski <github@guihermetiscoski.com>\n"
"PO-Revision-Date: 2025-09-15 13:57-0300\n"
"Last-Translator: Nilton Perim Neto <niltonperimneto@gmail.com>\n"
"Language-Team: Brazilian Portuguese <ldpbr-translation@lists.sourceforge."
"net>\n"
"Language: pt_BR\n"
@ -26,7 +27,7 @@ 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."
msgstr "Deixe em branco para restaurar o título padrão."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
@ -315,8 +316,8 @@ msgstr "Configuração recarregada"
#: src/apprt/gtk/Window.zig:1019
msgid "Ghostty Developers"
msgstr "Desenvolvedores Ghostty"
msgstr "Desenvolvedores do Ghostty"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Inspetor de terminal"
msgstr "Ghostty: Inspetor do terminal"

View File

@ -66,6 +66,12 @@ font_grid_key: font.SharedGridSet.Key,
font_size: font.face.DesiredSize,
font_metrics: font.Metrics,
/// This keeps track of if the font size was ever modified. If it wasn't,
/// then config reloading will change the font. If it was manually adjusted,
/// we don't change it on config reload since we assume the user wants
/// a specific size.
font_size_adjusted: bool,
/// The renderer for this surface.
renderer: Renderer,
@ -514,6 +520,7 @@ pub fn init(
.rt_surface = rt_surface,
.font_grid_key = font_grid_key,
.font_size = font_size,
.font_size_adjusted = false,
.font_metrics = font_grid.metrics,
.renderer = renderer_impl,
.renderer_thread = render_thread,
@ -863,18 +870,24 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, .unlocked);
},
.color_change => |change| {
.color_change => |change| color_change: {
// Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color.
_ = try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
.kind = switch (change.kind) {
.background => .background,
.foreground => .foreground,
.cursor => .cursor,
.kind = switch (change.target) {
.palette => |v| @enumFromInt(v),
.dynamic => |dyn| switch (dyn) {
.foreground => .foreground,
.background => .background,
.cursor => .cursor,
// Unsupported dynamic color change notification type
else => break :color_change,
},
// Special colors aren't supported for change notification
.special => break :color_change,
},
.r = change.color.r,
.g = change.color.g,
@ -1440,7 +1453,21 @@ pub fn updateConfig(
// but this is easier and pretty rare so it's not a performance concern.
//
// (Calling setFontSize builds and sends a new font grid to the renderer.)
try self.setFontSize(self.font_size);
try self.setFontSize(font_size: {
// If we have manually adjusted the font size, keep it that way.
if (self.font_size_adjusted) {
log.info("font size manually adjusted, preserving previous size on config reload", .{});
break :font_size self.font_size;
}
// If we haven't, then we update to the configured font size.
// This allows config changes to update the font size. We used to
// never do this but it was a common source of confusion and people
// assumed that Ghostty was broken! This logic makes more sense.
var size = self.font_size;
size.points = std.math.clamp(config.@"font-size", 1.0, 255.0);
break :font_size size;
});
// We need to store our configs in a heap-allocated pointer so that
// our messages aren't huge.
@ -3985,7 +4012,7 @@ pub fn cursorPosCallback(
// Stop selection scrolling when inside the viewport within a 1px buffer
// for fullscreen windows, but only when selection scrolling is active.
if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) {
if (pos.y >= 1 and self.selection_scroll_active) {
self.io.queueMessage(
.{ .selection_scroll = false },
.locked,
@ -4631,10 +4658,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
log.debug("increase font size={}", .{clamped_delta});
var size = self.font_size;
// Max point size is somewhat arbitrary.
var size = self.font_size;
size.points = @min(size.points + clamped_delta, 255);
try self.setFontSize(size);
// Mark that we manually adjusted the font size
self.font_size_adjusted = true;
},
.decrease_font_size => |delta| {
@ -4646,6 +4676,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
var size = self.font_size;
size.points = @max(1, size.points - clamped_delta);
try self.setFontSize(size);
// Mark that we manually adjusted the font size
self.font_size_adjusted = true;
},
.reset_font_size => {
@ -4654,6 +4687,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
var size = self.font_size;
size.points = self.config.original_font_size;
try self.setFontSize(size);
// Reset font size also resets the manual adjustment state
self.font_size_adjusted = false;
},
.set_font_size => |points| {
@ -4662,6 +4698,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
var size = self.font_size;
size.points = std.math.clamp(points, 1.0, 255.0);
try self.setFontSize(size);
// Mark that we manually adjusted the font size
self.font_size_adjusted = true;
},
.prompt_surface_title => return try self.rt_app.performAction(

View File

@ -78,10 +78,7 @@ pub const Message = union(enum) {
password_input: bool,
/// A terminal color was changed using OSC sequences.
color_change: struct {
kind: terminal.osc.Command.ColorOperation.Kind,
color: terminal.color.RGB,
},
color_change: terminal.osc.color.ColoredTarget,
/// Notifies the surface that a tick of the timer that is timing
/// out selection scrolling has occurred. "selection scrolling"

View File

@ -20,7 +20,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 4 };
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 1 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,

View File

@ -120,7 +120,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Themes
if (b.lazyDependency("iterm2_themes", .{})) |upstream| {
const install_step = b.addInstallDirectory(.{
.source_dir = upstream.path("ghostty"),
.source_dir = upstream.path(""),
.install_dir = .{ .custom = "share" },
.install_subdir = b.pathJoin(&.{ "ghostty", "themes" }),
.exclude_extensions = &.{".md"},

View File

@ -127,9 +127,6 @@ pub const compatibility = std.StaticStringMap(
/// this within config files if you want to clear previously set values in
/// configuration files or on the CLI if you want to clear values set on the
/// CLI.
///
/// Changing this configuration at runtime will only affect new terminals, i.e.
/// new windows, tabs, etc.
@"font-family": RepeatableString = .{},
@"font-family-bold": RepeatableString = .{},
@"font-family-italic": RepeatableString = .{},
@ -214,11 +211,12 @@ pub const compatibility = std.StaticStringMap(
///
/// For example, 13.5pt @ 2px/pt = 27px
///
/// Changing this configuration at runtime will only affect new terminals,
/// i.e. new windows, tabs, etc. Note that you may still not see the change
/// depending on your `window-inherit-font-size` setting. If that setting is
/// true, only the first window will be affected by this change since all
/// subsequent windows will inherit the font size of the previous window.
/// Changing this configuration at runtime will only affect existing
/// terminals that have NOT manually adjusted their font size in some way
/// (e.g. increased or decreased the font size). Terminals that have manually
/// adjusted their font size will retain their manually adjusted size.
/// Otherwise, the font size of existing terminals will be updated on
/// reload.
///
/// On Linux with GTK, font size is scaled according to both display-wide and
/// text-specific scaling factors, which are often managed by your desktop
@ -515,7 +513,7 @@ pub const compatibility = std.StaticStringMap(
///
/// To specify a different theme for light and dark mode, use the following
/// syntax: `light:theme-name,dark:theme-name`. For example:
/// `light:rose-pine-dawn,dark:rose-pine`. Whitespace around all values are
/// `light:Rose Pine Dawn,dark:Rose Pine`. Whitespace around all values are
/// trimmed and order of light and dark does not matter. Both light and dark
/// must be specified in this form. In this form, the theme used will be
/// based on the current desktop environment theme.
@ -2146,6 +2144,8 @@ keybind: Keybinds = .{},
/// from the first by a comma (`,`). Percentage and pixel sizes can be mixed
/// together: for instance, a size of `50%,500px` for a top-positioned quick
/// terminal would be half a screen tall, and 500 pixels wide.
///
/// Available since: 1.2.0
@"quick-terminal-size": QuickTerminalSize = .{},
/// The layer of the quick terminal window. The higher the layer,
@ -5564,6 +5564,17 @@ pub const Keybinds = struct {
);
{
try self.set.put(
alloc,
.{ .key = .{ .physical = .copy } },
.{ .copy_to_clipboard = {} },
);
try self.set.put(
alloc,
.{ .key = .{ .physical = .paste } },
.{ .paste_from_clipboard = {} },
);
// On non-MacOS desktop envs (Windows, KDE, Gnome, Xfce), ctrl+insert is an
// alt keybinding for Copy and shift+ins is an alt keybinding for Paste
//

View File

@ -38,9 +38,11 @@ pub const Shaper = switch (options.backend) {
/// for a shaping call. Note all terminal cells may be present; only
/// cells that have a glyph that needs to be rendered.
pub const Cell = struct {
/// The column that this cell occupies. Since a set of shaper cells is
/// always on the same line, only the X is stored. It is expected the
/// caller has access to the original screen cell.
/// The X position of this shaper cell relative to the offset of the
/// run. Because runs are always within a single row, it is expected
/// that the caller can reconstruct the full position of the cell by
/// using the known Y position of the cell and adding the X position
/// to the run offset.
x: u16,
/// An additional offset to apply to the rendering.

View File

@ -17,9 +17,15 @@ pub const TextRun = struct {
/// lower the chance of hash collisions if they become a problem. If
/// there are hash collisions, it would result in rendering issues but
/// the core data would be correct.
///
/// The hash is position-independent within the row by using relative
/// cluster positions. This allows identical runs in different positions
/// to share the same cache entry, improving cache efficiency.
hash: u64,
/// The offset in the row where this run started
/// The offset in the row where this run started. This is added to the
/// X position of the final shaped cells to get the absolute position
/// in the row where they belong.
offset: u16,
/// The total number of cells produced by this run.
@ -77,7 +83,11 @@ pub const RunIterator = struct {
// Go through cell by cell and accumulate while we build our run.
var j: usize = self.i;
while (j < max) : (j += 1) {
const cluster = j;
// Use relative cluster positions (offset from run start) to make
// the shaping cache position-independent. This ensures that runs
// with identical content but different starting positions in the
// row produce the same hash, enabling cache reuse.
const cluster = j - self.i;
const cell = &cells[j];
// If we have a selection and we're at a boundary point, then

View File

@ -64,11 +64,35 @@ pub const Parser = struct {
const flags, const start_idx = try parseFlags(raw_input);
const input = raw_input[start_idx..];
// Find the last = which splits are mapping into the trigger
// and action, respectively.
// We use the last = because the keybind itself could contain
// raw equal signs (for the = codepoint)
const eql_idx = std.mem.lastIndexOf(u8, input, "=") orelse return Error.InvalidFormat;
// Find the equal sign. This is more complicated than it seems on
// the surface because we need to ignore equal signs that are
// part of the trigger.
const eql_idx: usize = eql: {
// TODO: We should change this parser into a real state machine
// based parser that parses the trigger fully, then yields the
// action after. The loop below is a total mess.
var offset: usize = 0;
while (std.mem.indexOfScalar(
u8,
input[offset..],
'=',
)) |offset_idx| {
// Find: '=+ctrl' or '==action'
const idx = offset + offset_idx;
if (idx < input.len - 1 and
(input[idx + 1] == '+' or
input[idx + 1] == '='))
{
offset += offset_idx + 1;
continue;
}
// Looks like the real equal sign.
break :eql idx;
}
return Error.InvalidFormat;
};
// Sequence iterator goes up to the equal, action is after. We can
// parse the action now.
@ -698,7 +722,7 @@ pub const Action = union(enum) {
/// All actions are only undoable/redoable for a limited time.
/// For example, restoring a closed split can only be done for
/// some number of seconds since the split was closed. The exact
/// amount is configured with `TODO`.
/// amount is configured with the `undo-timeout` configuration settings.
///
/// The undo/redo actions being limited ensures that there is
/// bounded memory usage over time, closed surfaces don't continue running
@ -2301,6 +2325,39 @@ test "parse: equals sign" {
try testing.expectError(Error.InvalidFormat, parseSingle("=ignore"));
}
test "parse: text action equals sign" {
const testing = std.testing;
{
const binding = try parseSingle("==text:=");
try testing.expectEqual(Trigger{ .key = .{ .unicode = '=' } }, binding.trigger);
try testing.expectEqualStrings("=", binding.action.text);
}
{
const binding = try parseSingle("==text:=hello");
try testing.expectEqual(Trigger{ .key = .{ .unicode = '=' } }, binding.trigger);
try testing.expectEqualStrings("=hello", binding.action.text);
}
{
const binding = try parseSingle("ctrl+==text:=hello");
try testing.expectEqual(Trigger{
.key = .{ .unicode = '=' },
.mods = .{ .ctrl = true },
}, binding.trigger);
try testing.expectEqualStrings("=hello", binding.action.text);
}
{
const binding = try parseSingle("=+ctrl=text:=hello");
try testing.expectEqual(Trigger{
.key = .{ .unicode = '=' },
.mods = .{ .ctrl = true },
}, binding.trigger);
try testing.expectEqualStrings("=hello", binding.action.text);
}
}
// For Ghostty 1.2+ we changed our key names to match the W3C and removed
// `physical:`. This tests the backwards compatibility with the old format.
// Note that our backwards compatibility isn't 100% perfect since triggers

View File

@ -47,6 +47,7 @@ pub const locales = [_][:0]const u8{
"es_AR.UTF-8",
"pt_BR.UTF-8",
"ca_ES.UTF-8",
"it_IT.UTF-8",
"bg_BG.UTF-8",
"ga_IE.UTF-8",
"hu_HU.UTF-8",

View File

@ -2528,9 +2528,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :cache cells;
};
const cells = shaper_cells.?;
// Advance our index until we reach or pass
// our current x position in the shaper cells.
while (shaper_cells.?[shaper_cells_i].x < x) {
while (run.offset + cells[shaper_cells_i].x < x) {
shaper_cells_i += 1;
}
}
@ -2769,13 +2771,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x
// position monotonically increasing.
assert(cells[shaper_cells_i].x >= x);
assert(run.offset + cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is
// violated, this logic breaks.
while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
while (shaper_cells_i < cells.len and run.offset + cells[shaper_cells_i].x == x) : ({
shaper_cells_i += 1;
}) {
self.addGlyph(

View File

@ -103,7 +103,7 @@ fi
# SSH Integration
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
ssh() {
function ssh() {
builtin local ssh_term ssh_opts
ssh_term="xterm-256color"
ssh_opts=()

View File

@ -915,21 +915,53 @@ test "osc: 112 incomplete sequence" {
const cmd = a[0].?.osc_dispatch;
try testing.expect(cmd == .color_operation);
try testing.expectEqual(cmd.color_operation.terminator, .bel);
try testing.expect(cmd.color_operation.source == .reset_cursor);
try testing.expect(cmd.color_operation.operations.count() == 1);
var it = cmd.color_operation.operations.constIterator(0);
try testing.expect(cmd.color_operation.op == .osc_112);
try testing.expect(cmd.color_operation.requests.count() == 1);
var it = cmd.color_operation.requests.constIterator(0);
{
const op = it.next().?;
try testing.expect(op.* == .reset);
try testing.expectEqual(
osc.Command.ColorOperation.Kind.cursor,
op.reset,
osc.color.Request{ .reset = .{ .dynamic = .cursor } },
op.*,
);
}
try std.testing.expect(it.next() == null);
}
}
test "osc: 104 empty" {
var p: Parser = init();
defer p.deinit();
p.osc_parser.alloc = std.testing.allocator;
_ = p.next(0x1B);
_ = p.next(']');
_ = p.next('1');
_ = p.next('0');
_ = p.next('4');
{
const a = p.next(0x07);
try testing.expect(p.state == .ground);
try testing.expect(a[0].? == .osc_dispatch);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
const cmd = a[0].?.osc_dispatch;
try testing.expect(cmd == .color_operation);
try testing.expectEqual(cmd.color_operation.terminator, .bel);
try testing.expect(cmd.color_operation.op == .osc_104);
try testing.expect(cmd.color_operation.requests.count() == 1);
var it = cmd.color_operation.requests.constIterator(0);
{
const op = it.next().?;
try testing.expect(op.* == .reset_palette);
}
try std.testing.expect(it.next() == null);
}
}
test "csi: too many params" {
var p = init();
_ = p.next(0x1B);

View File

@ -94,6 +94,85 @@ pub const Name = enum(u8) {
}
};
/// The "special colors" as denoted by xterm. These can be set via
/// OSC 5 or via OSC 4 by adding the palette length to it.
///
/// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
pub const Special = enum(u3) {
bold = 0,
underline = 1,
blink = 2,
reverse = 3,
italic = 4,
pub fn osc4(self: Special) u16 {
// "The special colors can also be set by adding the maximum
// number of colors (e.g., 88 or 256) to these codes in an
// OSC 4 control" - xterm ctlseqs
const max = @typeInfo(Palette).array.len;
return @as(u16, @intCast(@intFromEnum(self))) + max;
}
test "osc4" {
const testing = std.testing;
try testing.expectEqual(256, Special.bold.osc4());
try testing.expectEqual(257, Special.underline.osc4());
try testing.expectEqual(258, Special.blink.osc4());
try testing.expectEqual(259, Special.reverse.osc4());
try testing.expectEqual(260, Special.italic.osc4());
}
};
test Special {
_ = Special;
}
/// The "dynamic colors" as denoted by xterm. These can be set via
/// OSC 10 through 19.
pub const Dynamic = enum(u5) {
foreground = 10,
background = 11,
cursor = 12,
pointer_foreground = 13,
pointer_background = 14,
tektronix_foreground = 15,
tektronix_background = 16,
highlight_background = 17,
tektronix_cursor = 18,
highlight_foreground = 19,
/// The next dynamic color sequentially. This is required because
/// specifying colors sequentially without their index will automatically
/// use the next dynamic color.
///
/// "Each successive parameter changes the next color in the list. The
/// value of Ps tells the starting point in the list."
pub fn next(self: Dynamic) ?Dynamic {
return std.meta.intToEnum(
Dynamic,
@intFromEnum(self) + 1,
) catch null;
}
test "next" {
const testing = std.testing;
try testing.expectEqual(.background, Dynamic.foreground.next());
try testing.expectEqual(.cursor, Dynamic.background.next());
try testing.expectEqual(.pointer_foreground, Dynamic.cursor.next());
try testing.expectEqual(.pointer_background, Dynamic.pointer_foreground.next());
try testing.expectEqual(.tektronix_foreground, Dynamic.pointer_background.next());
try testing.expectEqual(.tektronix_background, Dynamic.tektronix_foreground.next());
try testing.expectEqual(.highlight_background, Dynamic.tektronix_background.next());
try testing.expectEqual(.tektronix_cursor, Dynamic.highlight_background.next());
try testing.expectEqual(.highlight_foreground, Dynamic.tektronix_cursor.next());
try testing.expectEqual(null, Dynamic.highlight_foreground.next());
}
};
test Dynamic {
_ = Dynamic;
}
/// RGB
pub const RGB = packed struct(u24) {
r: u8 = 0,

File diff suppressed because it is too large Load Diff

705
src/terminal/osc/color.zig Normal file
View File

@ -0,0 +1,705 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const DynamicColor = @import("../color.zig").Dynamic;
const SpecialColor = @import("../color.zig").Special;
const RGB = @import("../color.zig").RGB;
pub const ParseError = Allocator.Error || error{
MissingOperation,
};
/// The possible operations we support for colors.
pub const Operation = enum {
osc_4,
osc_5,
osc_10,
osc_11,
osc_12,
osc_13,
osc_14,
osc_15,
osc_16,
osc_17,
osc_18,
osc_19,
osc_104,
osc_105,
osc_110,
osc_111,
osc_112,
osc_113,
osc_114,
osc_115,
osc_116,
osc_117,
osc_118,
osc_119,
};
/// Parse any color operation string. This should NOT include the operation
/// itself, but only the body of the operation. e.g. for "4;a;b;c" the body
/// should be "a;b;c" and the operation should be set accordingly.
///
/// Color parsing is fairly complicated so we pull this out to a specialized
/// function rather than go through our OSC parsing state machine. This is
/// much slower and requires more memory (since we need to buffer the full
/// request) but grants us an easier to understand and testable implementation.
///
/// If color changing ends up being a bottleneck we can optimize this later.
pub fn parse(
alloc: Allocator,
op: Operation,
buf: []const u8,
) ParseError!List {
var it = std.mem.tokenizeScalar(u8, buf, ';');
return switch (op) {
.osc_4 => try parseGetSetAnsiColor(alloc, .osc_4, &it),
.osc_5 => try parseGetSetAnsiColor(alloc, .osc_5, &it),
.osc_104 => try parseResetAnsiColor(alloc, .osc_104, &it),
.osc_105 => try parseResetAnsiColor(alloc, .osc_105, &it),
.osc_10 => try parseGetSetDynamicColor(alloc, .foreground, &it),
.osc_11 => try parseGetSetDynamicColor(alloc, .background, &it),
.osc_12 => try parseGetSetDynamicColor(alloc, .cursor, &it),
.osc_13 => try parseGetSetDynamicColor(alloc, .pointer_foreground, &it),
.osc_14 => try parseGetSetDynamicColor(alloc, .pointer_background, &it),
.osc_15 => try parseGetSetDynamicColor(alloc, .tektronix_foreground, &it),
.osc_16 => try parseGetSetDynamicColor(alloc, .tektronix_background, &it),
.osc_17 => try parseGetSetDynamicColor(alloc, .highlight_background, &it),
.osc_18 => try parseGetSetDynamicColor(alloc, .tektronix_cursor, &it),
.osc_19 => try parseGetSetDynamicColor(alloc, .highlight_foreground, &it),
.osc_110 => try parseResetDynamicColor(alloc, .foreground, &it),
.osc_111 => try parseResetDynamicColor(alloc, .background, &it),
.osc_112 => try parseResetDynamicColor(alloc, .cursor, &it),
.osc_113 => try parseResetDynamicColor(alloc, .pointer_foreground, &it),
.osc_114 => try parseResetDynamicColor(alloc, .pointer_background, &it),
.osc_115 => try parseResetDynamicColor(alloc, .tektronix_foreground, &it),
.osc_116 => try parseResetDynamicColor(alloc, .tektronix_background, &it),
.osc_117 => try parseResetDynamicColor(alloc, .highlight_background, &it),
.osc_118 => try parseResetDynamicColor(alloc, .tektronix_cursor, &it),
.osc_119 => try parseResetDynamicColor(alloc, .highlight_foreground, &it),
};
}
/// OSC 4/5
fn parseGetSetAnsiColor(
alloc: Allocator,
comptime op: Operation,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
// Note: in ANY error scenario below we return the accumulated results.
// This matches the xterm behavior (see misc.c ChangeAnsiColorRequest)
var result: List = .{};
errdefer result.deinit(alloc);
while (true) {
// We expect a `c; spec` pair. If either doesn't exist then
// we return the results up to this point.
const color_str = it.next() orelse return result;
const spec_str = it.next() orelse return result;
// Color must be numeric. u9 because that'll fit our palette + special
const color: u9 = std.fmt.parseInt(
u9,
color_str,
10,
) catch return result;
// Parse the color.
const target: Target = switch (op) {
// OSC5 maps directly to the Special enum.
.osc_5 => .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color) orelse return result,
) catch return result },
// OSC4 maps 0-255 to palette, 256-259 to special offset
// by the palette count.
.osc_4 => if (std.math.cast(u8, color)) |idx| .{
.palette = idx,
} else .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color - 256) orelse return result,
) catch return result },
else => comptime unreachable,
};
// "?" always results in a query.
if (std.mem.eql(u8, spec_str, "?")) {
const req = try result.addOne(alloc);
req.* = .{ .query = target };
continue;
}
const rgb = RGB.parse(spec_str) catch return result;
const req = try result.addOne(alloc);
req.* = .{ .set = .{
.target = target,
.color = rgb,
} };
}
}
/// OSC 104/105: Reset ANSI Colors
fn parseResetAnsiColor(
alloc: Allocator,
comptime op: Operation,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
// Note: xterm stops parsing the reset list on any error, but we're
// more flexible and try the next value. This matches the behavior of
// Kitty and I don't see a downside to being more flexible here. Hopefully
// no one depends on the exact behavior of xterm.
var result: List = .{};
errdefer result.deinit(alloc);
while (true) {
const color_str = it.next() orelse {
// If no parameters are given, we reset the full table.
if (result.count() == 0) {
const req = try result.addOne(alloc);
req.* = switch (op) {
.osc_104 => .reset_palette,
.osc_105 => .reset_special,
else => comptime unreachable,
};
}
return result;
};
// Empty color strings are ignored, not treated as an error.
if (color_str.len == 0) continue;
// Color must be numeric. u9 because that'll fit our palette + special
const color: u9 = std.fmt.parseInt(
u9,
color_str,
10,
) catch continue;
// Parse the color.
const target: Target = switch (op) {
// OSC105 maps directly to the Special enum.
.osc_105 => .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color) orelse continue,
) catch continue },
// OSC104 maps 0-255 to palette, 256-259 to special offset
// by the palette count.
.osc_104 => if (std.math.cast(u8, color)) |idx| .{
.palette = idx,
} else .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color - 256) orelse continue,
) catch continue },
else => comptime unreachable,
};
const req = try result.addOne(alloc);
req.* = .{ .reset = target };
}
}
/// OSC 10-19: Get/Set Dynamic Colors
fn parseGetSetDynamicColor(
alloc: Allocator,
start: DynamicColor,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
// Note: in ANY error scenario below we return the accumulated results.
// This matches the xterm behavior (see misc.c ChangeColorsRequest)
var result: List = .{};
var color: DynamicColor = start;
while (true) {
const spec_str = it.next() orelse return result;
if (std.mem.eql(u8, spec_str, "?")) {
const req = try result.addOne(alloc);
req.* = .{ .query = .{ .dynamic = color } };
} else {
const rgb = RGB.parse(spec_str) catch return result;
const req = try result.addOne(alloc);
req.* = .{ .set = .{
.target = .{ .dynamic = color },
.color = rgb,
} };
}
// Each successive value uses the next color so long as it exists.
color = color.next() orelse return result;
}
}
/// OSC 110-119: Reset Dynamic Colors
fn parseResetDynamicColor(
alloc: Allocator,
color: DynamicColor,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
var result: List = .{};
errdefer result.deinit(alloc);
if (it.next() != null) return result;
const req = try result.addOne(alloc);
req.* = .{ .reset = .{ .dynamic = color } };
return result;
}
/// A segmented list is used to avoid copying when many operations
/// are given in a single OSC. In most cases, OSC 4/104/etc. send
/// very few so the prealloc is optimized for that.
///
/// The exact prealloc value is chosen arbitrarily assuming most
/// color ops have very few. If we can get empirical data on more
/// typical values we can switch to that.
pub const List = std.SegmentedList(
Request,
2,
);
/// A single operation related to the terminal color palette.
pub const Request = union(enum) {
set: ColoredTarget,
query: Target,
reset: Target,
reset_palette,
reset_special,
};
pub const Target = union(enum) {
palette: u8,
special: SpecialColor,
dynamic: DynamicColor,
};
pub const ColoredTarget = struct {
target: Target,
color: RGB,
};
test "osc4" {
const testing = std.testing;
const alloc = testing.allocator;
// Test every palette index
for (0..std.math.maxInt(u8)) |idx| {
// Simple color set
// printf '\e]4;0;red\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = @intCast(idx) },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
// Simple color query
// printf '\e]4;0;?\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};?",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .query = .{ .palette = @intCast(idx) } },
list.at(0).*,
);
}
// Trailing invalid data produces results up to that point
// printf '\e]4;0;red;\e\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red;",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = @intCast(idx) },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
// Whitespace doesn't produce a working value in xterm but we
// allow it because Kitty does and it seems harmless.
//
// printf '\e]4;0;red \e\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red ",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = @intCast(idx) },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
// Test every special color
for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
const special = try std.meta.intToEnum(SpecialColor, i);
// Simple color set
// printf '\e]4;256;red\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red",
.{256 + i},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .special = special },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
}
test "osc5" {
const testing = std.testing;
const alloc = testing.allocator;
// Test every special color
for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
const special = try std.meta.intToEnum(SpecialColor, i);
// Simple color set
// printf '\e]4;256;red\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red",
.{i},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_5, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .special = special },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
}
test "osc4: multiple requests" {
const testing = std.testing;
const alloc = testing.allocator;
// printf '\e]4;0;red;1;blue\e\\'
{
var list = try parse(
alloc,
.osc_4,
"0;red;1;blue",
);
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 0 },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 1 },
.color = RGB{ .r = 0, .g = 0, .b = 255 },
} },
list.at(1).*,
);
}
// Multiple requests with same index overwrite each other
// printf '\e]4;0;red;0;blue\e\\'
{
var list = try parse(
alloc,
.osc_4,
"0;red;0;blue",
);
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 0 },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 0 },
.color = RGB{ .r = 0, .g = 0, .b = 255 },
} },
list.at(1).*,
);
}
}
test "osc104" {
const testing = std.testing;
const alloc = testing.allocator;
// Test every palette index
for (0..std.math.maxInt(u8)) |idx| {
// Simple color set
// printf '\e]104;0\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d}",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_104, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .palette = @intCast(idx) } },
list.at(0).*,
);
}
}
// Test every special color
for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
const special = try std.meta.intToEnum(SpecialColor, i);
// Simple color set
// printf '\e]104;256\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d}",
.{256 + i},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_104, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .special = special } },
list.at(0).*,
);
}
}
}
test "osc104 empty index" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_104, "0;;1");
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .reset = .{ .palette = 0 } },
list.at(0).*,
);
try testing.expectEqual(
Request{ .reset = .{ .palette = 1 } },
list.at(1).*,
);
}
test "osc104 invalid index" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_104, "ffff;1");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .palette = 1 } },
list.at(0).*,
);
}
test "osc104 reset all" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_104, "");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset_palette = {} },
list.at(0).*,
);
}
test "osc105 reset all" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_105, "");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset_special = {} },
list.at(0).*,
);
}
// OSC 10-19: Get/Set Dynamic Colors
test "dynamic" {
const testing = std.testing;
const alloc = testing.allocator;
inline for (@typeInfo(DynamicColor).@"enum".fields) |field| {
const color = @field(DynamicColor, field.name);
const op = @field(Operation, std.fmt.comptimePrint(
"osc_{d}",
.{field.value},
));
// Example script:
// printf '\e]10;red\e\\'
{
var list = try parse(alloc, op, "red");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .dynamic = color },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
}
test "dynamic multiple" {
const testing = std.testing;
const alloc = testing.allocator;
// Example script:
// printf '\e]11;red;blue\e\\'
{
var list = try parse(
alloc,
.osc_11,
"red;blue",
);
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .dynamic = .background },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
try testing.expectEqual(
Request{ .set = .{
.target = .{ .dynamic = .cursor },
.color = RGB{ .r = 0, .g = 0, .b = 255 },
} },
list.at(1).*,
);
}
}
// OSC 110-119: Reset Dynamic Colors
test "reset dynamic" {
const testing = std.testing;
const alloc = testing.allocator;
inline for (@typeInfo(DynamicColor).@"enum".fields) |field| {
const color = @field(DynamicColor, field.name);
const op = @field(Operation, std.fmt.comptimePrint(
"osc_1{d}",
.{field.value},
));
// Example script:
// printf '\e]110\e\\'
{
var list = try parse(alloc, op, "");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .dynamic = color } },
list.at(0).*,
);
}
// xterm allows a trailing semicolon. script to verify:
//
// printf '\e]110;\e\\'
{
var list = try parse(alloc, op, ";");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .dynamic = color } },
list.at(0).*,
);
}
// xterm does NOT allow any whitespace
//
// printf '\e]110 \e\\'
{
var list = try parse(alloc, op, " ");
defer list.deinit(alloc);
try testing.expectEqual(0, list.count());
}
}
}

View File

@ -1565,7 +1565,11 @@ pub fn Stream(comptime Handler: type) type {
.color_operation => |v| {
if (@hasDecl(T, "handleColorOperation")) {
try self.handler.handleColorOperation(v.source, &v.operations, v.terminator);
try self.handler.handleColorOperation(
v.op,
&v.requests,
v.terminator,
);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},

View File

@ -1187,12 +1187,15 @@ pub const StreamHandler = struct {
pub fn handleColorOperation(
self: *StreamHandler,
source: terminal.osc.Command.ColorOperation.Source,
operations: *const terminal.osc.Command.ColorOperation.List,
op: terminal.osc.color.Operation,
requests: *const terminal.osc.color.List,
terminator: terminal.osc.Terminator,
) !void {
// We'll need op one day if we ever implement reporting special colors.
_ = op;
// return early if there is nothing to do
if (operations.count() == 0) return;
if (requests.count() == 0) return;
var buffer: [1024]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = .init(&buffer);
@ -1201,63 +1204,71 @@ pub const StreamHandler = struct {
var response: std.ArrayListUnmanaged(u8) = .empty;
const writer = response.writer(alloc);
var report: bool = false;
try writer.print("\x1b]{}", .{source});
var it = operations.constIterator(0);
while (it.next()) |op| {
switch (op.*) {
var it = requests.constIterator(0);
while (it.next()) |req| {
switch (req.*) {
.set => |set| {
switch (set.kind) {
switch (set.target) {
.palette => |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = set.color;
self.terminal.color_palette.mask.set(i);
},
.foreground => {
self.foreground_color = set.color;
_ = self.renderer_mailbox.push(.{
.foreground_color = set.color,
}, .{ .forever = {} });
},
.background => {
self.background_color = set.color;
_ = self.renderer_mailbox.push(.{
.background_color = set.color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = set.color;
_ = self.renderer_mailbox.push(.{
.cursor_color = set.color,
}, .{ .forever = {} });
.dynamic => |dynamic| switch (dynamic) {
.foreground => {
self.foreground_color = set.color;
_ = self.renderer_mailbox.push(.{
.foreground_color = set.color,
}, .{ .forever = {} });
},
.background => {
self.background_color = set.color;
_ = self.renderer_mailbox.push(.{
.background_color = set.color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = set.color;
_ = self.renderer_mailbox.push(.{
.cursor_color = set.color,
}, .{ .forever = {} });
},
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> log.info("setting dynamic color {s} not implemented", .{
@tagName(dynamic),
}),
},
.special => log.info("setting special colors not implemented", .{}),
}
// Notify the surface of the color change
self.surfaceMessageWriter(.{ .color_change = .{
.kind = set.kind,
.target = set.target,
.color = set.color,
} });
},
.reset => |kind| {
switch (kind) {
.palette => |i| {
const mask = &self.terminal.color_palette.mask;
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
mask.unset(i);
.reset => |target| switch (target) {
.palette => |i| {
const mask = &self.terminal.color_palette.mask;
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
mask.unset(i);
self.surfaceMessageWriter(.{
.color_change = .{
.kind = .{ .palette = @intCast(i) },
.color = self.terminal.color_palette.colors[i],
},
});
},
self.surfaceMessageWriter(.{
.color_change = .{
.target = target,
.color = self.terminal.color_palette.colors[i],
},
});
},
.dynamic => |dynamic| switch (dynamic) {
.foreground => {
self.foreground_color = null;
_ = self.renderer_mailbox.push(.{
@ -1265,7 +1276,7 @@ pub const StreamHandler = struct {
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .foreground,
.target = target,
.color = self.default_foreground_color,
} });
},
@ -1276,7 +1287,7 @@ pub const StreamHandler = struct {
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .background,
.target = target,
.color = self.default_background_color,
} });
},
@ -1289,33 +1300,83 @@ pub const StreamHandler = struct {
if (self.default_cursor_color) |color| {
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .cursor,
.target = target,
.color = color,
} });
}
},
}
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> log.warn("resetting dynamic color {s} not implemented", .{
@tagName(dynamic),
}),
},
.special => log.info("resetting special colors not implemented", .{}),
},
.report => |kind| report: {
if (self.osc_color_report_format == .none) break :report;
.reset_palette => {
const mask = &self.terminal.color_palette.mask;
var mask_iterator = mask.iterator(.{});
while (mask_iterator.next()) |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
self.surfaceMessageWriter(.{
.color_change = .{
.target = .{ .palette = @intCast(i) },
.color = self.terminal.color_palette.colors[i],
},
});
}
mask.* = .initEmpty();
},
report = true;
.reset_special => log.warn(
"resetting all special colors not implemented",
.{},
),
.query => |kind| report: {
if (self.osc_color_report_format == .none) break :report;
const color = switch (kind) {
.palette => |i| self.terminal.color_palette.colors[i],
.foreground => self.foreground_color orelse self.default_foreground_color,
.background => self.background_color orelse self.default_background_color,
.cursor => self.cursor_color orelse
self.default_cursor_color orelse
self.foreground_color orelse
self.default_foreground_color,
.dynamic => |dynamic| switch (dynamic) {
.foreground => self.foreground_color orelse self.default_foreground_color,
.background => self.background_color orelse self.default_background_color,
.cursor => self.cursor_color orelse
self.default_cursor_color orelse
self.foreground_color orelse
self.default_foreground_color,
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> {
log.info(
"reporting dynamic color {s} not implemented",
.{@tagName(dynamic)},
);
break :report;
},
},
.special => {
log.info("reporting special colors not implemented", .{});
break :report;
},
};
switch (self.osc_color_report_format) {
.@"16-bit" => switch (kind) {
.palette => |i| try writer.print(
";{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}",
"\x1b]4;{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}",
.{
i,
@as(u16, color.r) * 257,
@ -1323,19 +1384,21 @@ pub const StreamHandler = struct {
@as(u16, color.b) * 257,
},
),
else => try writer.print(
";rgb:{x:0>4}/{x:0>4}/{x:0>4}",
.dynamic => |dynamic| try writer.print(
"\x1b]{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}",
.{
@intFromEnum(dynamic),
@as(u16, color.r) * 257,
@as(u16, color.g) * 257,
@as(u16, color.b) * 257,
},
),
.special => unreachable,
},
.@"8-bit" => switch (kind) {
.palette => |i| try writer.print(
";{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}",
"\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.{
i,
@as(u16, color.r),
@ -1343,25 +1406,29 @@ pub const StreamHandler = struct {
@as(u16, color.b),
},
),
else => try writer.print(
";rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.dynamic => |dynamic| try writer.print(
"\x1b]{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.{
@intFromEnum(dynamic),
@as(u16, color.r),
@as(u16, color.g),
@as(u16, color.b),
},
),
.special => unreachable,
},
.none => unreachable,
}
try writer.writeAll(terminator.string());
},
}
}
if (report) {
if (response.items.len > 0) {
// If any of the operations were reports, finalize the report
// string and send it to the terminal.
try writer.writeAll(terminator.string());
const msg = try termio.Message.writeReq(self.alloc, response.items);
self.messageWriter(msg);
}