Merge remote-tracking branch 'origin/main' into shaping-positions
commit
942f326c58
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
run: nix build .#ghostty
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
title: Update iTerm2 colorschemes
|
||||
base: main
|
||||
|
|
|
|||
|
|
@ -30,4 +30,5 @@ A file for [guiding coding agents](https://agents.md/).
|
|||
|
||||
- Do not use `xcodebuild`
|
||||
- Use `zig build` to build the macOS app and any shared Zig code
|
||||
- Use `zig build run` to build and run the macOS app
|
||||
- Run Xcode tests using `zig build test`
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const buildpkg = @import("src/build/main.zig");
|
||||
|
||||
const appVersion = @import("build.zig.zon").version;
|
||||
const minimumZigVersion = @import("build.zig.zon").minimum_zig_version;
|
||||
|
||||
|
|
@ -317,3 +318,8 @@ pub fn build(b: *std.Build) !void {
|
|||
try translations_step.addError("cannot update translations when i18n is disabled", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker used by Config.zig to detect if ghostty is the build root.
|
||||
/// This avoids running logic such as Git tag checking when Ghostty
|
||||
/// is used as a dependency.
|
||||
pub const _ghostty_build_root = true;
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@
|
|||
.lazy = true,
|
||||
},
|
||||
.gobject = .{
|
||||
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
|
||||
// https://github.com/ghostty-org/zig-gobject based on zig_gobject
|
||||
// Temporary until we generate them at build time automatically.
|
||||
.url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||
.url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
.lazy = true,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@
|
|||
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
|
||||
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
|
||||
},
|
||||
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": {
|
||||
"gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-": {
|
||||
"name": "gobject",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
"hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
"hash": "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg="
|
||||
},
|
||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||
"name": "gtk4_layer_shell",
|
||||
|
|
|
|||
|
|
@ -123,11 +123,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV";
|
||||
name = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-";
|
||||
path = fetchZigArtifact {
|
||||
name = "gobject";
|
||||
url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst";
|
||||
hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=";
|
||||
url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst";
|
||||
hash = "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ 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/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
|
||||
https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst
|
||||
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
|
||||
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
|
||||
https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz
|
||||
|
|
@ -27,6 +26,7 @@ https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.t
|
|||
https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
|
||||
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst
|
||||
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||
https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz
|
||||
|
|
|
|||
37
flake.lock
37
flake.lock
|
|
@ -3,11 +3,11 @@
|
|||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1747046372,
|
||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -36,30 +36,17 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 315532800,
|
||||
"narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=",
|
||||
"rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469",
|
||||
"lastModified": 1763191728,
|
||||
"narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=",
|
||||
"rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre877938.82c2e0d6dde5/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre896415.1d4c88323ac3/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1758360447,
|
||||
"narHash": "sha256-XDY3A83bclygHDtesRoaRTafUd80Q30D/Daf9KSG6bs=",
|
||||
"rev": "8eaee110344796db060382e15d3af0a9fc396e0e",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre864002.8eaee1103447/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
|
|
@ -97,11 +84,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760401936,
|
||||
"narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=",
|
||||
"lastModified": 1763295135,
|
||||
"narHash": "sha256-sGv/NHCmEnJivguGwB5w8LRmVqr1P72OjS+NzcJsssE=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "365085b6652259753b598d43b723858184980bbe",
|
||||
"rev": "64f8b42cfc615b2cf99144adf2b7728c7847c72a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -112,7 +99,9 @@
|
|||
},
|
||||
"zon2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1758405547,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
# glibc versions used by our dependencies from Nix are compatible with the
|
||||
# system glibc that the user is building for.
|
||||
#
|
||||
# We are currently on unstable to get Zig 0.15 for our package.nix
|
||||
# We are currently on nixpkgs-unstable to get Zig 0.15 for our package.nix and
|
||||
# Gnome 49/Gtk 4.20.
|
||||
#
|
||||
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
|
|
@ -28,10 +30,7 @@
|
|||
zon2nix = {
|
||||
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
|
||||
inputs = {
|
||||
# Don't override nixpkgs until Zig 0.15 is available in the Nix branch
|
||||
# we are using for "normal" builds.
|
||||
#
|
||||
# nixpkgs.follows = "nixpkgs";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a"
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
"sha256": "d9bd4306f0081d8e4b848b6adfeabd2fab49822ee2b679eb4801dcedf5d60c48"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
|
|||
|
|
@ -714,6 +714,7 @@ typedef struct {
|
|||
typedef enum {
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
|
||||
} ghostty_action_close_tab_mode_e;
|
||||
|
||||
// apprt.surface.Message.ChildExited
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
"Helpers/Extensions/NSAppearance+Extension.swift",
|
||||
"Helpers/Extensions/NSApplication+Extension.swift",
|
||||
"Helpers/Extensions/NSImage+Extension.swift",
|
||||
"Helpers/Extensions/NSMenu+Extension.swift",
|
||||
"Helpers/Extensions/NSMenuItem+Extension.swift",
|
||||
"Helpers/Extensions/NSPasteboard+Extension.swift",
|
||||
"Helpers/Extensions/NSScreen+Extension.swift",
|
||||
|
|
|
|||
|
|
@ -566,6 +566,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
private func syncAppearance() {
|
||||
guard let window else { return }
|
||||
|
||||
defer { updateColorSchemeForSurfaceTree() }
|
||||
// Change the collection behavior of the window depending on the configuration.
|
||||
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ class BaseTerminalController: NSWindowController,
|
|||
/// The previous frame information from the window
|
||||
private var savedFrame: SavedFrame? = nil
|
||||
|
||||
/// Cache previously applied appearance to avoid unnecessary updates
|
||||
private var appliedColorScheme: ghostty_color_scheme_e?
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
|
|
@ -1163,4 +1166,35 @@ extension BaseTerminalController: NSMenuItemValidation {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Surface Color Scheme
|
||||
|
||||
/// Update the surface tree's color scheme only when it actually changes.
|
||||
///
|
||||
/// Calling ``ghostty_surface_set_color_scheme`` triggers
|
||||
/// ``syncAppearance(_:)`` via notification,
|
||||
/// so we avoid redundant calls.
|
||||
func updateColorSchemeForSurfaceTree() {
|
||||
/// Derive the target scheme from `window-theme` or system appearance.
|
||||
/// We set the scheme on surfaces so they pick the correct theme
|
||||
/// and let ``syncAppearance(_:)`` update the window accordingly.
|
||||
///
|
||||
/// Using App's effectiveAppearance here to prevent incorrect updates.
|
||||
let themeAppearance = NSApplication.shared.effectiveAppearance
|
||||
let scheme: ghostty_color_scheme_e
|
||||
if themeAppearance.isDark {
|
||||
scheme = GHOSTTY_COLOR_SCHEME_DARK
|
||||
} else {
|
||||
scheme = GHOSTTY_COLOR_SCHEME_LIGHT
|
||||
}
|
||||
guard scheme != appliedColorScheme else {
|
||||
return
|
||||
}
|
||||
for surfaceView in surfaceTree {
|
||||
if let surface = surfaceView.surface {
|
||||
ghostty_surface_set_color_scheme(surface, scheme)
|
||||
}
|
||||
}
|
||||
appliedColorScheme = scheme
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
selector: #selector(onCloseOtherTabs),
|
||||
name: .ghosttyCloseOtherTabs,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onCloseTabsOnTheRight),
|
||||
name: .ghosttyCloseTabsOnTheRight,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onResetWindowSize),
|
||||
|
|
@ -425,15 +430,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// This is a surface-level config update. If we have the surface, we
|
||||
// update our appearance based on it.
|
||||
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(surfaceView) else { return }
|
||||
|
||||
// We can't use surfaceView.derivedConfig because it may not be updated
|
||||
// yet since it also responds to notifications.
|
||||
syncAppearance(.init(config))
|
||||
/// Surface-level config will be updated in
|
||||
/// ``Ghostty/Ghostty/SurfaceView/derivedConfig`` then
|
||||
/// ``TerminalController/focusedSurfaceDidChange(to:)``
|
||||
}
|
||||
|
||||
/// Update the accessory view of each tab according to the keyboard
|
||||
|
|
@ -633,6 +632,46 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
|
||||
private func closeTabsOnTheRightImmediately() {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return }
|
||||
|
||||
let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex }
|
||||
guard !tabsToClose.isEmpty else { return }
|
||||
|
||||
undoManager?.beginUndoGrouping()
|
||||
defer {
|
||||
undoManager?.endUndoGrouping()
|
||||
}
|
||||
|
||||
for (_, candidate) in tabsToClose {
|
||||
if let controller = candidate.windowController as? TerminalController {
|
||||
controller.closeTabImmediately(registerRedo: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let undoManager {
|
||||
undoManager.setActionName("Close Tabs to the Right")
|
||||
|
||||
undoManager.registerUndo(
|
||||
withTarget: self,
|
||||
expiresAfter: undoExpiration
|
||||
) { target in
|
||||
DispatchQueue.main.async {
|
||||
target.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
undoManager.registerUndo(
|
||||
withTarget: target,
|
||||
expiresAfter: target.undoExpiration
|
||||
) { target in
|
||||
target.closeTabsOnTheRightImmediately()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the current window (including any other tabs) immediately and without
|
||||
/// confirmation. This will setup proper undo state so the action can be undone.
|
||||
private func closeWindowImmediately() {
|
||||
|
|
@ -1084,24 +1123,24 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
// If we only have one window then we have no other tabs to close
|
||||
guard tabGroup.windows.count > 1 else { return }
|
||||
|
||||
|
||||
// Check if we have to confirm close.
|
||||
guard tabGroup.windows.contains(where: { window in
|
||||
// Ignore ourself
|
||||
if window == self.window { return false }
|
||||
|
||||
|
||||
// Ignore non-terminals
|
||||
guard let controller = window.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Check if any surfaces require confirmation
|
||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||
}) else {
|
||||
self.closeOtherTabsImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
confirmClose(
|
||||
messageText: "Close Other Tabs?",
|
||||
informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed."
|
||||
|
|
@ -1110,6 +1149,35 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
|
||||
@IBAction func closeTabsOnTheRight(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return }
|
||||
|
||||
let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex }
|
||||
guard !tabsToClose.isEmpty else { return }
|
||||
|
||||
let needsConfirm = tabsToClose.contains { (_, candidate) in
|
||||
guard let controller = candidate.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
|
||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||
}
|
||||
|
||||
if !needsConfirm {
|
||||
self.closeTabsOnTheRightImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
confirmClose(
|
||||
messageText: "Close Tabs on the Right?",
|
||||
informativeText: "At least one tab to the right still has a running process. If you close the tab the process will be killed."
|
||||
) {
|
||||
self.closeTabsOnTheRightImmediately()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func returnToDefaultSize(_ sender: Any?) {
|
||||
guard let window, let defaultSize else { return }
|
||||
defaultSize.apply(to: window)
|
||||
|
|
@ -1311,6 +1379,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
closeOtherTabs(self)
|
||||
}
|
||||
|
||||
@objc private func onCloseTabsOnTheRight(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
closeTabsOnTheRight(self)
|
||||
}
|
||||
|
||||
@objc private func onCloseWindow(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
|
|
@ -1373,6 +1447,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
extension TerminalController {
|
||||
override func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
switch item.action {
|
||||
case #selector(closeTabsOnTheRight):
|
||||
guard let window, let tabGroup = window.tabGroup else { return false }
|
||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false }
|
||||
return tabGroup.windows.enumerated().contains { $0.offset > currentIndex }
|
||||
|
||||
case #selector(returnToDefaultSize):
|
||||
guard let window else { return false }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ class TerminalWindow: NSWindow {
|
|||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private(set) var derivedConfig: DerivedConfig = .init()
|
||||
|
||||
private var tabMenuObserver: NSObjectProtocol? = nil
|
||||
|
||||
/// Whether this window supports the update accessory. If this is false, then views within this
|
||||
/// window should determine how to show update notifications.
|
||||
|
|
@ -53,6 +55,17 @@ class TerminalWindow: NSWindow {
|
|||
override func awakeFromNib() {
|
||||
// Notify that this terminal window has loaded
|
||||
NotificationCenter.default.post(name: Self.terminalDidAwake, object: self)
|
||||
|
||||
// This is fragile, but there doesn't seem to be an official API for customizing
|
||||
// native tab bar menus.
|
||||
tabMenuObserver = NotificationCenter.default.addObserver(
|
||||
forName: Notification.Name(rawValue: "NSMenuWillOpenNotification"),
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] n in
|
||||
guard let self, let menu = n.object as? NSMenu else { return }
|
||||
self.configureTabContextMenuIfNeeded(menu)
|
||||
}
|
||||
|
||||
// This is required so that window restoration properly creates our tabs
|
||||
// again. I'm not sure why this is required. If you don't do this, then
|
||||
|
|
@ -202,6 +215,8 @@ class TerminalWindow: NSWindow {
|
|||
/// added.
|
||||
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
|
||||
|
||||
private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem")
|
||||
|
||||
func findTitlebarView() -> NSView? {
|
||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
||||
//
|
||||
|
|
@ -277,6 +292,52 @@ class TerminalWindow: NSWindow {
|
|||
}
|
||||
}
|
||||
|
||||
private func configureTabContextMenuIfNeeded(_ menu: NSMenu) {
|
||||
guard isTabContextMenu(menu) else { return }
|
||||
|
||||
// Get the target from an existing menu item. The native tab context menu items
|
||||
// target the specific window/controller that was right-clicked, not the focused one.
|
||||
// We need to use that same target so validation and action use the correct tab.
|
||||
let targetController = menu.items
|
||||
.first { $0.action == NSSelectorFromString("performClose:") }
|
||||
.flatMap { $0.target as? NSWindow }
|
||||
.flatMap { $0.windowController as? TerminalController }
|
||||
|
||||
// Close tabs to the right
|
||||
let item = NSMenuItem(title: "Close Tabs to the Right", action: #selector(TerminalController.closeTabsOnTheRight(_:)), keyEquivalent: "")
|
||||
item.identifier = Self.closeTabsOnRightMenuItemIdentifier
|
||||
item.target = targetController
|
||||
item.setImageIfDesired(systemSymbolName: "xmark")
|
||||
if !menu.insertItem(item, after: NSSelectorFromString("performCloseOtherTabs:")) &&
|
||||
!menu.insertItem(item, after: NSSelectorFromString("performClose:")) {
|
||||
menu.addItem(item)
|
||||
}
|
||||
|
||||
// Other close items should have the xmark to match Safari on macOS 26
|
||||
for menuItem in menu.items {
|
||||
if menuItem.action == NSSelectorFromString("performClose:") ||
|
||||
menuItem.action == NSSelectorFromString("performCloseOtherTabs:") {
|
||||
menuItem.setImageIfDesired(systemSymbolName: "xmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
|
||||
guard NSApp.keyWindow === self else { return false }
|
||||
|
||||
// These are the target selectors, at least for macOS 26.
|
||||
let tabContextSelectors: Set<String> = [
|
||||
"performClose:",
|
||||
"performCloseOtherTabs:",
|
||||
"moveTabToNewWindow:",
|
||||
"toggleTabOverview:"
|
||||
]
|
||||
|
||||
let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) })
|
||||
return !selectorNames.isDisjoint(with: tabContextSelectors)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Tab Key Equivalents
|
||||
|
||||
var keyEquivalent: String? = nil {
|
||||
|
|
@ -419,6 +480,7 @@ class TerminalWindow: NSWindow {
|
|||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
guard isVisible else { return }
|
||||
defer { updateColorSchemeForSurfaceTree() }
|
||||
|
||||
// Basic properties
|
||||
appearance = surfaceConfig.windowAppearance
|
||||
|
|
@ -481,6 +543,10 @@ class TerminalWindow: NSWindow {
|
|||
return derivedConfig.backgroundColor.withAlphaComponent(alpha)
|
||||
}
|
||||
|
||||
func updateColorSchemeForSurfaceTree() {
|
||||
terminalController?.updateColorSchemeForSurfaceTree()
|
||||
}
|
||||
|
||||
private func setInitialWindowPosition(x: Int16?, y: Int16?) {
|
||||
// If we don't have an X/Y then we try to use the previously saved window pos.
|
||||
guard let x, let y else {
|
||||
|
|
@ -512,6 +578,12 @@ class TerminalWindow: NSWindow {
|
|||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
standardWindowButton(.zoomButton)?.isHidden = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let observer = tabMenuObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Config
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,38 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
|||
|
||||
viewModel.isMainWindow = false
|
||||
}
|
||||
|
||||
/// On our Tahoe titlebar tabs, we need to fix up right click events because they don't work
|
||||
/// naturally due to whatever mess we made.
|
||||
override func sendEvent(_ event: NSEvent) {
|
||||
guard viewModel.hasTabBar else {
|
||||
super.sendEvent(event)
|
||||
return
|
||||
}
|
||||
|
||||
let isRightClick =
|
||||
event.type == .rightMouseDown ||
|
||||
(event.type == .otherMouseDown && event.buttonNumber == 2) ||
|
||||
(event.type == .leftMouseDown && event.modifierFlags.contains(.control))
|
||||
guard isRightClick else {
|
||||
super.sendEvent(event)
|
||||
return
|
||||
}
|
||||
|
||||
guard let tabBarView = findTabBar() else {
|
||||
super.sendEvent(event)
|
||||
return
|
||||
}
|
||||
|
||||
let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil)
|
||||
guard tabBarView.bounds.contains(locationInTabBar) else {
|
||||
super.sendEvent(event)
|
||||
return
|
||||
}
|
||||
|
||||
tabBarView.rightMouseDown(with: event)
|
||||
}
|
||||
|
||||
// This is called by macOS for native tabbing in order to add the tab bar. We hook into
|
||||
// this, detect the tab bar being added, and override its behavior.
|
||||
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
||||
|
|
|
|||
|
|
@ -861,6 +861,13 @@ extension Ghostty {
|
|||
)
|
||||
return
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT:
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyCloseTabsOnTheRight,
|
||||
object: surfaceView
|
||||
)
|
||||
return
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,6 +380,9 @@ extension Notification.Name {
|
|||
/// Close other tabs
|
||||
static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs")
|
||||
|
||||
/// Close tabs to the right of the focused tab
|
||||
static let ghosttyCloseTabsOnTheRight = Notification.Name("com.mitchellh.ghostty.closeTabsOnTheRight")
|
||||
|
||||
/// Close window
|
||||
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")
|
||||
|
||||
|
|
|
|||
|
|
@ -34,10 +34,15 @@ class SurfaceScrollView: NSView {
|
|||
scrollView.hasHorizontalScroller = false
|
||||
scrollView.autohidesScrollers = false
|
||||
scrollView.usesPredominantAxisScrolling = true
|
||||
// Always use the overlay style. See mouseMoved for how we make
|
||||
// it usable without a scroll wheel or gestures.
|
||||
scrollView.scrollerStyle = .overlay
|
||||
// hide default background to show blur effect properly
|
||||
scrollView.drawsBackground = false
|
||||
// don't let the content view clip it's subviews, to enable the
|
||||
// don't let the content view clip its subviews, to enable the
|
||||
// surface to draw the background behind non-overlay scrollers
|
||||
// (we currently only use overlay scrollers, but might as well
|
||||
// configure the views correctly in case we change our mind)
|
||||
scrollView.contentView.clipsToBounds = false
|
||||
|
||||
// The document view is what the scrollview is actually going
|
||||
|
|
@ -107,7 +112,10 @@ class SurfaceScrollView: NSView {
|
|||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
// Since this observer is used to immediately override the event
|
||||
// that produced the notification, we let it run synchronously on
|
||||
// the posting thread.
|
||||
queue: nil
|
||||
) { [weak self] _ in
|
||||
self?.handleScrollerStyleChange()
|
||||
})
|
||||
|
|
@ -176,10 +184,10 @@ class SurfaceScrollView: NSView {
|
|||
private func synchronizeAppearance() {
|
||||
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
|
||||
scrollView.hasVerticalScroller = scrollbarConfig != .never
|
||||
scrollView.verticalScroller?.controlSize = .small
|
||||
let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
|
||||
// Make sure the scroller’s appearance matches the surface's background color.
|
||||
scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua)
|
||||
updateTrackingAreas()
|
||||
}
|
||||
|
||||
/// Positions the surface view to fill the currently visible rectangle.
|
||||
|
|
@ -240,6 +248,7 @@ class SurfaceScrollView: NSView {
|
|||
|
||||
/// Handles scrollbar style changes
|
||||
private func handleScrollerStyleChange() {
|
||||
scrollView.scrollerStyle = .overlay
|
||||
synchronizeCoreSurface()
|
||||
}
|
||||
|
||||
|
|
@ -350,4 +359,32 @@ class SurfaceScrollView: NSView {
|
|||
}
|
||||
return contentHeight
|
||||
}
|
||||
|
||||
// MARK: Mouse events
|
||||
|
||||
override func mouseMoved(with: NSEvent) {
|
||||
// When the OS preferred style is .legacy, the user should be able to
|
||||
// click and drag the scroller without using scroll wheels or gestures,
|
||||
// so we flash it when the mouse is moved over the scrollbar area.
|
||||
guard NSScroller.preferredScrollerStyle == .legacy else { return }
|
||||
scrollView.flashScrollers()
|
||||
}
|
||||
|
||||
override func updateTrackingAreas() {
|
||||
// To update our tracking area we just recreate it all.
|
||||
trackingAreas.forEach { removeTrackingArea($0) }
|
||||
|
||||
super.updateTrackingAreas()
|
||||
|
||||
// Our tracking area is the scroller frame
|
||||
guard let scroller = scrollView.verticalScroller else { return }
|
||||
addTrackingArea(NSTrackingArea(
|
||||
rect: convert(scroller.bounds, from: scroller),
|
||||
options: [
|
||||
.mouseMoved,
|
||||
.activeInKeyWindow,
|
||||
],
|
||||
owner: self,
|
||||
userInfo: nil))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,26 +369,6 @@ extension Ghostty {
|
|||
// Setup our tracking area so we get mouse moved events
|
||||
updateTrackingAreas()
|
||||
|
||||
// Observe our appearance so we can report the correct value to libghostty.
|
||||
// This is the best way I know of to get appearance change notifications.
|
||||
self.appearanceObserver = observe(\.effectiveAppearance, options: [.new, .initial]) { view, change in
|
||||
guard let appearance = change.newValue else { return }
|
||||
guard let surface = view.surface else { return }
|
||||
let scheme: ghostty_color_scheme_e
|
||||
switch (appearance.name) {
|
||||
case .aqua, .vibrantLight:
|
||||
scheme = GHOSTTY_COLOR_SCHEME_LIGHT
|
||||
|
||||
case .darkAqua, .vibrantDark:
|
||||
scheme = GHOSTTY_COLOR_SCHEME_DARK
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
ghostty_surface_set_color_scheme(surface, scheme)
|
||||
}
|
||||
|
||||
// The UTTypes that can be dragged onto this view.
|
||||
registerForDraggedTypes(Array(Self.dropTypes))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import AppKit
|
||||
|
||||
extension NSMenu {
|
||||
/// Inserts a menu item after an existing item with the specified action selector.
|
||||
///
|
||||
/// If an item with the same identifier already exists, it is removed first to avoid duplicates.
|
||||
/// This is useful when menus are cached and reused across different targets.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: The menu item to insert.
|
||||
/// - action: The action selector to search for. The new item will be inserted after the first
|
||||
/// item with this action.
|
||||
/// - Returns: `true` if the item was inserted after the specified action, `false` if the action
|
||||
/// was not found and the item was not inserted.
|
||||
@discardableResult
|
||||
func insertItem(_ item: NSMenuItem, after action: Selector) -> Bool {
|
||||
if let identifier = item.identifier,
|
||||
let existing = items.first(where: { $0.identifier == identifier }) {
|
||||
removeItem(existing)
|
||||
}
|
||||
|
||||
guard let idx = items.firstIndex(where: { $0.action == action }) else {
|
||||
return false
|
||||
}
|
||||
|
||||
insertItem(item, at: idx + 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ for any localization that they may add.
|
|||
|
||||
## GTK
|
||||
|
||||
In the GTK app runtime, translable strings are mainly sourced from Blueprint
|
||||
In the GTK app runtime, translatable strings are mainly sourced from Blueprint
|
||||
files (located under `src/apprt/gtk/ui`). Blueprints have a native syntax for
|
||||
translatable strings, which look like this:
|
||||
|
||||
|
|
|
|||
|
|
@ -5299,6 +5299,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
switch (v) {
|
||||
.this => .this,
|
||||
.other => .other,
|
||||
.right => .right,
|
||||
},
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -767,6 +767,8 @@ pub const CloseTabMode = enum(c_int) {
|
|||
this,
|
||||
/// Close all other tabs.
|
||||
other,
|
||||
/// Close all tabs to the right of the current tab.
|
||||
right,
|
||||
};
|
||||
|
||||
pub const CommandFinished = struct {
|
||||
|
|
|
|||
|
|
@ -1583,7 +1583,7 @@ pub const Application = extern struct {
|
|||
.dark;
|
||||
log.debug("style manager changed scheme={}", .{scheme});
|
||||
|
||||
const priv = self.private();
|
||||
const priv: *Private = self.private();
|
||||
const core_app = priv.core_app;
|
||||
core_app.colorSchemeEvent(self.rt(), scheme) catch |err| {
|
||||
log.warn("error updating app color scheme err={}", .{err});
|
||||
|
|
@ -1596,6 +1596,26 @@ pub const Application = extern struct {
|
|||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (gtk_version.atLeast(4, 20, 0)) {
|
||||
const gtk_scheme: gtk.InterfaceColorScheme = switch (scheme) {
|
||||
.light => gtk.InterfaceColorScheme.light,
|
||||
.dark => gtk.InterfaceColorScheme.dark,
|
||||
};
|
||||
var value = gobject.ext.Value.newFrom(gtk_scheme);
|
||||
gobject.Object.setProperty(
|
||||
priv.css_provider.as(gobject.Object),
|
||||
"prefers-color-scheme",
|
||||
&value,
|
||||
);
|
||||
for (priv.custom_css_providers.items) |css_provider| {
|
||||
gobject.Object.setProperty(
|
||||
css_provider.as(gobject.Object),
|
||||
"prefers-color-scheme",
|
||||
&value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handleReloadConfig(
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ pub const Tab = extern struct {
|
|||
switch (mode) {
|
||||
.this => tab_view.closePage(page),
|
||||
.other => tab_view.closeOtherPages(page),
|
||||
.right => tab_view.closePagesAfter(page),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -218,6 +218,22 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
|
|||
try std.SemanticVersion.parse(v)
|
||||
else version: {
|
||||
const app_version = try std.SemanticVersion.parse(appVersion);
|
||||
|
||||
// Detect if ghostty is being built as a dependency by checking if the
|
||||
// build root has our marker. When used as a dependency, we skip git
|
||||
// detection entirely to avoid reading the downstream project's git state.
|
||||
const is_dependency = !@hasDecl(
|
||||
@import("root"),
|
||||
"_ghostty_build_root",
|
||||
);
|
||||
if (is_dependency) {
|
||||
break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
};
|
||||
}
|
||||
|
||||
// If no explicit version is given, we try to detect it from git.
|
||||
const vsn = GitVersion.detect(b) catch |err| switch (err) {
|
||||
// If Git isn't available we just make an unknown dev version.
|
||||
|
|
|
|||
|
|
@ -719,15 +719,19 @@ pub fn addSimd(
|
|||
}
|
||||
|
||||
// Highway
|
||||
if (b.lazyDependency("highway", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |highway_dep| {
|
||||
m.linkLibrary(highway_dep.artifact("highway"));
|
||||
if (static_libs) |v| try v.append(
|
||||
b.allocator,
|
||||
highway_dep.artifact("highway").getEmittedBin(),
|
||||
);
|
||||
if (b.systemIntegrationOption("highway", .{ .default = false })) {
|
||||
m.linkSystemLibrary("libhwy", dynamic_link_opts);
|
||||
} else {
|
||||
if (b.lazyDependency("highway", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |highway_dep| {
|
||||
m.linkLibrary(highway_dep.artifact("highway"));
|
||||
if (static_libs) |v| try v.append(
|
||||
b.allocator,
|
||||
highway_dep.artifact("highway").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// utfcpp - This is used as a dependency on our hand-written C++ code
|
||||
|
|
@ -746,6 +750,7 @@ pub fn addSimd(
|
|||
m.addIncludePath(b.path("src"));
|
||||
{
|
||||
// From hwy/detect_targets.h
|
||||
const HWY_AVX10_2: c_int = 1 << 3;
|
||||
const HWY_AVX3_SPR: c_int = 1 << 4;
|
||||
const HWY_AVX3_ZEN4: c_int = 1 << 6;
|
||||
const HWY_AVX3_DL: c_int = 1 << 7;
|
||||
|
|
@ -756,7 +761,7 @@ pub fn addSimd(
|
|||
// The performance difference between AVX2 and AVX512 is not
|
||||
// significant for our use case and AVX512 is very rare on consumer
|
||||
// hardware anyways.
|
||||
const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
|
||||
const HWY_DISABLED_TARGETS: c_int = HWY_AVX10_2 | HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
|
||||
|
||||
m.addCSourceFiles(.{
|
||||
.files = &.{
|
||||
|
|
|
|||
|
|
@ -600,9 +600,8 @@ pub const Action = union(enum) {
|
|||
/// of the `confirm-close-surface` configuration setting.
|
||||
close_surface,
|
||||
|
||||
/// Close the current tab and all splits therein _or_ close all tabs and
|
||||
/// splits thein of tabs _other_ than the current tab, depending on the
|
||||
/// mode.
|
||||
/// Close the current tab and all splits therein, close all other tabs, or
|
||||
/// close every tab to the right of the current one depending on the mode.
|
||||
///
|
||||
/// If the mode is not specified, defaults to closing the current tab.
|
||||
///
|
||||
|
|
@ -1005,6 +1004,7 @@ pub const Action = union(enum) {
|
|||
pub const CloseTabMode = enum {
|
||||
this,
|
||||
other,
|
||||
right,
|
||||
|
||||
pub const default: CloseTabMode = .this;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -538,6 +538,11 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
.title = "Close Other Tabs",
|
||||
.description = "Close all tabs in this window except the current one.",
|
||||
},
|
||||
.{
|
||||
.action = .{ .close_tab = .right },
|
||||
.title = "Close Tabs to the Right",
|
||||
.description = "Close all tabs to the right of the current one.",
|
||||
},
|
||||
},
|
||||
|
||||
.close_window => comptime &.{.{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/// Generates bytes.
|
||||
//! Generates bytes.
|
||||
const Bytes = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
|
@ -7,9 +7,7 @@ const Generator = @import("Generator.zig");
|
|||
/// Random number generator.
|
||||
rand: std.Random,
|
||||
|
||||
/// The minimum and maximum length of the generated bytes. The maximum
|
||||
/// length will be capped to the length of the buffer passed in if the
|
||||
/// buffer length is smaller.
|
||||
/// The minimum and maximum length of the generated bytes.
|
||||
min_len: usize = 1,
|
||||
max_len: usize = std.math.maxInt(usize),
|
||||
|
||||
|
|
@ -18,23 +16,79 @@ max_len: usize = std.math.maxInt(usize),
|
|||
/// side effect of the generator, not an intended use case.
|
||||
alphabet: ?[]const u8 = null,
|
||||
|
||||
/// Predefined alphabets.
|
||||
pub const Alphabet = struct {
|
||||
pub const ascii = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~";
|
||||
};
|
||||
/// Generate an alphabet given a function that returns true/false for a
|
||||
/// given byte.
|
||||
pub fn generateAlphabet(comptime func: fn (u8) bool) []const u8 {
|
||||
@setEvalBranchQuota(3000);
|
||||
var count = 0;
|
||||
for (0..256) |c| {
|
||||
if (func(c)) count += 1;
|
||||
}
|
||||
var alphabet: [count]u8 = undefined;
|
||||
var i = 0;
|
||||
for (0..256) |c| {
|
||||
if (func(c)) {
|
||||
alphabet[i] = c;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
const result = alphabet;
|
||||
return &result;
|
||||
}
|
||||
|
||||
pub fn generator(self: *Bytes) Generator {
|
||||
return .init(self, next);
|
||||
}
|
||||
|
||||
pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Error!void {
|
||||
std.debug.assert(max_len >= 1);
|
||||
const len = @min(
|
||||
self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len),
|
||||
max_len,
|
||||
);
|
||||
/// Return a copy of the Bytes, but with a new alphabet.
|
||||
pub fn newAlphabet(self: *const Bytes, new_alphabet: ?[]const u8) Bytes {
|
||||
return .{
|
||||
.rand = self.rand,
|
||||
.alphabet = new_alphabet,
|
||||
.min_len = self.min_len,
|
||||
.max_len = self.max_len,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return a copy of the Bytes, but with a new min_len. The new min
|
||||
/// len cannot be more than the previous max_len.
|
||||
pub fn atLeast(self: *const Bytes, new_min_len: usize) Bytes {
|
||||
return .{
|
||||
.rand = self.rand,
|
||||
.alphabet = self.alphabet,
|
||||
.min_len = @min(self.max_len, new_min_len),
|
||||
.max_len = self.max_len,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return a copy of the Bytes, but with a new max_len. The new max_len cannot
|
||||
/// be more the previous max_len.
|
||||
pub fn atMost(self: *const Bytes, new_max_len: usize) Bytes {
|
||||
return .{
|
||||
.rand = self.rand,
|
||||
.alphabet = self.alphabet,
|
||||
.min_len = @min(self.min_len, @min(self.max_len, new_max_len)),
|
||||
.max_len = @min(self.max_len, new_max_len),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next(self: *const Bytes, writer: *std.Io.Writer, max_len: usize) std.Io.Writer.Error!void {
|
||||
_ = try self.atMost(max_len).write(writer);
|
||||
}
|
||||
|
||||
pub fn format(self: *const Bytes, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
_ = try self.write(writer);
|
||||
}
|
||||
|
||||
/// Write some random data and return the number of bytes written.
|
||||
pub fn write(self: *const Bytes, writer: *std.Io.Writer) std.Io.Writer.Error!usize {
|
||||
std.debug.assert(self.min_len >= 1);
|
||||
std.debug.assert(self.max_len >= self.min_len);
|
||||
|
||||
const len = self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len);
|
||||
|
||||
var buf: [8]u8 = undefined;
|
||||
|
||||
var remaining = len;
|
||||
while (remaining > 0) {
|
||||
const data = buf[0..@min(remaining, buf.len)];
|
||||
|
|
@ -45,6 +99,8 @@ pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Erro
|
|||
try writer.writeAll(data);
|
||||
remaining -= data.len;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
test "bytes" {
|
||||
|
|
@ -52,9 +108,11 @@ test "bytes" {
|
|||
var prng = std.Random.DefaultPrng.init(0);
|
||||
var buf: [256]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
var v: Bytes = .{ .rand = prng.random() };
|
||||
v.min_len = buf.len;
|
||||
v.max_len = buf.len;
|
||||
var v: Bytes = .{
|
||||
.rand = prng.random(),
|
||||
.min_len = buf.len,
|
||||
.max_len = buf.len,
|
||||
};
|
||||
const gen = v.generator();
|
||||
try gen.next(&writer, buf.len);
|
||||
try testing.expectEqual(buf.len, writer.buffered().len);
|
||||
|
|
|
|||
|
|
@ -35,19 +35,26 @@ p_valid: f64 = 1.0,
|
|||
p_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0),
|
||||
p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0),
|
||||
|
||||
/// The alphabet for random bytes (omitting 0x1B and 0x07).
|
||||
const bytes_alphabet: []const u8 = alphabet: {
|
||||
var alphabet: [256]u8 = undefined;
|
||||
for (0..alphabet.len) |i| {
|
||||
if (i == 0x1B or i == 0x07) {
|
||||
alphabet[i] = @intCast(i + 1);
|
||||
} else {
|
||||
alphabet[i] = @intCast(i);
|
||||
}
|
||||
}
|
||||
const result = alphabet;
|
||||
break :alphabet &result;
|
||||
};
|
||||
fn checkKvAlphabet(c: u8) bool {
|
||||
return switch (c) {
|
||||
std.ascii.control_code.esc, std.ascii.control_code.bel, ';', '=' => false,
|
||||
else => std.ascii.isPrint(c),
|
||||
};
|
||||
}
|
||||
|
||||
/// The alphabet for random bytes in OSC key/value pairs (omitting 0x1B,
|
||||
/// 0x07, ';', '=').
|
||||
pub const kv_alphabet = Bytes.generateAlphabet(checkKvAlphabet);
|
||||
|
||||
fn checkOscAlphabet(c: u8) bool {
|
||||
return switch (c) {
|
||||
std.ascii.control_code.esc, std.ascii.control_code.bel => false,
|
||||
else => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// The alphabet for random bytes in OSCs (omitting 0x1B and 0x07).
|
||||
pub const osc_alphabet = Bytes.generateAlphabet(checkOscAlphabet);
|
||||
|
||||
pub fn generator(self: *Osc) Generator {
|
||||
return .init(self, next);
|
||||
|
|
@ -99,35 +106,43 @@ fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.E
|
|||
|
||||
fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void {
|
||||
switch (k) {
|
||||
.change_window_title => {
|
||||
try writer.writeAll("0;"); // Set window title
|
||||
var bytes_gen = self.bytes();
|
||||
try bytes_gen.next(writer, max_len - 2);
|
||||
.change_window_title => change_window_title: {
|
||||
if (max_len < 3) break :change_window_title;
|
||||
try writer.print("0;{f}", .{self.bytes().atMost(max_len - 3)}); // Set window title
|
||||
},
|
||||
|
||||
.prompt_start => {
|
||||
.prompt_start => prompt_start: {
|
||||
if (max_len < 4) break :prompt_start;
|
||||
var remaining = max_len;
|
||||
|
||||
try writer.writeAll("133;A"); // Start prompt
|
||||
remaining -= 4;
|
||||
|
||||
// aid
|
||||
if (self.rand.boolean()) {
|
||||
var bytes_gen = self.bytes();
|
||||
bytes_gen.max_len = 16;
|
||||
if (self.rand.boolean()) aid: {
|
||||
if (remaining < 6) break :aid;
|
||||
try writer.writeAll(";aid=");
|
||||
try bytes_gen.next(writer, max_len);
|
||||
remaining -= 5;
|
||||
remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(16, remaining)).write(writer);
|
||||
}
|
||||
|
||||
// redraw
|
||||
if (self.rand.boolean()) {
|
||||
if (self.rand.boolean()) redraw: {
|
||||
if (remaining < 9) break :redraw;
|
||||
try writer.writeAll(";redraw=");
|
||||
if (self.rand.boolean()) {
|
||||
try writer.writeAll("1");
|
||||
} else {
|
||||
try writer.writeAll("0");
|
||||
}
|
||||
remaining -= 9;
|
||||
}
|
||||
},
|
||||
|
||||
.prompt_end => try writer.writeAll("133;B"), // End prompt
|
||||
.prompt_end => prompt_end: {
|
||||
if (max_len < 4) break :prompt_end;
|
||||
try writer.writeAll("133;B"); // End prompt
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,14 +154,11 @@ fn nextUnwrappedInvalidExact(
|
|||
) Generator.Error!void {
|
||||
switch (k) {
|
||||
.random => {
|
||||
var bytes_gen = self.bytes();
|
||||
try bytes_gen.next(writer, max_len);
|
||||
try self.bytes().atMost(max_len).format(writer);
|
||||
},
|
||||
|
||||
.good_prefix => {
|
||||
try writer.writeAll("133;");
|
||||
var bytes_gen = self.bytes();
|
||||
try bytes_gen.next(writer, max_len - 4);
|
||||
try writer.print("133;{f}", .{self.bytes().atMost(max_len - 4)});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +166,7 @@ fn nextUnwrappedInvalidExact(
|
|||
fn bytes(self: *const Osc) Bytes {
|
||||
return .{
|
||||
.rand = self.rand,
|
||||
.alphabet = bytes_alphabet,
|
||||
.alphabet = osc_alphabet,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,21 @@ const Ascii = @This();
|
|||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const synthetic = @import("../main.zig");
|
||||
const Bytes = @import("../Bytes.zig");
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
fn checkAsciiAlphabet(c: u8) bool {
|
||||
return switch (c) {
|
||||
' ' => false,
|
||||
else => std.ascii.isPrint(c),
|
||||
};
|
||||
}
|
||||
|
||||
pub const ascii = Bytes.generateAlphabet(checkAsciiAlphabet);
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
|
|
@ -23,12 +32,10 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void {
|
|||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
|
||||
_ = self;
|
||||
|
||||
var gen: synthetic.Bytes = .{
|
||||
pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
|
||||
var gen: Bytes = .{
|
||||
.rand = rand,
|
||||
.alphabet = synthetic.Bytes.Alphabet.ascii,
|
||||
.alphabet = ascii,
|
||||
};
|
||||
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ pub const output = @import("tmux/output.zig");
|
|||
pub const ControlParser = control.Parser;
|
||||
pub const ControlNotification = control.Notification;
|
||||
pub const Layout = layout.Layout;
|
||||
pub const Viewer = @import("tmux/viewer.zig").Viewer;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
|
|
|||
|
|
@ -531,6 +531,30 @@ pub const Notification = union(enum) {
|
|||
session_id: usize,
|
||||
name: []const u8,
|
||||
},
|
||||
|
||||
pub fn format(self: Notification, writer: *std.Io.Writer) !void {
|
||||
const T = Notification;
|
||||
const info = @typeInfo(T).@"union";
|
||||
|
||||
try writer.writeAll(@typeName(T));
|
||||
if (info.tag_type) |TagType| {
|
||||
try writer.writeAll("{ .");
|
||||
try writer.writeAll(@tagName(@as(TagType, self)));
|
||||
try writer.writeAll(" = ");
|
||||
|
||||
inline for (info.fields) |u_field| {
|
||||
if (self == @field(TagType, u_field.name)) {
|
||||
const value = @field(self, u_field.name);
|
||||
switch (u_field.type) {
|
||||
[]const u8 => try writer.print("\"{s}\"", .{std.mem.trim(u8, value, " \t\r\n")}),
|
||||
else => try writer.print("{any}", .{value}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "tmux begin/end empty" {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,36 @@ pub fn parseFormatStruct(
|
|||
return result;
|
||||
}
|
||||
|
||||
pub fn comptimeFormat(
|
||||
comptime vars: []const Variable,
|
||||
comptime delimiter: u8,
|
||||
) []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(50000);
|
||||
var counter: std.Io.Writer.Discarding = .init(&.{});
|
||||
try format(&counter.writer, vars, delimiter);
|
||||
|
||||
var buf: [counter.count]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try format(&writer, vars, delimiter);
|
||||
const final = buf;
|
||||
return final[0..writer.end];
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a set of variables into the proper format string for tmux
|
||||
/// that we can handle with `parseFormatStruct`.
|
||||
pub fn format(
|
||||
writer: *std.Io.Writer,
|
||||
vars: []const Variable,
|
||||
delimiter: u8,
|
||||
) std.Io.Writer.Error!void {
|
||||
for (vars, 0..) |variable, i| {
|
||||
if (i != 0) try writer.writeByte(delimiter);
|
||||
try writer.print("#{{{t}}}", .{variable});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a struct type that contains fields for each of the given
|
||||
/// format variables. This can be used with `parseFormatStruct` to
|
||||
/// parse an output string into a format struct.
|
||||
|
|
@ -65,16 +95,109 @@ pub fn FormatStruct(comptime vars: []const Variable) type {
|
|||
/// a subset of them here that are relevant to the use case of implementing
|
||||
/// control mode for terminal emulators.
|
||||
pub const Variable = enum {
|
||||
/// 1 if pane is in alternate screen.
|
||||
alternate_on,
|
||||
/// Saved cursor X in alternate screen.
|
||||
alternate_saved_x,
|
||||
/// Saved cursor Y in alternate screen.
|
||||
alternate_saved_y,
|
||||
/// 1 if bracketed paste mode is enabled.
|
||||
bracketed_paste,
|
||||
/// 1 if the cursor is blinking.
|
||||
cursor_blinking,
|
||||
/// Cursor colour in pane. Possible formats:
|
||||
/// - Named colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`,
|
||||
/// `cyan`, `white`, `default`, `terminal`, or bright variants.
|
||||
/// - 256 colors: `colour<N>` where N is 0-255 (e.g., `colour100`).
|
||||
/// - RGB hex: `#RRGGBB` (e.g., `#ff0000`).
|
||||
/// - Empty string if unset.
|
||||
cursor_colour,
|
||||
/// Pane cursor flag.
|
||||
cursor_flag,
|
||||
/// Cursor shape in pane. Possible values: `block`, `underline`, `bar`,
|
||||
/// or `default`.
|
||||
cursor_shape,
|
||||
/// Cursor X position in pane.
|
||||
cursor_x,
|
||||
/// Cursor Y position in pane.
|
||||
cursor_y,
|
||||
/// 1 if focus reporting is enabled.
|
||||
focus_flag,
|
||||
/// Pane insert flag.
|
||||
insert_flag,
|
||||
/// Pane keypad cursor flag.
|
||||
keypad_cursor_flag,
|
||||
/// Pane keypad flag.
|
||||
keypad_flag,
|
||||
/// Pane mouse all flag.
|
||||
mouse_all_flag,
|
||||
/// Pane mouse any flag.
|
||||
mouse_any_flag,
|
||||
/// Pane mouse button flag.
|
||||
mouse_button_flag,
|
||||
/// Pane mouse SGR flag.
|
||||
mouse_sgr_flag,
|
||||
/// Pane mouse standard flag.
|
||||
mouse_standard_flag,
|
||||
/// Pane mouse UTF-8 flag.
|
||||
mouse_utf8_flag,
|
||||
/// Pane origin flag.
|
||||
origin_flag,
|
||||
/// Unique pane ID prefixed with `%` (e.g., `%0`, `%42`).
|
||||
pane_id,
|
||||
/// Pane tab positions as a comma-separated list of 0-indexed column
|
||||
/// numbers (e.g., `8,16,24,32`). Empty string if no tabs are set.
|
||||
pane_tabs,
|
||||
/// Bottom of scroll region in pane.
|
||||
scroll_region_lower,
|
||||
/// Top of scroll region in pane.
|
||||
scroll_region_upper,
|
||||
/// Unique session ID prefixed with `$` (e.g., `$0`, `$42`).
|
||||
session_id,
|
||||
/// Server version (e.g., `3.5a`).
|
||||
version,
|
||||
/// Unique window ID prefixed with `@` (e.g., `@0`, `@42`).
|
||||
window_id,
|
||||
/// Width of window.
|
||||
window_width,
|
||||
/// Height of window.
|
||||
window_height,
|
||||
/// Window layout description, ignoring zoomed window panes. Format is
|
||||
/// `<checksum>,<layout>` where checksum is a 4-digit hex CRC16 and layout
|
||||
/// encodes pane dimensions as `WxH,X,Y[,ID]` with `{...}` for horizontal
|
||||
/// splits and `[...]` for vertical splits.
|
||||
window_layout,
|
||||
/// Pane wrap flag.
|
||||
wrap_flag,
|
||||
|
||||
/// Parse the given string value into the appropriate resulting
|
||||
/// type for this variable.
|
||||
pub fn parse(comptime self: Variable, value: []const u8) !Type(self) {
|
||||
return switch (self) {
|
||||
.alternate_on,
|
||||
.bracketed_paste,
|
||||
.cursor_blinking,
|
||||
.cursor_flag,
|
||||
.focus_flag,
|
||||
.insert_flag,
|
||||
.keypad_cursor_flag,
|
||||
.keypad_flag,
|
||||
.mouse_all_flag,
|
||||
.mouse_any_flag,
|
||||
.mouse_button_flag,
|
||||
.mouse_sgr_flag,
|
||||
.mouse_standard_flag,
|
||||
.mouse_utf8_flag,
|
||||
.origin_flag,
|
||||
.wrap_flag,
|
||||
=> std.mem.eql(u8, value, "1"),
|
||||
.alternate_saved_x,
|
||||
.alternate_saved_y,
|
||||
.cursor_x,
|
||||
.cursor_y,
|
||||
.scroll_region_lower,
|
||||
.scroll_region_upper,
|
||||
=> try std.fmt.parseInt(usize, value, 10),
|
||||
.session_id => if (value.len >= 2 and value[0] == '$')
|
||||
try std.fmt.parseInt(usize, value[1..], 10)
|
||||
else
|
||||
|
|
@ -83,24 +206,107 @@ pub const Variable = enum {
|
|||
try std.fmt.parseInt(usize, value[1..], 10)
|
||||
else
|
||||
return error.FormatError,
|
||||
.pane_id => if (value.len >= 2 and value[0] == '%')
|
||||
try std.fmt.parseInt(usize, value[1..], 10)
|
||||
else
|
||||
return error.FormatError,
|
||||
.window_width => try std.fmt.parseInt(usize, value, 10),
|
||||
.window_height => try std.fmt.parseInt(usize, value, 10),
|
||||
.window_layout => value,
|
||||
.cursor_colour,
|
||||
.cursor_shape,
|
||||
.pane_tabs,
|
||||
.version,
|
||||
.window_layout,
|
||||
=> value,
|
||||
};
|
||||
}
|
||||
|
||||
/// The type of the parsed value for this variable type.
|
||||
pub fn Type(comptime self: Variable) type {
|
||||
return switch (self) {
|
||||
.session_id => usize,
|
||||
.window_id => usize,
|
||||
.window_width => usize,
|
||||
.window_height => usize,
|
||||
.window_layout => []const u8,
|
||||
.alternate_on,
|
||||
.bracketed_paste,
|
||||
.cursor_blinking,
|
||||
.cursor_flag,
|
||||
.focus_flag,
|
||||
.insert_flag,
|
||||
.keypad_cursor_flag,
|
||||
.keypad_flag,
|
||||
.mouse_all_flag,
|
||||
.mouse_any_flag,
|
||||
.mouse_button_flag,
|
||||
.mouse_sgr_flag,
|
||||
.mouse_standard_flag,
|
||||
.mouse_utf8_flag,
|
||||
.origin_flag,
|
||||
.wrap_flag,
|
||||
=> bool,
|
||||
.alternate_saved_x,
|
||||
.alternate_saved_y,
|
||||
.cursor_x,
|
||||
.cursor_y,
|
||||
.scroll_region_lower,
|
||||
.scroll_region_upper,
|
||||
.session_id,
|
||||
.window_id,
|
||||
.pane_id,
|
||||
.window_width,
|
||||
.window_height,
|
||||
=> usize,
|
||||
.cursor_colour,
|
||||
.cursor_shape,
|
||||
.pane_tabs,
|
||||
.version,
|
||||
.window_layout,
|
||||
=> []const u8,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "parse alternate_on" {
|
||||
try testing.expectEqual(true, try Variable.parse(.alternate_on, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, "true"));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, "yes"));
|
||||
}
|
||||
|
||||
test "parse alternate_saved_x" {
|
||||
try testing.expectEqual(0, try Variable.parse(.alternate_saved_x, "0"));
|
||||
try testing.expectEqual(42, try Variable.parse(.alternate_saved_x, "42"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.alternate_saved_x, "abc"));
|
||||
}
|
||||
|
||||
test "parse alternate_saved_y" {
|
||||
try testing.expectEqual(0, try Variable.parse(.alternate_saved_y, "0"));
|
||||
try testing.expectEqual(42, try Variable.parse(.alternate_saved_y, "42"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.alternate_saved_y, "abc"));
|
||||
}
|
||||
|
||||
test "parse cursor_x" {
|
||||
try testing.expectEqual(0, try Variable.parse(.cursor_x, "0"));
|
||||
try testing.expectEqual(79, try Variable.parse(.cursor_x, "79"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.cursor_x, "abc"));
|
||||
}
|
||||
|
||||
test "parse cursor_y" {
|
||||
try testing.expectEqual(0, try Variable.parse(.cursor_y, "0"));
|
||||
try testing.expectEqual(23, try Variable.parse(.cursor_y, "23"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.cursor_y, "abc"));
|
||||
}
|
||||
|
||||
test "parse scroll_region_upper" {
|
||||
try testing.expectEqual(0, try Variable.parse(.scroll_region_upper, "0"));
|
||||
try testing.expectEqual(5, try Variable.parse(.scroll_region_upper, "5"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.scroll_region_upper, "abc"));
|
||||
}
|
||||
|
||||
test "parse scroll_region_lower" {
|
||||
try testing.expectEqual(0, try Variable.parse(.scroll_region_lower, "0"));
|
||||
try testing.expectEqual(23, try Variable.parse(.scroll_region_lower, "23"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.scroll_region_lower, "abc"));
|
||||
}
|
||||
|
||||
test "parse session id" {
|
||||
try testing.expectEqual(42, try Variable.parse(.session_id, "$42"));
|
||||
try testing.expectEqual(0, try Variable.parse(.session_id, "$0"));
|
||||
|
|
@ -146,6 +352,147 @@ test "parse window layout" {
|
|||
try testing.expectEqualStrings("a]b,c{d}e(f)", try Variable.parse(.window_layout, "a]b,c{d}e(f)"));
|
||||
}
|
||||
|
||||
test "parse cursor_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.cursor_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse insert_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.insert_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.insert_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.insert_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.insert_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse keypad_cursor_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.keypad_cursor_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse keypad_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.keypad_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_any_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_any_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_button_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_button_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_sgr_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_sgr_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_standard_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_standard_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_utf8_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_utf8_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse wrap_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.wrap_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.wrap_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.wrap_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.wrap_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse bracketed_paste" {
|
||||
try testing.expectEqual(true, try Variable.parse(.bracketed_paste, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.bracketed_paste, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "true"));
|
||||
}
|
||||
|
||||
test "parse cursor_blinking" {
|
||||
try testing.expectEqual(true, try Variable.parse(.cursor_blinking, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_blinking, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "true"));
|
||||
}
|
||||
|
||||
test "parse focus_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.focus_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.focus_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.focus_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.focus_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_all_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_all_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse origin_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.origin_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.origin_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.origin_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.origin_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse pane_id" {
|
||||
try testing.expectEqual(42, try Variable.parse(.pane_id, "%42"));
|
||||
try testing.expectEqual(0, try Variable.parse(.pane_id, "%0"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, "0"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, "@0"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, "%"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, ""));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.pane_id, "%abc"));
|
||||
}
|
||||
|
||||
test "parse cursor_colour" {
|
||||
try testing.expectEqualStrings("red", try Variable.parse(.cursor_colour, "red"));
|
||||
try testing.expectEqualStrings("#ff0000", try Variable.parse(.cursor_colour, "#ff0000"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.cursor_colour, ""));
|
||||
}
|
||||
|
||||
test "parse cursor_shape" {
|
||||
try testing.expectEqualStrings("block", try Variable.parse(.cursor_shape, "block"));
|
||||
try testing.expectEqualStrings("underline", try Variable.parse(.cursor_shape, "underline"));
|
||||
try testing.expectEqualStrings("bar", try Variable.parse(.cursor_shape, "bar"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.cursor_shape, ""));
|
||||
}
|
||||
|
||||
test "parse pane_tabs" {
|
||||
try testing.expectEqualStrings("0,8,16,24", try Variable.parse(.pane_tabs, "0,8,16,24"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.pane_tabs, ""));
|
||||
try testing.expectEqualStrings("0", try Variable.parse(.pane_tabs, "0"));
|
||||
}
|
||||
|
||||
test "parse version" {
|
||||
try testing.expectEqualStrings("3.5a", try Variable.parse(.version, "3.5a"));
|
||||
try testing.expectEqualStrings("3.5", try Variable.parse(.version, "3.5"));
|
||||
try testing.expectEqualStrings("next-3.5", try Variable.parse(.version, "next-3.5"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.version, ""));
|
||||
}
|
||||
|
||||
test "parseFormatStruct single field" {
|
||||
const T = FormatStruct(&.{.session_id});
|
||||
const result = try parseFormatStruct(T, "$42", ' ');
|
||||
|
|
@ -203,3 +550,41 @@ test "parseFormatStruct with empty layout field" {
|
|||
try testing.expectEqual(1, result.session_id);
|
||||
try testing.expectEqualStrings("", result.window_layout);
|
||||
}
|
||||
|
||||
fn testFormat(
|
||||
comptime vars: []const Variable,
|
||||
comptime delimiter: u8,
|
||||
comptime expected: []const u8,
|
||||
) !void {
|
||||
const comptime_result = comptime comptimeFormat(vars, delimiter);
|
||||
try testing.expectEqualStrings(expected, comptime_result);
|
||||
|
||||
var buf: [256]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try format(&writer, vars, delimiter);
|
||||
try testing.expectEqualStrings(expected, buf[0..writer.end]);
|
||||
}
|
||||
|
||||
test "format single variable" {
|
||||
try testFormat(&.{.session_id}, ' ', "#{session_id}");
|
||||
}
|
||||
|
||||
test "format multiple variables" {
|
||||
try testFormat(&.{ .session_id, .window_id, .window_width, .window_height }, ' ', "#{session_id} #{window_id} #{window_width} #{window_height}");
|
||||
}
|
||||
|
||||
test "format with comma delimiter" {
|
||||
try testFormat(&.{ .window_id, .window_layout }, ',', "#{window_id},#{window_layout}");
|
||||
}
|
||||
|
||||
test "format with tab delimiter" {
|
||||
try testFormat(&.{ .window_width, .window_height }, '\t', "#{window_width}\t#{window_height}");
|
||||
}
|
||||
|
||||
test "format empty variables" {
|
||||
try testFormat(&.{}, ' ', "");
|
||||
}
|
||||
|
||||
test "format all variables" {
|
||||
try testFormat(&.{ .session_id, .window_id, .window_width, .window_height, .window_layout }, ' ', "#{session_id} #{window_id} #{window_width} #{window_height} #{window_layout}");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -70,6 +70,9 @@ pub const StreamHandler = struct {
|
|||
/// such as XTGETTCAP.
|
||||
dcs: terminal.dcs.Handler = .{},
|
||||
|
||||
/// The tmux control mode viewer state.
|
||||
tmux_viewer: if (tmux_enabled) ?*terminal.tmux.Viewer else void = if (tmux_enabled) null else {},
|
||||
|
||||
/// This is set to true when a message was written to the termio
|
||||
/// mailbox. This can be used by callers to determine if they need
|
||||
/// to wake up the termio thread.
|
||||
|
|
@ -81,9 +84,18 @@ pub const StreamHandler = struct {
|
|||
|
||||
pub const Stream = terminal.Stream(StreamHandler);
|
||||
|
||||
/// True if we have tmux control mode built in.
|
||||
pub const tmux_enabled = terminal.options.tmux_control_mode;
|
||||
|
||||
pub fn deinit(self: *StreamHandler) void {
|
||||
self.apc.deinit();
|
||||
self.dcs.deinit();
|
||||
if (comptime tmux_enabled) tmux: {
|
||||
const viewer = self.tmux_viewer orelse break :tmux;
|
||||
viewer.deinit();
|
||||
self.alloc.destroy(viewer);
|
||||
self.tmux_viewer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// This queues a render operation with the renderer thread. The render
|
||||
|
|
@ -368,9 +380,73 @@ pub const StreamHandler = struct {
|
|||
fn dcsCommand(self: *StreamHandler, cmd: *terminal.dcs.Command) !void {
|
||||
// log.warn("DCS command: {}", .{cmd});
|
||||
switch (cmd.*) {
|
||||
.tmux => |tmux| {
|
||||
// TODO: process it
|
||||
log.warn("tmux control mode event unimplemented cmd={}", .{tmux});
|
||||
.tmux => |tmux| tmux: {
|
||||
// If tmux control mode is disabled at the build level,
|
||||
// then this whole block shouldn't be analyzed.
|
||||
if (comptime !tmux_enabled) break :tmux;
|
||||
log.info("tmux control mode event cmd={f}", .{tmux});
|
||||
|
||||
switch (tmux) {
|
||||
.enter => {
|
||||
// Setup our viewer state
|
||||
assert(self.tmux_viewer == null);
|
||||
const viewer = try self.alloc.create(terminal.tmux.Viewer);
|
||||
errdefer self.alloc.destroy(viewer);
|
||||
viewer.* = try .init(self.alloc);
|
||||
errdefer viewer.deinit();
|
||||
self.tmux_viewer = viewer;
|
||||
break :tmux;
|
||||
},
|
||||
|
||||
.exit => if (self.tmux_viewer) |viewer| {
|
||||
// Free our viewer state
|
||||
viewer.deinit();
|
||||
self.alloc.destroy(viewer);
|
||||
self.tmux_viewer = null;
|
||||
break :tmux;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
assert(tmux != .enter);
|
||||
assert(tmux != .exit);
|
||||
|
||||
const viewer = self.tmux_viewer orelse {
|
||||
// This can only really happen if we failed to
|
||||
// initialize the viewer on enter.
|
||||
log.info(
|
||||
"received tmux control mode command without viewer: {f}",
|
||||
.{tmux},
|
||||
);
|
||||
|
||||
break :tmux;
|
||||
};
|
||||
|
||||
for (viewer.next(.{ .tmux = tmux })) |action| {
|
||||
log.info("tmux viewer action={f}", .{action});
|
||||
switch (action) {
|
||||
.exit => {
|
||||
// We ignore this because we will fully exit when
|
||||
// our DCS connection ends. We may want to handle
|
||||
// this in the future to notify our GUI we're
|
||||
// disconnected though.
|
||||
},
|
||||
|
||||
.command => |command| {
|
||||
assert(command.len > 0);
|
||||
assert(command[command.len - 1] == '\n');
|
||||
self.messageWriter(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
command,
|
||||
));
|
||||
},
|
||||
|
||||
.windows => {
|
||||
// TODO
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
.xtgettcap => |*gettcap| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue