Merge remote-tracking branch 'origin/main' into shaping-positions
commit
942f326c58
|
|
@ -62,7 +62,7 @@ jobs:
|
||||||
run: nix build .#ghostty
|
run: nix build .#ghostty
|
||||||
|
|
||||||
- name: Create pull request
|
- 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:
|
with:
|
||||||
title: Update iTerm2 colorschemes
|
title: Update iTerm2 colorschemes
|
||||||
base: main
|
base: main
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,5 @@ A file for [guiding coding agents](https://agents.md/).
|
||||||
|
|
||||||
- Do not use `xcodebuild`
|
- Do not use `xcodebuild`
|
||||||
- Use `zig build` to build the macOS app and any shared Zig code
|
- 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`
|
- Run Xcode tests using `zig build test`
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const buildpkg = @import("src/build/main.zig");
|
const buildpkg = @import("src/build/main.zig");
|
||||||
|
|
||||||
const appVersion = @import("build.zig.zon").version;
|
const appVersion = @import("build.zig.zon").version;
|
||||||
const minimumZigVersion = @import("build.zig.zon").minimum_zig_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", .{});
|
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,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.gobject = .{
|
.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.
|
// Temporary until we generate them at build time automatically.
|
||||||
.url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
.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-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@
|
||||||
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
|
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
|
||||||
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
|
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
|
||||||
},
|
},
|
||||||
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": {
|
"gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-": {
|
||||||
"name": "gobject",
|
"name": "gobject",
|
||||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
"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-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="
|
"hash": "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg="
|
||||||
},
|
},
|
||||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||||
"name": "gtk4_layer_shell",
|
"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 {
|
path = fetchZigArtifact {
|
||||||
name = "gobject";
|
name = "gobject";
|
||||||
url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst";
|
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-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=";
|
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/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
|
||||||
https://deps.files.ghostty.org/gettext-0.24.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/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/gtk4-layer-shell-1.1.0.tar.gz
|
||||||
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
|
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
|
||||||
https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz
|
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_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
|
||||||
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
||||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.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/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||||
https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.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
|
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-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1747046372,
|
"lastModified": 1761588595,
|
||||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||||
"owner": "edolstra",
|
"owner": "edolstra",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -36,30 +36,17 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 315532800,
|
"lastModified": 1763191728,
|
||||||
"narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=",
|
"narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=",
|
||||||
"rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469",
|
"rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
"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": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
|
|
@ -97,11 +84,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760401936,
|
"lastModified": 1763295135,
|
||||||
"narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=",
|
"narHash": "sha256-sGv/NHCmEnJivguGwB5w8LRmVqr1P72OjS+NzcJsssE=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "365085b6652259753b598d43b723858184980bbe",
|
"rev": "64f8b42cfc615b2cf99144adf2b7728c7847c72a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -112,7 +99,9 @@
|
||||||
},
|
},
|
||||||
"zon2nix": {
|
"zon2nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1758405547,
|
"lastModified": 1758405547,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
# glibc versions used by our dependencies from Nix are compatible with the
|
# glibc versions used by our dependencies from Nix are compatible with the
|
||||||
# system glibc that the user is building for.
|
# 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";
|
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
|
@ -28,10 +30,7 @@
|
||||||
zon2nix = {
|
zon2nix = {
|
||||||
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
|
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
|
||||||
inputs = {
|
inputs = {
|
||||||
# Don't override nixpkgs until Zig 0.15 is available in the Nix branch
|
nixpkgs.follows = "nixpkgs";
|
||||||
# we are using for "normal" builds.
|
|
||||||
#
|
|
||||||
# nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
"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-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
"dest": "vendor/p/gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||||
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a"
|
"sha256": "d9bd4306f0081d8e4b848b6adfeabd2fab49822ee2b679eb4801dcedf5d60c48"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
|
|
|
||||||
|
|
@ -714,6 +714,7 @@ typedef struct {
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
|
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
|
||||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
|
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
|
||||||
|
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
|
||||||
} ghostty_action_close_tab_mode_e;
|
} ghostty_action_close_tab_mode_e;
|
||||||
|
|
||||||
// apprt.surface.Message.ChildExited
|
// apprt.surface.Message.ChildExited
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@
|
||||||
"Helpers/Extensions/NSAppearance+Extension.swift",
|
"Helpers/Extensions/NSAppearance+Extension.swift",
|
||||||
"Helpers/Extensions/NSApplication+Extension.swift",
|
"Helpers/Extensions/NSApplication+Extension.swift",
|
||||||
"Helpers/Extensions/NSImage+Extension.swift",
|
"Helpers/Extensions/NSImage+Extension.swift",
|
||||||
|
"Helpers/Extensions/NSMenu+Extension.swift",
|
||||||
"Helpers/Extensions/NSMenuItem+Extension.swift",
|
"Helpers/Extensions/NSMenuItem+Extension.swift",
|
||||||
"Helpers/Extensions/NSPasteboard+Extension.swift",
|
"Helpers/Extensions/NSPasteboard+Extension.swift",
|
||||||
"Helpers/Extensions/NSScreen+Extension.swift",
|
"Helpers/Extensions/NSScreen+Extension.swift",
|
||||||
|
|
|
||||||
|
|
@ -566,6 +566,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
private func syncAppearance() {
|
private func syncAppearance() {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
|
||||||
|
defer { updateColorSchemeForSurfaceTree() }
|
||||||
// Change the collection behavior of the window depending on the configuration.
|
// Change the collection behavior of the window depending on the configuration.
|
||||||
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
|
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,9 @@ class BaseTerminalController: NSWindowController,
|
||||||
/// The previous frame information from the window
|
/// The previous frame information from the window
|
||||||
private var savedFrame: SavedFrame? = nil
|
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.
|
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||||
private var derivedConfig: DerivedConfig
|
private var derivedConfig: DerivedConfig
|
||||||
|
|
||||||
|
|
@ -1163,4 +1166,35 @@ extension BaseTerminalController: NSMenuItemValidation {
|
||||||
return true
|
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),
|
selector: #selector(onCloseOtherTabs),
|
||||||
name: .ghosttyCloseOtherTabs,
|
name: .ghosttyCloseOtherTabs,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(onCloseTabsOnTheRight),
|
||||||
|
name: .ghosttyCloseTabsOnTheRight,
|
||||||
|
object: nil)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(onResetWindowSize),
|
selector: #selector(onResetWindowSize),
|
||||||
|
|
@ -425,15 +430,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
/// Surface-level config will be updated in
|
||||||
// This is a surface-level config update. If we have the surface, we
|
/// ``Ghostty/Ghostty/SurfaceView/derivedConfig`` then
|
||||||
// update our appearance based on it.
|
/// ``TerminalController/focusedSurfaceDidChange(to:)``
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the accessory view of each tab according to the keyboard
|
/// 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
|
/// Closes the current window (including any other tabs) immediately and without
|
||||||
/// confirmation. This will setup proper undo state so the action can be undone.
|
/// confirmation. This will setup proper undo state so the action can be undone.
|
||||||
private func closeWindowImmediately() {
|
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
|
// If we only have one window then we have no other tabs to close
|
||||||
guard tabGroup.windows.count > 1 else { return }
|
guard tabGroup.windows.count > 1 else { return }
|
||||||
|
|
||||||
// Check if we have to confirm close.
|
// Check if we have to confirm close.
|
||||||
guard tabGroup.windows.contains(where: { window in
|
guard tabGroup.windows.contains(where: { window in
|
||||||
// Ignore ourself
|
// Ignore ourself
|
||||||
if window == self.window { return false }
|
if window == self.window { return false }
|
||||||
|
|
||||||
// Ignore non-terminals
|
// Ignore non-terminals
|
||||||
guard let controller = window.windowController as? TerminalController else {
|
guard let controller = window.windowController as? TerminalController else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any surfaces require confirmation
|
// Check if any surfaces require confirmation
|
||||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||||
}) else {
|
}) else {
|
||||||
self.closeOtherTabsImmediately()
|
self.closeOtherTabsImmediately()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmClose(
|
confirmClose(
|
||||||
messageText: "Close Other Tabs?",
|
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."
|
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?) {
|
@IBAction func returnToDefaultSize(_ sender: Any?) {
|
||||||
guard let window, let defaultSize else { return }
|
guard let window, let defaultSize else { return }
|
||||||
defaultSize.apply(to: window)
|
defaultSize.apply(to: window)
|
||||||
|
|
@ -1311,6 +1379,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
closeOtherTabs(self)
|
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) {
|
@objc private func onCloseWindow(notification: SwiftUI.Notification) {
|
||||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
guard surfaceTree.contains(target) else { return }
|
guard surfaceTree.contains(target) else { return }
|
||||||
|
|
@ -1373,6 +1447,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
extension TerminalController {
|
extension TerminalController {
|
||||||
override func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
override func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||||
switch item.action {
|
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):
|
case #selector(returnToDefaultSize):
|
||||||
guard let window else { return false }
|
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.
|
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||||
private(set) var derivedConfig: DerivedConfig = .init()
|
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
|
/// Whether this window supports the update accessory. If this is false, then views within this
|
||||||
/// window should determine how to show update notifications.
|
/// window should determine how to show update notifications.
|
||||||
|
|
@ -53,6 +55,17 @@ class TerminalWindow: NSWindow {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
// Notify that this terminal window has loaded
|
// Notify that this terminal window has loaded
|
||||||
NotificationCenter.default.post(name: Self.terminalDidAwake, object: self)
|
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
|
// 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
|
// again. I'm not sure why this is required. If you don't do this, then
|
||||||
|
|
@ -202,6 +215,8 @@ class TerminalWindow: NSWindow {
|
||||||
/// added.
|
/// added.
|
||||||
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
|
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
|
||||||
|
|
||||||
|
private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem")
|
||||||
|
|
||||||
func findTitlebarView() -> NSView? {
|
func findTitlebarView() -> NSView? {
|
||||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
// 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
|
// MARK: Tab Key Equivalents
|
||||||
|
|
||||||
var keyEquivalent: String? = nil {
|
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
|
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||||
// at some point when a surface becomes focused.
|
// at some point when a surface becomes focused.
|
||||||
guard isVisible else { return }
|
guard isVisible else { return }
|
||||||
|
defer { updateColorSchemeForSurfaceTree() }
|
||||||
|
|
||||||
// Basic properties
|
// Basic properties
|
||||||
appearance = surfaceConfig.windowAppearance
|
appearance = surfaceConfig.windowAppearance
|
||||||
|
|
@ -481,6 +543,10 @@ class TerminalWindow: NSWindow {
|
||||||
return derivedConfig.backgroundColor.withAlphaComponent(alpha)
|
return derivedConfig.backgroundColor.withAlphaComponent(alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateColorSchemeForSurfaceTree() {
|
||||||
|
terminalController?.updateColorSchemeForSurfaceTree()
|
||||||
|
}
|
||||||
|
|
||||||
private func setInitialWindowPosition(x: Int16?, y: Int16?) {
|
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.
|
// If we don't have an X/Y then we try to use the previously saved window pos.
|
||||||
guard let x, let y else {
|
guard let x, let y else {
|
||||||
|
|
@ -512,6 +578,12 @@ class TerminalWindow: NSWindow {
|
||||||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||||
standardWindowButton(.zoomButton)?.isHidden = true
|
standardWindowButton(.zoomButton)?.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let observer = tabMenuObserver {
|
||||||
|
NotificationCenter.default.removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Config
|
// MARK: Config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,38 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
|
|
||||||
viewModel.isMainWindow = false
|
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 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.
|
// this, detect the tab bar being added, and override its behavior.
|
||||||
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
||||||
|
|
|
||||||
|
|
@ -861,6 +861,13 @@ extension Ghostty {
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT:
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .ghosttyCloseTabsOnTheRight,
|
||||||
|
object: surfaceView
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,9 @@ extension Notification.Name {
|
||||||
/// Close other tabs
|
/// Close other tabs
|
||||||
static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs")
|
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
|
/// Close window
|
||||||
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")
|
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,15 @@ class SurfaceScrollView: NSView {
|
||||||
scrollView.hasHorizontalScroller = false
|
scrollView.hasHorizontalScroller = false
|
||||||
scrollView.autohidesScrollers = false
|
scrollView.autohidesScrollers = false
|
||||||
scrollView.usesPredominantAxisScrolling = true
|
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
|
// hide default background to show blur effect properly
|
||||||
scrollView.drawsBackground = false
|
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
|
// 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
|
scrollView.contentView.clipsToBounds = false
|
||||||
|
|
||||||
// The document view is what the scrollview is actually going
|
// The document view is what the scrollview is actually going
|
||||||
|
|
@ -107,7 +112,10 @@ class SurfaceScrollView: NSView {
|
||||||
observers.append(NotificationCenter.default.addObserver(
|
observers.append(NotificationCenter.default.addObserver(
|
||||||
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
|
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
|
||||||
object: nil,
|
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
|
) { [weak self] _ in
|
||||||
self?.handleScrollerStyleChange()
|
self?.handleScrollerStyleChange()
|
||||||
})
|
})
|
||||||
|
|
@ -176,10 +184,10 @@ class SurfaceScrollView: NSView {
|
||||||
private func synchronizeAppearance() {
|
private func synchronizeAppearance() {
|
||||||
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
|
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
|
||||||
scrollView.hasVerticalScroller = scrollbarConfig != .never
|
scrollView.hasVerticalScroller = scrollbarConfig != .never
|
||||||
scrollView.verticalScroller?.controlSize = .small
|
|
||||||
let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
|
let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
|
||||||
// Make sure the scroller’s appearance matches the surface's background color.
|
// Make sure the scroller’s appearance matches the surface's background color.
|
||||||
scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua)
|
scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua)
|
||||||
|
updateTrackingAreas()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Positions the surface view to fill the currently visible rectangle.
|
/// Positions the surface view to fill the currently visible rectangle.
|
||||||
|
|
@ -240,6 +248,7 @@ class SurfaceScrollView: NSView {
|
||||||
|
|
||||||
/// Handles scrollbar style changes
|
/// Handles scrollbar style changes
|
||||||
private func handleScrollerStyleChange() {
|
private func handleScrollerStyleChange() {
|
||||||
|
scrollView.scrollerStyle = .overlay
|
||||||
synchronizeCoreSurface()
|
synchronizeCoreSurface()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,4 +359,32 @@ class SurfaceScrollView: NSView {
|
||||||
}
|
}
|
||||||
return contentHeight
|
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
|
// Setup our tracking area so we get mouse moved events
|
||||||
updateTrackingAreas()
|
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.
|
// The UTTypes that can be dragged onto this view.
|
||||||
registerForDraggedTypes(Array(Self.dropTypes))
|
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
|
## 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
|
files (located under `src/apprt/gtk/ui`). Blueprints have a native syntax for
|
||||||
translatable strings, which look like this:
|
translatable strings, which look like this:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5299,6 +5299,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||||
switch (v) {
|
switch (v) {
|
||||||
.this => .this,
|
.this => .this,
|
||||||
.other => .other,
|
.other => .other,
|
||||||
|
.right => .right,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -767,6 +767,8 @@ pub const CloseTabMode = enum(c_int) {
|
||||||
this,
|
this,
|
||||||
/// Close all other tabs.
|
/// Close all other tabs.
|
||||||
other,
|
other,
|
||||||
|
/// Close all tabs to the right of the current tab.
|
||||||
|
right,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const CommandFinished = struct {
|
pub const CommandFinished = struct {
|
||||||
|
|
|
||||||
|
|
@ -1583,7 +1583,7 @@ pub const Application = extern struct {
|
||||||
.dark;
|
.dark;
|
||||||
log.debug("style manager changed scheme={}", .{scheme});
|
log.debug("style manager changed scheme={}", .{scheme});
|
||||||
|
|
||||||
const priv = self.private();
|
const priv: *Private = self.private();
|
||||||
const core_app = priv.core_app;
|
const core_app = priv.core_app;
|
||||||
core_app.colorSchemeEvent(self.rt(), scheme) catch |err| {
|
core_app.colorSchemeEvent(self.rt(), scheme) catch |err| {
|
||||||
log.warn("error updating app color scheme err={}", .{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(
|
fn handleReloadConfig(
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,7 @@ pub const Tab = extern struct {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
.this => tab_view.closePage(page),
|
.this => tab_view.closePage(page),
|
||||||
.other => tab_view.closeOtherPages(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)
|
try std.SemanticVersion.parse(v)
|
||||||
else version: {
|
else version: {
|
||||||
const app_version = try std.SemanticVersion.parse(appVersion);
|
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.
|
// If no explicit version is given, we try to detect it from git.
|
||||||
const vsn = GitVersion.detect(b) catch |err| switch (err) {
|
const vsn = GitVersion.detect(b) catch |err| switch (err) {
|
||||||
// If Git isn't available we just make an unknown dev version.
|
// If Git isn't available we just make an unknown dev version.
|
||||||
|
|
|
||||||
|
|
@ -719,15 +719,19 @@ pub fn addSimd(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highway
|
// Highway
|
||||||
if (b.lazyDependency("highway", .{
|
if (b.systemIntegrationOption("highway", .{ .default = false })) {
|
||||||
.target = target,
|
m.linkSystemLibrary("libhwy", dynamic_link_opts);
|
||||||
.optimize = optimize,
|
} else {
|
||||||
})) |highway_dep| {
|
if (b.lazyDependency("highway", .{
|
||||||
m.linkLibrary(highway_dep.artifact("highway"));
|
.target = target,
|
||||||
if (static_libs) |v| try v.append(
|
.optimize = optimize,
|
||||||
b.allocator,
|
})) |highway_dep| {
|
||||||
highway_dep.artifact("highway").getEmittedBin(),
|
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
|
// 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"));
|
m.addIncludePath(b.path("src"));
|
||||||
{
|
{
|
||||||
// From hwy/detect_targets.h
|
// From hwy/detect_targets.h
|
||||||
|
const HWY_AVX10_2: c_int = 1 << 3;
|
||||||
const HWY_AVX3_SPR: c_int = 1 << 4;
|
const HWY_AVX3_SPR: c_int = 1 << 4;
|
||||||
const HWY_AVX3_ZEN4: c_int = 1 << 6;
|
const HWY_AVX3_ZEN4: c_int = 1 << 6;
|
||||||
const HWY_AVX3_DL: c_int = 1 << 7;
|
const HWY_AVX3_DL: c_int = 1 << 7;
|
||||||
|
|
@ -756,7 +761,7 @@ pub fn addSimd(
|
||||||
// The performance difference between AVX2 and AVX512 is not
|
// The performance difference between AVX2 and AVX512 is not
|
||||||
// significant for our use case and AVX512 is very rare on consumer
|
// significant for our use case and AVX512 is very rare on consumer
|
||||||
// hardware anyways.
|
// 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(.{
|
m.addCSourceFiles(.{
|
||||||
.files = &.{
|
.files = &.{
|
||||||
|
|
|
||||||
|
|
@ -600,9 +600,8 @@ pub const Action = union(enum) {
|
||||||
/// of the `confirm-close-surface` configuration setting.
|
/// of the `confirm-close-surface` configuration setting.
|
||||||
close_surface,
|
close_surface,
|
||||||
|
|
||||||
/// Close the current tab and all splits therein _or_ close all tabs and
|
/// Close the current tab and all splits therein, close all other tabs, or
|
||||||
/// splits thein of tabs _other_ than the current tab, depending on the
|
/// close every tab to the right of the current one depending on the mode.
|
||||||
/// mode.
|
|
||||||
///
|
///
|
||||||
/// If the mode is not specified, defaults to closing the current tab.
|
/// 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 {
|
pub const CloseTabMode = enum {
|
||||||
this,
|
this,
|
||||||
other,
|
other,
|
||||||
|
right,
|
||||||
|
|
||||||
pub const default: CloseTabMode = .this;
|
pub const default: CloseTabMode = .this;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,11 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||||
.title = "Close Other Tabs",
|
.title = "Close Other Tabs",
|
||||||
.description = "Close all tabs in this window except the current one.",
|
.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 &.{.{
|
.close_window => comptime &.{.{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/// Generates bytes.
|
//! Generates bytes.
|
||||||
const Bytes = @This();
|
const Bytes = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
@ -7,9 +7,7 @@ const Generator = @import("Generator.zig");
|
||||||
/// Random number generator.
|
/// Random number generator.
|
||||||
rand: std.Random,
|
rand: std.Random,
|
||||||
|
|
||||||
/// The minimum and maximum length of the generated bytes. The maximum
|
/// The minimum and maximum length of the generated bytes.
|
||||||
/// length will be capped to the length of the buffer passed in if the
|
|
||||||
/// buffer length is smaller.
|
|
||||||
min_len: usize = 1,
|
min_len: usize = 1,
|
||||||
max_len: usize = std.math.maxInt(usize),
|
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.
|
/// side effect of the generator, not an intended use case.
|
||||||
alphabet: ?[]const u8 = null,
|
alphabet: ?[]const u8 = null,
|
||||||
|
|
||||||
/// Predefined alphabets.
|
/// Generate an alphabet given a function that returns true/false for a
|
||||||
pub const Alphabet = struct {
|
/// given byte.
|
||||||
pub const ascii = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~";
|
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 {
|
pub fn generator(self: *Bytes) Generator {
|
||||||
return .init(self, next);
|
return .init(self, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Error!void {
|
/// Return a copy of the Bytes, but with a new alphabet.
|
||||||
std.debug.assert(max_len >= 1);
|
pub fn newAlphabet(self: *const Bytes, new_alphabet: ?[]const u8) Bytes {
|
||||||
const len = @min(
|
return .{
|
||||||
self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len),
|
.rand = self.rand,
|
||||||
max_len,
|
.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 buf: [8]u8 = undefined;
|
||||||
|
|
||||||
var remaining = len;
|
var remaining = len;
|
||||||
while (remaining > 0) {
|
while (remaining > 0) {
|
||||||
const data = buf[0..@min(remaining, buf.len)];
|
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);
|
try writer.writeAll(data);
|
||||||
remaining -= data.len;
|
remaining -= data.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
test "bytes" {
|
test "bytes" {
|
||||||
|
|
@ -52,9 +108,11 @@ test "bytes" {
|
||||||
var prng = std.Random.DefaultPrng.init(0);
|
var prng = std.Random.DefaultPrng.init(0);
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
var writer: std.Io.Writer = .fixed(&buf);
|
var writer: std.Io.Writer = .fixed(&buf);
|
||||||
var v: Bytes = .{ .rand = prng.random() };
|
var v: Bytes = .{
|
||||||
v.min_len = buf.len;
|
.rand = prng.random(),
|
||||||
v.max_len = buf.len;
|
.min_len = buf.len,
|
||||||
|
.max_len = buf.len,
|
||||||
|
};
|
||||||
const gen = v.generator();
|
const gen = v.generator();
|
||||||
try gen.next(&writer, buf.len);
|
try gen.next(&writer, buf.len);
|
||||||
try testing.expectEqual(buf.len, writer.buffered().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_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0),
|
||||||
p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0),
|
p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0),
|
||||||
|
|
||||||
/// The alphabet for random bytes (omitting 0x1B and 0x07).
|
fn checkKvAlphabet(c: u8) bool {
|
||||||
const bytes_alphabet: []const u8 = alphabet: {
|
return switch (c) {
|
||||||
var alphabet: [256]u8 = undefined;
|
std.ascii.control_code.esc, std.ascii.control_code.bel, ';', '=' => false,
|
||||||
for (0..alphabet.len) |i| {
|
else => std.ascii.isPrint(c),
|
||||||
if (i == 0x1B or i == 0x07) {
|
};
|
||||||
alphabet[i] = @intCast(i + 1);
|
}
|
||||||
} else {
|
|
||||||
alphabet[i] = @intCast(i);
|
/// The alphabet for random bytes in OSC key/value pairs (omitting 0x1B,
|
||||||
}
|
/// 0x07, ';', '=').
|
||||||
}
|
pub const kv_alphabet = Bytes.generateAlphabet(checkKvAlphabet);
|
||||||
const result = alphabet;
|
|
||||||
break :alphabet &result;
|
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 {
|
pub fn generator(self: *Osc) Generator {
|
||||||
return .init(self, next);
|
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 {
|
fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
.change_window_title => {
|
.change_window_title => change_window_title: {
|
||||||
try writer.writeAll("0;"); // Set window title
|
if (max_len < 3) break :change_window_title;
|
||||||
var bytes_gen = self.bytes();
|
try writer.print("0;{f}", .{self.bytes().atMost(max_len - 3)}); // Set window title
|
||||||
try bytes_gen.next(writer, max_len - 2);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
.prompt_start => {
|
.prompt_start => prompt_start: {
|
||||||
|
if (max_len < 4) break :prompt_start;
|
||||||
|
var remaining = max_len;
|
||||||
|
|
||||||
try writer.writeAll("133;A"); // Start prompt
|
try writer.writeAll("133;A"); // Start prompt
|
||||||
|
remaining -= 4;
|
||||||
|
|
||||||
// aid
|
// aid
|
||||||
if (self.rand.boolean()) {
|
if (self.rand.boolean()) aid: {
|
||||||
var bytes_gen = self.bytes();
|
if (remaining < 6) break :aid;
|
||||||
bytes_gen.max_len = 16;
|
|
||||||
try writer.writeAll(";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
|
// redraw
|
||||||
if (self.rand.boolean()) {
|
if (self.rand.boolean()) redraw: {
|
||||||
|
if (remaining < 9) break :redraw;
|
||||||
try writer.writeAll(";redraw=");
|
try writer.writeAll(";redraw=");
|
||||||
if (self.rand.boolean()) {
|
if (self.rand.boolean()) {
|
||||||
try writer.writeAll("1");
|
try writer.writeAll("1");
|
||||||
} else {
|
} else {
|
||||||
try writer.writeAll("0");
|
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 {
|
) Generator.Error!void {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
.random => {
|
.random => {
|
||||||
var bytes_gen = self.bytes();
|
try self.bytes().atMost(max_len).format(writer);
|
||||||
try bytes_gen.next(writer, max_len);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
.good_prefix => {
|
.good_prefix => {
|
||||||
try writer.writeAll("133;");
|
try writer.print("133;{f}", .{self.bytes().atMost(max_len - 4)});
|
||||||
var bytes_gen = self.bytes();
|
|
||||||
try bytes_gen.next(writer, max_len - 4);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +166,7 @@ fn nextUnwrappedInvalidExact(
|
||||||
fn bytes(self: *const Osc) Bytes {
|
fn bytes(self: *const Osc) Bytes {
|
||||||
return .{
|
return .{
|
||||||
.rand = self.rand,
|
.rand = self.rand,
|
||||||
.alphabet = bytes_alphabet,
|
.alphabet = osc_alphabet,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,21 @@ const Ascii = @This();
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const synthetic = @import("../main.zig");
|
const Bytes = @import("../Bytes.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||||
|
|
||||||
pub const Options = struct {};
|
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.
|
/// Create a new terminal stream handler for the given arguments.
|
||||||
pub fn create(
|
pub fn create(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
|
@ -23,12 +32,10 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void {
|
||||||
alloc.destroy(self);
|
alloc.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
|
pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
|
||||||
_ = self;
|
var gen: Bytes = .{
|
||||||
|
|
||||||
var gen: synthetic.Bytes = .{
|
|
||||||
.rand = rand,
|
.rand = rand,
|
||||||
.alphabet = synthetic.Bytes.Alphabet.ascii,
|
.alphabet = ascii,
|
||||||
};
|
};
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub const output = @import("tmux/output.zig");
|
||||||
pub const ControlParser = control.Parser;
|
pub const ControlParser = control.Parser;
|
||||||
pub const ControlNotification = control.Notification;
|
pub const ControlNotification = control.Notification;
|
||||||
pub const Layout = layout.Layout;
|
pub const Layout = layout.Layout;
|
||||||
|
pub const Viewer = @import("tmux/viewer.zig").Viewer;
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
|
|
||||||
|
|
@ -531,6 +531,30 @@ pub const Notification = union(enum) {
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
name: []const u8,
|
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" {
|
test "tmux begin/end empty" {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,36 @@ pub fn parseFormatStruct(
|
||||||
return result;
|
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
|
/// Returns a struct type that contains fields for each of the given
|
||||||
/// format variables. This can be used with `parseFormatStruct` to
|
/// format variables. This can be used with `parseFormatStruct` to
|
||||||
/// parse an output string into a format struct.
|
/// 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
|
/// a subset of them here that are relevant to the use case of implementing
|
||||||
/// control mode for terminal emulators.
|
/// control mode for terminal emulators.
|
||||||
pub const Variable = enum {
|
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,
|
session_id,
|
||||||
|
/// Server version (e.g., `3.5a`).
|
||||||
|
version,
|
||||||
|
/// Unique window ID prefixed with `@` (e.g., `@0`, `@42`).
|
||||||
window_id,
|
window_id,
|
||||||
|
/// Width of window.
|
||||||
window_width,
|
window_width,
|
||||||
|
/// Height of window.
|
||||||
window_height,
|
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,
|
window_layout,
|
||||||
|
/// Pane wrap flag.
|
||||||
|
wrap_flag,
|
||||||
|
|
||||||
/// Parse the given string value into the appropriate resulting
|
/// Parse the given string value into the appropriate resulting
|
||||||
/// type for this variable.
|
/// type for this variable.
|
||||||
pub fn parse(comptime self: Variable, value: []const u8) !Type(self) {
|
pub fn parse(comptime self: Variable, value: []const u8) !Type(self) {
|
||||||
return switch (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] == '$')
|
.session_id => if (value.len >= 2 and value[0] == '$')
|
||||||
try std.fmt.parseInt(usize, value[1..], 10)
|
try std.fmt.parseInt(usize, value[1..], 10)
|
||||||
else
|
else
|
||||||
|
|
@ -83,24 +206,107 @@ pub const Variable = enum {
|
||||||
try std.fmt.parseInt(usize, value[1..], 10)
|
try std.fmt.parseInt(usize, value[1..], 10)
|
||||||
else
|
else
|
||||||
return error.FormatError,
|
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_width => try std.fmt.parseInt(usize, value, 10),
|
||||||
.window_height => 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.
|
/// The type of the parsed value for this variable type.
|
||||||
pub fn Type(comptime self: Variable) type {
|
pub fn Type(comptime self: Variable) type {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.session_id => usize,
|
.alternate_on,
|
||||||
.window_id => usize,
|
.bracketed_paste,
|
||||||
.window_width => usize,
|
.cursor_blinking,
|
||||||
.window_height => usize,
|
.cursor_flag,
|
||||||
.window_layout => []const u8,
|
.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" {
|
test "parse session id" {
|
||||||
try testing.expectEqual(42, try Variable.parse(.session_id, "$42"));
|
try testing.expectEqual(42, try Variable.parse(.session_id, "$42"));
|
||||||
try testing.expectEqual(0, try Variable.parse(.session_id, "$0"));
|
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)"));
|
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" {
|
test "parseFormatStruct single field" {
|
||||||
const T = FormatStruct(&.{.session_id});
|
const T = FormatStruct(&.{.session_id});
|
||||||
const result = try parseFormatStruct(T, "$42", ' ');
|
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.expectEqual(1, result.session_id);
|
||||||
try testing.expectEqualStrings("", result.window_layout);
|
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.
|
/// such as XTGETTCAP.
|
||||||
dcs: terminal.dcs.Handler = .{},
|
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
|
/// 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
|
/// mailbox. This can be used by callers to determine if they need
|
||||||
/// to wake up the termio thread.
|
/// to wake up the termio thread.
|
||||||
|
|
@ -81,9 +84,18 @@ pub const StreamHandler = struct {
|
||||||
|
|
||||||
pub const Stream = terminal.Stream(StreamHandler);
|
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 {
|
pub fn deinit(self: *StreamHandler) void {
|
||||||
self.apc.deinit();
|
self.apc.deinit();
|
||||||
self.dcs.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
|
/// 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 {
|
fn dcsCommand(self: *StreamHandler, cmd: *terminal.dcs.Command) !void {
|
||||||
// log.warn("DCS command: {}", .{cmd});
|
// log.warn("DCS command: {}", .{cmd});
|
||||||
switch (cmd.*) {
|
switch (cmd.*) {
|
||||||
.tmux => |tmux| {
|
.tmux => |tmux| tmux: {
|
||||||
// TODO: process it
|
// If tmux control mode is disabled at the build level,
|
||||||
log.warn("tmux control mode event unimplemented cmd={}", .{tmux});
|
// 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| {
|
.xtgettcap => |*gettcap| {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue