mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
Merge branch 'main' into dmehala/conemu-osc9
This commit is contained in:
70
.github/workflows/test.yml
vendored
70
.github/workflows/test.yml
vendored
@ -376,6 +376,41 @@ jobs:
|
||||
-Dgtk-adwaita=${{ matrix.adwaita }} \
|
||||
-Dgtk-x11=${{ matrix.x11 }}
|
||||
|
||||
test-sentry-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sentry: ["true", "false"]
|
||||
name: Build -Dsentry=${{ matrix.sentry }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.2.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test Sentry Build
|
||||
run: |
|
||||
nix develop -c zig build -Dsentry=${{ matrix.sentry }}
|
||||
|
||||
test-macos:
|
||||
runs-on: namespace-profile-ghostty-macos
|
||||
needs: test
|
||||
@ -478,3 +513,38 @@ jobs:
|
||||
useDaemon: false # sometimes fails on short jobs
|
||||
- name: typos check
|
||||
run: nix develop -c typos
|
||||
|
||||
test-pkg-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg: ["wuffs"]
|
||||
name: Test pkg/${{ matrix.pkg }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.2.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test ${{ matrix.pkg }} Build
|
||||
run: |
|
||||
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test"
|
||||
|
110
build.zig
110
build.zig
@ -24,6 +24,8 @@ const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig");
|
||||
const Version = @import("src/build/Version.zig");
|
||||
const Command = @import("src/Command.zig");
|
||||
|
||||
const Scanner = @import("zig_wayland").Scanner;
|
||||
|
||||
comptime {
|
||||
// This is the required Zig version for building this project. We allow
|
||||
// any patch version but the major and minor must match exactly.
|
||||
@ -105,19 +107,19 @@ pub fn build(b: *std.Build) !void {
|
||||
"Enables the use of Adwaita when using the GTK rendering backend.",
|
||||
) orelse true;
|
||||
|
||||
config.x11 = b.option(
|
||||
bool,
|
||||
"gtk-x11",
|
||||
"Enables linking against X11 libraries when using the GTK rendering backend.",
|
||||
) orelse x11: {
|
||||
if (target.result.os.tag != .linux) break :x11 false;
|
||||
var x11 = false;
|
||||
var wayland = false;
|
||||
|
||||
if (target.result.os.tag == .linux) pkgconfig: {
|
||||
var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator);
|
||||
|
||||
pkgconfig.stdout_behavior = .Pipe;
|
||||
pkgconfig.stderr_behavior = .Pipe;
|
||||
|
||||
try pkgconfig.spawn();
|
||||
pkgconfig.spawn() catch |err| {
|
||||
std.log.warn("failed to spawn pkg-config - disabling X11 and Wayland integrations: {}", .{err});
|
||||
break :pkgconfig;
|
||||
};
|
||||
|
||||
const output_max_size = 50 * 1024;
|
||||
|
||||
@ -139,17 +141,44 @@ pub fn build(b: *std.Build) !void {
|
||||
switch (term) {
|
||||
.Exited => |code| {
|
||||
if (code == 0) {
|
||||
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| break :x11 true;
|
||||
break :x11 false;
|
||||
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| x11 = true;
|
||||
if (std.mem.indexOf(u8, stdout.items, "wayland")) |_| wayland = true;
|
||||
} else {
|
||||
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
||||
return error.Unexpected;
|
||||
}
|
||||
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
||||
return error.Unexpected;
|
||||
},
|
||||
inline else => |code| {
|
||||
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
||||
return error.Unexpected;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
config.x11 = b.option(
|
||||
bool,
|
||||
"gtk-x11",
|
||||
"Enables linking against X11 libraries when using the GTK rendering backend.",
|
||||
) orelse x11;
|
||||
|
||||
config.wayland = b.option(
|
||||
bool,
|
||||
"gtk-wayland",
|
||||
"Enables linking against Wayland libraries when using the GTK rendering backend.",
|
||||
) orelse wayland;
|
||||
|
||||
config.sentry = b.option(
|
||||
bool,
|
||||
"sentry",
|
||||
"Build with Sentry crash reporting. Default for macOS is true, false for any other system.",
|
||||
) orelse sentry: {
|
||||
switch (target.result.os.tag) {
|
||||
.macos, .ios => break :sentry true,
|
||||
|
||||
// Note its false for linux because the crash reports on Linux
|
||||
// don't have much useful information.
|
||||
else => break :sentry false,
|
||||
}
|
||||
};
|
||||
|
||||
const pie = b.option(
|
||||
@ -158,6 +187,16 @@ pub fn build(b: *std.Build) !void {
|
||||
"Build a Position Independent Executable. Default true for system packages.",
|
||||
) orelse system_package;
|
||||
|
||||
const strip = b.option(
|
||||
bool,
|
||||
"strip",
|
||||
"Strip the final executable. Default true for fast and small releases",
|
||||
) orelse switch (optimize) {
|
||||
.Debug => false,
|
||||
.ReleaseSafe => false,
|
||||
.ReleaseFast, .ReleaseSmall => true,
|
||||
};
|
||||
|
||||
const conformance = b.option(
|
||||
[]const u8,
|
||||
"conformance",
|
||||
@ -342,11 +381,7 @@ pub fn build(b: *std.Build) !void {
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.strip = switch (optimize) {
|
||||
.Debug => false,
|
||||
.ReleaseSafe => false,
|
||||
.ReleaseFast, .ReleaseSmall => true,
|
||||
},
|
||||
.strip = strip,
|
||||
}) else null;
|
||||
|
||||
// Exe
|
||||
@ -669,7 +704,12 @@ pub fn build(b: *std.Build) !void {
|
||||
b.installFile("images/icons/icon_128.png", "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png");
|
||||
b.installFile("images/icons/icon_256.png", "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png");
|
||||
b.installFile("images/icons/icon_512.png", "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png");
|
||||
b.installFile("images/icons/icon_1024.png", "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png");
|
||||
|
||||
// Flatpaks only support icons up to 512x512.
|
||||
if (!config.flatpak) {
|
||||
b.installFile("images/icons/icon_1024.png", "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png");
|
||||
}
|
||||
|
||||
b.installFile("images/icons/icon_16@2x.png", "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png");
|
||||
b.installFile("images/icons/icon_32@2x.png", "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png");
|
||||
b.installFile("images/icons/icon_128@2x.png", "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png");
|
||||
@ -685,6 +725,7 @@ pub fn build(b: *std.Build) !void {
|
||||
.root_source_file = b.path("src/main_c.zig"),
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
.strip = strip,
|
||||
});
|
||||
_ = try addDeps(b, lib, config);
|
||||
|
||||
@ -702,6 +743,7 @@ pub fn build(b: *std.Build) !void {
|
||||
.root_source_file = b.path("src/main_c.zig"),
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
.strip = strip,
|
||||
});
|
||||
_ = try addDeps(b, lib, config);
|
||||
|
||||
@ -1240,13 +1282,15 @@ fn addDeps(
|
||||
}
|
||||
|
||||
// Sentry
|
||||
const sentry_dep = b.dependency("sentry", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.backend = .breakpad,
|
||||
});
|
||||
step.root_module.addImport("sentry", sentry_dep.module("sentry"));
|
||||
if (target.result.os.tag != .windows) {
|
||||
if (config.sentry) {
|
||||
const sentry_dep = b.dependency("sentry", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.backend = .breakpad,
|
||||
});
|
||||
|
||||
step.root_module.addImport("sentry", sentry_dep.module("sentry"));
|
||||
|
||||
// Sentry
|
||||
step.linkLibrary(sentry_dep.artifact("sentry"));
|
||||
try static_libs.append(sentry_dep.artifact("sentry").getEmittedBin());
|
||||
@ -1430,6 +1474,24 @@ fn addDeps(
|
||||
if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
|
||||
if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
|
||||
|
||||
if (config.wayland) {
|
||||
const scanner = Scanner.create(b, .{});
|
||||
|
||||
const wayland = b.createModule(.{ .root_source_file = scanner.result });
|
||||
|
||||
const plasma_wayland_protocols = b.dependency("plasma_wayland_protocols", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
|
||||
|
||||
scanner.generate("wl_compositor", 1);
|
||||
scanner.generate("org_kde_kwin_blur_manager", 1);
|
||||
|
||||
step.root_module.addImport("wayland", wayland);
|
||||
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
|
||||
}
|
||||
|
||||
{
|
||||
const gresource = @import("src/apprt/gtk/gresource.zig");
|
||||
|
||||
|
@ -25,6 +25,10 @@
|
||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
|
||||
.hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
|
||||
},
|
||||
|
||||
// C libs
|
||||
.cimgui = .{ .path = "./pkg/cimgui" },
|
||||
@ -49,8 +53,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e030599a6a6e19fcd1ea047c7714021170129d56.tar.gz",
|
||||
.hash = "1220cc25b537556a42b0948437c791214c229efb78b551c80b1e9b18d70bf0498620",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4762ad5bd6d3906e28babdc2bda8a967d63a63be.tar.gz",
|
||||
.hash = "1220a263b22113273d01bd33e3c06b8119cb2f63b4e5d414a85d88e3aa95bb68a2de",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
@ -64,5 +68,9 @@
|
||||
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
||||
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
||||
},
|
||||
.plasma_wayland_protocols = .{
|
||||
.url = "git+https://invent.kde.org/libraries/plasma-wayland-protocols.git?ref=master#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
|
||||
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
13
default.nix
Normal file
13
default.nix
Normal file
@ -0,0 +1,13 @@
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url =
|
||||
lock.nodes.${nodeName}.locked.url
|
||||
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
) {src = ./.;})
|
||||
.defaultNix
|
@ -375,9 +375,9 @@ typedef enum {
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_SPLIT_PREVIOUS,
|
||||
GHOSTTY_GOTO_SPLIT_NEXT,
|
||||
GHOSTTY_GOTO_SPLIT_TOP,
|
||||
GHOSTTY_GOTO_SPLIT_UP,
|
||||
GHOSTTY_GOTO_SPLIT_LEFT,
|
||||
GHOSTTY_GOTO_SPLIT_BOTTOM,
|
||||
GHOSTTY_GOTO_SPLIT_DOWN,
|
||||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
} ghostty_action_goto_split_e;
|
||||
|
||||
@ -559,6 +559,7 @@ typedef struct {
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
GHOSTTY_ACTION_NEW_TAB,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
@ -681,10 +682,11 @@ void ghostty_config_open();
|
||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
ghostty_config_t);
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
bool ghostty_app_tick(ghostty_app_t);
|
||||
void ghostty_app_tick(ghostty_app_t);
|
||||
void* ghostty_app_userdata(ghostty_app_t);
|
||||
void ghostty_app_set_focus(ghostty_app_t, bool);
|
||||
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
|
||||
bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
|
||||
void ghostty_app_keyboard_changed(ghostty_app_t);
|
||||
void ghostty_app_open_config(ghostty_app_t);
|
||||
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
|
||||
@ -712,7 +714,8 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t,
|
||||
ghostty_color_scheme_e);
|
||||
ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s);
|
||||
void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
|
||||
bool ghostty_surface_mouse_captured(ghostty_surface_t);
|
||||
bool ghostty_surface_mouse_button(ghostty_surface_t,
|
||||
|
@ -10,8 +10,8 @@
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
@ -71,6 +71,7 @@
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
@ -87,6 +88,8 @@
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */; };
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||
@ -108,8 +111,8 @@
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
|
||||
@ -162,6 +165,7 @@
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
|
||||
@ -177,6 +181,8 @@
|
||||
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
||||
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSEvent+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Event.swift; sourceTree = "<group>"; };
|
||||
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = "<group>"; };
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = "<group>"; };
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -263,6 +269,7 @@
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
@ -351,12 +358,14 @@
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
|
||||
A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */,
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */,
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */,
|
||||
);
|
||||
path = Ghostty;
|
||||
sourceTree = "<group>";
|
||||
@ -399,13 +408,13 @@
|
||||
children = (
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */,
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
A586167B2B7703CC009BDB1D /* fish */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
A5985CE52C33060F00C57AD3 /* man */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */,
|
||||
9351BE8E2D22937F003B3499 /* nvim */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@ -611,12 +620,14 @@
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */,
|
||||
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
|
||||
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
|
||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
@ -647,6 +658,7 @@
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */,
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
|
@ -358,8 +358,8 @@ class AppDelegate: NSObject,
|
||||
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(config, action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(config, action: "goto_split:up", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(config, action: "goto_split:down", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||
@ -425,6 +425,15 @@ class AppDelegate: NSObject,
|
||||
// because we let it capture and propagate.
|
||||
guard NSApp.mainWindow == nil else { return event }
|
||||
|
||||
// If this event as-is would result in a key binding then we send it.
|
||||
if let app = ghostty.app,
|
||||
ghostty_app_key_is_binding(
|
||||
app,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) {
|
||||
ghostty_app_key(app, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))
|
||||
return nil
|
||||
}
|
||||
|
||||
// If this event would be handled by our menu then we do nothing.
|
||||
if let mainMenu = NSApp.mainMenu,
|
||||
mainMenu.performKeyEquivalent(with: event) {
|
||||
@ -438,13 +447,7 @@ class AppDelegate: NSObject,
|
||||
guard let ghostty = self.ghostty.app else { return event }
|
||||
|
||||
// Build our event input and call ghostty
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = GHOSTTY_ACTION_PRESS
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
if (ghostty_app_key(ghostty, key_ev)) {
|
||||
if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
|
||||
// The key was used so we want to stop it from going to our Mac app
|
||||
Ghostty.logger.debug("local key event handled event=\(event)")
|
||||
return nil
|
||||
@ -486,15 +489,16 @@ class AppDelegate: NSObject,
|
||||
|
||||
// Sync our auto-update settings. If SUEnableAutomaticChecks (in our Info.plist) is
|
||||
// explicitly false (NO), auto-updates are disabled. Otherwise, we use the behavior
|
||||
// defined by our "auto-update" configuration.
|
||||
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool != false {
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
config.autoUpdate == .check || config.autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
config.autoUpdate == .download
|
||||
} else {
|
||||
// defined by our "auto-update" configuration (if set) or fall back to Sparkle
|
||||
// user-based defaults.
|
||||
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
|
||||
updaterController.updater.automaticallyChecksForUpdates = false
|
||||
updaterController.updater.automaticallyDownloadsUpdates = false
|
||||
} else if let autoUpdate = config.autoUpdate {
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
autoUpdate == .check || autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
autoUpdate == .download
|
||||
}
|
||||
|
||||
// Config could change keybindings, so update everything that depends on that
|
||||
|
@ -89,13 +89,13 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.frame.minX, y: -window.frame.height)
|
||||
|
||||
case .left:
|
||||
return .init(x: -window.frame.width, y: 0)
|
||||
return .init(x: screen.frame.minX-window.frame.width, y: 0)
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.frame.maxX, y: 0)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.width)
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: (screen.visibleFrame.maxY - window.frame.height) / 2)
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -540,11 +540,11 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusAbove(_ sender: Any) {
|
||||
splitMoveFocus(direction: .top)
|
||||
splitMoveFocus(direction: .up)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusBelow(_ sender: Any) {
|
||||
splitMoveFocus(direction: .bottom)
|
||||
splitMoveFocus(direction: .down)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusLeft(_ sender: Any) {
|
||||
|
@ -101,6 +101,12 @@ class TerminalController: BaseTerminalController {
|
||||
// When our fullscreen state changes, we resync our appearance because some
|
||||
// properties change when fullscreen or not.
|
||||
guard let focusedSurface else { return }
|
||||
if (!(fullscreenStyle?.isFullscreen ?? false) &&
|
||||
ghostty.config.macosTitlebarStyle == "hidden")
|
||||
{
|
||||
applyHiddenTitlebarStyle()
|
||||
}
|
||||
|
||||
syncAppearance(focusedSurface.derivedConfig)
|
||||
}
|
||||
|
||||
@ -244,7 +250,9 @@ class TerminalController: BaseTerminalController {
|
||||
let backgroundColor: OSColor
|
||||
if let surfaceTree {
|
||||
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.0)
|
||||
// Similar to above, an alpha component of "0" causes compositor issues, so
|
||||
// we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001)
|
||||
} else {
|
||||
// We don't have a focused surface or our surface doesn't border the
|
||||
// top. We choose to match the color of the top-left most surface.
|
||||
@ -267,6 +275,28 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
|
||||
guard let window else { return }
|
||||
|
||||
// If we don't have both an X and Y we center.
|
||||
guard let x, let y else {
|
||||
window.center()
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer the screen our window is being placed on otherwise our primary screen.
|
||||
guard let screen = window.screen ?? NSScreen.screens.first else {
|
||||
window.center()
|
||||
return
|
||||
}
|
||||
|
||||
// Orient based on the top left of the primary monitor
|
||||
let frame = screen.visibleFrame
|
||||
window.setFrameOrigin(.init(
|
||||
x: frame.minX + CGFloat(x),
|
||||
y: frame.maxY - (CGFloat(y) + window.frame.height)))
|
||||
}
|
||||
|
||||
//MARK: - NSWindowController
|
||||
|
||||
override func windowWillLoad() {
|
||||
@ -274,6 +304,43 @@ class TerminalController: BaseTerminalController {
|
||||
shouldCascadeWindows = false
|
||||
}
|
||||
|
||||
fileprivate func applyHiddenTitlebarStyle() {
|
||||
guard let window else { return }
|
||||
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
// Hide the title
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
||||
// Hide the traffic lights (window control buttons)
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
|
||||
window.tabbingMode = .disallowed
|
||||
|
||||
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
|
||||
// some operations that appear to bring back the titlebar visibility so this ensures
|
||||
// it is gone forever.
|
||||
if let themeFrame = window.contentView?.superview,
|
||||
let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") {
|
||||
titleBarContainer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
@ -325,9 +392,12 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
// Center the window to start, we'll move the window frame automatically
|
||||
// when cascading.
|
||||
window.center()
|
||||
// Set our window positioning to coordinates if config value exists, otherwise
|
||||
// fallback to original centering behavior
|
||||
setInitialWindowPosition(
|
||||
x: config.windowPositionX,
|
||||
y: config.windowPositionY,
|
||||
windowDecorations: config.windowDecorations)
|
||||
|
||||
// Make sure our theme is set on the window so styling is correct.
|
||||
if let windowTheme = config.windowTheme {
|
||||
@ -365,38 +435,7 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
// If our titlebar style is "hidden" we adjust the style appropriately
|
||||
if (config.macosTitlebarStyle == "hidden") {
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
// Hide the title
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
||||
// Hide the traffic lights (window control buttons)
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
|
||||
window.tabbingMode = .disallowed
|
||||
|
||||
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
|
||||
// some operations that appear to bring back the titlebar visibility so this ensures
|
||||
// it is gone forever.
|
||||
if let themeFrame = window.contentView?.superview,
|
||||
let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") {
|
||||
titleBarContainer.isHidden = true
|
||||
}
|
||||
applyHiddenTitlebarStyle()
|
||||
}
|
||||
|
||||
// In various situations, macOS automatically tabs new windows. Ghostty handles
|
||||
|
@ -117,23 +117,7 @@ extension Ghostty {
|
||||
|
||||
func appTick() {
|
||||
guard let app = self.app else { return }
|
||||
|
||||
// Tick our app, which lets us know if we want to quit
|
||||
let exit = ghostty_app_tick(app)
|
||||
if (!exit) { return }
|
||||
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
ghostty_app_tick(app)
|
||||
}
|
||||
|
||||
func openConfig() {
|
||||
@ -454,6 +438,9 @@ extension Ghostty {
|
||||
|
||||
// Action dispatch
|
||||
switch (action.tag) {
|
||||
case GHOSTTY_ACTION_QUIT:
|
||||
quit(app)
|
||||
|
||||
case GHOSTTY_ACTION_NEW_WINDOW:
|
||||
newWindow(app, target: target)
|
||||
|
||||
@ -559,6 +546,21 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func quit(_ app: ghostty_app_t) {
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
|
@ -149,6 +149,20 @@ extension Ghostty {
|
||||
guard let ptr = v else { return "" }
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
var windowPositionX: Int16? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: Int16 = 0
|
||||
let key = "window-position-x"
|
||||
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
|
||||
}
|
||||
|
||||
var windowPositionY: Int16? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: Int16 = 0
|
||||
let key = "window-position-y"
|
||||
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
|
||||
}
|
||||
|
||||
var windowNewTabPosition: String {
|
||||
guard let config = self.config else { return "" }
|
||||
@ -361,13 +375,24 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
// This isn't actually a configurable value currently but it could be done day.
|
||||
// We put it here because it is a color that changes depending on the configuration.
|
||||
var splitDividerColor: Color {
|
||||
let backgroundColor = OSColor(backgroundColor)
|
||||
let isLightBackground = backgroundColor.isLightColor
|
||||
let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4)
|
||||
return Color(newColor)
|
||||
|
||||
guard let config = self.config else { return Color(newColor) }
|
||||
|
||||
var color: ghostty_config_color_s = .init();
|
||||
let key = "split-divider-color"
|
||||
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
|
||||
return Color(newColor)
|
||||
}
|
||||
|
||||
return .init(
|
||||
red: Double(color.r) / 255,
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
@ -437,15 +462,14 @@ extension Ghostty {
|
||||
return v;
|
||||
}
|
||||
|
||||
var autoUpdate: AutoUpdate {
|
||||
let defaultValue = AutoUpdate.check
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var autoUpdate: AutoUpdate? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "auto-update"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
|
||||
guard let ptr = v else { return nil }
|
||||
let str = String(cString: ptr)
|
||||
return AutoUpdate(rawValue: str) ?? defaultValue
|
||||
return AutoUpdate(rawValue: str)
|
||||
}
|
||||
|
||||
var autoUpdateChannel: AutoUpdateChannel {
|
||||
|
15
macos/Sources/Ghostty/Ghostty.Event.swift
Normal file
15
macos/Sources/Ghostty/Ghostty.Event.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// A comparable event.
|
||||
struct ComparableKeyEvent: Equatable {
|
||||
let keyCode: UInt16
|
||||
let flags: NSEvent.ModifierFlags
|
||||
|
||||
init(event: NSEvent) {
|
||||
self.keyCode = event.keyCode
|
||||
self.flags = event.modifierFlags
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ extension Ghostty {
|
||||
/// Returns the view that would prefer receiving focus in this tree. This is always the
|
||||
/// top-left-most view. This is used when creating a split or closing a split to find the
|
||||
/// next view to send focus to.
|
||||
func preferredFocus(_ direction: SplitFocusDirection = .top) -> SurfaceView {
|
||||
func preferredFocus(_ direction: SplitFocusDirection = .up) -> SurfaceView {
|
||||
let container: Container
|
||||
switch (self) {
|
||||
case .leaf(let leaf):
|
||||
@ -64,10 +64,10 @@ extension Ghostty {
|
||||
|
||||
let node: SplitNode
|
||||
switch (direction) {
|
||||
case .previous, .top, .left:
|
||||
case .previous, .up, .left:
|
||||
node = container.bottomRight
|
||||
|
||||
case .next, .bottom, .right:
|
||||
case .next, .down, .right:
|
||||
node = container.topLeft
|
||||
}
|
||||
|
||||
@ -431,12 +431,12 @@ extension Ghostty {
|
||||
struct Neighbors {
|
||||
var left: SplitNode?
|
||||
var right: SplitNode?
|
||||
var top: SplitNode?
|
||||
var bottom: SplitNode?
|
||||
var up: SplitNode?
|
||||
var down: SplitNode?
|
||||
|
||||
/// These are the previous/next nodes. It will certainly be one of the above as well
|
||||
/// but we keep track of these separately because depending on the split direction
|
||||
/// of the containing node, previous may be left OR top (same for next).
|
||||
/// of the containing node, previous may be left OR up (same for next).
|
||||
var previous: SplitNode?
|
||||
var next: SplitNode?
|
||||
|
||||
@ -448,8 +448,8 @@ extension Ghostty {
|
||||
let map: [SplitFocusDirection : KeyPath<Self, SplitNode?>] = [
|
||||
.previous: \.previous,
|
||||
.next: \.next,
|
||||
.top: \.top,
|
||||
.bottom: \.bottom,
|
||||
.up: \.up,
|
||||
.down: \.down,
|
||||
.left: \.left,
|
||||
.right: \.right,
|
||||
]
|
||||
|
@ -308,7 +308,7 @@ extension Ghostty {
|
||||
resizeIncrements: .init(width: 1, height: 1),
|
||||
resizePublisher: container.resizeEvent,
|
||||
left: {
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.bottom
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.down
|
||||
|
||||
TerminalSplitNested(
|
||||
node: closeableTopLeft(),
|
||||
@ -318,7 +318,7 @@ extension Ghostty {
|
||||
])
|
||||
)
|
||||
}, right: {
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.left : \.top
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.left : \.up
|
||||
|
||||
TerminalSplitNested(
|
||||
node: closeableBottomRight(),
|
||||
|
15
macos/Sources/Ghostty/NSEvent+Extension.swift
Normal file
15
macos/Sources/Ghostty/NSEvent+Extension.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension NSEvent {
|
||||
/// Create a Ghostty key event for a given keyboard action.
|
||||
func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s {
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(modifierFlags)
|
||||
key_ev.keycode = UInt32(keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
return key_ev
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ extension Ghostty {
|
||||
|
||||
/// An enum that is used for the directions that a split focus event can change.
|
||||
enum SplitFocusDirection {
|
||||
case previous, next, top, bottom, left, right
|
||||
case previous, next, up, down, left, right
|
||||
|
||||
/// Initialize from a Ghostty API enum.
|
||||
static func from(direction: ghostty_action_goto_split_e) -> Self? {
|
||||
@ -77,11 +77,11 @@ extension Ghostty {
|
||||
case GHOSTTY_GOTO_SPLIT_NEXT:
|
||||
return .next
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_TOP:
|
||||
return .top
|
||||
case GHOSTTY_GOTO_SPLIT_UP:
|
||||
return .up
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_BOTTOM:
|
||||
return .bottom
|
||||
case GHOSTTY_GOTO_SPLIT_DOWN:
|
||||
return .down
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_LEFT:
|
||||
return .left
|
||||
@ -102,11 +102,11 @@ extension Ghostty {
|
||||
case .next:
|
||||
return GHOSTTY_GOTO_SPLIT_NEXT
|
||||
|
||||
case .top:
|
||||
return GHOSTTY_GOTO_SPLIT_TOP
|
||||
case .up:
|
||||
return GHOSTTY_GOTO_SPLIT_UP
|
||||
|
||||
case .bottom:
|
||||
return GHOSTTY_GOTO_SPLIT_BOTTOM
|
||||
case .down:
|
||||
return GHOSTTY_GOTO_SPLIT_DOWN
|
||||
|
||||
case .left:
|
||||
return GHOSTTY_GOTO_SPLIT_LEFT
|
||||
|
@ -113,6 +113,9 @@ extension Ghostty {
|
||||
// A small delay that is introduced before a title change to avoid flickers
|
||||
private var titleChangeTimer: Timer?
|
||||
|
||||
/// Event monitor (see individual events for why)
|
||||
private var eventMonitor: Any? = nil
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
|
||||
@ -170,6 +173,15 @@ extension Ghostty {
|
||||
name: NSWindow.didChangeScreenNotification,
|
||||
object: nil)
|
||||
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
|
||||
matching: [
|
||||
// We need keyUp because command+key events don't trigger keyUp.
|
||||
.keyUp
|
||||
]
|
||||
) { [weak self] event in self?.localEventHandler(event) }
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||
@ -212,6 +224,11 @@ extension Ghostty {
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
// Remove our event monitor
|
||||
if let eventMonitor {
|
||||
NSEvent.removeMonitor(eventMonitor)
|
||||
}
|
||||
|
||||
// Whenever the surface is removed, we need to note that our restorable
|
||||
// state is invalid to prevent the surface from being restored.
|
||||
invalidateRestorableState()
|
||||
@ -356,6 +373,30 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Local Events
|
||||
|
||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||
return switch event.type {
|
||||
case .keyUp:
|
||||
localEventKeyUp(event)
|
||||
|
||||
default:
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
private func localEventKeyUp(_ event: NSEvent) -> NSEvent? {
|
||||
// We only care about events with "command" because all others will
|
||||
// trigger the normal responder chain.
|
||||
if (!event.modifierFlags.contains(.command)) { return event }
|
||||
|
||||
// Command keyUp events are never sent to the normal responder chain
|
||||
// so we send them here.
|
||||
guard focused else { return event }
|
||||
self.keyUp(with: event)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||
@ -764,8 +805,23 @@ extension Ghostty {
|
||||
// know if these events cleared it.
|
||||
let markedTextBefore = markedText.length > 0
|
||||
|
||||
// We need to know the keyboard layout before below because some keyboard
|
||||
// input events will change our keyboard layout and we don't want those
|
||||
// going to the terminal.
|
||||
let keyboardIdBefore: String? = if (!markedTextBefore) {
|
||||
KeyboardLayout.id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
self.interpretKeyEvents([translationEvent])
|
||||
|
||||
// If our keyboard changed from this we just assume an input method
|
||||
// grabbed it and do nothing.
|
||||
if (!markedTextBefore && keyboardIdBefore != KeyboardLayout.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we have text, then we've composed a character, send that down. We do this
|
||||
// first because if we completed a preedit, the text will be available here
|
||||
// AND we'll have a preedit.
|
||||
@ -773,7 +829,7 @@ extension Ghostty {
|
||||
if let list = keyTextAccumulator, list.count > 0 {
|
||||
handled = true
|
||||
for text in list {
|
||||
keyAction(action, event: event, text: text)
|
||||
_ = keyAction(action, event: event, text: text)
|
||||
}
|
||||
}
|
||||
|
||||
@ -783,38 +839,49 @@ extension Ghostty {
|
||||
// the preedit.
|
||||
if (markedText.length > 0 || markedTextBefore) {
|
||||
handled = true
|
||||
keyAction(action, event: event, preedit: markedText.string)
|
||||
_ = keyAction(action, event: event, preedit: markedText.string)
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
// No text or anything, we want to handle this manually.
|
||||
keyAction(action, event: event)
|
||||
_ = keyAction(action, event: event)
|
||||
}
|
||||
}
|
||||
|
||||
override func keyUp(with event: NSEvent) {
|
||||
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
_ = keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
}
|
||||
|
||||
/// Special case handling for some control keys
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
// Only process key down events
|
||||
if (event.type != .keyDown) {
|
||||
switch (event.type) {
|
||||
case .keyDown:
|
||||
// Continue, we care about key down events
|
||||
break
|
||||
|
||||
default:
|
||||
// Any other key event we don't care about. I don't think its even
|
||||
// possible to receive any other event type.
|
||||
return false
|
||||
}
|
||||
|
||||
// Only process events if we're focused. Some key events like C-/ macOS
|
||||
// appears to send to the first view in the hierarchy rather than the
|
||||
// the first responder (I don't know why). This prevents us from handling it.
|
||||
// Besides C-/, its important we don't process key equivalents if unfocused
|
||||
// because there are other event listeners for that (i.e. AppDelegate's
|
||||
// local event handler).
|
||||
if (!focused) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only process keys when Control is active. All known issues we're
|
||||
// resolving happen only in this scenario. This probably isn't fully robust
|
||||
// but we can broaden the scope as we find more cases.
|
||||
if (!event.modifierFlags.contains(.control)) {
|
||||
return false
|
||||
// If this event as-is would result in a key binding then we send it.
|
||||
if let surface,
|
||||
ghostty_surface_key_is_binding(
|
||||
surface,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) {
|
||||
self.keyDown(with: event)
|
||||
return true
|
||||
}
|
||||
|
||||
let equivalent: String
|
||||
@ -832,14 +899,25 @@ extension Ghostty {
|
||||
case "\r":
|
||||
// Pass C-<return> through verbatim
|
||||
// (prevent the default context menu equivalent)
|
||||
if (!event.modifierFlags.contains(.control)) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "\r"
|
||||
|
||||
case ".":
|
||||
if (!event.modifierFlags.contains(.command)) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "."
|
||||
|
||||
default:
|
||||
// Ignore other events
|
||||
return false
|
||||
}
|
||||
|
||||
let newEvent = NSEvent.keyEvent(
|
||||
let finalEvent = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: event.locationInWindow,
|
||||
modifierFlags: event.modifierFlags,
|
||||
@ -852,7 +930,7 @@ extension Ghostty {
|
||||
keyCode: event.keyCode
|
||||
)
|
||||
|
||||
self.keyDown(with: newEvent!)
|
||||
self.keyDown(with: finalEvent!)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -897,45 +975,38 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
keyAction(action, event: event)
|
||||
_ = keyAction(action, event: event)
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
return ghostty_surface_key(surface, event.ghosttyKeyEvent(action))
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) {
|
||||
guard let surface = self.surface else { return }
|
||||
private func keyAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
event: NSEvent, preedit: String
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
preedit.withCString { ptr in
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
return preedit.withCString { ptr in
|
||||
var key_ev = event.ghosttyKeyEvent(action)
|
||||
key_ev.text = ptr
|
||||
key_ev.composing = true
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, text: String) {
|
||||
guard let surface = self.surface else { return }
|
||||
private func keyAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
event: NSEvent, text: String
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
text.withCString { ptr in
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
return text.withCString { ptr in
|
||||
var key_ev = event.ghosttyKeyEvent(action)
|
||||
key_ev.text = ptr
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
}
|
||||
}
|
||||
|
||||
|
14
macos/Sources/Helpers/KeyboardLayout.swift
Normal file
14
macos/Sources/Helpers/KeyboardLayout.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Carbon
|
||||
|
||||
class KeyboardLayout {
|
||||
/// Return a string ID of the current keyboard input source.
|
||||
static var id: String? {
|
||||
if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
|
||||
let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) {
|
||||
let sourceId = unsafeBitCast(sourceIdPointer, to: CFString.self)
|
||||
return sourceId as String
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
@ -51,6 +51,9 @@
|
||||
pandoc,
|
||||
hyperfine,
|
||||
typos,
|
||||
wayland,
|
||||
wayland-scanner,
|
||||
wayland-protocols,
|
||||
}: let
|
||||
# See package.nix. Keep in sync.
|
||||
rpathLibs =
|
||||
@ -80,6 +83,7 @@
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
wayland
|
||||
];
|
||||
in
|
||||
mkShell {
|
||||
@ -153,6 +157,9 @@ in
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
wayland
|
||||
wayland-scanner
|
||||
wayland-protocols
|
||||
];
|
||||
|
||||
# This should be set onto the rpath of the ghostty binary if you want
|
||||
|
@ -10,10 +10,6 @@
|
||||
oniguruma,
|
||||
zlib,
|
||||
libGL,
|
||||
libX11,
|
||||
libXcursor,
|
||||
libXi,
|
||||
libXrandr,
|
||||
glib,
|
||||
gtk4,
|
||||
libadwaita,
|
||||
@ -26,7 +22,15 @@
|
||||
pandoc,
|
||||
revision ? "dirty",
|
||||
optimize ? "Debug",
|
||||
x11 ? true,
|
||||
enableX11 ? true,
|
||||
libX11,
|
||||
libXcursor,
|
||||
libXi,
|
||||
libXrandr,
|
||||
enableWayland ? true,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
}: let
|
||||
# The Zig hook has no way to select the release type without actual
|
||||
# overriding of the default flags.
|
||||
@ -114,14 +118,19 @@ in
|
||||
version = "1.0.2";
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
git
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
];
|
||||
nativeBuildInputs =
|
||||
[
|
||||
git
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland-scanner
|
||||
wayland-protocols
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
[
|
||||
@ -142,16 +151,19 @@ in
|
||||
glib
|
||||
gsettings-desktop-schemas
|
||||
]
|
||||
++ lib.optionals x11 [
|
||||
++ lib.optionals enableX11 [
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString x11}";
|
||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
|
||||
|
||||
preBuild = ''
|
||||
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-ot5onG1yq7EWQkNUgTNBuqvsnLuaoFs2UDS96IqgJmU="
|
||||
"sha256-eUY6MS3//r6pA/w9b+E4+YqmqUbzpUfL3afJJlnMhLY="
|
||||
|
@ -1,10 +1,10 @@
|
||||
.{
|
||||
.name = "cimgui",
|
||||
.version = "1.89.9",
|
||||
.version = "1.90.6", // -docking branch
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
// This should be kept in sync with the submodule in the cimgui source
|
||||
// code to be safe that they're compatible.
|
||||
// code in ./vendor/ to be safe that they're compatible.
|
||||
.imgui = .{
|
||||
.url = "https://github.com/ocornut/imgui/archive/e391fe2e66eb1c96b1624ae8444dc64c23146ef4.tar.gz",
|
||||
.hash = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402",
|
||||
|
@ -13,7 +13,56 @@ pub fn build(b: *std.Build) !void {
|
||||
) orelse (target.result.os.tag != .windows);
|
||||
const freetype_enabled = b.option(bool, "enable-freetype", "Build freetype") orelse true;
|
||||
|
||||
const module = b.addModule("fontconfig", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("fontconfig", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
|
||||
if (b.systemIntegrationOption("fontconfig", .{})) {
|
||||
module.linkSystemLibrary("fontconfig", dynamic_link_opts);
|
||||
test_exe.linkSystemLibrary2("fontconfig", dynamic_link_opts);
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.libxml2_enabled = libxml2_enabled,
|
||||
.libxml2_iconv_enabled = libxml2_iconv_enabled,
|
||||
.freetype_enabled = freetype_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
test_exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const libxml2_enabled = options.libxml2_enabled;
|
||||
const libxml2_iconv_enabled = options.libxml2_iconv_enabled;
|
||||
const freetype_enabled = options.freetype_enabled;
|
||||
|
||||
const upstream = b.dependency("fontconfig", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
@ -131,19 +180,13 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
// Freetype2
|
||||
_ = b.systemIntegrationOption("freetype", .{}); // So it shows up in help
|
||||
if (freetype_enabled) {
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
const freetype_dep = b.dependency(
|
||||
"freetype",
|
||||
@ -194,16 +237,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
return lib;
|
||||
}
|
||||
|
||||
const headers = &.{
|
||||
|
@ -5,7 +5,61 @@ pub fn build(b: *std.Build) !void {
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const libpng_enabled = b.option(bool, "enable-libpng", "Build libpng") orelse false;
|
||||
|
||||
const module = b.addModule("freetype", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("freetype", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var test_exe: ?*std.Build.Step.Compile = null;
|
||||
if (target.query.isNative()) {
|
||||
test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const tests_run = b.addRunArtifact(test_exe.?);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
|
||||
module.addIncludePath(b.path(""));
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
module.linkSystemLibrary("freetype2", dynamic_link_opts);
|
||||
if (test_exe) |exe| {
|
||||
exe.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
}
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.libpng_enabled = libpng_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const libpng_enabled = options.libpng_enabled;
|
||||
|
||||
const upstream = b.dependency("freetype", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
@ -21,16 +75,6 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
|
||||
module.addIncludePath(upstream.path("include"));
|
||||
module.addIncludePath(b.path(""));
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
try flags.appendSlice(&.{
|
||||
@ -44,6 +88,8 @@ pub fn build(b: *std.Build) !void {
|
||||
"-fno-sanitize=undefined",
|
||||
});
|
||||
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
// Zlib
|
||||
if (b.systemIntegrationOption("zlib", .{})) {
|
||||
lib.linkSystemLibrary2("zlib", dynamic_link_opts);
|
||||
@ -113,18 +159,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
if (target.query.isNative()) {
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
const srcs: []const []const u8 = &.{
|
||||
|
@ -14,7 +14,6 @@ pub fn build(b: *std.Build) !void {
|
||||
.@"enable-libpng" = true,
|
||||
});
|
||||
const macos = b.dependency("macos", .{ .target = target, .optimize = optimize });
|
||||
const upstream = b.dependency("harfbuzz", .{});
|
||||
|
||||
const module = b.addModule("harfbuzz", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
@ -26,6 +25,66 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
{
|
||||
var it = module.import_table.iterator();
|
||||
while (it.next()) |entry| test_exe.root_module.addImport(entry.key_ptr.*, entry.value_ptr.*);
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
test_exe.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
test_exe.linkLibrary(freetype.artifact("freetype"));
|
||||
}
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
|
||||
if (b.systemIntegrationOption("harfbuzz", .{})) {
|
||||
module.linkSystemLibrary("harfbuzz", dynamic_link_opts);
|
||||
test_exe.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.coretext_enabled = coretext_enabled,
|
||||
.freetype_enabled = freetype_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
test_exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const coretext_enabled = options.coretext_enabled;
|
||||
const freetype_enabled = options.freetype_enabled;
|
||||
|
||||
const freetype = b.dependency("freetype", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.@"enable-libpng" = true,
|
||||
});
|
||||
|
||||
const upstream = b.dependency("harfbuzz", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "harfbuzz",
|
||||
.target = target,
|
||||
@ -41,13 +100,7 @@ pub fn build(b: *std.Build) !void {
|
||||
try apple_sdk.addPaths(b, module);
|
||||
}
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
@ -102,20 +155,5 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
{
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
|
||||
var it = module.import_table.iterator();
|
||||
while (it.next()) |entry| test_exe.root_module.addImport(entry.key_ptr.*, entry.value_ptr.*);
|
||||
test_exe.linkLibrary(freetype.artifact("freetype"));
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
@ -5,36 +5,59 @@ pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const module = b.addModule("oniguruma", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("oniguruma", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const upstream = b.dependency("oniguruma", .{});
|
||||
const lib = try buildOniguruma(b, upstream, target, optimize);
|
||||
module.addIncludePath(upstream.path("src"));
|
||||
b.installArtifact(lib);
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var test_exe: ?*std.Build.Step.Compile = null;
|
||||
if (target.query.isNative()) {
|
||||
const test_exe = b.addTest(.{
|
||||
test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const tests_run = b.addRunArtifact(test_exe.?);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
|
||||
// Uncomment this if we're debugging tests
|
||||
b.installArtifact(test_exe);
|
||||
b.installArtifact(test_exe.?);
|
||||
}
|
||||
|
||||
if (b.systemIntegrationOption("oniguruma", .{})) {
|
||||
module.linkSystemLibrary("oniguruma", dynamic_link_opts);
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkSystemLibrary2("oniguruma", dynamic_link_opts);
|
||||
}
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buildOniguruma(
|
||||
b: *std.Build,
|
||||
upstream: *std.Build.Dependency,
|
||||
target: std.Build.ResolvedTarget,
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
) !*std.Build.Step.Compile {
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const upstream = b.dependency("oniguruma", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "oniguruma",
|
||||
.target = target,
|
||||
@ -43,6 +66,7 @@ fn buildOniguruma(
|
||||
const t = target.result;
|
||||
lib.linkLibC();
|
||||
lib.addIncludePath(upstream.path("src"));
|
||||
module.addIncludePath(upstream.path("src"));
|
||||
|
||||
if (target.result.isDarwin()) {
|
||||
const apple_sdk = @import("apple_sdk");
|
||||
@ -134,5 +158,7 @@ fn buildOniguruma(
|
||||
.{ .include_extensions = &.{".h"} },
|
||||
);
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
return lib;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.{
|
||||
.name = "simdutf",
|
||||
.version = "4.0.9",
|
||||
.version = "5.2.8",
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
|
@ -30,4 +30,36 @@ pub fn build(b: *std.Build) !void {
|
||||
.file = wuffs.path("release/c/wuffs-v0.4.c"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
|
||||
const unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
unit_tests.linkLibC();
|
||||
unit_tests.addIncludePath(wuffs.path("release/c"));
|
||||
unit_tests.addCSourceFile(.{
|
||||
.file = wuffs.path("release/c/wuffs-v0.4.c"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
|
||||
const pixels = b.dependency("pixels", .{});
|
||||
|
||||
inline for (.{ "000000", "FFFFFF" }) |color| {
|
||||
inline for (.{ "gif", "jpg", "png", "ppm" }) |extension| {
|
||||
const filename = std.fmt.comptimePrint("1x1#{s}.{s}", .{ color, extension });
|
||||
unit_tests.root_module.addAnonymousImport(
|
||||
filename,
|
||||
.{
|
||||
.root_source_file = pixels.path(filename),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_unit_tests.step);
|
||||
}
|
||||
|
@ -3,8 +3,13 @@
|
||||
.version = "0.0.0",
|
||||
.dependencies = .{
|
||||
.wuffs = .{
|
||||
.url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.8.tar.gz",
|
||||
.hash = "12200984439edc817fbcbbaff564020e5104a0d04a2d0f53080700827052de700462",
|
||||
.url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.9.tar.gz",
|
||||
.hash = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd",
|
||||
},
|
||||
|
||||
.pixels = .{
|
||||
.url = "git+https://github.com/make-github-pseudonymous-again/pixels?ref=main#d843c2714d32e15b48b8d7eeb480295af537f877",
|
||||
.hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806",
|
||||
},
|
||||
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
|
@ -1,3 +1,13 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
pub const Error = std.mem.Allocator.Error || error{WuffsError};
|
||||
|
||||
pub fn check(log: anytype, status: *const c.struct_wuffs_base__status__struct) error{WuffsError}!void {
|
||||
if (!c.wuffs_base__status__is_ok(status)) {
|
||||
const e = c.wuffs_base__status__message(status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
}
|
||||
|
143
pkg/wuffs/src/jpeg.zig
Normal file
143
pkg/wuffs/src/jpeg.zig
Normal file
@ -0,0 +1,143 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const Error = @import("error.zig").Error;
|
||||
const check = @import("error.zig").check;
|
||||
const ImageData = @import("main.zig").ImageData;
|
||||
|
||||
const log = std.log.scoped(.wuffs_jpeg);
|
||||
|
||||
/// Decode a JPEG image.
|
||||
pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
// Work around some weirdness in WUFFS/Zig, there are some structs that
|
||||
// are defined as "extern" by the Zig compiler which means that Zig won't
|
||||
// allocate them on the stack at compile time. WUFFS has functions for
|
||||
// dynamically allocating these structs but they use the C malloc/free. This
|
||||
// gets around that by using the Zig allocator to allocate enough memory for
|
||||
// the struct and then casts it to the appropriate pointer.
|
||||
|
||||
const decoder_buf = try alloc.alloc(u8, c.sizeof__wuffs_jpeg__decoder());
|
||||
defer alloc.free(decoder_buf);
|
||||
|
||||
const decoder: ?*c.wuffs_jpeg__decoder = @ptrCast(decoder_buf);
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__initialize(
|
||||
decoder,
|
||||
c.sizeof__wuffs_jpeg__decoder(),
|
||||
c.WUFFS_VERSION,
|
||||
0,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var source_buffer: c.wuffs_base__io_buffer = .{
|
||||
.data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len },
|
||||
.meta = .{
|
||||
.wi = data.len,
|
||||
.ri = 0,
|
||||
.pos = 0,
|
||||
.closed = true,
|
||||
},
|
||||
};
|
||||
|
||||
var image_config: c.wuffs_base__image_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_image_config(
|
||||
decoder,
|
||||
&image_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
const width = c.wuffs_base__pixel_config__width(&image_config.pixcfg);
|
||||
const height = c.wuffs_base__pixel_config__height(&image_config.pixcfg);
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
const destination = try alloc.alloc(
|
||||
u8,
|
||||
width * height * @sizeOf(c.wuffs_base__color_u32_argb_premul),
|
||||
);
|
||||
errdefer alloc.free(destination);
|
||||
|
||||
// temporary buffer for intermediate processing of image
|
||||
const work_buffer = try alloc.alloc(
|
||||
u8,
|
||||
|
||||
// The type of this is a u64 on all systems but our allocator
|
||||
// uses a usize which is a u32 on 32-bit systems.
|
||||
std.math.cast(
|
||||
usize,
|
||||
c.wuffs_jpeg__decoder__workbuf_len(decoder).max_incl,
|
||||
) orelse return error.OutOfMemory,
|
||||
);
|
||||
defer alloc.free(work_buffer);
|
||||
|
||||
const work_slice = c.wuffs_base__make_slice_u8(
|
||||
work_buffer.ptr,
|
||||
work_buffer.len,
|
||||
);
|
||||
|
||||
var pixel_buffer: c.wuffs_base__pixel_buffer = undefined;
|
||||
{
|
||||
const status = c.wuffs_base__pixel_buffer__set_from_slice(
|
||||
&pixel_buffer,
|
||||
&image_config.pixcfg,
|
||||
c.wuffs_base__make_slice_u8(destination.ptr, destination.len),
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame_config(
|
||||
decoder,
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame(
|
||||
decoder,
|
||||
&pixel_buffer,
|
||||
&source_buffer,
|
||||
c.WUFFS_BASE__PIXEL_BLEND__SRC,
|
||||
work_slice,
|
||||
null,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
return .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.data = destination,
|
||||
};
|
||||
}
|
||||
|
||||
test "jpeg_decode_000000" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#000000.jpg"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 255 }, data.data);
|
||||
}
|
||||
|
||||
test "jpeg_decode_FFFFFF" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#FFFFFF.jpg"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, data.data);
|
||||
}
|
@ -1,2 +1,15 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const png = @import("png.zig");
|
||||
pub const jpeg = @import("jpeg.zig");
|
||||
pub const swizzle = @import("swizzle.zig");
|
||||
|
||||
pub const ImageData = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const Error = @import("error.zig").Error;
|
||||
const check = @import("error.zig").check;
|
||||
const ImageData = @import("main.zig").ImageData;
|
||||
|
||||
const log = std.log.scoped(.wuffs_png);
|
||||
|
||||
/// Decode a PNG image.
|
||||
pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []const u8,
|
||||
} {
|
||||
pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
// Work around some weirdness in WUFFS/Zig, there are some structs that
|
||||
// are defined as "extern" by the Zig compiler which means that Zig won't
|
||||
// allocate them on the stack at compile time. WUFFS has functions for
|
||||
@ -29,11 +27,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
c.WUFFS_VERSION,
|
||||
0,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var source_buffer: c.wuffs_base__io_buffer = .{
|
||||
@ -53,11 +47,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
&image_config,
|
||||
&source_buffer,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
const width = c.wuffs_base__pixel_config__width(&image_config.pixcfg);
|
||||
@ -102,11 +92,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
&image_config.pixcfg,
|
||||
c.wuffs_base__make_slice_u8(destination.ptr, destination.len),
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
@ -116,11 +102,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
@ -132,11 +114,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
work_slice,
|
||||
null,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
return .{
|
||||
@ -145,3 +123,21 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
.data = destination,
|
||||
};
|
||||
}
|
||||
|
||||
test "png_decode_000000" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#000000.png"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 255 }, data.data);
|
||||
}
|
||||
|
||||
test "png_decode_FFFFFF" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#FFFFFF.png"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, data.data);
|
||||
}
|
||||
|
44
src/App.zig
44
src/App.zig
@ -54,9 +54,6 @@ focused_surface: ?*Surface = null,
|
||||
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||
mailbox: Mailbox.Queue,
|
||||
|
||||
/// Set to true once we're quitting. This never goes false again.
|
||||
quit: bool,
|
||||
|
||||
/// The set of font GroupCache instances shared by surfaces with the
|
||||
/// same font configuration.
|
||||
font_grid_set: font.SharedGridSet,
|
||||
@ -98,7 +95,6 @@ pub fn create(
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.quit = false,
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
};
|
||||
@ -125,9 +121,7 @@ pub fn destroy(self: *App) void {
|
||||
/// Tick ticks the app loop. This will drain our mailbox and process those
|
||||
/// events. This should be called by the application runtime on every loop
|
||||
/// tick.
|
||||
///
|
||||
/// This returns whether the app should quit or not.
|
||||
pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
pub fn tick(self: *App, rt_app: *apprt.App) !void {
|
||||
// If any surfaces are closing, destroy them
|
||||
var i: usize = 0;
|
||||
while (i < self.surfaces.items.len) {
|
||||
@ -142,13 +136,6 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
|
||||
// Drain our mailbox
|
||||
try self.drainMailbox(rt_app);
|
||||
|
||||
// No matter what, we reset the quit flag after a tick. If the apprt
|
||||
// doesn't want to quit, then we can't force it to.
|
||||
defer self.quit = false;
|
||||
|
||||
// We quit if our quit flag is on
|
||||
return self.quit;
|
||||
}
|
||||
|
||||
/// Update the configuration associated with the app. This can only be
|
||||
@ -272,7 +259,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
// can try to quit as quickly as possible.
|
||||
.quit => {
|
||||
log.info("quit message received, short circuiting mailbox drain", .{});
|
||||
self.setQuit();
|
||||
try self.performAction(rt_app, .quit);
|
||||
return;
|
||||
},
|
||||
}
|
||||
@ -314,12 +301,6 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
);
|
||||
}
|
||||
|
||||
/// Start quitting
|
||||
pub fn setQuit(self: *App) void {
|
||||
if (self.quit) return;
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// Handle an app-level focus event. This should be called whenever
|
||||
/// the focus state of the entire app containing Ghostty changes.
|
||||
/// This is separate from surface focus events. See the `focused`
|
||||
@ -332,6 +313,25 @@ pub fn focusEvent(self: *App, focused: bool) void {
|
||||
self.focused = focused;
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
_ = self;
|
||||
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return rt_app.config.keybind.set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Handle a key event at the app-scope. If this key event is used,
|
||||
/// this will return true and the caller shouldn't continue processing
|
||||
/// the event. If the event is not used, this will return false.
|
||||
@ -437,7 +437,7 @@ pub fn performAction(
|
||||
switch (action) {
|
||||
.unbind => unreachable,
|
||||
.ignore => {},
|
||||
.quit => self.setQuit(),
|
||||
.quit => try rt_app.performAction(.app, .quit, {}),
|
||||
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
||||
.open_config => try rt_app.performAction(.app, .open_config, {}),
|
||||
.reload_config => try rt_app.performAction(.app, .reload_config, .{}),
|
||||
|
@ -18,6 +18,7 @@ const Command = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const global_state = &@import("global.zig").state;
|
||||
const internal_os = @import("os/main.zig");
|
||||
const windows = internal_os.windows;
|
||||
const TempDir = internal_os.TempDir;
|
||||
@ -175,6 +176,10 @@ fn startPosix(self: *Command, arena: Allocator) !void {
|
||||
// We don't log because that'll show up in the output.
|
||||
};
|
||||
|
||||
// Restore any rlimits that were set by Ghostty. This might fail but
|
||||
// any failures are ignored (its best effort).
|
||||
global_state.rlimits.restore();
|
||||
|
||||
// If the user requested a pre exec callback, call it now.
|
||||
if (self.pre_exec) |f| f(self);
|
||||
|
||||
|
@ -1156,7 +1156,6 @@ pub fn updateConfig(
|
||||
}
|
||||
|
||||
// If we are in the middle of a key sequence, clear it.
|
||||
self.keyboard.bindings = null;
|
||||
self.endKeySequence(.drop, .free);
|
||||
|
||||
// Before sending any other config changes, we give the renderer a new font
|
||||
@ -1638,6 +1637,31 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
///
|
||||
/// Note that this function does not check if the binding itself
|
||||
/// is performable, only if the key event would trigger a binding.
|
||||
/// If a performable binding is found and the event is not performable,
|
||||
/// then Ghosty will act as though the binding does not exist.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// Our keybinding set is either our current nested set (for
|
||||
// sequences) or the root set.
|
||||
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Called for any key events. This handles keybindings, encoding and
|
||||
/// sending to the terminal, etc.
|
||||
pub fn keyCallback(
|
||||
@ -1853,9 +1877,6 @@ fn maybeHandleBinding(
|
||||
if (self.keyboard.bindings != null and
|
||||
!event.key.modifier())
|
||||
{
|
||||
// Reset to the root set
|
||||
self.keyboard.bindings = null;
|
||||
|
||||
// Encode everything up to this point
|
||||
self.endKeySequence(.flush, .retain);
|
||||
}
|
||||
@ -1941,10 +1962,21 @@ fn maybeHandleBinding(
|
||||
return .closed;
|
||||
}
|
||||
|
||||
// If we have the performable flag and the action was not performed,
|
||||
// then we act as though a binding didn't exist.
|
||||
if (leaf.flags.performable and !performed) {
|
||||
// If we're in a sequence, we treat this as if we pressed a key
|
||||
// that doesn't exist in the sequence. Reset our sequence and flush
|
||||
// any queued events.
|
||||
self.endKeySequence(.flush, .retain);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we consume this event, then we are done. If we don't consume
|
||||
// it, we processed the action but we still want to process our
|
||||
// encodings, too.
|
||||
if (performed and consumed) {
|
||||
if (consumed) {
|
||||
// If we had queued events, we deinit them since we consumed
|
||||
self.endKeySequence(.drop, .retain);
|
||||
|
||||
@ -1986,6 +2018,10 @@ fn endKeySequence(
|
||||
);
|
||||
};
|
||||
|
||||
// No matter what we clear our current binding set. This restores
|
||||
// the set we look at to the root set.
|
||||
self.keyboard.bindings = null;
|
||||
|
||||
if (self.keyboard.queued.items.len > 0) {
|
||||
switch (action) {
|
||||
.flush => for (self.keyboard.queued.items) |write_req| {
|
||||
@ -3889,7 +3925,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
.paste_from_clipboard => try self.startClipboardRequest(
|
||||
|
@ -70,6 +70,9 @@ pub const Action = union(Key) {
|
||||
// entry. If the value type is void then only the key needs to be
|
||||
// added. Ensure the order matches exactly with the Zig code.
|
||||
|
||||
/// Quit the application.
|
||||
quit,
|
||||
|
||||
/// Open a new window. The target determines whether properties such
|
||||
/// as font size should be inherited.
|
||||
new_window,
|
||||
@ -219,6 +222,7 @@ pub const Action = union(Key) {
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
new_window,
|
||||
new_tab,
|
||||
new_split,
|
||||
@ -332,9 +336,9 @@ pub const GotoSplit = enum(c_int) {
|
||||
previous,
|
||||
next,
|
||||
|
||||
top,
|
||||
up,
|
||||
left,
|
||||
bottom,
|
||||
down,
|
||||
right,
|
||||
};
|
||||
|
||||
|
@ -147,12 +147,12 @@ pub const App = struct {
|
||||
self.core_app.focusEvent(focused);
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
/// Convert a C key event into a Zig key event.
|
||||
fn coreKeyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
) !?input.KeyEvent {
|
||||
const action = event.action;
|
||||
const keycode = event.keycode;
|
||||
const mods = event.mods;
|
||||
@ -243,7 +243,7 @@ pub const App = struct {
|
||||
result.text,
|
||||
) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@ -251,7 +251,7 @@ pub const App = struct {
|
||||
.app => {},
|
||||
.surface => |surface| surface.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
@ -335,7 +335,7 @@ pub const App = struct {
|
||||
} else .invalid;
|
||||
|
||||
// Build our final key event
|
||||
const input_event: input.KeyEvent = .{
|
||||
return .{
|
||||
.action = action,
|
||||
.key = key,
|
||||
.physical_key = physical_key,
|
||||
@ -345,24 +345,39 @@ pub const App = struct {
|
||||
.utf8 = result.text,
|
||||
.unshifted_codepoint = unshifted_codepoint,
|
||||
};
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
// Convert our C key event into a Zig one.
|
||||
const input_event: input.KeyEvent = (try self.coreKeyEvent(
|
||||
target,
|
||||
event,
|
||||
)) orelse return false;
|
||||
|
||||
// Invoke the core Ghostty logic to handle this input.
|
||||
const effect: CoreSurface.InputEffect = switch (target) {
|
||||
.app => if (self.core_app.keyEvent(
|
||||
self,
|
||||
input_event,
|
||||
))
|
||||
.consumed
|
||||
else
|
||||
.ignored,
|
||||
)) .consumed else .ignored,
|
||||
|
||||
.surface => |surface| try surface.core_surface.keyCallback(input_event),
|
||||
.surface => |surface| try surface.core_surface.keyCallback(
|
||||
input_event,
|
||||
),
|
||||
};
|
||||
|
||||
return switch (effect) {
|
||||
.closed => true,
|
||||
.ignored => false,
|
||||
.consumed => consumed: {
|
||||
const is_down = input_event.action == .press or
|
||||
input_event.action == .repeat;
|
||||
|
||||
if (is_down) {
|
||||
// If we consume the key then we want to reset the dead
|
||||
// key state.
|
||||
@ -1332,10 +1347,9 @@ pub const CAPI = struct {
|
||||
|
||||
/// Tick the event loop. This should be called whenever the "wakeup"
|
||||
/// callback is invoked for the runtime.
|
||||
export fn ghostty_app_tick(v: *App) bool {
|
||||
return v.core_app.tick(v) catch |err| err: {
|
||||
export fn ghostty_app_tick(v: *App) void {
|
||||
v.core_app.tick(v) catch |err| {
|
||||
log.err("error app tick err={}", .{err});
|
||||
break :err false;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1372,6 +1386,28 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_app_key_is_binding(
|
||||
app: *App,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = app.coreKeyEvent(
|
||||
.app,
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return app.core_app.keyEventIsBinding(app, core_event);
|
||||
}
|
||||
|
||||
/// Notify the app that the keyboard was changed. This causes the
|
||||
/// keyboard layout to be reloaded from the OS.
|
||||
export fn ghostty_app_keyboard_changed(v: *App) void {
|
||||
@ -1592,16 +1628,38 @@ pub const CAPI = struct {
|
||||
export fn ghostty_surface_key(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) void {
|
||||
_ = surface.app.keyEvent(
|
||||
) bool {
|
||||
return surface.app.keyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_surface_key_is_binding(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = surface.app.coreKeyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return surface.core_surface.keyEventIsBinding(core_event);
|
||||
}
|
||||
|
||||
/// Send raw text to the terminal. This is treated like a paste
|
||||
/// so this isn't useful for sending escape sequences. For that,
|
||||
/// individual key input should be used.
|
||||
@ -1895,7 +1953,7 @@ pub const CAPI = struct {
|
||||
_ = CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||
@intCast(config.@"background-blur-radius"),
|
||||
@intCast(config.@"background-blur-radius".cval()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,10 @@ pub const App = struct {
|
||||
app: *CoreApp,
|
||||
config: Config,
|
||||
|
||||
/// Flips to true to quit on the next event loop tick. This
|
||||
/// never goes false and forces the event loop to exit.
|
||||
quit: bool = false,
|
||||
|
||||
/// Mac-specific state.
|
||||
darwin: if (Darwin.enabled) Darwin else void,
|
||||
|
||||
@ -124,8 +128,10 @@ pub const App = struct {
|
||||
glfw.waitEvents();
|
||||
|
||||
// Tick the terminal app
|
||||
const should_quit = try self.app.tick(self);
|
||||
if (should_quit or self.app.surfaces.items.len == 0) {
|
||||
try self.app.tick(self);
|
||||
|
||||
// If the tick caused us to quit, then we're done.
|
||||
if (self.quit or self.app.surfaces.items.len == 0) {
|
||||
for (self.app.surfaces.items) |surface| {
|
||||
surface.close(false);
|
||||
}
|
||||
@ -149,6 +155,8 @@ pub const App = struct {
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.quit => self.quit = true,
|
||||
|
||||
.new_window => _ = try self.newSurface(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
@ -510,6 +518,13 @@ pub const Surface = struct {
|
||||
) orelse return glfw.mustGetErrorCode();
|
||||
errdefer win.destroy();
|
||||
|
||||
// Setup our
|
||||
setInitialWindowPosition(
|
||||
win,
|
||||
app.config.@"window-position-x",
|
||||
app.config.@"window-position-y",
|
||||
);
|
||||
|
||||
// Get our physical DPI - debug only because we don't have a use for
|
||||
// this but the logging of it may be useful
|
||||
if (builtin.mode == .Debug) {
|
||||
@ -663,6 +678,17 @@ pub const Surface = struct {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the initial window position. This is called exactly once at
|
||||
/// surface initialization time. This may be called before "self"
|
||||
/// is fully initialized.
|
||||
fn setInitialWindowPosition(win: glfw.Window, x: ?i16, y: ?i16) void {
|
||||
const start_position_x = x orelse return;
|
||||
const start_position_y = y orelse return;
|
||||
|
||||
log.debug("setting initial window position ({},{})", .{ start_position_x, start_position_y });
|
||||
win.setPos(.{ .x = start_position_x, .y = start_position_y });
|
||||
}
|
||||
|
||||
/// Set the size limits of the window.
|
||||
/// Note: this interface is not good, we should redo it if we plan
|
||||
/// to use this more. i.e. you can't set max width but no max height,
|
||||
|
@ -37,6 +37,7 @@ const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const wayland = @import("wayland.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -73,6 +74,9 @@ running: bool = true,
|
||||
/// Xkb state (X11 only). Will be null on Wayland.
|
||||
x11_xkb: ?x11.Xkb = null,
|
||||
|
||||
/// Wayland app state. Will be null on X11.
|
||||
wayland: ?wayland.AppState = null,
|
||||
|
||||
/// The base path of the transient cgroup used to put all surfaces
|
||||
/// into their own cgroup. This is only set if cgroups are enabled
|
||||
/// and initialization was successful.
|
||||
@ -81,6 +85,9 @@ transient_cgroup_base: ?[]const u8 = null,
|
||||
/// CSS Provider for any styles based on ghostty configuration values
|
||||
css_provider: *c.GtkCssProvider,
|
||||
|
||||
/// Providers for loading custom stylesheets defined by user
|
||||
custom_css_providers: std.ArrayListUnmanaged(*c.GtkCssProvider) = .{},
|
||||
|
||||
/// The timer used to quit the application after the last window is closed.
|
||||
quit_timer: union(enum) {
|
||||
off: void,
|
||||
@ -108,7 +115,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan");
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl");
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-no-fractional");
|
||||
} else if (version.atLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
@ -123,7 +130,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// - "vulkan-disable" - disable Vulkan, Ghostty can't use Vulkan
|
||||
// and initializing a Vulkan context was causing a longer delay
|
||||
// on some systems.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable");
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable,gl-no-fractional");
|
||||
} else {
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
@ -394,6 +401,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
break :x11_xkb try x11.Xkb.init(display);
|
||||
};
|
||||
|
||||
// Initialize Wayland state
|
||||
var wl = wayland.AppState.init(display);
|
||||
if (wl) |*w| try w.register();
|
||||
|
||||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if the config allows it (this allows
|
||||
// for launching Ghostty in the "background" without immediately opening
|
||||
@ -419,6 +430,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
.ctx = ctx,
|
||||
.cursor_none = cursor_none,
|
||||
.x11_xkb = x11_xkb,
|
||||
.wayland = wl,
|
||||
.single_instance = single_instance,
|
||||
// If we are NOT the primary instance, then we never want to run.
|
||||
// This means that another instance of the GTK app is running and
|
||||
@ -441,6 +453,11 @@ pub fn terminate(self: *App) void {
|
||||
if (self.context_menu) |context_menu| c.g_object_unref(context_menu);
|
||||
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
|
||||
|
||||
for (self.custom_css_providers.items) |provider| {
|
||||
c.g_object_unref(provider);
|
||||
}
|
||||
self.custom_css_providers.deinit(self.core_app.alloc);
|
||||
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
@ -452,6 +469,7 @@ pub fn performAction(
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.quit => self.quit(),
|
||||
.new_window => _ = try self.newWindow(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
@ -786,6 +804,7 @@ fn setInitialSize(
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn showDesktopNotification(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
@ -828,9 +847,11 @@ fn configChange(
|
||||
new_config: *const Config,
|
||||
) void {
|
||||
switch (target) {
|
||||
// We don't do anything for surface config change events. There
|
||||
// is nothing to sync with regards to a surface today.
|
||||
.surface => {},
|
||||
.surface => |surface| {
|
||||
if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| {
|
||||
log.warn("error syncing appearance changes to window err={}", .{err});
|
||||
};
|
||||
},
|
||||
|
||||
.app => {
|
||||
// We clone (to take ownership) and update our configuration.
|
||||
@ -892,7 +913,7 @@ fn syncConfigChanges(self: *App) !void {
|
||||
try self.updateConfigErrors();
|
||||
try self.syncActionAccelerators();
|
||||
|
||||
// Load our runtime CSS. If this fails then our window is just stuck
|
||||
// Load our runtime and custom CSS. If this fails then our window is just stuck
|
||||
// with the old CSS but we don't want to fail the entire sync operation.
|
||||
self.loadRuntimeCss() catch |err| switch (err) {
|
||||
error.OutOfMemory => log.warn(
|
||||
@ -900,6 +921,9 @@ fn syncConfigChanges(self: *App) !void {
|
||||
.{},
|
||||
),
|
||||
};
|
||||
self.loadCustomCss() catch |err| {
|
||||
log.warn("Failed to load custom CSS, no custom CSS applied, err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// This should be called whenever the configuration changes to update
|
||||
@ -983,6 +1007,27 @@ fn loadRuntimeCss(
|
||||
unfocused_fill.b,
|
||||
});
|
||||
|
||||
if (config.@"split-divider-color") |color| {
|
||||
try writer.print(
|
||||
\\.terminal-window .notebook separator {{
|
||||
\\ color: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\ background: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\}}
|
||||
, .{
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
.b = color.b,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.@"window-title-font-family") |font_family| {
|
||||
try writer.print(
|
||||
\\.window headerbar {{
|
||||
\\ font-family: "{[font_family]s}";
|
||||
\\}}
|
||||
, .{ .font_family = font_family });
|
||||
}
|
||||
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
@ -1033,11 +1078,66 @@ fn loadRuntimeCss(
|
||||
}
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
c.gtk_css_provider_load_from_data(
|
||||
self.css_provider,
|
||||
buf.items.ptr,
|
||||
@intCast(buf.items.len),
|
||||
);
|
||||
loadCssProviderFromData(self.css_provider, buf.items);
|
||||
}
|
||||
|
||||
fn loadCustomCss(self: *App) !void {
|
||||
const display = c.gdk_display_get_default();
|
||||
|
||||
// unload the previously loaded style providers
|
||||
for (self.custom_css_providers.items) |provider| {
|
||||
c.gtk_style_context_remove_provider_for_display(
|
||||
display,
|
||||
@ptrCast(provider),
|
||||
);
|
||||
c.g_object_unref(provider);
|
||||
}
|
||||
self.custom_css_providers.clearRetainingCapacity();
|
||||
|
||||
for (self.config.@"gtk-custom-css".value.items) |p| {
|
||||
const path, const optional = switch (p) {
|
||||
.optional => |path| .{ path, true },
|
||||
.required => |path| .{ path, false },
|
||||
};
|
||||
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
|
||||
if (err != error.FileNotFound or !optional) {
|
||||
log.err("error opening gtk-custom-css file {s}: {}", .{ path, err });
|
||||
}
|
||||
continue;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
log.info("loading gtk-custom-css path={s}", .{path});
|
||||
const contents = try file.reader().readAllAlloc(self.core_app.alloc, 5 * 1024 * 1024 // 5MB
|
||||
);
|
||||
defer self.core_app.alloc.free(contents);
|
||||
|
||||
const provider = c.gtk_css_provider_new();
|
||||
c.gtk_style_context_add_provider_for_display(
|
||||
display,
|
||||
@ptrCast(provider),
|
||||
c.GTK_STYLE_PROVIDER_PRIORITY_USER,
|
||||
);
|
||||
|
||||
loadCssProviderFromData(provider, contents);
|
||||
|
||||
try self.custom_css_providers.append(self.core_app.alloc, provider);
|
||||
}
|
||||
}
|
||||
|
||||
fn loadCssProviderFromData(provider: *c.GtkCssProvider, data: []const u8) void {
|
||||
if (version.atLeast(4, 12, 0)) {
|
||||
const g_bytes = c.g_bytes_new(data.ptr, data.len);
|
||||
defer c.g_bytes_unref(g_bytes);
|
||||
|
||||
c.gtk_css_provider_load_from_bytes(provider, g_bytes);
|
||||
} else {
|
||||
c.gtk_css_provider_load_from_data(
|
||||
provider,
|
||||
data.ptr,
|
||||
@intCast(data.len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by CoreApp to wake up the event loop.
|
||||
@ -1105,14 +1205,10 @@ pub fn run(self: *App) !void {
|
||||
_ = c.g_main_context_iteration(self.ctx, 1);
|
||||
|
||||
// Tick the terminal app and see if we should quit.
|
||||
const should_quit = try self.core_app.tick(self);
|
||||
try self.core_app.tick(self);
|
||||
|
||||
// Check if we must quit based on the current state.
|
||||
const must_quit = q: {
|
||||
// If we've been told by GTK that we should quit, do so regardless
|
||||
// of any other setting.
|
||||
if (should_quit) break :q true;
|
||||
|
||||
// If we are configured to always stay running, don't quit.
|
||||
if (!self.config.@"quit-after-last-window-closed") break :q false;
|
||||
|
||||
@ -1216,6 +1312,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
}
|
||||
|
||||
fn quit(self: *App) void {
|
||||
// If we're already not running, do nothing.
|
||||
if (!self.running) return;
|
||||
|
||||
// If we have no toplevel windows, then we're done.
|
||||
const list = c.gtk_window_list_toplevels();
|
||||
if (list == null) {
|
||||
@ -1556,7 +1655,9 @@ fn gtkActionQuit(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
self.core_app.setQuit();
|
||||
self.core_app.performAction(self, .quit) catch |err| {
|
||||
log.err("error quitting err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Action sent by the window manager asking us to present a specific surface to
|
||||
@ -1575,7 +1676,7 @@ fn gtkActionPresentSurface(
|
||||
|
||||
// Convert that u64 to pointer to a core surface. A value of zero
|
||||
// means that there was no target surface for the notification so
|
||||
// we dont' focus any surface.
|
||||
// we don't focus any surface.
|
||||
const ptr_int: u64 = c.g_variant_get_uint64(parameter);
|
||||
if (ptr_int == 0) return;
|
||||
const surface: *CoreSurface = @ptrFromInt(ptr_int);
|
||||
|
@ -64,6 +64,8 @@ fn init(
|
||||
c.gtk_window_set_title(gtk_window, titleText(request));
|
||||
c.gtk_window_set_default_size(gtk_window, 550, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "clipboard-confirmation-window");
|
||||
_ = c.g_signal_connect_data(
|
||||
window,
|
||||
"destroy",
|
||||
|
@ -55,6 +55,8 @@ fn init(self: *ConfigErrors, app: *App) !void {
|
||||
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "config-errors-window");
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Set some state
|
||||
|
@ -111,16 +111,6 @@ pub fn init(
|
||||
// Keep a long-lived reference, which we unref in destroy.
|
||||
_ = c.g_object_ref(paned);
|
||||
|
||||
// Clicks
|
||||
const gesture_click = c.gtk_gesture_click_new();
|
||||
errdefer c.g_object_unref(gesture_click);
|
||||
c.gtk_event_controller_set_propagation_phase(@ptrCast(gesture_click), c.GTK_PHASE_CAPTURE);
|
||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_click), 1);
|
||||
c.gtk_widget_add_controller(paned, @ptrCast(gesture_click));
|
||||
|
||||
// Signals
|
||||
_ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Update all of our containers to point to the right place.
|
||||
// The split has to point to where the sibling pointed to because
|
||||
// we're inheriting its parent. The sibling points to its location
|
||||
@ -246,19 +236,6 @@ pub fn equalize(self: *Split) f64 {
|
||||
return weight;
|
||||
}
|
||||
|
||||
fn gtkMouseDown(
|
||||
_: *c.GtkGestureClick,
|
||||
n_press: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
if (n_press == 2) {
|
||||
const self: *Split = @ptrCast(@alignCast(ud));
|
||||
_ = equalize(self);
|
||||
}
|
||||
}
|
||||
|
||||
// maxPosition returns the maximum position of the GtkPaned, which is the
|
||||
// "max-position" attribute.
|
||||
fn maxPosition(self: *Split) f64 {
|
||||
@ -339,7 +316,7 @@ pub fn directionMap(self: *const Split, from: Side) DirectionMap {
|
||||
// This behavior matches the behavior of macOS at the time of writing
|
||||
// this. There is an open issue (#524) to make this depend on the
|
||||
// actual physical location of the current split.
|
||||
result.put(.top, prev.surface);
|
||||
result.put(.up, prev.surface);
|
||||
result.put(.left, prev.surface);
|
||||
}
|
||||
}
|
||||
@ -347,7 +324,7 @@ pub fn directionMap(self: *const Split, from: Side) DirectionMap {
|
||||
if (self.directionNext(from)) |next| {
|
||||
result.put(.next, next.surface);
|
||||
if (!next.wrapped) {
|
||||
result.put(.bottom, next.surface);
|
||||
result.put(.down, next.surface);
|
||||
result.put(.right, next.surface);
|
||||
}
|
||||
}
|
||||
|
@ -794,10 +794,11 @@ pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
||||
// can support fractional scaling.
|
||||
const gtk_scale: f32 = @floatFromInt(c.gtk_widget_get_scale_factor(@ptrCast(self.gl_area)));
|
||||
|
||||
// If we are on X11, we also have to scale using Xft.dpi
|
||||
const xft_dpi_scale = if (!x11.is_current_display_server()) 1.0 else xft_scale: {
|
||||
// Here we use GTK to retrieve gtk-xft-dpi, which is Xft.dpi multiplied
|
||||
// by 1024. See https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
|
||||
// Also scale using font-specific DPI, which is often exposed to the user
|
||||
// via DE accessibility settings (see https://docs.gtk.org/gtk4/class.Settings.html).
|
||||
const xft_dpi_scale = xft_scale: {
|
||||
// gtk-xft-dpi is font DPI multiplied by 1024. See
|
||||
// https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
|
||||
const settings = c.gtk_settings_get_default();
|
||||
|
||||
var value: c.GValue = std.mem.zeroes(c.GValue);
|
||||
@ -806,10 +807,9 @@ pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
||||
c.g_object_get_property(@ptrCast(@alignCast(settings)), "gtk-xft-dpi", &value);
|
||||
const gtk_xft_dpi = c.g_value_get_int(&value);
|
||||
|
||||
// As noted above Xft.dpi is multiplied by 1024, so we divide by 1024,
|
||||
// then divide by the default value of Xft.dpi (96) to derive a scale.
|
||||
// Note that gtk-xft-dpi can be fractional, so we use floating point
|
||||
// math here.
|
||||
// As noted above gtk-xft-dpi is multiplied by 1024, so we divide by
|
||||
// 1024, then divide by the default value (96) to derive a scale. Note
|
||||
// gtk-xft-dpi can be fractional, so we use floating point math here.
|
||||
const xft_dpi: f32 = @as(f32, @floatFromInt(gtk_xft_dpi)) / 1024;
|
||||
break :xft_scale xft_dpi / 96;
|
||||
};
|
||||
@ -1080,6 +1080,13 @@ pub fn setClipboardString(
|
||||
if (!confirm) {
|
||||
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
||||
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
||||
// We only toast if we are copying to the standard clipboard.
|
||||
if (clipboard_type == .standard and
|
||||
self.app.config.@"adw-toast".@"clipboard-copy")
|
||||
{
|
||||
if (self.container.window()) |window|
|
||||
window.sendToast("Copied to clipboard");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1426,15 +1433,23 @@ fn gtkMouseMotion(
|
||||
.y = @floatCast(scaled.y),
|
||||
};
|
||||
|
||||
// Our pos changed, update
|
||||
self.cursor_pos = pos;
|
||||
// When the GLArea is resized under the mouse, GTK issues a mouse motion
|
||||
// event. This has the unfortunate side effect of causing focus to potentially
|
||||
// change when `focus-follows-mouse` is enabled. To prevent this, we check
|
||||
// if the cursor is still in the same place as the last event and only grab
|
||||
// focus if it has moved.
|
||||
const is_cursor_still = @abs(self.cursor_pos.x - pos.x) < 1 and
|
||||
@abs(self.cursor_pos.y - pos.y) < 1;
|
||||
|
||||
// If we don't have focus, and we want it, grab it.
|
||||
const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
|
||||
if (!is_cursor_still and c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
// Our pos changed, update
|
||||
self.cursor_pos = pos;
|
||||
|
||||
// Get our modifiers
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
|
@ -76,7 +76,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
||||
|
||||
// Set the userdata of the box to point to this tab.
|
||||
c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self);
|
||||
try window.notebook.addTab(self, "Ghostty");
|
||||
window.notebook.addTab(self, "Ghostty");
|
||||
|
||||
// Attach all events
|
||||
_ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
@ -25,6 +25,7 @@ const gtk_key = @import("key.zig");
|
||||
const Notebook = @import("notebook.zig").Notebook;
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
const version = @import("version.zig");
|
||||
const wayland = @import("wayland.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@ -55,6 +56,8 @@ toast_overlay: ?*c.GtkWidget,
|
||||
/// See adwTabOverviewOpen for why we have this.
|
||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||
|
||||
wayland: ?wayland.SurfaceState,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
// allocations but windows and other GUI requirements are so minimal
|
||||
@ -79,6 +82,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
.notebook = undefined,
|
||||
.context_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
.wayland = null,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
@ -99,6 +103,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
self.window = gtk_window;
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "terminal-window");
|
||||
|
||||
// GTK4 grabs F10 input by default to focus the menubar icon. We want
|
||||
// to disable this so that terminal programs can capture F10 (such as htop)
|
||||
@ -113,21 +119,16 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
|
||||
}
|
||||
|
||||
// Remove the window's background if any of the widgets need to be transparent
|
||||
if (app.config.@"background-opacity" < 1) {
|
||||
c.gtk_widget_remove_css_class(@ptrCast(window), "background");
|
||||
}
|
||||
|
||||
// Create our box which will hold our widgets in the main content area.
|
||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
// Setup our notebook
|
||||
self.notebook = Notebook.create(self);
|
||||
self.notebook.init();
|
||||
|
||||
// If we are using Adwaita, then we can support the tab overview.
|
||||
self.tab_overview = if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&self.app.config) and adwaita.versionAtLeast(1, 4, 0)) overview: {
|
||||
const tab_overview = c.adw_tab_overview_new();
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
|
||||
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
|
||||
_ = c.g_signal_connect_data(
|
||||
tab_overview,
|
||||
@ -189,7 +190,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
.hidden => btn: {
|
||||
const btn = c.adw_tab_button_new();
|
||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw_tab_view);
|
||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
|
||||
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
||||
break :btn btn;
|
||||
},
|
||||
@ -267,8 +268,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// If we have a tab overview then we can set it on our notebook.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable;
|
||||
assert(self.notebook == .adw_tab_view);
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
|
||||
assert(self.notebook == .adw);
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
|
||||
}
|
||||
|
||||
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
|
||||
@ -288,6 +289,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// All of our events
|
||||
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
|
||||
@ -305,7 +307,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
if (self.app.config.@"gtk-tabs-location" != .hidden) {
|
||||
const tab_bar = c.adw_tab_bar_new();
|
||||
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view);
|
||||
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view);
|
||||
|
||||
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||
|
||||
@ -338,9 +340,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
);
|
||||
} else tab_bar: {
|
||||
switch (self.notebook) {
|
||||
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
.adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
|
||||
|
||||
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
|
||||
// an AdwToolbarView.
|
||||
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
|
||||
@ -360,12 +361,12 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
),
|
||||
.hidden => unreachable,
|
||||
}
|
||||
c.adw_tab_bar_set_view(tab_bar, tab_view);
|
||||
c.adw_tab_bar_set_view(tab_bar, adw.tab_view);
|
||||
|
||||
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||
},
|
||||
|
||||
.gtk_notebook => {},
|
||||
.gtk => {},
|
||||
}
|
||||
|
||||
// The box is our main child
|
||||
@ -386,6 +387,28 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_show(window);
|
||||
}
|
||||
|
||||
/// Updates appearance based on config settings. Will be called once upon window
|
||||
/// realization, and every time the config is reloaded.
|
||||
///
|
||||
/// TODO: Many of the initial style settings in `create` could possibly be made
|
||||
/// reactive by moving them here.
|
||||
pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
|
||||
if (config.@"background-opacity" < 1) {
|
||||
c.gtk_widget_remove_css_class(@ptrCast(self.window), "background");
|
||||
} else {
|
||||
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
|
||||
}
|
||||
|
||||
if (self.wayland) |*wl| {
|
||||
const blurred = switch (config.@"background-blur-radius") {
|
||||
.false => false,
|
||||
.true => true,
|
||||
.radius => |v| v > 0,
|
||||
};
|
||||
try wl.setBlur(blurred);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
||||
/// menus and such. The menu is defined in App.zig but the action is defined
|
||||
/// here. The string name binds them.
|
||||
@ -423,6 +446,8 @@ fn initActions(self: *Window) void {
|
||||
pub fn deinit(self: *Window) void {
|
||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
||||
|
||||
if (self.wayland) |*wl| wl.deinit();
|
||||
|
||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||
_ = c.g_source_remove(timer);
|
||||
}
|
||||
@ -542,7 +567,7 @@ pub fn onConfigReloaded(self: *Window) void {
|
||||
self.sendToast("Reloaded the configuration");
|
||||
}
|
||||
|
||||
fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
pub fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) return;
|
||||
const toast_overlay = self.toast_overlay orelse return;
|
||||
const toast = c.adw_toast_new(title);
|
||||
@ -550,6 +575,20 @@ fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
|
||||
}
|
||||
|
||||
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
if (self.app.wayland) |*wl| {
|
||||
self.wayland = wayland.SurfaceState.init(v, wl);
|
||||
}
|
||||
|
||||
self.syncAppearance(&self.app.config) catch |err| {
|
||||
log.err("failed to initialize appearance={}", .{err});
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
||||
// sends an undefined value.
|
||||
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
@ -570,7 +609,7 @@ fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwT
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const surface = self.actionSurface();
|
||||
const tab = Tab.create(alloc, self, surface) catch return null;
|
||||
return c.adw_tab_view_get_page(self.notebook.adw_tab_view, @ptrCast(@alignCast(tab.box)));
|
||||
return c.adw_tab_view_get_page(self.notebook.adw.tab_view, @ptrCast(@alignCast(tab.box)));
|
||||
}
|
||||
|
||||
fn adwTabOverviewOpen(
|
||||
@ -894,8 +933,6 @@ fn gtkActionCopy(
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
self.sendToast("Copied to clipboard");
|
||||
}
|
||||
|
||||
fn gtkActionPaste(
|
||||
|
@ -14,6 +14,9 @@ pub const c = @cImport({
|
||||
// Xkb for X11 state handling
|
||||
@cInclude("X11/XKBlib.h");
|
||||
}
|
||||
if (build_options.wayland) {
|
||||
@cInclude("gdk/wayland/gdkwayland.h");
|
||||
}
|
||||
|
||||
// generated header files
|
||||
@cInclude("ghostty_resources.h");
|
||||
|
@ -143,6 +143,8 @@ const Window = struct {
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "inspector-window");
|
||||
|
||||
// Initialize our imgui widget
|
||||
try self.imgui_widget.init();
|
||||
|
@ -129,7 +129,7 @@ pub fn eventMods(
|
||||
|
||||
// On Wayland, we have to use the GDK device because the mods sent
|
||||
// to this event do not have the modifier key applied if it was
|
||||
// presssed (i.e. left control).
|
||||
// pressed (i.e. left control).
|
||||
break :mods translateMods(c.gdk_device_get_modifier_state(device));
|
||||
};
|
||||
|
||||
|
@ -4,161 +4,76 @@ const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const NotebookAdw = @import("notebook_adw.zig").NotebookAdw;
|
||||
const NotebookGtk = @import("notebook_gtk.zig").NotebookGtk;
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
|
||||
|
||||
/// An abstraction over the GTK notebook and Adwaita tab view to manage
|
||||
/// all the terminal tabs in a window.
|
||||
/// An abstraction over the GTK notebook and Adwaita tab view to manage
|
||||
/// all the terminal tabs in a window.
|
||||
pub const Notebook = union(enum) {
|
||||
adw_tab_view: *AdwTabView,
|
||||
gtk_notebook: *c.GtkNotebook,
|
||||
adw: NotebookAdw,
|
||||
gtk: NotebookGtk,
|
||||
|
||||
pub fn create(window: *Window) Notebook {
|
||||
pub fn init(self: *Notebook) void {
|
||||
const window: *Window = @fieldParentPtr("notebook", self);
|
||||
const app = window.app;
|
||||
if (adwaita.enabled(&app.config)) return initAdw(window);
|
||||
return initGtk(window);
|
||||
if (adwaita.enabled(&app.config)) return NotebookAdw.init(self);
|
||||
|
||||
return NotebookGtk.init(self);
|
||||
}
|
||||
|
||||
fn initGtk(window: *Window) Notebook {
|
||||
const app = window.app;
|
||||
|
||||
// Create a notebook to hold our tabs.
|
||||
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
|
||||
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
|
||||
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
|
||||
.top, .hidden => c.GTK_POS_TOP,
|
||||
.bottom => c.GTK_POS_BOTTOM,
|
||||
.left => c.GTK_POS_LEFT,
|
||||
.right => c.GTK_POS_RIGHT,
|
||||
};
|
||||
c.gtk_notebook_set_tab_pos(notebook, notebook_tab_pos);
|
||||
c.gtk_notebook_set_scrollable(notebook, 1);
|
||||
c.gtk_notebook_set_show_tabs(notebook, 0);
|
||||
c.gtk_notebook_set_show_border(notebook, 0);
|
||||
|
||||
// This enables all Ghostty terminal tabs to be exchanged across windows.
|
||||
c.gtk_notebook_set_group_name(notebook, "ghostty-terminal-tabs");
|
||||
|
||||
// This is important so the notebook expands to fit available space.
|
||||
// Otherwise, it will be zero/zero in the box below.
|
||||
c.gtk_widget_set_vexpand(notebook_widget, 1);
|
||||
c.gtk_widget_set_hexpand(notebook_widget, 1);
|
||||
|
||||
// Remove the background from the stack widget
|
||||
const stack = c.gtk_widget_get_last_child(notebook_widget);
|
||||
c.gtk_widget_add_css_class(stack, "transparent");
|
||||
|
||||
// All of our events
|
||||
_ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
return .{ .gtk_notebook = notebook };
|
||||
}
|
||||
|
||||
fn initAdw(window: *Window) Notebook {
|
||||
const app = window.app;
|
||||
assert(adwaita.enabled(&app.config));
|
||||
|
||||
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
||||
|
||||
if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
||||
}
|
||||
|
||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
return .{ .adw_tab_view = tab_view };
|
||||
}
|
||||
|
||||
pub fn asWidget(self: Notebook) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.adw_tab_view => |tab_view| @ptrCast(@alignCast(tab_view)),
|
||||
.gtk_notebook => |notebook| @ptrCast(@alignCast(notebook)),
|
||||
pub fn asWidget(self: *Notebook) *c.GtkWidget {
|
||||
return switch (self.*) {
|
||||
.adw => |*adw| adw.asWidget(),
|
||||
.gtk => |*gtk| gtk.asWidget(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn nPages(self: Notebook) c_int {
|
||||
return switch (self) {
|
||||
.gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook),
|
||||
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0))
|
||||
c.adw_tab_view_get_n_pages(tab_view)
|
||||
else
|
||||
unreachable,
|
||||
pub fn nPages(self: *Notebook) c_int {
|
||||
return switch (self.*) {
|
||||
.adw => |*adw| adw.nPages(),
|
||||
.gtk => |*gtk| gtk.nPages(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the index of the currently selected page.
|
||||
/// Returns null if the notebook has no pages.
|
||||
fn currentPage(self: Notebook) ?c_int {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null;
|
||||
return c.adw_tab_view_get_page_position(tab_view, page);
|
||||
},
|
||||
|
||||
.gtk_notebook => |notebook| {
|
||||
const current = c.gtk_notebook_get_current_page(notebook);
|
||||
return if (current == -1) null else current;
|
||||
},
|
||||
}
|
||||
fn currentPage(self: *Notebook) ?c_int {
|
||||
return switch (self.*) {
|
||||
.adw => |*adw| adw.currentPage(),
|
||||
.gtk => |*gtk| gtk.currentPage(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the currently selected tab or null if there are none.
|
||||
pub fn currentTab(self: Notebook) ?*Tab {
|
||||
const child = switch (self) {
|
||||
.adw_tab_view => |tab_view| child: {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null;
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
break :child child;
|
||||
},
|
||||
|
||||
.gtk_notebook => |notebook| child: {
|
||||
const page = self.currentPage() orelse return null;
|
||||
break :child c.gtk_notebook_get_nth_page(notebook, page);
|
||||
},
|
||||
pub fn currentTab(self: *Notebook) ?*Tab {
|
||||
return switch (self.*) {
|
||||
.adw => |*adw| adw.currentTab(),
|
||||
.gtk => |*gtk| gtk.currentTab(),
|
||||
};
|
||||
return @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn gotoNthTab(self: Notebook, position: c_int) void {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page_to_select = c.adw_tab_view_get_nth_page(tab_view, position);
|
||||
c.adw_tab_view_set_selected_page(tab_view, page_to_select);
|
||||
},
|
||||
.gtk_notebook => |notebook| c.gtk_notebook_set_current_page(notebook, position),
|
||||
pub fn gotoNthTab(self: *Notebook, position: c_int) void {
|
||||
switch (self.*) {
|
||||
.adw => |*adw| adw.gotoNthTab(position),
|
||||
.gtk => |*gtk| gtk.gotoNthTab(position),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getTabPosition(self: Notebook, tab: *Tab) ?c_int {
|
||||
return switch (self) {
|
||||
.adw_tab_view => |tab_view| page_idx: {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return null;
|
||||
break :page_idx c.adw_tab_view_get_page_position(tab_view, page);
|
||||
},
|
||||
.gtk_notebook => |notebook| page_idx: {
|
||||
const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return null;
|
||||
break :page_idx getNotebookPageIndex(page);
|
||||
},
|
||||
pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
|
||||
return switch (self.*) {
|
||||
.adw => |*adw| adw.getTabPosition(tab),
|
||||
.gtk => |*gtk| gtk.getTabPosition(tab),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn gotoPreviousTab(self: Notebook, tab: *Tab) void {
|
||||
pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) void {
|
||||
const page_idx = self.getTabPosition(tab) orelse return;
|
||||
|
||||
// The next index is the previous or we wrap around.
|
||||
@ -173,7 +88,7 @@ pub const Notebook = union(enum) {
|
||||
self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn gotoNextTab(self: Notebook, tab: *Tab) void {
|
||||
pub fn gotoNextTab(self: *Notebook, tab: *Tab) void {
|
||||
const page_idx = self.getTabPosition(tab) orelse return;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
@ -183,7 +98,7 @@ pub const Notebook = union(enum) {
|
||||
self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn moveTab(self: Notebook, tab: *Tab, position: c_int) void {
|
||||
pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
|
||||
const page_idx = self.getTabPosition(tab) orelse return;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
@ -199,42 +114,28 @@ pub const Notebook = union(enum) {
|
||||
self.reorderPage(tab, new_position);
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: Notebook, tab: *Tab, position: c_int) void {
|
||||
switch (self) {
|
||||
.gtk_notebook => |notebook| {
|
||||
c.gtk_notebook_reorder_child(notebook, @ptrCast(tab.box), position);
|
||||
},
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
|
||||
_ = c.adw_tab_view_reorder_page(tab_view, page, position);
|
||||
},
|
||||
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
|
||||
switch (self.*) {
|
||||
.adw => |*adw| adw.reorderPage(tab, position),
|
||||
.gtk => |*gtk| gtk.reorderPage(tab, position),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
|
||||
c.adw_tab_page_set_title(page, title.ptr);
|
||||
},
|
||||
.gtk_notebook => c.gtk_label_set_text(tab.label_text, title.ptr),
|
||||
pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||
switch (self.*) {
|
||||
.adw => |*adw| adw.setTabLabel(tab, title),
|
||||
.gtk => |*gtk| gtk.setTabLabel(tab, title),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTabTooltip(self: Notebook, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
|
||||
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
|
||||
},
|
||||
.gtk_notebook => c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr),
|
||||
pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
switch (self.*) {
|
||||
.adw => |*adw| adw.setTabTooltip(tab, tooltip),
|
||||
.gtk => |*gtk| gtk.setTabTooltip(tab, tooltip),
|
||||
}
|
||||
}
|
||||
|
||||
fn newTabInsertPosition(self: Notebook, tab: *Tab) c_int {
|
||||
fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
|
||||
const numPages = self.nPages();
|
||||
return switch (tab.window.app.config.@"window-new-tab-position") {
|
||||
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
||||
@ -243,249 +144,23 @@ pub const Notebook = union(enum) {
|
||||
}
|
||||
|
||||
/// Adds a new tab with the given title to the notebook.
|
||||
pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !void {
|
||||
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
|
||||
const page = c.adw_tab_view_insert(tab_view, box_widget, self.newTabInsertPosition(tab));
|
||||
c.adw_tab_page_set_title(page, title.ptr);
|
||||
|
||||
// Switch to the new tab
|
||||
c.adw_tab_view_set_selected_page(tab_view, page);
|
||||
},
|
||||
.gtk_notebook => |notebook| {
|
||||
// Build the tab label
|
||||
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
|
||||
const label_text_widget = c.gtk_label_new(title.ptr);
|
||||
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
|
||||
c.gtk_box_append(label_box, label_text_widget);
|
||||
tab.label_text = label_text;
|
||||
|
||||
const window = tab.window;
|
||||
if (window.app.config.@"gtk-wide-tabs") {
|
||||
c.gtk_widget_set_hexpand(label_box_widget, 1);
|
||||
c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
|
||||
c.gtk_widget_set_hexpand(label_text_widget, 1);
|
||||
c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL);
|
||||
|
||||
// This ensures that tabs are always equal width. If they're too
|
||||
// long, they'll be truncated with an ellipsis.
|
||||
c.gtk_label_set_max_width_chars(label_text, 1);
|
||||
c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END);
|
||||
|
||||
// We need to set a minimum width so that at a certain point
|
||||
// the notebook will have an arrow button rather than shrinking tabs
|
||||
// to an unreadably small size.
|
||||
c.gtk_widget_set_size_request(label_text_widget, 100, 1);
|
||||
}
|
||||
|
||||
// Build the close button for the tab
|
||||
const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic");
|
||||
const label_close: *c.GtkButton = @ptrCast(label_close_widget);
|
||||
c.gtk_button_set_has_frame(label_close, 0);
|
||||
c.gtk_box_append(label_box, label_close_widget);
|
||||
|
||||
const page_idx = c.gtk_notebook_insert_page(
|
||||
notebook,
|
||||
box_widget,
|
||||
label_box_widget,
|
||||
self.newTabInsertPosition(tab),
|
||||
);
|
||||
|
||||
// Clicks
|
||||
const gesture_tab_click = c.gtk_gesture_click_new();
|
||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
|
||||
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
|
||||
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(notebook, box_widget, 1);
|
||||
c.gtk_notebook_set_tab_detachable(notebook, box_widget, 1);
|
||||
|
||||
if (self.nPages() > 1) {
|
||||
c.gtk_notebook_set_show_tabs(notebook, 1);
|
||||
}
|
||||
|
||||
// Switch to the new tab
|
||||
c.gtk_notebook_set_current_page(notebook, page_idx);
|
||||
},
|
||||
pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||
const position = self.newTabInsertPosition(tab);
|
||||
switch (self.*) {
|
||||
.adw => |*adw| adw.addTab(tab, position, title),
|
||||
.gtk => |*gtk| gtk.addTab(tab, position, title),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closeTab(self: Notebook, tab: *Tab) void {
|
||||
const window = tab.window;
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return;
|
||||
c.adw_tab_view_close_page(tab_view, page);
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
// libadw versions <= 1.3.x leak the final page view
|
||||
// which causes our surface to not properly cleanup. We
|
||||
// unref to force the cleanup. This will trigger a critical
|
||||
// warning from GTK, but I don't know any other workaround.
|
||||
// Note: I'm not actually sure if 1.4.0 contains the fix,
|
||||
// I just know that 1.3.x is broken and 1.5.1 is fixed.
|
||||
// If we know that 1.4.0 is fixed, we can change this.
|
||||
if (!adwaita.versionAtLeast(1, 4, 0)) {
|
||||
c.g_object_unref(tab.box);
|
||||
}
|
||||
|
||||
c.gtk_window_destroy(window.window);
|
||||
}
|
||||
},
|
||||
.gtk_notebook => |notebook| {
|
||||
const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return;
|
||||
|
||||
// Find page and tab which we're closing
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
|
||||
// Remove the page. This will destroy the GTK widgets in the page which
|
||||
// will trigger Tab cleanup. The `tab` variable is therefore unusable past that point.
|
||||
c.gtk_notebook_remove_page(notebook, page_idx);
|
||||
|
||||
const remaining = self.nPages();
|
||||
switch (remaining) {
|
||||
// If we have no more tabs we close the window
|
||||
0 => c.gtk_window_destroy(tab.window.window),
|
||||
|
||||
// If we have one more tab we hide the tab bar
|
||||
1 => c.gtk_notebook_set_show_tabs(notebook, 0),
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// If we have remaining tabs, we need to make sure we grab focus.
|
||||
if (remaining > 0) window.focusCurrentTab();
|
||||
},
|
||||
pub fn closeTab(self: *Notebook, tab: *Tab) void {
|
||||
switch (self.*) {
|
||||
.adw => |*adw| adw.closeTab(tab),
|
||||
.gtk => |*gtk| gtk.closeTab(tab),
|
||||
}
|
||||
}
|
||||
|
||||
fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
|
||||
var value: c.GValue = std.mem.zeroes(c.GValue);
|
||||
defer c.g_value_unset(&value);
|
||||
_ = c.g_value_init(&value, c.G_TYPE_INT);
|
||||
c.g_object_get_property(
|
||||
@ptrCast(@alignCast(page)),
|
||||
"position",
|
||||
&value,
|
||||
);
|
||||
|
||||
return c.g_value_get_int(&value);
|
||||
}
|
||||
};
|
||||
|
||||
fn gtkPageRemoved(
|
||||
_: *c.GtkNotebook,
|
||||
_: *c.GtkWidget,
|
||||
_: c.guint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
const notebook: *c.GtkNotebook = self.notebook.gtk_notebook;
|
||||
|
||||
// Hide the tab bar if we only have one tab after removal
|
||||
const remaining = c.gtk_notebook_get_n_pages(notebook);
|
||||
if (remaining == 1) {
|
||||
c.gtk_notebook_set_show_tabs(notebook, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn adwPageAttached(tab_view: *AdwTabView, page: *c.AdwTabPage, position: c_int, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = position;
|
||||
_ = tab_view;
|
||||
const self: *Window = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
|
||||
tab.window = self;
|
||||
|
||||
self.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn gtkPageAdded(
|
||||
notebook: *c.GtkNotebook,
|
||||
_: *c.GtkWidget,
|
||||
page_idx: c.guint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
// The added page can come from another window with drag and drop, thus we migrate the tab
|
||||
// window to be self.
|
||||
const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx));
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
|
||||
));
|
||||
tab.window = self;
|
||||
|
||||
// Whenever a new page is added, we always grab focus of the
|
||||
// currently selected page. This was added specifically so that when
|
||||
// we drag a tab out to create a new window ("create-window" event)
|
||||
// we grab focus in the new window. Without this, the terminal didn't
|
||||
// have focus.
|
||||
self.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view) orelse return;
|
||||
const title = c.adw_tab_page_get_title(page);
|
||||
c.gtk_window_set_title(window.window, title);
|
||||
}
|
||||
|
||||
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(window.notebook.gtk_notebook, page)));
|
||||
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
|
||||
const label_text = c.gtk_label_get_text(gtk_label);
|
||||
c.gtk_window_set_title(window.window, label_text);
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *AdwTabView,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) ?*AdwTabView {
|
||||
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const window = createWindow(currentWindow) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
return window.notebook.adw_tab_view;
|
||||
}
|
||||
|
||||
fn gtkNotebookCreateWindow(
|
||||
_: *c.GtkNotebook,
|
||||
page: *c.GtkWidget,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) ?*c.GtkNotebook {
|
||||
// The tab for the page is stored in the widget data.
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
|
||||
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const window = createWindow(currentWindow) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
// And add it to the new window.
|
||||
tab.window = window;
|
||||
|
||||
return window.notebook.gtk_notebook;
|
||||
}
|
||||
|
||||
fn createWindow(currentWindow: *Window) !*Window {
|
||||
pub fn createWindow(currentWindow: *Window) !*Window {
|
||||
const alloc = currentWindow.app.core_app.alloc;
|
||||
const app = currentWindow.app;
|
||||
|
||||
|
163
src/apprt/gtk/notebook_adw.zig
Normal file
163
src/apprt/gtk/notebook_adw.zig
Normal file
@ -0,0 +1,163 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Notebook = @import("notebook.zig").Notebook;
|
||||
const createWindow = @import("notebook.zig").createWindow;
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
|
||||
const AdwTabPage = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabPage else anyopaque;
|
||||
|
||||
pub const NotebookAdw = struct {
|
||||
/// the tab view
|
||||
tab_view: *AdwTabView,
|
||||
|
||||
pub fn init(notebook: *Notebook) void {
|
||||
const window: *Window = @fieldParentPtr("notebook", notebook);
|
||||
const app = window.app;
|
||||
assert(adwaita.enabled(&app.config));
|
||||
|
||||
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
|
||||
|
||||
if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
||||
}
|
||||
|
||||
notebook.* = .{
|
||||
.adw = .{
|
||||
.tab_view = tab_view,
|
||||
},
|
||||
};
|
||||
|
||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
||||
}
|
||||
|
||||
pub fn asWidget(self: *NotebookAdw) *c.GtkWidget {
|
||||
return @ptrCast(@alignCast(self.tab_view));
|
||||
}
|
||||
|
||||
pub fn nPages(self: *NotebookAdw) c_int {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0))
|
||||
return c.adw_tab_view_get_n_pages(self.tab_view)
|
||||
else
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Returns the index of the currently selected page.
|
||||
/// Returns null if the notebook has no pages.
|
||||
pub fn currentPage(self: *NotebookAdw) ?c_int {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
|
||||
return c.adw_tab_view_get_page_position(self.tab_view, page);
|
||||
}
|
||||
|
||||
/// Returns the currently selected tab or null if there are none.
|
||||
pub fn currentTab(self: *NotebookAdw) ?*Tab {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
return @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn gotoNthTab(self: *NotebookAdw, position: c_int) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position);
|
||||
c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
|
||||
}
|
||||
|
||||
pub fn getTabPosition(self: *NotebookAdw, tab: *Tab) ?c_int {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
|
||||
return c.adw_tab_view_get_page_position(self.tab_view, page);
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: *NotebookAdw, tab: *Tab, position: c_int) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
|
||||
_ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
|
||||
}
|
||||
|
||||
pub fn setTabLabel(self: *NotebookAdw, tab: *Tab, title: [:0]const u8) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
|
||||
c.adw_tab_page_set_title(page, title.ptr);
|
||||
}
|
||||
|
||||
pub fn setTabTooltip(self: *NotebookAdw, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
|
||||
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
|
||||
}
|
||||
|
||||
pub fn addTab(self: *NotebookAdw, tab: *Tab, position: c_int, title: [:0]const u8) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
|
||||
const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
|
||||
c.adw_tab_page_set_title(page, title.ptr);
|
||||
c.adw_tab_view_set_selected_page(self.tab_view, page);
|
||||
}
|
||||
|
||||
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
|
||||
c.adw_tab_view_close_page(self.tab_view, page);
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
// libadw versions <= 1.3.x leak the final page view
|
||||
// which causes our surface to not properly cleanup. We
|
||||
// unref to force the cleanup. This will trigger a critical
|
||||
// warning from GTK, but I don't know any other workaround.
|
||||
// Note: I'm not actually sure if 1.4.0 contains the fix,
|
||||
// I just know that 1.3.x is broken and 1.5.1 is fixed.
|
||||
// If we know that 1.4.0 is fixed, we can change this.
|
||||
if (!adwaita.versionAtLeast(1, 4, 0)) {
|
||||
c.g_object_unref(tab.box);
|
||||
}
|
||||
|
||||
c.gtk_window_destroy(tab.window.window);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
|
||||
tab.window = window;
|
||||
|
||||
window.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *AdwTabView,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) ?*AdwTabView {
|
||||
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const window = createWindow(currentWindow) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
return window.notebook.adw.tab_view;
|
||||
}
|
||||
|
||||
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return;
|
||||
const title = c.adw_tab_page_get_title(page);
|
||||
c.gtk_window_set_title(window.window, title);
|
||||
}
|
285
src/apprt/gtk/notebook_gtk.zig
Normal file
285
src/apprt/gtk/notebook_gtk.zig
Normal file
@ -0,0 +1,285 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Notebook = @import("notebook.zig").Notebook;
|
||||
const createWindow = @import("notebook.zig").createWindow;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// An abstraction over the GTK notebook and Adwaita tab view to manage
|
||||
/// all the terminal tabs in a window.
|
||||
pub const NotebookGtk = struct {
|
||||
notebook: *c.GtkNotebook,
|
||||
|
||||
pub fn init(notebook: *Notebook) void {
|
||||
const window: *Window = @fieldParentPtr("notebook", notebook);
|
||||
const app = window.app;
|
||||
|
||||
// Create a notebook to hold our tabs.
|
||||
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
|
||||
c.gtk_widget_add_css_class(notebook_widget, "notebook");
|
||||
|
||||
const gtk_notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
|
||||
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
|
||||
.top, .hidden => c.GTK_POS_TOP,
|
||||
.bottom => c.GTK_POS_BOTTOM,
|
||||
.left => c.GTK_POS_LEFT,
|
||||
.right => c.GTK_POS_RIGHT,
|
||||
};
|
||||
c.gtk_notebook_set_tab_pos(gtk_notebook, notebook_tab_pos);
|
||||
c.gtk_notebook_set_scrollable(gtk_notebook, 1);
|
||||
c.gtk_notebook_set_show_tabs(gtk_notebook, 0);
|
||||
c.gtk_notebook_set_show_border(gtk_notebook, 0);
|
||||
|
||||
// This enables all Ghostty terminal tabs to be exchanged across windows.
|
||||
c.gtk_notebook_set_group_name(gtk_notebook, "ghostty-terminal-tabs");
|
||||
|
||||
// This is important so the notebook expands to fit available space.
|
||||
// Otherwise, it will be zero/zero in the box below.
|
||||
c.gtk_widget_set_vexpand(notebook_widget, 1);
|
||||
c.gtk_widget_set_hexpand(notebook_widget, 1);
|
||||
|
||||
// Remove the background from the stack widget
|
||||
const stack = c.gtk_widget_get_last_child(notebook_widget);
|
||||
c.gtk_widget_add_css_class(stack, "transparent");
|
||||
|
||||
notebook.* = .{
|
||||
.gtk = .{
|
||||
.notebook = gtk_notebook,
|
||||
},
|
||||
};
|
||||
|
||||
// All of our events
|
||||
_ = c.g_signal_connect_data(gtk_notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gtk_notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gtk_notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gtk_notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
}
|
||||
|
||||
/// return the underlying widget as a generic GtkWidget
|
||||
pub fn asWidget(self: *NotebookGtk) *c.GtkWidget {
|
||||
return @ptrCast(@alignCast(self.notebook));
|
||||
}
|
||||
|
||||
/// returns the number of pages in the notebook
|
||||
pub fn nPages(self: *NotebookGtk) c_int {
|
||||
return c.gtk_notebook_get_n_pages(self.notebook);
|
||||
}
|
||||
|
||||
/// Returns the index of the currently selected page.
|
||||
/// Returns null if the notebook has no pages.
|
||||
pub fn currentPage(self: *NotebookGtk) ?c_int {
|
||||
const current = c.gtk_notebook_get_current_page(self.notebook);
|
||||
return if (current == -1) null else current;
|
||||
}
|
||||
|
||||
/// Returns the currently selected tab or null if there are none.
|
||||
pub fn currentTab(self: *NotebookGtk) ?*Tab {
|
||||
log.warn("currentTab", .{});
|
||||
const page = self.currentPage() orelse return null;
|
||||
const child = c.gtk_notebook_get_nth_page(self.notebook, page);
|
||||
return @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
}
|
||||
|
||||
/// focus the nth tab
|
||||
pub fn gotoNthTab(self: *NotebookGtk, position: c_int) void {
|
||||
c.gtk_notebook_set_current_page(self.notebook, position);
|
||||
}
|
||||
|
||||
/// get the position of the current tab
|
||||
pub fn getTabPosition(self: *NotebookGtk, tab: *Tab) ?c_int {
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return null;
|
||||
return getNotebookPageIndex(page);
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: *NotebookGtk, tab: *Tab, position: c_int) void {
|
||||
c.gtk_notebook_reorder_child(self.notebook, @ptrCast(tab.box), position);
|
||||
}
|
||||
|
||||
pub fn setTabLabel(_: *NotebookGtk, tab: *Tab, title: [:0]const u8) void {
|
||||
c.gtk_label_set_text(tab.label_text, title.ptr);
|
||||
}
|
||||
|
||||
pub fn setTabTooltip(_: *NotebookGtk, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr);
|
||||
}
|
||||
|
||||
/// Adds a new tab with the given title to the notebook.
|
||||
pub fn addTab(self: *NotebookGtk, tab: *Tab, position: c_int, title: [:0]const u8) void {
|
||||
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
|
||||
|
||||
// Build the tab label
|
||||
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
|
||||
const label_text_widget = c.gtk_label_new(title.ptr);
|
||||
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
|
||||
c.gtk_box_append(label_box, label_text_widget);
|
||||
tab.label_text = label_text;
|
||||
|
||||
const window = tab.window;
|
||||
if (window.app.config.@"gtk-wide-tabs") {
|
||||
c.gtk_widget_set_hexpand(label_box_widget, 1);
|
||||
c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
|
||||
c.gtk_widget_set_hexpand(label_text_widget, 1);
|
||||
c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL);
|
||||
|
||||
// This ensures that tabs are always equal width. If they're too
|
||||
// long, they'll be truncated with an ellipsis.
|
||||
c.gtk_label_set_max_width_chars(label_text, 1);
|
||||
c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END);
|
||||
|
||||
// We need to set a minimum width so that at a certain point
|
||||
// the notebook will have an arrow button rather than shrinking tabs
|
||||
// to an unreadably small size.
|
||||
c.gtk_widget_set_size_request(label_text_widget, 100, 1);
|
||||
}
|
||||
|
||||
// Build the close button for the tab
|
||||
const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic");
|
||||
const label_close: *c.GtkButton = @ptrCast(label_close_widget);
|
||||
c.gtk_button_set_has_frame(label_close, 0);
|
||||
c.gtk_box_append(label_box, label_close_widget);
|
||||
|
||||
const page_idx = c.gtk_notebook_insert_page(
|
||||
self.notebook,
|
||||
box_widget,
|
||||
label_box_widget,
|
||||
position,
|
||||
);
|
||||
|
||||
// Clicks
|
||||
const gesture_tab_click = c.gtk_gesture_click_new();
|
||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
|
||||
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
|
||||
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
|
||||
c.gtk_notebook_set_tab_detachable(self.notebook, box_widget, 1);
|
||||
|
||||
if (self.nPages() > 1) {
|
||||
c.gtk_notebook_set_show_tabs(self.notebook, 1);
|
||||
}
|
||||
|
||||
// Switch to the new tab
|
||||
c.gtk_notebook_set_current_page(self.notebook, page_idx);
|
||||
}
|
||||
|
||||
pub fn closeTab(self: *NotebookGtk, tab: *Tab) void {
|
||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
||||
|
||||
// Find page and tab which we're closing
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
|
||||
// Remove the page. This will destroy the GTK widgets in the page which
|
||||
// will trigger Tab cleanup. The `tab` variable is therefore unusable past that point.
|
||||
c.gtk_notebook_remove_page(self.notebook, page_idx);
|
||||
|
||||
const remaining = self.nPages();
|
||||
switch (remaining) {
|
||||
// If we have no more tabs we close the window
|
||||
0 => c.gtk_window_destroy(tab.window.window),
|
||||
|
||||
// If we have one more tab we hide the tab bar
|
||||
1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// If we have remaining tabs, we need to make sure we grab focus.
|
||||
if (remaining > 0)
|
||||
(self.currentTab() orelse return).window.focusCurrentTab();
|
||||
}
|
||||
};
|
||||
|
||||
fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
|
||||
var value: c.GValue = std.mem.zeroes(c.GValue);
|
||||
defer c.g_value_unset(&value);
|
||||
_ = c.g_value_init(&value, c.G_TYPE_INT);
|
||||
c.g_object_get_property(
|
||||
@ptrCast(@alignCast(page)),
|
||||
"position",
|
||||
&value,
|
||||
);
|
||||
|
||||
return c.g_value_get_int(&value);
|
||||
}
|
||||
|
||||
fn gtkPageAdded(
|
||||
notebook: *c.GtkNotebook,
|
||||
_: *c.GtkWidget,
|
||||
page_idx: c.guint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
// The added page can come from another window with drag and drop, thus we migrate the tab
|
||||
// window to be self.
|
||||
const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx));
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
|
||||
));
|
||||
tab.window = self;
|
||||
|
||||
// Whenever a new page is added, we always grab focus of the
|
||||
// currently selected page. This was added specifically so that when
|
||||
// we drag a tab out to create a new window ("create-window" event)
|
||||
// we grab focus in the new window. Without this, the terminal didn't
|
||||
// have focus.
|
||||
self.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn gtkPageRemoved(
|
||||
_: *c.GtkNotebook,
|
||||
_: *c.GtkWidget,
|
||||
_: c.guint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
log.warn("gtkPageRemoved", .{});
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
// Hide the tab bar if we only have one tab after removal
|
||||
const remaining = c.gtk_notebook_get_n_pages(window.notebook.gtk.notebook);
|
||||
|
||||
if (remaining == 1) {
|
||||
c.gtk_notebook_set_show_tabs(window.notebook.gtk.notebook, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const self = &window.notebook.gtk;
|
||||
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
|
||||
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
|
||||
const label_text = c.gtk_label_get_text(gtk_label);
|
||||
c.gtk_window_set_title(window.window, label_text);
|
||||
}
|
||||
|
||||
fn gtkNotebookCreateWindow(
|
||||
_: *c.GtkNotebook,
|
||||
page: *c.GtkWidget,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) ?*c.GtkNotebook {
|
||||
// The tab for the page is stored in the widget data.
|
||||
const tab: *Tab = @ptrCast(@alignCast(
|
||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
|
||||
));
|
||||
|
||||
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const newWindow = createWindow(currentWindow) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
// And add it to the new window.
|
||||
tab.window = newWindow;
|
||||
|
||||
return newWindow.notebook.gtk.notebook;
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
separator {
|
||||
.terminal-window .notebook separator {
|
||||
background-color: rgba(36, 36, 36, 1);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ window.without-window-decoration-and-with-titlebar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
separator {
|
||||
.terminal-window .notebook separator {
|
||||
background-color: rgba(250, 250, 250, 1);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
125
src/apprt/gtk/wayland.zig
Normal file
125
src/apprt/gtk/wayland.zig
Normal file
@ -0,0 +1,125 @@
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const log = std.log.scoped(.gtk_wayland);
|
||||
|
||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||
pub const AppState = struct {
|
||||
display: *wl.Display,
|
||||
blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
pub fn init(display: ?*c.GdkDisplay) ?AppState {
|
||||
if (comptime !build_options.wayland) return null;
|
||||
|
||||
// It should really never be null
|
||||
const display_ = display orelse return null;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(display_)),
|
||||
c.gdk_wayland_display_get_type(),
|
||||
) == 0)
|
||||
return null;
|
||||
|
||||
const wl_display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(display_) orelse return null);
|
||||
|
||||
return .{
|
||||
.display = wl_display,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn register(self: *AppState) !void {
|
||||
const registry = try self.display.getRegistry();
|
||||
|
||||
registry.setListener(*AppState, registryListener, self);
|
||||
if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
log.debug("app wayland init={}", .{self});
|
||||
}
|
||||
};
|
||||
|
||||
/// Wayland state that contains Wayland objects associated with a window (e.g. wl_surface).
|
||||
pub const SurfaceState = struct {
|
||||
app_state: *AppState,
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState {
|
||||
if (comptime !build_options.wayland) return null;
|
||||
|
||||
const surface = c.gtk_native_get_surface(@ptrCast(window)) orelse return null;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(surface)),
|
||||
c.gdk_wayland_surface_get_type(),
|
||||
) == 0)
|
||||
return null;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return null);
|
||||
|
||||
return .{
|
||||
.app_state = app_state,
|
||||
.surface = wl_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *SurfaceState) void {
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
}
|
||||
|
||||
pub fn setBlur(self: *SurfaceState, blurred: bool) !void {
|
||||
log.debug("setting blur={}", .{blurred});
|
||||
|
||||
const mgr = self.app_state.blur_manager orelse {
|
||||
log.warn("can't set blur: org_kde_kwin_blur_manager protocol unavailable", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.blur_token) |blur| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blurred) {
|
||||
mgr.unset(self.surface);
|
||||
blur.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blurred) {
|
||||
const blur_token = try mgr.create(self.surface);
|
||||
blur_token.commit();
|
||||
self.blur_token = blur_token;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void {
|
||||
switch (event) {
|
||||
.global => |global| {
|
||||
log.debug("got global interface={s}", .{global.interface});
|
||||
if (bindInterface(org.KdeKwinBlurManager, registry, global, 1)) |iface| {
|
||||
state.blur_manager = iface;
|
||||
return;
|
||||
}
|
||||
},
|
||||
.global_remove => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn bindInterface(comptime T: type, registry: *wl.Registry, global: anytype, version: u32) ?*T {
|
||||
if (std.mem.orderZ(u8, global.interface, T.interface.name) == .eq) {
|
||||
return registry.bind(global.name, T, version) catch |err| {
|
||||
log.warn("encountered error={} while binding interface {s}", .{ err, global.interface });
|
||||
return null;
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -23,6 +23,8 @@ pub const BuildConfig = struct {
|
||||
flatpak: bool = false,
|
||||
adwaita: bool = false,
|
||||
x11: bool = false,
|
||||
wayland: bool = false,
|
||||
sentry: bool = true,
|
||||
app_runtime: apprt.Runtime = .none,
|
||||
renderer: rendererpkg.Impl = .opengl,
|
||||
font_backend: font.Backend = .freetype,
|
||||
@ -43,6 +45,8 @@ pub const BuildConfig = struct {
|
||||
step.addOption(bool, "flatpak", self.flatpak);
|
||||
step.addOption(bool, "adwaita", self.adwaita);
|
||||
step.addOption(bool, "x11", self.x11);
|
||||
step.addOption(bool, "wayland", self.wayland);
|
||||
step.addOption(bool, "sentry", self.sentry);
|
||||
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
|
||||
step.addOption(font.Backend, "font_backend", self.font_backend);
|
||||
step.addOption(rendererpkg.Impl, "renderer", self.renderer);
|
||||
|
@ -533,7 +533,7 @@ fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parseBool(v: []const u8) !bool {
|
||||
pub fn parseBool(v: []const u8) !bool {
|
||||
const t = &[_][]const u8{ "1", "t", "T", "true" };
|
||||
const f = &[_][]const u8{ "0", "f", "F", "false" };
|
||||
|
||||
|
@ -63,7 +63,7 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
try stdout.writeAll(
|
||||
\\
|
||||
\\Specify `+<action> --help` to see the help for a specific action,
|
||||
\\where `<action>` is one of actions listed below.
|
||||
\\where `<action>` is one of actions listed above.
|
||||
\\
|
||||
);
|
||||
|
||||
|
@ -20,8 +20,9 @@ pub const Options = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// The `list-actions` command is used to list all the available keybind actions
|
||||
/// for Ghostty.
|
||||
/// The `list-actions` command is used to list all the available keybind
|
||||
/// actions for Ghostty. These are distinct from the CLI Actions which can
|
||||
/// be listed via `+help`
|
||||
///
|
||||
/// The `--docs` argument will print out the documentation for each action.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
|
@ -11,6 +11,12 @@ const global_state = &@import("../global.zig").state;
|
||||
const vaxis = @import("vaxis");
|
||||
const zf = @import("zf");
|
||||
|
||||
// When the number of filtered themes is less than or equal to this threshold,
|
||||
// the window position will be reset to 0 to show all results from the top.
|
||||
// This ensures better visibility for small result sets while maintaining
|
||||
// scroll position for larger lists.
|
||||
const SMALL_LIST_THRESHOLD = 10;
|
||||
|
||||
pub const Options = struct {
|
||||
/// If true, print the full path to the theme.
|
||||
path: bool = false,
|
||||
@ -323,9 +329,15 @@ const Preview = struct {
|
||||
}
|
||||
|
||||
self.current, self.window = current: {
|
||||
if (selected.len == 0) break :current .{ 0, 0 };
|
||||
|
||||
for (self.filtered.items, 0..) |index, i| {
|
||||
if (std.mem.eql(u8, self.themes[index].theme, selected))
|
||||
break :current .{ i, i -| relative };
|
||||
if (std.mem.eql(u8, self.themes[index].theme, selected)) {
|
||||
// Keep the relative position but ensure all search results are visible
|
||||
const new_window = i -| relative;
|
||||
// If the new window would hide some results at the top, adjust it
|
||||
break :current .{ i, if (self.filtered.items.len <= SMALL_LIST_THRESHOLD) 0 else new_window };
|
||||
}
|
||||
}
|
||||
break :current .{ 0, 0 };
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const build_config = @import("../build_config.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const xev = @import("xev");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").c else void;
|
||||
@ -37,6 +38,7 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
|
||||
try stdout.print(" - libxev : {}\n", .{xev.backend});
|
||||
if (comptime build_config.app_runtime == .gtk) {
|
||||
try stdout.print(" - desktop env: {s}\n", .{@tagName(internal_os.desktopEnvironment())});
|
||||
try stdout.print(" - GTK version:\n", .{});
|
||||
try stdout.print(" build : {d}.{d}.{d}\n", .{
|
||||
gtk.GTK_MAJOR_VERSION,
|
||||
@ -66,6 +68,14 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
} else {
|
||||
try stdout.print(" - libX11 : disabled\n", .{});
|
||||
}
|
||||
|
||||
// We say `libwayland` since it is possible to build Ghostty without
|
||||
// Wayland integration but with Wayland-enabled GTK
|
||||
if (comptime build_options.wayland) {
|
||||
try stdout.print(" - libwayland : enabled\n", .{});
|
||||
} else {
|
||||
try stdout.print(" - libwayland : disabled\n", .{});
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -147,23 +147,28 @@ const c = @cImport({
|
||||
/// By default, synthetic styles are enabled.
|
||||
@"font-synthetic-style": FontSyntheticStyle = .{},
|
||||
|
||||
/// Apply a font feature. This can be repeated multiple times to enable multiple
|
||||
/// font features. You can NOT set multiple font features with a single value
|
||||
/// (yet).
|
||||
/// Apply a font feature. To enable multiple font features you can repeat
|
||||
/// this multiple times or use a comma-separated list of feature settings.
|
||||
///
|
||||
/// The syntax for feature settings is as follows, where `feat` is a feature:
|
||||
///
|
||||
/// * Enable features with e.g. `feat`, `+feat`, `feat on`, `feat=1`.
|
||||
/// * Disabled features with e.g. `-feat`, `feat off`, `feat=0`.
|
||||
/// * Set a feature value with e.g. `feat=2`, `feat = 3`, `feat 4`.
|
||||
/// * Feature names may be wrapped in quotes, meaning this config should be
|
||||
/// syntactically compatible with the `font-feature-settings` CSS property.
|
||||
///
|
||||
/// The syntax is fairly loose, but invalid settings will be silently ignored.
|
||||
///
|
||||
/// The font feature will apply to all fonts rendered by Ghostty. A future
|
||||
/// enhancement will allow targeting specific faces.
|
||||
///
|
||||
/// A valid value is the name of a feature. Prefix the feature with a `-` to
|
||||
/// explicitly disable it. Example: `ss20` or `-ss20`.
|
||||
///
|
||||
/// To disable programming ligatures, use `-calt` since this is the typical
|
||||
/// feature name for programming ligatures. To look into what font features
|
||||
/// your font has and what they do, use a font inspection tool such as
|
||||
/// [fontdrop.info](https://fontdrop.info).
|
||||
///
|
||||
/// To generally disable most ligatures, use `-calt`, `-liga`, and `-dlig` (as
|
||||
/// separate repetitive entries in your config).
|
||||
/// To generally disable most ligatures, use `-calt, -liga, -dlig`.
|
||||
@"font-feature": RepeatableString = .{},
|
||||
|
||||
/// Font size in points. This value can be a non-integer and the nearest integer
|
||||
@ -177,6 +182,10 @@ const c = @cImport({
|
||||
/// depending on your `window-inherit-font-size` setting. If that setting is
|
||||
/// true, only the first window will be affected by this change since all
|
||||
/// subsequent windows will inherit the font size of the previous window.
|
||||
///
|
||||
/// On Linux with GTK, font size is scaled according to both display-wide and
|
||||
/// text-specific scaling factors, which are often managed by your desktop
|
||||
/// environment (e.g. the GNOME display scale and large text settings).
|
||||
@"font-size": f32 = switch (builtin.os.tag) {
|
||||
// On macOS we default a little bigger since this tends to look better. This
|
||||
// is purely subjective but this is easy to modify.
|
||||
@ -225,10 +234,20 @@ const c = @cImport({
|
||||
/// i.e. new windows, tabs, etc.
|
||||
@"font-codepoint-map": RepeatableCodepointMap = .{},
|
||||
|
||||
/// Draw fonts with a thicker stroke, if supported. This is only supported
|
||||
/// currently on macOS.
|
||||
/// Draw fonts with a thicker stroke, if supported.
|
||||
/// This is currently only supported on macOS.
|
||||
@"font-thicken": bool = false,
|
||||
|
||||
/// Strength of thickening when `font-thicken` is enabled.
|
||||
///
|
||||
/// Valid values are integers between `0` and `255`. `0` does not correspond to
|
||||
/// *no* thickening, rather it corresponds to the lightest available thickening.
|
||||
///
|
||||
/// Has no effect when `font-thicken` is set to `false`.
|
||||
///
|
||||
/// This is currently only supported on macOS.
|
||||
@"font-thicken-strength": u8 = 255,
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined by the
|
||||
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
|
||||
/// etc.). In each case, the values represent the amount to change the original
|
||||
@ -320,7 +339,7 @@ const c = @cImport({
|
||||
|
||||
/// FreeType load flags to enable. The format of this is a list of flags to
|
||||
/// enable separated by commas. If you prefix a flag with `no-` then it is
|
||||
/// disabled. If you omit a flag, it's default value is used, so you must
|
||||
/// disabled. If you omit a flag, its default value is used, so you must
|
||||
/// explicitly disable flags you don't want. You can also use `true` or `false`
|
||||
/// to turn all flags on or off.
|
||||
///
|
||||
@ -398,14 +417,17 @@ const c = @cImport({
|
||||
theme: ?Theme = null,
|
||||
|
||||
/// Background color for the window.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||
|
||||
/// Foreground color for the window.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
||||
|
||||
/// The foreground and background color for selection. If this is not set, then
|
||||
/// the selection color is just the inverted window background and foreground
|
||||
/// (note: not to be confused with the cell bg/fg).
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"selection-foreground": ?Color = null,
|
||||
@"selection-background": ?Color = null,
|
||||
|
||||
@ -431,15 +453,20 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
||||
@"minimum-contrast": f64 = 1,
|
||||
|
||||
/// Color palette for the 256 color form that many terminal applications use.
|
||||
/// The syntax of this configuration is `N=HEXCODE` where `N` is 0 to 255 (for
|
||||
/// the 256 colors in the terminal color table) and `HEXCODE` is a typical RGB
|
||||
/// color code such as `#AABBCC`.
|
||||
/// The syntax of this configuration is `N=COLOR` where `N` is 0 to 255 (for
|
||||
/// the 256 colors in the terminal color table) and `COLOR` is a typical RGB
|
||||
/// color code such as `#AABBCC` or `AABBCC`, or a named X11 color.
|
||||
///
|
||||
/// For definitions on all the codes [see this cheat
|
||||
/// sheet](https://www.ditig.com/256-colors-cheat-sheet).
|
||||
/// The palette index can be in decimal, binary, octal, or hexadecimal.
|
||||
/// Decimal is assumed unless a prefix is used: `0b` for binary, `0o` for octal,
|
||||
/// and `0x` for hexadecimal.
|
||||
///
|
||||
/// For definitions on the color indices and what they canonically map to,
|
||||
/// [see this cheat sheet](https://www.ditig.com/256-colors-cheat-sheet).
|
||||
palette: Palette = .{},
|
||||
|
||||
/// The color of the cursor. If this is not set, a default will be chosen.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"cursor-color": ?Color = null,
|
||||
|
||||
/// Swap the foreground and background colors of the cell under the cursor. This
|
||||
@ -493,6 +520,7 @@ palette: Palette = .{},
|
||||
|
||||
/// The color of the text under the cursor. If this is not set, a default will
|
||||
/// be chosen.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"cursor-text": ?Color = null,
|
||||
|
||||
/// Enables the ability to move the cursor at prompts by using `alt+click` on
|
||||
@ -548,7 +576,7 @@ palette: Palette = .{},
|
||||
/// than 0.01 or greater than 10,000 will be clamped to the nearest valid
|
||||
/// value.
|
||||
///
|
||||
/// A value of "1" (default) scrolls te default amount. A value of "2" scrolls
|
||||
/// A value of "1" (default) scrolls the default amount. A value of "2" scrolls
|
||||
/// double the default amount. A value of "0.5" scrolls half the default amount.
|
||||
/// Et cetera.
|
||||
@"mouse-scroll-multiplier": f64 = 1.0,
|
||||
@ -560,15 +588,42 @@ palette: Palette = .{},
|
||||
/// On macOS, background opacity is disabled when the terminal enters native
|
||||
/// fullscreen. This is because the background becomes gray and it can cause
|
||||
/// widgets to show through which isn't generally desirable.
|
||||
///
|
||||
/// On macOS, changing this configuration requires restarting Ghostty completely.
|
||||
@"background-opacity": f64 = 1.0,
|
||||
|
||||
/// A positive value enables blurring of the background when background-opacity
|
||||
/// is less than 1. The value is the blur radius to apply. A value of 20
|
||||
/// is reasonable for a good looking blur. Higher values will cause strange
|
||||
/// rendering issues as well as performance issues.
|
||||
/// Whether to blur the background when `background-opacity` is less than 1.
|
||||
///
|
||||
/// This is only supported on macOS.
|
||||
@"background-blur-radius": u8 = 0,
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * a nonnegative integer specifying the *blur intensity*
|
||||
/// * `false`, equivalent to a blur intensity of 0
|
||||
/// * `true`, equivalent to the default blur intensity of 20, which is
|
||||
/// reasonable for a good looking blur. Higher blur intensities may
|
||||
/// cause strange rendering and performance issues.
|
||||
///
|
||||
/// Supported on macOS and on some Linux desktop environments, including:
|
||||
///
|
||||
/// * KDE Plasma (Wayland only)
|
||||
///
|
||||
/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting
|
||||
/// this setting to either `true` or any positive blur intensity value would
|
||||
/// achieve the same effect. The reason is that KWin, the window compositor
|
||||
/// powering Plasma, only has one global blur setting and does not allow
|
||||
/// applications to specify individual blur settings.
|
||||
///
|
||||
/// To configure KWin's global blur setting, open System Settings and go to
|
||||
/// "Apps & Windows" > "Window Management" > "Desktop Effects" and select the
|
||||
/// "Blur" plugin. If disabled, enable it by ticking the checkbox to the left.
|
||||
/// Then click on the "Configure" button and there will be two sliders that
|
||||
/// allow you to set background blur and noise intensities for all apps,
|
||||
/// including Ghostty.
|
||||
///
|
||||
/// All other Linux desktop environments are as of now unsupported. Users may
|
||||
/// need to set environment-specific settings and/or install third-party plugins
|
||||
/// in order to support background blur, as there isn't a unified interface for
|
||||
/// doing so.
|
||||
@"background-blur-radius": BackgroundBlur = .false,
|
||||
|
||||
/// The opacity level (opposite of transparency) of an unfocused split.
|
||||
/// Unfocused splits by default are slightly faded out to make it easier to see
|
||||
@ -586,8 +641,14 @@ palette: Palette = .{},
|
||||
/// that rectangle and can be used to carefully control the dimming effect.
|
||||
///
|
||||
/// This will default to the background color.
|
||||
///
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"unfocused-split-fill": ?Color = null,
|
||||
|
||||
/// The color of the split divider. If this is not set, a default will be chosen.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"split-divider-color": ?Color = null,
|
||||
|
||||
/// The command to run, usually a shell. If this is not an absolute path, it'll
|
||||
/// be looked up in the `PATH`. If this is not set, a default will be looked up
|
||||
/// from your system. The rules for the default lookup are:
|
||||
@ -724,7 +785,7 @@ fullscreen: bool = false,
|
||||
/// This configuration can be reloaded at runtime. If it is set, the title
|
||||
/// will update for all windows. If it is unset, the next title change escape
|
||||
/// sequence will be honored but previous changes will not retroactively
|
||||
/// be set. This latter case may require you restart programs such as neovim
|
||||
/// be set. This latter case may require you to restart programs such as Neovim
|
||||
/// to get the new title.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
@ -907,6 +968,15 @@ class: ?[:0]const u8 = null,
|
||||
/// Since they are not associated with a specific terminal surface,
|
||||
/// they're never encoded.
|
||||
///
|
||||
/// * `performable:` - Only consume the input if the action is able to be
|
||||
/// performed. For example, the `copy_to_clipboard` action will only
|
||||
/// consume the input if there is a selection to copy. If there is no
|
||||
/// selection, Ghostty behaves as if the keybind was not set. This has
|
||||
/// no effect with `global:` or `all:`-prefixed keybinds. For key
|
||||
/// sequences, this will reset the sequence if the action is not
|
||||
/// performable (acting identically to not having a keybind set at
|
||||
/// all).
|
||||
///
|
||||
/// Keybind triggers are not unique per prefix combination. For example,
|
||||
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
|
||||
/// set later will overwrite the keybind set earlier. In this case, the
|
||||
@ -1042,7 +1112,10 @@ keybind: Keybinds = .{},
|
||||
|
||||
/// The font that will be used for the application's window and tab titles.
|
||||
///
|
||||
/// This is currently only supported on macOS.
|
||||
/// If this setting is left unset, the system default font will be used.
|
||||
///
|
||||
/// Note: any font available on the system may be used, this font is not
|
||||
/// required to be a fixed-width font.
|
||||
@"window-title-font-family": ?[:0]const u8 = null,
|
||||
|
||||
/// The theme to use for the windows. Valid values:
|
||||
@ -1104,6 +1177,32 @@ keybind: Keybinds = .{},
|
||||
@"window-height": u32 = 0,
|
||||
@"window-width": u32 = 0,
|
||||
|
||||
/// The starting window position. This position is in pixels and is relative
|
||||
/// to the top-left corner of the primary monitor. Both values must be set to take
|
||||
/// effect. If only one value is set, it is ignored.
|
||||
///
|
||||
/// Note that the window manager may put limits on the position or override
|
||||
/// the position. For example, a tiling window manager may force the window
|
||||
/// to be a certain position to fit within the grid. There is nothing Ghostty
|
||||
/// will do about this, but it will make an effort.
|
||||
///
|
||||
/// Also note that negative values are also up to the operating system and
|
||||
/// window manager. Some window managers may not allow windows to be placed
|
||||
/// off-screen.
|
||||
///
|
||||
/// Invalid positions are runtime-specific, but generally the positions are
|
||||
/// clamped to the nearest valid position.
|
||||
///
|
||||
/// On macOS, the window position is relative to the top-left corner of
|
||||
/// the visible screen area. This means that if the menu bar is visible, the
|
||||
/// window will be placed below the menu bar.
|
||||
///
|
||||
/// Note: this is only supported on macOS and Linux GLFW builds. The GTK
|
||||
/// runtime does not support setting the window position (this is a limitation
|
||||
/// of GTK 4.0).
|
||||
@"window-position-x": ?i16 = null,
|
||||
@"window-position-y": ?i16 = null,
|
||||
|
||||
/// Whether to enable saving and restoring window state. Window state includes
|
||||
/// their position, size, tabs, splits, etc. Some window state requires shell
|
||||
/// integration, such as preserving working directories. See `shell-integration`
|
||||
@ -1152,11 +1251,15 @@ keybind: Keybinds = .{},
|
||||
/// Background color for the window titlebar. This only takes effect if
|
||||
/// window-theme is set to ghostty. Currently only supported in the GTK app
|
||||
/// runtime.
|
||||
///
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"window-titlebar-background": ?Color = null,
|
||||
|
||||
/// Foreground color for the window titlebar. This only takes effect if
|
||||
/// window-theme is set to ghostty. Currently only supported in the GTK app
|
||||
/// runtime.
|
||||
///
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"window-titlebar-foreground": ?Color = null,
|
||||
|
||||
/// This controls when resize overlays are shown. Resize overlays are a
|
||||
@ -1772,21 +1875,19 @@ keybind: Keybinds = .{},
|
||||
|
||||
/// The color of the ghost in the macOS app icon.
|
||||
///
|
||||
/// The format of the color is the same as the `background` configuration;
|
||||
/// see that for more information.
|
||||
///
|
||||
/// Note: This configuration is required when `macos-icon` is set to
|
||||
/// `custom-style`.
|
||||
///
|
||||
/// This only has an effect when `macos-icon` is set to `custom-style`.
|
||||
///
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"macos-icon-ghost-color": ?Color = null,
|
||||
|
||||
/// The color of the screen in the macOS app icon.
|
||||
///
|
||||
/// The screen is a gradient so you can specify multiple colors that
|
||||
/// make up the gradient. Colors should be separated by commas. The
|
||||
/// format of the color is the same as the `background` configuration;
|
||||
/// see that for more information.
|
||||
/// make up the gradient. Comma-separated colors may be specified as
|
||||
/// as either hex (`#RRGGBB` or `RRGGBB`) or as named X11 colors.
|
||||
///
|
||||
/// Note: This configuration is required when `macos-icon` is set to
|
||||
/// `custom-style`.
|
||||
@ -1905,6 +2006,29 @@ keybind: Keybinds = .{},
|
||||
/// Changing this value at runtime will only affect new windows.
|
||||
@"adw-toolbar-style": AdwToolbarStyle = .raised,
|
||||
|
||||
/// Control the toasts that Ghostty shows. Toasts are small notifications
|
||||
/// that appear overlaid on top of the terminal window. They are used to
|
||||
/// show information that is not critical but may be important.
|
||||
///
|
||||
/// Possible toasts are:
|
||||
///
|
||||
/// - `clipboard-copy` (default: true) - Show a toast when text is copied
|
||||
/// to the clipboard.
|
||||
///
|
||||
/// To specify a toast to enable, specify the name of the toast. To specify
|
||||
/// a toast to disable, prefix the name with `no-`. For example, to disable
|
||||
/// the clipboard-copy toast, set this configuration to `no-clipboard-copy`.
|
||||
/// To enable the clipboard-copy toast, set this configuration to
|
||||
/// `clipboard-copy`.
|
||||
///
|
||||
/// Multiple toasts can be enabled or disabled by separating them with a comma.
|
||||
///
|
||||
/// A value of "false" will disable all toasts. A value of "true" will
|
||||
/// enable all toasts.
|
||||
///
|
||||
/// This configuration only applies to GTK with Adwaita enabled.
|
||||
@"adw-toast": AdwToast = .{},
|
||||
|
||||
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
|
||||
/// are the new typical Gnome style where tabs fill their available space.
|
||||
/// If you set this to `false` then tabs will only take up space they need,
|
||||
@ -1925,6 +2049,15 @@ keybind: Keybinds = .{},
|
||||
/// Adwaita support.
|
||||
@"gtk-adwaita": bool = true,
|
||||
|
||||
/// Custom CSS files to be loaded.
|
||||
///
|
||||
/// This configuration can be repeated multiple times to load multiple files.
|
||||
/// Prepend a ? character to the file path to suppress errors if the file does
|
||||
/// not exist. If you want to include a file that begins with a literal ?
|
||||
/// character, surround the file path in double quotes (").
|
||||
/// The file size limit for a single stylesheet is 5MiB.
|
||||
@"gtk-custom-css": RepeatablePath = .{},
|
||||
|
||||
/// If `true` (default), applications running in the terminal can show desktop
|
||||
/// notifications using certain escape sequences such as OSC 9 or OSC 777.
|
||||
@"desktop-notifications": bool = true,
|
||||
@ -1963,10 +2096,11 @@ term: []const u8 = "xterm-ghostty",
|
||||
/// * `download` - Check for updates, automatically download the update,
|
||||
/// notify the user, but do not automatically install the update.
|
||||
///
|
||||
/// The default value is `check`.
|
||||
/// If unset, we defer to Sparkle's default behavior, which respects the
|
||||
/// preference stored in the standard user defaults (`defaults(1)`).
|
||||
///
|
||||
/// Changing this value at runtime works after a small delay.
|
||||
@"auto-update": AutoUpdate = .check,
|
||||
@"auto-update": ?AutoUpdate = null,
|
||||
|
||||
/// The release channel to use for auto-updates.
|
||||
///
|
||||
@ -2066,6 +2200,25 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
);
|
||||
|
||||
{
|
||||
// On non-MacOS desktop envs (Windows, KDE, Gnome, Xfce), ctrl+insert is an
|
||||
// alt keybinding for Copy and shift+ins is an alt keybinding for Paste
|
||||
//
|
||||
// The order of these blocks is important. The *last* added keybind for a given action is
|
||||
// what will display in the menu. We want the more typical keybinds after this block to be
|
||||
// the standard
|
||||
if (!builtin.target.isDarwin()) {
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .insert }, .mods = .{ .ctrl = true } },
|
||||
.{ .copy_to_clipboard = {} },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .insert }, .mods = .{ .shift = true } },
|
||||
.{ .paste_from_clipboard = {} },
|
||||
);
|
||||
}
|
||||
|
||||
// On macOS we default to super but Linux ctrl+shift since
|
||||
// ctrl+c is to kill the process.
|
||||
const mods: inputpkg.Mods = if (builtin.target.isDarwin())
|
||||
@ -2124,45 +2277,53 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
);
|
||||
|
||||
// Expand Selection
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .left }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .left },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .right }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .right },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .up }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .up },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .down }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .down },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .page_up },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .page_down },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .home }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .home },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .end }, .mods = .{ .shift = true } },
|
||||
.{ .adjust_selection = .end },
|
||||
.{ .performable = true },
|
||||
);
|
||||
|
||||
// Tabs common to all platforms
|
||||
@ -2247,12 +2408,12 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .up }, .mods = .{ .ctrl = true, .alt = true } },
|
||||
.{ .goto_split = .top },
|
||||
.{ .goto_split = .up },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .down }, .mods = .{ .ctrl = true, .alt = true } },
|
||||
.{ .goto_split = .bottom },
|
||||
.{ .goto_split = .down },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
@ -2412,10 +2573,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
.{ .key = .{ .translated = .q }, .mods = .{ .super = true } },
|
||||
.{ .quit = {} },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
try result.keybind.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .k }, .mods = .{ .super = true } },
|
||||
.{ .clear_screen = {} },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
@ -2516,12 +2678,12 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .up }, .mods = .{ .super = true, .alt = true } },
|
||||
.{ .goto_split = .top },
|
||||
.{ .goto_split = .up },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .down }, .mods = .{ .super = true, .alt = true } },
|
||||
.{ .goto_split = .bottom },
|
||||
.{ .goto_split = .down },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
@ -2695,6 +2857,9 @@ pub fn loadOptionalFile(
|
||||
|
||||
fn writeConfigTemplate(path: []const u8) !void {
|
||||
log.info("creating template config file: path={s}", .{path});
|
||||
if (std.fs.path.dirname(path)) |dir_path| {
|
||||
try std.fs.makeDirAbsolute(dir_path);
|
||||
}
|
||||
const file = try std.fs.createFileAbsolute(path, .{});
|
||||
defer file.close();
|
||||
try std.fmt.format(
|
||||
@ -2798,7 +2963,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
||||
self.@"config-default-files" = true;
|
||||
|
||||
// Keep track of the replay steps up to this point so we
|
||||
// can replay if we are disgarding the default files.
|
||||
// can replay if we are discarding the default files.
|
||||
const replay_len_start = self._replay_steps.items.len;
|
||||
|
||||
// Keep track of font families because if they are set from the CLI
|
||||
@ -4036,7 +4201,7 @@ pub const Palette = struct {
|
||||
const eqlIdx = std.mem.indexOf(u8, value, "=") orelse
|
||||
return error.InvalidValue;
|
||||
|
||||
const key = try std.fmt.parseInt(u8, value[0..eqlIdx], 10);
|
||||
const key = try std.fmt.parseInt(u8, value[0..eqlIdx], 0);
|
||||
const rgb = try Color.parseCLI(value[eqlIdx + 1 ..]);
|
||||
self.value[key] = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
|
||||
}
|
||||
@ -4076,6 +4241,28 @@ pub const Palette = struct {
|
||||
try testing.expect(p.value[0].b == 0xCC);
|
||||
}
|
||||
|
||||
test "parseCLI base" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Self = .{};
|
||||
|
||||
try p.parseCLI("0b1=#014589");
|
||||
try p.parseCLI("0o7=#234567");
|
||||
try p.parseCLI("0xF=#ABCDEF");
|
||||
|
||||
try testing.expect(p.value[0b1].r == 0x01);
|
||||
try testing.expect(p.value[0b1].g == 0x45);
|
||||
try testing.expect(p.value[0b1].b == 0x89);
|
||||
|
||||
try testing.expect(p.value[0o7].r == 0x23);
|
||||
try testing.expect(p.value[0o7].g == 0x45);
|
||||
try testing.expect(p.value[0o7].b == 0x67);
|
||||
|
||||
try testing.expect(p.value[0xF].r == 0xAB);
|
||||
try testing.expect(p.value[0xF].g == 0xCD);
|
||||
try testing.expect(p.value[0xF].b == 0xEF);
|
||||
}
|
||||
|
||||
test "parseCLI overflow" {
|
||||
const testing = std.testing;
|
||||
|
||||
@ -4340,6 +4527,45 @@ pub const RepeatablePath = struct {
|
||||
// If it isn't absolute, we need to make it absolute relative
|
||||
// to the base.
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
|
||||
// Check if the path starts with a tilde and expand it to the
|
||||
// home directory on Linux/macOS. We explicitly look for "~/"
|
||||
// because we don't support alternate users such as "~alice/"
|
||||
if (std.mem.startsWith(u8, path, "~/")) expand: {
|
||||
// Windows isn't supported yet
|
||||
if (comptime builtin.os.tag == .windows) break :expand;
|
||||
|
||||
const expanded: []const u8 = internal_os.expandHome(
|
||||
path,
|
||||
&buf,
|
||||
) catch |err| {
|
||||
try diags.append(alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
alloc,
|
||||
"error expanding home directory for path {s}: {}",
|
||||
.{ path, err },
|
||||
),
|
||||
});
|
||||
|
||||
// Blank this path so that we don't attempt to resolve it
|
||||
// again
|
||||
self.value.items[i] = .{ .required = "" };
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
log.debug(
|
||||
"expanding file path from home directory: path={s}",
|
||||
.{expanded},
|
||||
);
|
||||
|
||||
switch (self.value.items[i]) {
|
||||
.optional, .required => |*p| p.* = try alloc.dupeZ(u8, expanded),
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const abs = dir.realpath(path, &buf) catch |err| abs: {
|
||||
if (err == error.FileNotFound) {
|
||||
// The file doesn't exist. Try to resolve the relative path
|
||||
@ -5348,6 +5574,11 @@ pub const AdwToolbarStyle = enum {
|
||||
@"raised-border",
|
||||
};
|
||||
|
||||
/// See adw-toast
|
||||
pub const AdwToast = packed struct {
|
||||
@"clipboard-copy": bool = true,
|
||||
};
|
||||
|
||||
/// See mouse-shift-capture
|
||||
pub const MouseShiftCapture = enum {
|
||||
false,
|
||||
@ -5441,6 +5672,70 @@ pub const AutoUpdate = enum {
|
||||
download,
|
||||
};
|
||||
|
||||
/// See background-blur-radius
|
||||
pub const BackgroundBlur = union(enum) {
|
||||
false,
|
||||
true,
|
||||
radius: u8,
|
||||
|
||||
pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void {
|
||||
const input_ = input orelse {
|
||||
// Emulate behavior for bools
|
||||
self.* = .true;
|
||||
return;
|
||||
};
|
||||
|
||||
self.* = if (cli.args.parseBool(input_)) |b|
|
||||
if (b) .true else .false
|
||||
else |_|
|
||||
.{ .radius = std.fmt.parseInt(
|
||||
u8,
|
||||
input_,
|
||||
0,
|
||||
) catch return error.InvalidValue };
|
||||
}
|
||||
|
||||
pub fn cval(self: BackgroundBlur) u8 {
|
||||
return switch (self) {
|
||||
.false => 0,
|
||||
.true => 20,
|
||||
.radius => |v| v,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn formatEntry(
|
||||
self: BackgroundBlur,
|
||||
formatter: anytype,
|
||||
) !void {
|
||||
switch (self) {
|
||||
.false => try formatter.formatEntry(bool, false),
|
||||
.true => try formatter.formatEntry(bool, true),
|
||||
.radius => |v| try formatter.formatEntry(u8, v),
|
||||
}
|
||||
}
|
||||
|
||||
test "parse BackgroundBlur" {
|
||||
const testing = std.testing;
|
||||
var v: BackgroundBlur = undefined;
|
||||
|
||||
try v.parseCLI(null);
|
||||
try testing.expectEqual(.true, v);
|
||||
|
||||
try v.parseCLI("true");
|
||||
try testing.expectEqual(.true, v);
|
||||
|
||||
try v.parseCLI("false");
|
||||
try testing.expectEqual(.false, v);
|
||||
|
||||
try v.parseCLI("42");
|
||||
try testing.expectEqual(42, v.radius);
|
||||
|
||||
try testing.expectError(error.InvalidValue, v.parseCLI(""));
|
||||
try testing.expectError(error.InvalidValue, v.parseCLI("aaaa"));
|
||||
try testing.expectError(error.InvalidValue, v.parseCLI("420"));
|
||||
}
|
||||
};
|
||||
|
||||
/// See theme
|
||||
pub const Theme = struct {
|
||||
light: []const u8,
|
||||
|
@ -42,6 +42,11 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
|
||||
ptr.* = @intCast(value);
|
||||
},
|
||||
|
||||
i16 => {
|
||||
const ptr: *c_short = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = @intCast(value);
|
||||
},
|
||||
|
||||
f32, f64 => |Float| {
|
||||
const ptr: *Float = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = @floatCast(value);
|
||||
@ -79,6 +84,17 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
|
||||
ptr.* = @intCast(@as(Backing, @bitCast(value)));
|
||||
},
|
||||
|
||||
.Union => |_| {
|
||||
if (@hasDecl(T, "cval")) {
|
||||
const PtrT = @typeInfo(@TypeOf(T.cval)).Fn.return_type.?;
|
||||
const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = value.cval();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
else => return false,
|
||||
},
|
||||
}
|
||||
@ -167,3 +183,30 @@ test "c_get: optional" {
|
||||
try testing.expectEqual(0, cval.b);
|
||||
}
|
||||
}
|
||||
|
||||
test "c_get: background-blur" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var c = try Config.default(alloc);
|
||||
defer c.deinit();
|
||||
|
||||
{
|
||||
c.@"background-blur-radius" = .false;
|
||||
var cval: u8 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||
try testing.expectEqual(0, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur-radius" = .true;
|
||||
var cval: u8 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||
try testing.expectEqual(20, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur-radius" = .{ .radius = 42 };
|
||||
var cval: u8 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||
try testing.expectEqual(42, cval);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const build_config = @import("../build_config.zig");
|
||||
const sentry = @import("sentry");
|
||||
const build_options = @import("build_options");
|
||||
const sentry = if (build_options.sentry) @import("sentry");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const crash = @import("main.zig");
|
||||
const state = &@import("../global.zig").state;
|
||||
@ -47,6 +48,8 @@ pub threadlocal var thread_state: ?ThreadState = null;
|
||||
/// It is up to the user to grab the logs and manually send them to us
|
||||
/// (or they own Sentry instance) if they want to.
|
||||
pub fn init(gpa: Allocator) !void {
|
||||
if (comptime !build_options.sentry) return;
|
||||
|
||||
// Not supported on Windows currently, doesn't build.
|
||||
if (comptime builtin.os.tag == .windows) return;
|
||||
|
||||
@ -76,6 +79,8 @@ pub fn init(gpa: Allocator) !void {
|
||||
}
|
||||
|
||||
fn initThread(gpa: Allocator) !void {
|
||||
if (comptime !build_options.sentry) return;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
@ -101,7 +106,23 @@ fn initThread(gpa: Allocator) !void {
|
||||
sentry.c.sentry_options_set_before_send(opts, beforeSend, null);
|
||||
|
||||
// Determine the Sentry cache directory.
|
||||
const cache_dir = try internal_os.xdg.cache(alloc, .{ .subdir = "ghostty/sentry" });
|
||||
const cache_dir = cache_dir: {
|
||||
// On macOS, we prefer to use the NSCachesDirectory value to be
|
||||
// a more idiomatic macOS application. But if XDG env vars are set
|
||||
// we will respect them.
|
||||
if (comptime builtin.os.tag == .macos) macos: {
|
||||
if (std.posix.getenv("XDG_CACHE_HOME") != null) break :macos;
|
||||
break :cache_dir try internal_os.macos.cacheDir(
|
||||
alloc,
|
||||
"sentry",
|
||||
);
|
||||
}
|
||||
|
||||
break :cache_dir try internal_os.xdg.cache(
|
||||
alloc,
|
||||
.{ .subdir = "ghostty/sentry" },
|
||||
);
|
||||
};
|
||||
sentry.c.sentry_options_set_database_path_n(
|
||||
opts,
|
||||
cache_dir.ptr,
|
||||
@ -129,6 +150,8 @@ fn initThread(gpa: Allocator) !void {
|
||||
/// Process-wide deinitialization of our Sentry client. This ensures all
|
||||
/// our data is flushed.
|
||||
pub fn deinit() void {
|
||||
if (comptime !build_options.sentry) return;
|
||||
|
||||
if (comptime builtin.os.tag == .windows) return;
|
||||
|
||||
// If we're still initializing then wait for init to finish. This
|
||||
|
@ -551,7 +551,7 @@ pub const CoreText = struct {
|
||||
for (0..result.len) |i| {
|
||||
result[i] = list.getValueAtIndex(macos.text.FontDescriptor, i);
|
||||
|
||||
// We need to retain becauseonce the list is freed it will
|
||||
// We need to retain because once the list is freed it will
|
||||
// release all its members.
|
||||
result[i].retain();
|
||||
}
|
||||
|
@ -100,6 +100,15 @@ pub const RenderOptions = struct {
|
||||
///
|
||||
/// This only works with CoreText currently.
|
||||
thicken: bool = false,
|
||||
|
||||
/// "Strength" of the thickening, between `0` and `255`.
|
||||
/// Only has an effect when `thicken` is enabled.
|
||||
///
|
||||
/// `0` does not correspond to *no* thickening,
|
||||
/// just the *lightest* thickening available.
|
||||
///
|
||||
/// CoreText only.
|
||||
thicken_strength: u8 = 255,
|
||||
};
|
||||
|
||||
test {
|
||||
|
@ -354,7 +354,7 @@ pub const Face = struct {
|
||||
.depth = 1,
|
||||
.space = try macos.graphics.ColorSpace.createDeviceGray(),
|
||||
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.none),
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.only),
|
||||
} else .{
|
||||
.color = true,
|
||||
.depth = 4,
|
||||
@ -398,7 +398,7 @@ pub const Face = struct {
|
||||
if (color.color)
|
||||
context.setRGBFillColor(ctx, 1, 1, 1, 0)
|
||||
else
|
||||
context.setGrayFillColor(ctx, 0, 0);
|
||||
context.setGrayFillColor(ctx, 1, 0);
|
||||
context.fillRect(ctx, .{
|
||||
.origin = .{ .x = 0, .y = 0 },
|
||||
.size = .{
|
||||
@ -421,8 +421,9 @@ pub const Face = struct {
|
||||
context.setRGBFillColor(ctx, 1, 1, 1, 1);
|
||||
context.setRGBStrokeColor(ctx, 1, 1, 1, 1);
|
||||
} else {
|
||||
context.setGrayFillColor(ctx, 1, 1);
|
||||
context.setGrayStrokeColor(ctx, 1, 1);
|
||||
const strength: f64 = @floatFromInt(opts.thicken_strength);
|
||||
context.setGrayFillColor(ctx, strength / 255.0, 1);
|
||||
context.setGrayStrokeColor(ctx, strength / 255.0, 1);
|
||||
}
|
||||
|
||||
// If we are drawing with synthetic bold then use a fill stroke
|
||||
|
@ -1,6 +1,7 @@
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
const run = @import("shaper/run.zig");
|
||||
const feature = @import("shaper/feature.zig");
|
||||
pub const noop = @import("shaper/noop.zig");
|
||||
pub const harfbuzz = @import("shaper/harfbuzz.zig");
|
||||
pub const coretext = @import("shaper/coretext.zig");
|
||||
@ -8,6 +9,9 @@ pub const web_canvas = @import("shaper/web_canvas.zig");
|
||||
pub const Cache = @import("shaper/Cache.zig");
|
||||
pub const TextRun = run.TextRun;
|
||||
pub const RunIterator = run.RunIterator;
|
||||
pub const Feature = feature.Feature;
|
||||
pub const FeatureList = feature.FeatureList;
|
||||
pub const default_features = feature.default_features;
|
||||
|
||||
/// Shaper implementation for our compile options.
|
||||
pub const Shaper = switch (options.backend) {
|
||||
@ -49,10 +53,7 @@ pub const Cell = struct {
|
||||
|
||||
/// Options for shapers.
|
||||
pub const Options = struct {
|
||||
/// Font features to use when shaping. These can be in the following
|
||||
/// formats: "-feat" "+feat" "feat". A "-"-prefix is used to disable
|
||||
/// a feature and the others are used to enable a feature. If a feature
|
||||
/// isn't supported or is invalid, it will be ignored.
|
||||
/// Font features to use when shaping.
|
||||
///
|
||||
/// Note: eventually, this will move to font.Face probably as we may
|
||||
/// want to support per-face feature configuration. For now, we only
|
||||
|
@ -7,6 +7,9 @@ const trace = @import("tracy").trace;
|
||||
const font = @import("../main.zig");
|
||||
const os = @import("../../os/main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const Feature = font.shape.Feature;
|
||||
const FeatureList = font.shape.FeatureList;
|
||||
const default_features = font.shape.default_features;
|
||||
const Face = font.Face;
|
||||
const Collection = font.Collection;
|
||||
const DeferredFace = font.DeferredFace;
|
||||
@ -40,9 +43,10 @@ pub const Shaper = struct {
|
||||
/// The string used for shaping the current run.
|
||||
run_state: RunState,
|
||||
|
||||
/// The font features we want to use. The hardcoded features are always
|
||||
/// set first.
|
||||
features: FeatureList,
|
||||
/// CoreFoundation Dictionary which represents our font feature settings.
|
||||
features: *macos.foundation.Dictionary,
|
||||
/// A version of the features dictionary with the default features excluded.
|
||||
features_no_default: *macos.foundation.Dictionary,
|
||||
|
||||
/// The shared memory used for shaping results.
|
||||
cell_buf: CellBuf,
|
||||
@ -100,51 +104,17 @@ pub const Shaper = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// List of font features, parsed into the data structures used by
|
||||
/// the CoreText API. The CoreText API requires a pretty annoying wrapping
|
||||
/// to setup font features:
|
||||
///
|
||||
/// - The key parsed into a CFString
|
||||
/// - The value parsed into a CFNumber
|
||||
/// - The key and value are then put into a CFDictionary
|
||||
/// - The CFDictionary is then put into a CFArray
|
||||
/// - The CFArray is then put into another CFDictionary
|
||||
/// - The CFDictionary is then passed to the CoreText API to create
|
||||
/// a new font with the features set.
|
||||
///
|
||||
/// This structure handles up to the point that we have a CFArray of
|
||||
/// CFDictionary objects representing the font features and provides
|
||||
/// functions for creating the dictionary to init the font.
|
||||
const FeatureList = struct {
|
||||
list: *macos.foundation.MutableArray,
|
||||
/// Create a CoreFoundation Dictionary suitable for
|
||||
/// settings the font features of a CoreText font.
|
||||
fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary {
|
||||
const list = try macos.foundation.MutableArray.create();
|
||||
errdefer list.release();
|
||||
|
||||
pub fn init() !FeatureList {
|
||||
var list = try macos.foundation.MutableArray.create();
|
||||
errdefer list.release();
|
||||
return .{ .list = list };
|
||||
}
|
||||
|
||||
pub fn deinit(self: FeatureList) void {
|
||||
self.list.release();
|
||||
}
|
||||
|
||||
/// Append the given feature to the list. The feature syntax is
|
||||
/// the same as Harfbuzz: "feat" enables it and "-feat" disables it.
|
||||
pub fn append(self: *FeatureList, name_raw: []const u8) !void {
|
||||
// If the name is `-name` then we are disabling the feature,
|
||||
// otherwise we are enabling it, so we need to parse this out.
|
||||
const name = if (name_raw[0] == '-') name_raw[1..] else name_raw;
|
||||
const dict = try featureDict(name, name_raw[0] != '-');
|
||||
defer dict.release();
|
||||
self.list.appendValue(macos.foundation.Dictionary, dict);
|
||||
}
|
||||
|
||||
/// Create the dictionary for the given feature and value.
|
||||
fn featureDict(name: []const u8, v: bool) !*macos.foundation.Dictionary {
|
||||
const value_num: c_int = @intFromBool(v);
|
||||
for (feats) |feat| {
|
||||
const value_num: c_int = @intCast(feat.value);
|
||||
|
||||
// Keys can only be ASCII.
|
||||
var key = try macos.foundation.String.createWithBytes(name, .ascii, false);
|
||||
var key = try macos.foundation.String.createWithBytes(&feat.tag, .ascii, false);
|
||||
defer key.release();
|
||||
var value = try macos.foundation.Number.create(.int, &value_num);
|
||||
defer value.release();
|
||||
@ -154,50 +124,44 @@ pub const Shaper = struct {
|
||||
macos.text.c.kCTFontOpenTypeFeatureTag,
|
||||
macos.text.c.kCTFontOpenTypeFeatureValue,
|
||||
},
|
||||
&[_]?*const anyopaque{
|
||||
key,
|
||||
value,
|
||||
},
|
||||
&[_]?*const anyopaque{ key, value },
|
||||
);
|
||||
errdefer dict.release();
|
||||
return dict;
|
||||
defer dict.release();
|
||||
|
||||
list.appendValue(macos.foundation.Dictionary, dict);
|
||||
}
|
||||
|
||||
/// Returns the dictionary to use with the font API to set the
|
||||
/// features. This should be released by the caller.
|
||||
pub fn attrsDict(
|
||||
self: FeatureList,
|
||||
omit_defaults: bool,
|
||||
) !*macos.foundation.Dictionary {
|
||||
// Get our feature list. If we're omitting defaults then we
|
||||
// slice off the hardcoded features.
|
||||
const list = if (!omit_defaults) self.list else list: {
|
||||
const list = try macos.foundation.MutableArray.createCopy(@ptrCast(self.list));
|
||||
for (hardcoded_features) |_| list.removeValue(0);
|
||||
break :list list;
|
||||
};
|
||||
defer if (omit_defaults) list.release();
|
||||
var dict = try macos.foundation.Dictionary.create(
|
||||
&[_]?*const anyopaque{macos.text.c.kCTFontFeatureSettingsAttribute},
|
||||
&[_]?*const anyopaque{list},
|
||||
);
|
||||
errdefer dict.release();
|
||||
|
||||
var dict = try macos.foundation.Dictionary.create(
|
||||
&[_]?*const anyopaque{macos.text.c.kCTFontFeatureSettingsAttribute},
|
||||
&[_]?*const anyopaque{list},
|
||||
);
|
||||
errdefer dict.release();
|
||||
return dict;
|
||||
}
|
||||
};
|
||||
|
||||
// These features are hardcoded to always be on by default. Users
|
||||
// can turn them off by setting the features to "-liga" for example.
|
||||
const hardcoded_features = [_][]const u8{ "dlig", "liga" };
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// The cell_buf argument is the buffer to use for storing shaped results.
|
||||
/// This should be at least the number of columns in the terminal.
|
||||
pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper {
|
||||
var feats = try FeatureList.init();
|
||||
errdefer feats.deinit();
|
||||
for (hardcoded_features) |name| try feats.append(name);
|
||||
for (opts.features) |name| try feats.append(name);
|
||||
var feature_list: FeatureList = .{};
|
||||
defer feature_list.deinit(alloc);
|
||||
for (opts.features) |feature_str| {
|
||||
try feature_list.appendFromString(alloc, feature_str);
|
||||
}
|
||||
|
||||
// We need to construct two attrs dictionaries for font features;
|
||||
// one without the default features included, and one with them.
|
||||
const feats = feature_list.features.items;
|
||||
const feats_df = try alloc.alloc(Feature, feats.len + default_features.len);
|
||||
defer alloc.free(feats_df);
|
||||
|
||||
@memcpy(feats_df[0..default_features.len], &default_features);
|
||||
@memcpy(feats_df[default_features.len..], feats);
|
||||
|
||||
const features = try makeFeaturesDict(feats_df);
|
||||
errdefer features.release();
|
||||
const features_no_default = try makeFeaturesDict(feats);
|
||||
errdefer features_no_default.release();
|
||||
|
||||
var run_state = RunState.init();
|
||||
errdefer run_state.deinit(alloc);
|
||||
@ -242,7 +206,8 @@ pub const Shaper = struct {
|
||||
.alloc = alloc,
|
||||
.cell_buf = .{},
|
||||
.run_state = run_state,
|
||||
.features = feats,
|
||||
.features = features,
|
||||
.features_no_default = features_no_default,
|
||||
.writing_direction = writing_direction,
|
||||
.cached_fonts = .{},
|
||||
.cached_font_grid = 0,
|
||||
@ -255,7 +220,8 @@ pub const Shaper = struct {
|
||||
pub fn deinit(self: *Shaper) void {
|
||||
self.cell_buf.deinit(self.alloc);
|
||||
self.run_state.deinit(self.alloc);
|
||||
self.features.deinit();
|
||||
self.features.release();
|
||||
self.features_no_default.release();
|
||||
self.writing_direction.release();
|
||||
|
||||
{
|
||||
@ -509,8 +475,8 @@ pub const Shaper = struct {
|
||||
// If we have it, return the cached attr dict.
|
||||
if (self.cached_fonts.items[index_int]) |cached| return cached;
|
||||
|
||||
// Features dictionary, font descriptor, font
|
||||
try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 3);
|
||||
// Font descriptor, font
|
||||
try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 2);
|
||||
|
||||
const run_font = font: {
|
||||
// The CoreText shaper relies on CoreText and CoreText claims
|
||||
@ -533,8 +499,10 @@ pub const Shaper = struct {
|
||||
const face = try grid.resolver.collection.getFace(index);
|
||||
const original = face.font;
|
||||
|
||||
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
|
||||
self.cf_release_pool.appendAssumeCapacity(attrs);
|
||||
const attrs = if (face.quirks_disable_default_font_features)
|
||||
self.features_no_default
|
||||
else
|
||||
self.features;
|
||||
|
||||
const desc = try macos.text.FontDescriptor.createWithAttributes(attrs);
|
||||
self.cf_release_pool.appendAssumeCapacity(desc);
|
||||
|
390
src/font/shaper/feature.zig
Normal file
390
src/font/shaper/feature.zig
Normal file
@ -0,0 +1,390 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = std.log.scoped(.font_shaper);
|
||||
|
||||
/// Represents an OpenType font feature setting, which consists of a tag and
|
||||
/// a numeric parameter >= 0. Most features are boolean, so only parameters
|
||||
/// of 0 and 1 make sense for them, but some (e.g. 'cv01'..'cv99') can take
|
||||
/// parameters to choose between multiple variants of a given character or
|
||||
/// characters.
|
||||
///
|
||||
/// Ref:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#features-and-lookups
|
||||
/// - https://harfbuzz.github.io/shaping-opentype-features.html
|
||||
pub const Feature = struct {
|
||||
tag: [4]u8,
|
||||
value: u32,
|
||||
|
||||
pub fn fromString(str: []const u8) ?Feature {
|
||||
var fbs = std.io.fixedBufferStream(str);
|
||||
const reader = fbs.reader();
|
||||
return Feature.fromReader(reader);
|
||||
}
|
||||
|
||||
/// Parse a single font feature setting from a std.io.Reader, with a version
|
||||
/// of the syntax of HarfBuzz's font feature strings. Stops at end of stream
|
||||
/// or when a ',' is encountered.
|
||||
///
|
||||
/// This parsing aims to be as error-tolerant as possible while avoiding any
|
||||
/// assumptions in ambiguous scenarios. When invalid syntax is encountered,
|
||||
/// the reader is advanced to the next boundary (end-of-stream or ',') so
|
||||
/// that further features may be read.
|
||||
///
|
||||
/// Ref: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string
|
||||
pub fn fromReader(reader: anytype) ?Feature {
|
||||
var tag: [4]u8 = undefined;
|
||||
var value: ?u32 = null;
|
||||
|
||||
// TODO: when we move to Zig 0.14 this can be replaced with a
|
||||
// labeled switch continue pattern rather than this loop.
|
||||
var state: union(enum) {
|
||||
/// Initial state.
|
||||
start: void,
|
||||
/// Parsing the tag, data is index.
|
||||
tag: u2,
|
||||
/// In the space between the tag and the value.
|
||||
space: void,
|
||||
/// Parsing an integer parameter directly in to `value`.
|
||||
int: void,
|
||||
/// Parsing a boolean keyword parameter ("on"/"off").
|
||||
bool: void,
|
||||
/// Encountered an unrecoverable syntax error, advancing to boundary.
|
||||
err: void,
|
||||
/// Done parsing feature.
|
||||
done: void,
|
||||
} = .start;
|
||||
while (true) {
|
||||
// If we hit the end of the stream we just pretend it's a comma.
|
||||
const byte = reader.readByte() catch ',';
|
||||
switch (state) {
|
||||
// If we're done then we skip whitespace until we see a ','.
|
||||
.done => switch (byte) {
|
||||
' ', '\t' => continue,
|
||||
',' => break,
|
||||
// If we see something other than whitespace or a ','
|
||||
// then this is an error since the intent is unclear.
|
||||
else => {
|
||||
state = .err;
|
||||
continue;
|
||||
},
|
||||
},
|
||||
|
||||
// If we're fast-forwarding from an error we just wanna
|
||||
// stop at the first boundary and ignore all other bytes.
|
||||
.err => if (byte == ',') return null,
|
||||
|
||||
.start => switch (byte) {
|
||||
// Ignore leading whitespace.
|
||||
' ', '\t' => continue,
|
||||
// Empty feature string.
|
||||
',' => return null,
|
||||
// '+' prefix to explicitly enable feature.
|
||||
'+' => {
|
||||
value = 1;
|
||||
state = .{ .tag = 0 };
|
||||
continue;
|
||||
},
|
||||
// '-' prefix to explicitly disable feature.
|
||||
'-' => {
|
||||
value = 0;
|
||||
state = .{ .tag = 0 };
|
||||
continue;
|
||||
},
|
||||
// Quote mark introducing a tag.
|
||||
'"', '\'' => {
|
||||
state = .{ .tag = 0 };
|
||||
continue;
|
||||
},
|
||||
// First letter of tag.
|
||||
else => {
|
||||
tag[0] = byte;
|
||||
state = .{ .tag = 1 };
|
||||
continue;
|
||||
},
|
||||
},
|
||||
|
||||
.tag => |*i| switch (byte) {
|
||||
// If the tag is interrupted by a comma it's invalid.
|
||||
',' => return null,
|
||||
// Ignore quote marks.
|
||||
'"', '\'' => continue,
|
||||
// A prefix of '+' or '-'
|
||||
// In all other cases we add the byte to our tag.
|
||||
else => {
|
||||
tag[i.*] = byte;
|
||||
if (i.* == 3) {
|
||||
state = .space;
|
||||
continue;
|
||||
}
|
||||
i.* += 1;
|
||||
},
|
||||
},
|
||||
|
||||
.space => switch (byte) {
|
||||
' ', '\t' => continue,
|
||||
// Ignore quote marks since we might have a
|
||||
// closing quote from the tag still ahead.
|
||||
'"', '\'' => continue,
|
||||
// Allow an '=' (which we can safely ignore)
|
||||
// only if we don't already have a value due
|
||||
// to a '+' or '-' prefix.
|
||||
'=' => if (value != null) {
|
||||
state = .err;
|
||||
continue;
|
||||
},
|
||||
',' => {
|
||||
// Specifying only a tag turns a feature on.
|
||||
if (value == null) value = 1;
|
||||
break;
|
||||
},
|
||||
'0'...'9' => {
|
||||
// If we already have value because of a
|
||||
// '+' or '-' prefix then this is an error.
|
||||
if (value != null) {
|
||||
state = .err;
|
||||
continue;
|
||||
}
|
||||
value = byte - '0';
|
||||
state = .int;
|
||||
continue;
|
||||
},
|
||||
'o', 'O' => {
|
||||
// If we already have value because of a
|
||||
// '+' or '-' prefix then this is an error.
|
||||
if (value != null) {
|
||||
state = .err;
|
||||
continue;
|
||||
}
|
||||
state = .bool;
|
||||
continue;
|
||||
},
|
||||
else => {
|
||||
state = .err;
|
||||
continue;
|
||||
},
|
||||
},
|
||||
|
||||
.int => switch (byte) {
|
||||
',' => break,
|
||||
'0'...'9' => {
|
||||
// If our value gets too big while
|
||||
// parsing we consider it an error.
|
||||
value = std.math.mul(u32, value.?, 10) catch {
|
||||
state = .err;
|
||||
continue;
|
||||
};
|
||||
value.? += byte - '0';
|
||||
},
|
||||
else => {
|
||||
state = .err;
|
||||
continue;
|
||||
},
|
||||
},
|
||||
|
||||
.bool => switch (byte) {
|
||||
',' => return null,
|
||||
'n', 'N' => {
|
||||
// "ofn"
|
||||
if (value != null) {
|
||||
assert(value == 0);
|
||||
state = .err;
|
||||
continue;
|
||||
}
|
||||
value = 1;
|
||||
state = .done;
|
||||
continue;
|
||||
},
|
||||
'f', 'F' => {
|
||||
// To make sure we consume two 'f's.
|
||||
if (value == null) {
|
||||
value = 0;
|
||||
} else {
|
||||
assert(value == 0);
|
||||
state = .done;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
state = .err;
|
||||
continue;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
assert(value != null);
|
||||
|
||||
return .{
|
||||
.tag = tag,
|
||||
.value = value.?,
|
||||
};
|
||||
}
|
||||
|
||||
/// Serialize this feature to the provided buffer.
|
||||
/// The string that this produces should be valid to parse.
|
||||
pub fn toString(self: *const Feature, buf: []u8) !void {
|
||||
var fbs = std.io.fixedBufferStream(buf);
|
||||
try self.format("", .{}, fbs.writer());
|
||||
}
|
||||
|
||||
/// Formatter for logging
|
||||
pub fn format(
|
||||
self: Feature,
|
||||
comptime layout: []const u8,
|
||||
opts: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = layout;
|
||||
_ = opts;
|
||||
if (self.value <= 1) {
|
||||
// Format boolean options as "+tag" for on and "-tag" for off.
|
||||
try std.fmt.format(writer, "{c}{s}", .{
|
||||
"-+"[self.value],
|
||||
self.tag,
|
||||
});
|
||||
} else {
|
||||
// Format non-boolean tags as "tag=value".
|
||||
try std.fmt.format(writer, "{s}={d}", .{
|
||||
self.tag,
|
||||
self.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// A list of font feature settings (see `Feature` for more documentation).
|
||||
pub const FeatureList = struct {
|
||||
features: std.ArrayListUnmanaged(Feature) = .{},
|
||||
|
||||
pub fn deinit(self: *FeatureList, alloc: Allocator) void {
|
||||
self.features.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Parse a comma separated list of features.
|
||||
/// See `Feature.fromReader` for more docs.
|
||||
pub fn fromString(alloc: Allocator, str: []const u8) !FeatureList {
|
||||
var self: FeatureList = .{};
|
||||
try self.appendFromString(alloc, str);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Append features to this list from a string with a comma separated list.
|
||||
/// See `Feature.fromReader` for more docs.
|
||||
pub fn appendFromString(
|
||||
self: *FeatureList,
|
||||
alloc: Allocator,
|
||||
str: []const u8,
|
||||
) !void {
|
||||
var fbs = std.io.fixedBufferStream(str);
|
||||
const reader = fbs.reader();
|
||||
while (fbs.pos < fbs.buffer.len) {
|
||||
const i = fbs.pos;
|
||||
if (Feature.fromReader(reader)) |feature| {
|
||||
try self.features.append(alloc, feature);
|
||||
} else log.warn(
|
||||
"failed to parse font feature setting: \"{s}\"",
|
||||
.{fbs.buffer[i..fbs.pos]},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatter for logging
|
||||
pub fn format(
|
||||
self: FeatureList,
|
||||
comptime layout: []const u8,
|
||||
opts: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
for (self.features.items, 0..) |feature, i| {
|
||||
try feature.format(layout, opts, writer);
|
||||
if (i != std.features.items.len - 1) try writer.writeAll(", ");
|
||||
}
|
||||
if (self.value <= 1) {
|
||||
// Format boolean options as "+tag" for on and "-tag" for off.
|
||||
try std.fmt.format(writer, "{c}{s}", .{
|
||||
"-+"[self.value],
|
||||
self.tag,
|
||||
});
|
||||
} else {
|
||||
// Format non-boolean tags as "tag=value".
|
||||
try std.fmt.format(writer, "{s}={d}", .{
|
||||
self.tag,
|
||||
self.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// These features are hardcoded to always be on by default. Users
|
||||
/// can turn them off by setting the features to "-liga" for example.
|
||||
pub const default_features = [_]Feature{
|
||||
.{ .tag = "dlig".*, .value = 1 },
|
||||
.{ .tag = "liga".*, .value = 1 },
|
||||
};
|
||||
|
||||
test "Feature.fromString" {
|
||||
const testing = std.testing;
|
||||
|
||||
// This is not *complete* coverage of every possible
|
||||
// combination of syntax, but it covers quite a few.
|
||||
|
||||
// Boolean settings (on)
|
||||
const kern_on = Feature{ .tag = "kern".*, .value = 1 };
|
||||
try testing.expectEqual(kern_on, Feature.fromString("kern"));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("kern, "));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("kern on"));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("kern on, "));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("+kern"));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("+kern, "));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("\"kern\" = 1"));
|
||||
try testing.expectEqual(kern_on, Feature.fromString("\"kern\" = 1, "));
|
||||
|
||||
// Boolean settings (off)
|
||||
const kern_off = Feature{ .tag = "kern".*, .value = 0 };
|
||||
try testing.expectEqual(kern_off, Feature.fromString("kern off"));
|
||||
try testing.expectEqual(kern_off, Feature.fromString("kern off, "));
|
||||
try testing.expectEqual(kern_off, Feature.fromString("-'kern'"));
|
||||
try testing.expectEqual(kern_off, Feature.fromString("-'kern', "));
|
||||
try testing.expectEqual(kern_off, Feature.fromString("\"kern\" = 0"));
|
||||
try testing.expectEqual(kern_off, Feature.fromString("\"kern\" = 0, "));
|
||||
|
||||
// Non-boolean settings
|
||||
const aalt_2 = Feature{ .tag = "aalt".*, .value = 2 };
|
||||
try testing.expectEqual(aalt_2, Feature.fromString("aalt=2"));
|
||||
try testing.expectEqual(aalt_2, Feature.fromString("aalt=2, "));
|
||||
try testing.expectEqual(aalt_2, Feature.fromString("'aalt' 2"));
|
||||
try testing.expectEqual(aalt_2, Feature.fromString("'aalt' 2, "));
|
||||
|
||||
// Various ambiguous/error cases which should be null
|
||||
try testing.expectEqual(null, Feature.fromString("aalt=2x")); // bad number
|
||||
try testing.expectEqual(null, Feature.fromString("toolong")); // tag too long
|
||||
try testing.expectEqual(null, Feature.fromString("sht")); // tag too short
|
||||
try testing.expectEqual(null, Feature.fromString("-kern 1")); // redundant/conflicting
|
||||
try testing.expectEqual(null, Feature.fromString("-kern on")); // redundant/conflicting
|
||||
try testing.expectEqual(null, Feature.fromString("aalt=o,")); // bad keyword
|
||||
try testing.expectEqual(null, Feature.fromString("aalt=ofn,")); // bad keyword
|
||||
}
|
||||
|
||||
test "FeatureList.fromString" {
|
||||
const testing = std.testing;
|
||||
|
||||
const str =
|
||||
" kern, kern on , +kern, \"kern\" = 1," ++ // Boolean settings (on)
|
||||
"kern off, -'kern' , \"kern\"=0," ++ // Boolean settings (off)
|
||||
"aalt=2, 'aalt'\t2," ++ // Non-boolean settings
|
||||
"aalt=2x, toolong, sht, -kern 1, -kern on, aalt=o, aalt=ofn," ++ // Invalid cases
|
||||
"last"; // To ensure final element is included correctly.
|
||||
var feats = try FeatureList.fromString(testing.allocator, str);
|
||||
defer feats.deinit(testing.allocator);
|
||||
try testing.expectEqualSlices(
|
||||
Feature,
|
||||
&(.{Feature{ .tag = "kern".*, .value = 1 }} ** 4 ++
|
||||
.{Feature{ .tag = "kern".*, .value = 0 }} ** 3 ++
|
||||
.{Feature{ .tag = "aalt".*, .value = 2 }} ** 2 ++
|
||||
.{Feature{ .tag = "last".*, .value = 1 }}),
|
||||
feats.features.items,
|
||||
);
|
||||
}
|
@ -3,6 +3,10 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const harfbuzz = @import("harfbuzz");
|
||||
const font = @import("../main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const Feature = font.shape.Feature;
|
||||
const FeatureList = font.shape.FeatureList;
|
||||
const default_features = font.shape.default_features;
|
||||
const Face = font.Face;
|
||||
const Collection = font.Collection;
|
||||
const DeferredFace = font.DeferredFace;
|
||||
@ -10,7 +14,6 @@ const Library = font.Library;
|
||||
const SharedGrid = font.SharedGrid;
|
||||
const Style = font.Style;
|
||||
const Presentation = font.Presentation;
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
|
||||
const log = std.log.scoped(.font_shaper);
|
||||
|
||||
@ -27,38 +30,37 @@ pub const Shaper = struct {
|
||||
cell_buf: CellBuf,
|
||||
|
||||
/// The features to use for shaping.
|
||||
hb_feats: FeatureList,
|
||||
hb_feats: []harfbuzz.Feature,
|
||||
|
||||
const CellBuf = std.ArrayListUnmanaged(font.shape.Cell);
|
||||
const FeatureList = std.ArrayListUnmanaged(harfbuzz.Feature);
|
||||
|
||||
// These features are hardcoded to always be on by default. Users
|
||||
// can turn them off by setting the features to "-liga" for example.
|
||||
const hardcoded_features = [_][]const u8{ "dlig", "liga" };
|
||||
|
||||
/// The cell_buf argument is the buffer to use for storing shaped results.
|
||||
/// This should be at least the number of columns in the terminal.
|
||||
pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper {
|
||||
// Parse all the features we want to use. We use
|
||||
var hb_feats = hb_feats: {
|
||||
var list = try FeatureList.initCapacity(alloc, opts.features.len + hardcoded_features.len);
|
||||
errdefer list.deinit(alloc);
|
||||
|
||||
for (hardcoded_features) |name| {
|
||||
if (harfbuzz.Feature.fromString(name)) |feat| {
|
||||
try list.append(alloc, feat);
|
||||
} else log.warn("failed to parse font feature: {s}", .{name});
|
||||
// Parse all the features we want to use.
|
||||
const hb_feats = hb_feats: {
|
||||
var feature_list: FeatureList = .{};
|
||||
defer feature_list.deinit(alloc);
|
||||
try feature_list.features.appendSlice(alloc, &default_features);
|
||||
for (opts.features) |feature_str| {
|
||||
try feature_list.appendFromString(alloc, feature_str);
|
||||
}
|
||||
|
||||
for (opts.features) |name| {
|
||||
if (harfbuzz.Feature.fromString(name)) |feat| {
|
||||
try list.append(alloc, feat);
|
||||
} else log.warn("failed to parse font feature: {s}", .{name});
|
||||
var list = try alloc.alloc(harfbuzz.Feature, feature_list.features.items.len);
|
||||
errdefer alloc.free(list);
|
||||
|
||||
for (feature_list.features.items, 0..) |feature, i| {
|
||||
list[i] = .{
|
||||
.tag = std.mem.nativeToBig(u32, @bitCast(feature.tag)),
|
||||
.value = feature.value,
|
||||
.start = harfbuzz.c.HB_FEATURE_GLOBAL_START,
|
||||
.end = harfbuzz.c.HB_FEATURE_GLOBAL_END,
|
||||
};
|
||||
}
|
||||
|
||||
break :hb_feats list;
|
||||
};
|
||||
errdefer hb_feats.deinit(alloc);
|
||||
errdefer alloc.free(hb_feats);
|
||||
|
||||
return Shaper{
|
||||
.alloc = alloc,
|
||||
@ -71,7 +73,7 @@ pub const Shaper = struct {
|
||||
pub fn deinit(self: *Shaper) void {
|
||||
self.hb_buf.destroy();
|
||||
self.cell_buf.deinit(self.alloc);
|
||||
self.hb_feats.deinit(self.alloc);
|
||||
self.alloc.free(self.hb_feats);
|
||||
}
|
||||
|
||||
pub fn endFrame(self: *const Shaper) void {
|
||||
@ -125,10 +127,10 @@ pub const Shaper = struct {
|
||||
// If we are disabling default font features we just offset
|
||||
// our features by the hardcoded items because always
|
||||
// add those at the beginning.
|
||||
break :i hardcoded_features.len;
|
||||
break :i default_features.len;
|
||||
};
|
||||
|
||||
harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats.items[i..]);
|
||||
harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats[i..]);
|
||||
}
|
||||
|
||||
// If our buffer is empty, we short-circuit the rest of the work
|
||||
|
@ -27,6 +27,7 @@ pub const GlobalState = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
action: ?cli.Action,
|
||||
logging: Logging,
|
||||
rlimits: ResourceLimits = .{},
|
||||
|
||||
/// The app resources directory, equivalent to zig-out/share when we build
|
||||
/// from source. This is null if we can't detect it.
|
||||
@ -56,6 +57,7 @@ pub const GlobalState = struct {
|
||||
.alloc = undefined,
|
||||
.action = null,
|
||||
.logging = .{ .stderr = {} },
|
||||
.rlimits = .{},
|
||||
.resources_dir = null,
|
||||
};
|
||||
errdefer self.deinit();
|
||||
@ -123,8 +125,8 @@ pub const GlobalState = struct {
|
||||
std.log.info("renderer={}", .{renderer.Renderer});
|
||||
std.log.info("libxev backend={}", .{xev.backend});
|
||||
|
||||
// First things first, we fix our file descriptors
|
||||
internal_os.fixMaxFiles();
|
||||
// As early as possible, initialize our resource limits.
|
||||
self.rlimits = ResourceLimits.init();
|
||||
|
||||
// Initialize our crash reporting.
|
||||
crash.init(self.alloc) catch |err| {
|
||||
@ -174,3 +176,21 @@ pub const GlobalState = struct {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Maintains the Unix resource limits that we set for our process. This
|
||||
/// can be used to restore the limits to their original values.
|
||||
pub const ResourceLimits = struct {
|
||||
nofile: ?internal_os.rlimit = null,
|
||||
|
||||
pub fn init() ResourceLimits {
|
||||
return .{
|
||||
// Maximize the number of file descriptors we can have open
|
||||
// because we can consume a lot of them if we make many terminals.
|
||||
.nofile = internal_os.fixMaxFiles(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn restore(self: *const ResourceLimits) void {
|
||||
if (self.nofile) |lim| internal_os.restoreMaxFiles(lim);
|
||||
}
|
||||
};
|
||||
|
@ -36,6 +36,11 @@ pub const Flags = packed struct {
|
||||
/// and not just while Ghostty is focused. This may not work on all platforms.
|
||||
/// See the keybind config documentation for more information.
|
||||
global: bool = false,
|
||||
|
||||
/// True if this binding should only be triggered if the action can be
|
||||
/// performed. If the action can't be performed then the binding acts as
|
||||
/// if it doesn't exist.
|
||||
performable: bool = false,
|
||||
};
|
||||
|
||||
/// Full binding parser. The binding parser is implemented as an iterator
|
||||
@ -90,6 +95,9 @@ pub const Parser = struct {
|
||||
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||
if (!flags.consumed) return Error.InvalidFormat;
|
||||
flags.consumed = false;
|
||||
} else if (std.mem.eql(u8, prefix, "performable")) {
|
||||
if (flags.performable) return Error.InvalidFormat;
|
||||
flags.performable = true;
|
||||
} else {
|
||||
// If we don't recognize the prefix then we're done.
|
||||
// There are trigger-specific prefixes like "physical:" so
|
||||
@ -185,10 +193,29 @@ pub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool {
|
||||
if (rhs.trigger.mods.alt) count += 1;
|
||||
break :blk count;
|
||||
};
|
||||
if (lhs_count == rhs_count)
|
||||
|
||||
if (lhs_count != rhs_count)
|
||||
return lhs_count > rhs_count;
|
||||
|
||||
if (lhs.trigger.mods.int() != rhs.trigger.mods.int())
|
||||
return lhs.trigger.mods.int() > rhs.trigger.mods.int();
|
||||
|
||||
return lhs_count > rhs_count;
|
||||
const lhs_key: c_int = blk: {
|
||||
switch (lhs.trigger.key) {
|
||||
.translated => break :blk @intFromEnum(lhs.trigger.key.translated),
|
||||
.physical => break :blk @intFromEnum(lhs.trigger.key.physical),
|
||||
.unicode => break :blk @intCast(lhs.trigger.key.unicode),
|
||||
}
|
||||
};
|
||||
const rhs_key: c_int = blk: {
|
||||
switch (rhs.trigger.key) {
|
||||
.translated => break :blk @intFromEnum(rhs.trigger.key.translated),
|
||||
.physical => break :blk @intFromEnum(rhs.trigger.key.physical),
|
||||
.unicode => break :blk @intCast(rhs.trigger.key.unicode),
|
||||
}
|
||||
};
|
||||
|
||||
return lhs_key < rhs_key;
|
||||
}
|
||||
|
||||
/// The set of actions that a keybinding can take.
|
||||
@ -203,7 +230,7 @@ pub const Action = union(enum) {
|
||||
unbind: void,
|
||||
|
||||
/// Send a CSI sequence. The value should be the CSI sequence without the
|
||||
/// CSI header (`ESC ]` or `\x1b]`).
|
||||
/// CSI header (`ESC [` or `\x1b[`).
|
||||
csi: []const u8,
|
||||
|
||||
/// Send an `ESC` sequence.
|
||||
@ -302,7 +329,7 @@ pub const Action = union(enum) {
|
||||
goto_tab: usize,
|
||||
|
||||
/// Moves a tab by a relative offset.
|
||||
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1 for right).
|
||||
/// Adjusts the tab position based on `offset`. For example `move_tab:-1` for left, `move_tab:1` for right.
|
||||
/// If the new position is out of bounds, it wraps around cyclically within the tab range.
|
||||
move_tab: isize,
|
||||
|
||||
@ -311,17 +338,18 @@ pub const Action = union(enum) {
|
||||
toggle_tab_overview: void,
|
||||
|
||||
/// Create a new split in the given direction. The new split will appear in
|
||||
/// the direction given.
|
||||
/// the direction given. For example `new_split:up`. Valid values are left, right, up, down and auto.
|
||||
new_split: SplitDirection,
|
||||
|
||||
/// Focus on a split in a given direction.
|
||||
/// Focus on a split in a given direction. For example `goto_split:up`.
|
||||
/// Valid values are left, right, up, down, previous and next.
|
||||
goto_split: SplitFocusDirection,
|
||||
|
||||
/// zoom/unzoom the current split.
|
||||
toggle_split_zoom: void,
|
||||
|
||||
/// Resize the current split by moving the split divider in the given
|
||||
/// direction
|
||||
/// direction. For example `resize_split:left,10`. The valid directions are up, down, left and right.
|
||||
resize_split: SplitResizeParameter,
|
||||
|
||||
/// Equalize all splits in the current window
|
||||
@ -478,10 +506,42 @@ pub const Action = union(enum) {
|
||||
previous,
|
||||
next,
|
||||
|
||||
top,
|
||||
up,
|
||||
left,
|
||||
bottom,
|
||||
down,
|
||||
right,
|
||||
|
||||
pub fn parse(input: []const u8) !SplitFocusDirection {
|
||||
return std.meta.stringToEnum(SplitFocusDirection, input) orelse {
|
||||
// For backwards compatibility we map "top" and "bottom" onto the enum
|
||||
// values "up" and "down"
|
||||
if (std.mem.eql(u8, input, "top")) {
|
||||
return .up;
|
||||
} else if (std.mem.eql(u8, input, "bottom")) {
|
||||
return .down;
|
||||
} else {
|
||||
return Error.InvalidFormat;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "parse" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(.previous, try SplitFocusDirection.parse("previous"));
|
||||
try testing.expectEqual(.next, try SplitFocusDirection.parse("next"));
|
||||
|
||||
try testing.expectEqual(.up, try SplitFocusDirection.parse("up"));
|
||||
try testing.expectEqual(.left, try SplitFocusDirection.parse("left"));
|
||||
try testing.expectEqual(.down, try SplitFocusDirection.parse("down"));
|
||||
try testing.expectEqual(.right, try SplitFocusDirection.parse("right"));
|
||||
|
||||
try testing.expectEqual(.up, try SplitFocusDirection.parse("top"));
|
||||
try testing.expectEqual(.down, try SplitFocusDirection.parse("bottom"));
|
||||
|
||||
try testing.expectError(error.InvalidFormat, SplitFocusDirection.parse(""));
|
||||
try testing.expectError(error.InvalidFormat, SplitFocusDirection.parse("green"));
|
||||
}
|
||||
};
|
||||
|
||||
pub const SplitResizeDirection = enum {
|
||||
@ -524,7 +584,16 @@ pub const Action = union(enum) {
|
||||
comptime field: std.builtin.Type.UnionField,
|
||||
param: []const u8,
|
||||
) !field.type {
|
||||
return switch (@typeInfo(field.type)) {
|
||||
const field_info = @typeInfo(field.type);
|
||||
|
||||
// Fields can provide a custom "parse" function
|
||||
if (field_info == .Struct or field_info == .Union or field_info == .Enum) {
|
||||
if (@hasDecl(field.type, "parse") and @typeInfo(@TypeOf(field.type.parse)) == .Fn) {
|
||||
return field.type.parse(param);
|
||||
}
|
||||
}
|
||||
|
||||
return switch (field_info) {
|
||||
.Enum => try parseEnum(field.type, param),
|
||||
.Int => try parseInt(field.type, param),
|
||||
.Float => try parseFloat(field.type, param),
|
||||
@ -1647,6 +1716,16 @@ test "parse: triggers" {
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:physical:a+shift=ignore"));
|
||||
|
||||
// performable keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .performable = true },
|
||||
}, try parseSingle("performable:shift+a=ignore"));
|
||||
|
||||
// invalid key
|
||||
try testing.expectError(Error.InvalidFormat, parseSingle("foo=ignore"));
|
||||
|
||||
|
@ -282,7 +282,12 @@ fn legacy(
|
||||
// If we match a control sequence, we output that directly. For
|
||||
// ctrlSeq we have to use all mods because we want it to only
|
||||
// match ctrl+<char>.
|
||||
if (ctrlSeq(self.event.utf8, self.event.unshifted_codepoint, all_mods)) |char| {
|
||||
if (ctrlSeq(
|
||||
self.event.key,
|
||||
self.event.utf8,
|
||||
self.event.unshifted_codepoint,
|
||||
all_mods,
|
||||
)) |char| {
|
||||
// C0 sequences support alt-as-esc prefixing.
|
||||
if (binding_mods.alt) {
|
||||
if (buf.len < 2) return error.OutOfMemory;
|
||||
@ -538,19 +543,17 @@ fn pcStyleFunctionKey(
|
||||
/// into a C0 byte. There are many cases for this and you should read
|
||||
/// the source code to understand them.
|
||||
fn ctrlSeq(
|
||||
logical_key: key.Key,
|
||||
utf8: []const u8,
|
||||
unshifted_codepoint: u21,
|
||||
mods: key.Mods,
|
||||
) ?u8 {
|
||||
const ctrl_only = comptime (key.Mods{ .ctrl = true }).int();
|
||||
|
||||
// If ctrl is not pressed then we never do anything.
|
||||
if (!mods.ctrl) return null;
|
||||
|
||||
// If we don't have exactly one byte in our utf8 sequence, then
|
||||
// we don't do anything, since all our ctrl keys are based on ASCII.
|
||||
if (utf8.len != 1) return null;
|
||||
|
||||
const char, const unset_mods = unset_mods: {
|
||||
var char = utf8[0];
|
||||
var unset_mods = mods;
|
||||
|
||||
// Remove alt from our modifiers because it does not impact whether
|
||||
@ -558,6 +561,34 @@ fn ctrlSeq(
|
||||
// logic separately.
|
||||
unset_mods.alt = false;
|
||||
|
||||
var char: u8 = char: {
|
||||
// If we have exactly one UTF8 byte, we assume that is the
|
||||
// character we want to convert to a C0 byte.
|
||||
if (utf8.len == 1) break :char utf8[0];
|
||||
|
||||
// If we have a logical key that maps to a single byte
|
||||
// printable character, we use that. History to explain this:
|
||||
// this was added to support cyrillic keyboard layouts such
|
||||
// as Russian and Mongolian. These layouts have a `c` key that
|
||||
// maps to U+0441 (cyrillic small letter "c") but every
|
||||
// terminal I've tested encodes this as ctrl+c.
|
||||
if (logical_key.codepoint()) |cp| {
|
||||
if (std.math.cast(u8, cp)) |byte| {
|
||||
// For this specific case, we only map to the key if
|
||||
// we have exactly ctrl pressed. This is because shift
|
||||
// would modify the key and we don't know how to do that
|
||||
// properly here (don't have the layout). And we want
|
||||
// to encode shift as CSIu.
|
||||
if (unset_mods.int() != ctrl_only) return null;
|
||||
break :char byte;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise we don't have a character to convert that
|
||||
// we can reliably map to a C0 byte.
|
||||
return null;
|
||||
};
|
||||
|
||||
// Remove shift if we have something outside of the US letter
|
||||
// range. This is so that characters such as `ctrl+shift+-`
|
||||
// generate the correct ctrl-seq (used by emacs).
|
||||
@ -596,7 +627,6 @@ fn ctrlSeq(
|
||||
};
|
||||
|
||||
// After unsetting, we only continue if we have ONLY control set.
|
||||
const ctrl_only = comptime (key.Mods{ .ctrl = true }).int();
|
||||
if (unset_mods.int() != ctrl_only) return null;
|
||||
|
||||
// From Kitty's key encoding logic. I tried to discern the exact
|
||||
@ -2132,36 +2162,52 @@ test "legacy: hu layout ctrl+ő sends proper codepoint" {
|
||||
const actual = try enc.legacy(&buf);
|
||||
try testing.expectEqualStrings("[337;5u", actual[1..]);
|
||||
}
|
||||
|
||||
test "ctrlseq: normal ctrl c" {
|
||||
const seq = ctrlSeq("c", 'c', .{ .ctrl = true });
|
||||
const seq = ctrlSeq(.invalid, "c", 'c', .{ .ctrl = true });
|
||||
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||
}
|
||||
|
||||
test "ctrlseq: normal ctrl c, right control" {
|
||||
const seq = ctrlSeq("c", 'c', .{ .ctrl = true, .sides = .{ .ctrl = .right } });
|
||||
const seq = ctrlSeq(.invalid, "c", 'c', .{ .ctrl = true, .sides = .{ .ctrl = .right } });
|
||||
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||
}
|
||||
|
||||
test "ctrlseq: alt should be allowed" {
|
||||
const seq = ctrlSeq("c", 'c', .{ .alt = true, .ctrl = true });
|
||||
const seq = ctrlSeq(.invalid, "c", 'c', .{ .alt = true, .ctrl = true });
|
||||
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||
}
|
||||
|
||||
test "ctrlseq: no ctrl does nothing" {
|
||||
try testing.expect(ctrlSeq("c", 'c', .{}) == null);
|
||||
try testing.expect(ctrlSeq(.invalid, "c", 'c', .{}) == null);
|
||||
}
|
||||
|
||||
test "ctrlseq: shifted non-character" {
|
||||
const seq = ctrlSeq("_", '-', .{ .ctrl = true, .shift = true });
|
||||
const seq = ctrlSeq(.invalid, "_", '-', .{ .ctrl = true, .shift = true });
|
||||
try testing.expectEqual(@as(u8, 0x1F), seq.?);
|
||||
}
|
||||
|
||||
test "ctrlseq: caps ascii letter" {
|
||||
const seq = ctrlSeq("C", 'c', .{ .ctrl = true, .caps_lock = true });
|
||||
const seq = ctrlSeq(.invalid, "C", 'c', .{ .ctrl = true, .caps_lock = true });
|
||||
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||
}
|
||||
|
||||
test "ctrlseq: shift does not generate ctrl seq" {
|
||||
try testing.expect(ctrlSeq("C", 'c', .{ .shift = true }) == null);
|
||||
try testing.expect(ctrlSeq("C", 'c', .{ .shift = true, .ctrl = true }) == null);
|
||||
try testing.expect(ctrlSeq(.invalid, "C", 'c', .{ .shift = true }) == null);
|
||||
try testing.expect(ctrlSeq(.invalid, "C", 'c', .{ .shift = true, .ctrl = true }) == null);
|
||||
}
|
||||
|
||||
test "ctrlseq: russian ctrl c" {
|
||||
const seq = ctrlSeq(.c, "с", 0x0441, .{ .ctrl = true });
|
||||
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||
}
|
||||
|
||||
test "ctrlseq: russian shifted ctrl c" {
|
||||
const seq = ctrlSeq(.c, "с", 0x0441, .{ .ctrl = true, .shift = true });
|
||||
try testing.expect(seq == null);
|
||||
}
|
||||
|
||||
test "ctrlseq: russian alt ctrl c" {
|
||||
const seq = ctrlSeq(.c, "с", 0x0441, .{ .ctrl = true, .alt = true });
|
||||
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||
}
|
||||
|
@ -295,7 +295,7 @@ pub const Key = enum(c_int) {
|
||||
eight,
|
||||
nine,
|
||||
|
||||
// puncuation
|
||||
// punctuation
|
||||
semicolon,
|
||||
space,
|
||||
apostrophe,
|
||||
@ -411,7 +411,7 @@ pub const Key = enum(c_int) {
|
||||
/// may be from the number row or the keypad, but it always maps
|
||||
/// to '.zero'.
|
||||
///
|
||||
/// This is what we want, we awnt people to create keybindings that
|
||||
/// This is what we want, we want people to create keybindings that
|
||||
/// are independent of the physical key.
|
||||
pub fn fromASCII(ch: u8) ?Key {
|
||||
return switch (ch) {
|
||||
|
@ -14,6 +14,7 @@ const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const inspector = @import("main.zig");
|
||||
const units = @import("units.zig");
|
||||
|
||||
/// The window names. These are used with docking so we need to have access.
|
||||
const window_cell = "Cell";
|
||||
@ -440,7 +441,7 @@ fn renderScreenWindow(self: *Inspector) void {
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%d bytes", kitty_images.total_bytes);
|
||||
cimgui.c.igText("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,7 +453,7 @@ fn renderScreenWindow(self: *Inspector) void {
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%d bytes", kitty_images.total_limit);
|
||||
cimgui.c.igText("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit));
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,7 +519,7 @@ fn renderScreenWindow(self: *Inspector) void {
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%d bytes", pages.page_size);
|
||||
cimgui.c.igText("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size));
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,7 +531,7 @@ fn renderScreenWindow(self: *Inspector) void {
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%d bytes", pages.maxSize());
|
||||
cimgui.c.igText("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -724,7 +725,7 @@ fn renderSizeWindow(self: *Inspector) void {
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText(
|
||||
"%d pt",
|
||||
"%.2f pt",
|
||||
self.surface.font_size.points,
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("cimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const inspector = @import("main.zig");
|
||||
const units = @import("units.zig");
|
||||
|
||||
pub fn render(page: *const terminal.Page) void {
|
||||
cimgui.c.igPushID_Ptr(page);
|
||||
@ -25,7 +27,7 @@ pub fn render(page: *const terminal.Page) void {
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%d bytes", page.memory.len);
|
||||
cimgui.c.igText("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len));
|
||||
cimgui.c.igText("%d VM pages", page.memory.len / std.mem.page_size);
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ pub const VTEvent = struct {
|
||||
const Kind = enum { print, execute, csi, esc, osc, dcs, apc };
|
||||
const Metadata = std.StringHashMap([:0]const u8);
|
||||
|
||||
/// Initiaze the event information for the given parser action.
|
||||
/// Initialize the event information for the given parser action.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
surface: *Surface,
|
||||
@ -208,6 +208,20 @@ pub const VTEvent = struct {
|
||||
);
|
||||
},
|
||||
|
||||
.Union => |info| {
|
||||
const Tag = info.tag_type orelse @compileError("Unions must have a tag");
|
||||
const tag_name = @tagName(@as(Tag, v));
|
||||
inline for (info.fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, tag_name)) {
|
||||
if (field.type == void) {
|
||||
break try md.put("data", tag_name);
|
||||
} else {
|
||||
break try encodeMetadataSingle(alloc, md, tag_name, @field(v, field.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported type, see log");
|
||||
|
3
src/inspector/units.zig
Normal file
3
src/inspector/units.zig
Normal file
@ -0,0 +1,3 @@
|
||||
pub fn toKibiBytes(bytes: usize) usize {
|
||||
return bytes / 1024;
|
||||
}
|
@ -49,7 +49,8 @@ pub fn main() !MainReturn {
|
||||
|
||||
error.InvalidAction => try stderr.print(
|
||||
"Error: unknown CLI action specified. CLI actions are specified with\n" ++
|
||||
"the '+' character.\n",
|
||||
"the '+' character.\n\n" ++
|
||||
"All valid CLI actions can be listed with `ghostty +help`\n",
|
||||
.{},
|
||||
),
|
||||
|
||||
|
@ -59,3 +59,29 @@ pub fn launchedFromDesktop() bool {
|
||||
else => @compileError("unsupported platform"),
|
||||
};
|
||||
}
|
||||
|
||||
pub const DesktopEnvironment = enum {
|
||||
gnome,
|
||||
macos,
|
||||
other,
|
||||
windows,
|
||||
};
|
||||
|
||||
/// Detect what desktop environment we are running under. This is mainly used on
|
||||
/// Linux to enable or disable GTK client-side decorations but there may be more
|
||||
/// uses in the future.
|
||||
pub fn desktopEnvironment() DesktopEnvironment {
|
||||
return switch (comptime builtin.os.tag) {
|
||||
.macos => .macos,
|
||||
.windows => .windows,
|
||||
.linux => de: {
|
||||
if (@inComptime()) @compileError("Checking for the desktop environment on Linux must be done at runtime.");
|
||||
// use $XDG_SESSION_DESKTOP to determine what DE we are using on Linux
|
||||
// https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop=
|
||||
const de = posix.getenv("XDG_SESSION_DESKTOP") orelse break :de .other;
|
||||
if (std.ascii.eqlIgnoreCase("gnome", de)) break :de .gnome;
|
||||
break :de .other;
|
||||
},
|
||||
else => .other,
|
||||
};
|
||||
}
|
||||
|
@ -4,24 +4,27 @@ const posix = std.posix;
|
||||
|
||||
const log = std.log.scoped(.os);
|
||||
|
||||
pub const rlimit = if (@hasDecl(posix.system, "rlimit")) posix.rlimit else struct {};
|
||||
|
||||
/// This maximizes the number of file descriptors we can have open. We
|
||||
/// need to do this because each window consumes at least a handful of fds.
|
||||
/// This is extracted from the Zig compiler source code.
|
||||
pub fn fixMaxFiles() void {
|
||||
if (!@hasDecl(posix.system, "rlimit")) return;
|
||||
pub fn fixMaxFiles() ?rlimit {
|
||||
if (!@hasDecl(posix.system, "rlimit")) return null;
|
||||
|
||||
var lim = posix.getrlimit(.NOFILE) catch {
|
||||
const old = posix.getrlimit(.NOFILE) catch {
|
||||
log.warn("failed to query file handle limit, may limit max windows", .{});
|
||||
return; // Oh well; we tried.
|
||||
return null; // Oh well; we tried.
|
||||
};
|
||||
|
||||
// If we're already at the max, we're done.
|
||||
if (lim.cur >= lim.max) {
|
||||
log.debug("file handle limit already maximized value={}", .{lim.cur});
|
||||
return;
|
||||
if (old.cur >= old.max) {
|
||||
log.debug("file handle limit already maximized value={}", .{old.cur});
|
||||
return old;
|
||||
}
|
||||
|
||||
// Do a binary search for the limit.
|
||||
var lim = old;
|
||||
var min: posix.rlim_t = lim.cur;
|
||||
var max: posix.rlim_t = 1 << 20;
|
||||
// But if there's a defined upper bound, don't search, just set it.
|
||||
@ -41,6 +44,12 @@ pub fn fixMaxFiles() void {
|
||||
}
|
||||
|
||||
log.debug("file handle limit raised value={}", .{lim.cur});
|
||||
return old;
|
||||
}
|
||||
|
||||
pub fn restoreMaxFiles(lim: rlimit) void {
|
||||
if (!@hasDecl(posix.system, "rlimit")) return;
|
||||
posix.setrlimit(.NOFILE, lim) catch {};
|
||||
}
|
||||
|
||||
/// Return the recommended path for temporary files.
|
||||
|
@ -12,7 +12,7 @@ const Error = error{
|
||||
|
||||
/// Determine the home directory for the currently executing user. This
|
||||
/// is generally an expensive process so the value should be cached.
|
||||
pub inline fn home(buf: []u8) !?[]u8 {
|
||||
pub inline fn home(buf: []u8) !?[]const u8 {
|
||||
return switch (builtin.os.tag) {
|
||||
inline .linux, .macos => try homeUnix(buf),
|
||||
.windows => try homeWindows(buf),
|
||||
@ -24,7 +24,7 @@ pub inline fn home(buf: []u8) !?[]u8 {
|
||||
};
|
||||
}
|
||||
|
||||
fn homeUnix(buf: []u8) !?[]u8 {
|
||||
fn homeUnix(buf: []u8) !?[]const u8 {
|
||||
// First: if we have a HOME env var, then we use that.
|
||||
if (posix.getenv("HOME")) |result| {
|
||||
if (buf.len < result.len) return Error.BufferTooSmall;
|
||||
@ -77,7 +77,7 @@ fn homeUnix(buf: []u8) !?[]u8 {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn homeWindows(buf: []u8) !?[]u8 {
|
||||
fn homeWindows(buf: []u8) !?[]const u8 {
|
||||
const drive_len = blk: {
|
||||
var fba_instance = std.heap.FixedBufferAllocator.init(buf);
|
||||
const fba = fba_instance.allocator();
|
||||
@ -110,6 +110,68 @@ fn trimSpace(input: []const u8) []const u8 {
|
||||
return std.mem.trim(u8, input, " \n\t");
|
||||
}
|
||||
|
||||
pub const ExpandError = error{
|
||||
HomeDetectionFailed,
|
||||
BufferTooSmall,
|
||||
};
|
||||
|
||||
/// Expands a path that starts with a tilde (~) to the home directory of
|
||||
/// the current user.
|
||||
///
|
||||
/// Errors if `home` fails or if the size of the expanded path is larger
|
||||
/// than `buf.len`.
|
||||
pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 {
|
||||
return switch (builtin.os.tag) {
|
||||
.linux, .macos => try expandHomeUnix(path, buf),
|
||||
.ios => return path,
|
||||
else => @compileError("unimplemented"),
|
||||
};
|
||||
}
|
||||
|
||||
fn expandHomeUnix(path: []const u8, buf: []u8) ExpandError![]const u8 {
|
||||
if (!std.mem.startsWith(u8, path, "~/")) return path;
|
||||
const home_dir: []const u8 = if (home(buf)) |home_|
|
||||
home_ orelse return error.HomeDetectionFailed
|
||||
else |_|
|
||||
return error.HomeDetectionFailed;
|
||||
const rest = path[1..]; // Skip the ~
|
||||
const expanded_len = home_dir.len + rest.len;
|
||||
|
||||
if (expanded_len > buf.len) return Error.BufferTooSmall;
|
||||
@memcpy(buf[home_dir.len..expanded_len], rest);
|
||||
|
||||
return buf[0..expanded_len];
|
||||
}
|
||||
|
||||
test "expandHomeUnix" {
|
||||
const testing = std.testing;
|
||||
const allocator = testing.allocator;
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const home_dir = try expandHomeUnix("~/", &buf);
|
||||
// Joining the home directory `~` with the path `/`
|
||||
// the result should end with a separator here. (e.g. `/home/user/`)
|
||||
try testing.expect(home_dir[home_dir.len - 1] == std.fs.path.sep);
|
||||
|
||||
const downloads = try expandHomeUnix("~/Downloads/shader.glsl", &buf);
|
||||
const expected_downloads = try std.mem.concat(allocator, u8, &[_][]const u8{ home_dir, "Downloads/shader.glsl" });
|
||||
defer allocator.free(expected_downloads);
|
||||
try testing.expectEqualStrings(expected_downloads, downloads);
|
||||
|
||||
try testing.expectEqualStrings("~", try expandHomeUnix("~", &buf));
|
||||
try testing.expectEqualStrings("~abc/", try expandHomeUnix("~abc/", &buf));
|
||||
try testing.expectEqualStrings("/home/user", try expandHomeUnix("/home/user", &buf));
|
||||
try testing.expectEqualStrings("", try expandHomeUnix("", &buf));
|
||||
|
||||
// Expect an error if the buffer is large enough to hold the home directory,
|
||||
// but not the expanded path
|
||||
var small_buf = try allocator.alloc(u8, home_dir.len);
|
||||
defer allocator.free(small_buf);
|
||||
try testing.expectError(error.BufferTooSmall, expandHomeUnix(
|
||||
"~/Downloads",
|
||||
small_buf[0..],
|
||||
));
|
||||
}
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
||||
|
119
src/os/macos.zig
119
src/os/macos.zig
@ -25,41 +25,26 @@ pub fn appSupportDir(
|
||||
alloc: Allocator,
|
||||
sub_path: []const u8,
|
||||
) AppSupportDirError![]const u8 {
|
||||
comptime assert(builtin.target.isDarwin());
|
||||
|
||||
const NSFileManager = objc.getClass("NSFileManager").?;
|
||||
const manager = NSFileManager.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("defaultManager"),
|
||||
.{},
|
||||
return try commonDir(
|
||||
alloc,
|
||||
.NSApplicationSupportDirectory,
|
||||
&.{ build_config.bundle_id, sub_path },
|
||||
);
|
||||
}
|
||||
|
||||
const url = manager.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"),
|
||||
.{
|
||||
NSSearchPathDirectory.NSApplicationSupportDirectory,
|
||||
NSSearchPathDomainMask.NSUserDomainMask,
|
||||
@as(?*anyopaque, null),
|
||||
true,
|
||||
@as(?*anyopaque, null),
|
||||
},
|
||||
pub const CacheDirError = Allocator.Error || error{AppleAPIFailed};
|
||||
|
||||
/// Return the path to the system cache directory with the given sub path joined.
|
||||
/// This allocates the result using the given allocator.
|
||||
pub fn cacheDir(
|
||||
alloc: Allocator,
|
||||
sub_path: []const u8,
|
||||
) CacheDirError![]const u8 {
|
||||
return try commonDir(
|
||||
alloc,
|
||||
.NSCachesDirectory,
|
||||
&.{ build_config.bundle_id, sub_path },
|
||||
);
|
||||
|
||||
// I don't think this is possible but just in case.
|
||||
if (url.value == null) return error.AppleAPIFailed;
|
||||
|
||||
// Get the UTF-8 string from the URL.
|
||||
const path = url.getProperty(objc.Object, "path");
|
||||
const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse
|
||||
return error.AppleAPIFailed;
|
||||
const app_support_dir = std.mem.sliceTo(c_str, 0);
|
||||
|
||||
return try std.fs.path.join(alloc, &.{
|
||||
app_support_dir,
|
||||
build_config.bundle_id,
|
||||
sub_path,
|
||||
});
|
||||
}
|
||||
|
||||
pub const SetQosClassError = error{
|
||||
@ -110,9 +95,79 @@ pub const NSOperatingSystemVersion = extern struct {
|
||||
};
|
||||
|
||||
pub const NSSearchPathDirectory = enum(c_ulong) {
|
||||
NSCachesDirectory = 13,
|
||||
NSApplicationSupportDirectory = 14,
|
||||
};
|
||||
|
||||
pub const NSSearchPathDomainMask = enum(c_ulong) {
|
||||
NSUserDomainMask = 1,
|
||||
};
|
||||
|
||||
fn commonDir(
|
||||
alloc: Allocator,
|
||||
directory: NSSearchPathDirectory,
|
||||
sub_paths: []const []const u8,
|
||||
) (error{AppleAPIFailed} || Allocator.Error)![]const u8 {
|
||||
comptime assert(builtin.target.isDarwin());
|
||||
|
||||
const NSFileManager = objc.getClass("NSFileManager").?;
|
||||
const manager = NSFileManager.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("defaultManager"),
|
||||
.{},
|
||||
);
|
||||
|
||||
const url = manager.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"),
|
||||
.{
|
||||
directory,
|
||||
NSSearchPathDomainMask.NSUserDomainMask,
|
||||
@as(?*anyopaque, null),
|
||||
true,
|
||||
@as(?*anyopaque, null),
|
||||
},
|
||||
);
|
||||
|
||||
if (url.value == null) return error.AppleAPIFailed;
|
||||
|
||||
const path = url.getProperty(objc.Object, "path");
|
||||
const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse
|
||||
return error.AppleAPIFailed;
|
||||
const base_dir = std.mem.sliceTo(c_str, 0);
|
||||
|
||||
// Create a new array with base_dir as the first element
|
||||
var paths = try alloc.alloc([]const u8, sub_paths.len + 1);
|
||||
paths[0] = base_dir;
|
||||
@memcpy(paths[1..], sub_paths);
|
||||
defer alloc.free(paths);
|
||||
|
||||
return try std.fs.path.join(alloc, paths);
|
||||
}
|
||||
|
||||
test "cacheDir paths" {
|
||||
if (!builtin.target.isDarwin()) return;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Test base path
|
||||
{
|
||||
const cache_path = try cacheDir(alloc, "");
|
||||
defer alloc.free(cache_path);
|
||||
try testing.expect(std.mem.indexOf(u8, cache_path, "Caches") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, cache_path, build_config.bundle_id) != null);
|
||||
}
|
||||
|
||||
// Test with subdir
|
||||
{
|
||||
const cache_path = try cacheDir(alloc, "test");
|
||||
defer alloc.free(cache_path);
|
||||
try testing.expect(std.mem.indexOf(u8, cache_path, "Caches") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, cache_path, build_config.bundle_id) != null);
|
||||
|
||||
const bundle_path = try std.fmt.allocPrint(alloc, "{s}/test", .{build_config.bundle_id});
|
||||
defer alloc.free(bundle_path);
|
||||
try testing.expect(std.mem.indexOf(u8, cache_path, bundle_path) != null);
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +32,16 @@ pub const getenv = env.getenv;
|
||||
pub const setenv = env.setenv;
|
||||
pub const unsetenv = env.unsetenv;
|
||||
pub const launchedFromDesktop = desktop.launchedFromDesktop;
|
||||
pub const desktopEnvironment = desktop.desktopEnvironment;
|
||||
pub const rlimit = file.rlimit;
|
||||
pub const fixMaxFiles = file.fixMaxFiles;
|
||||
pub const restoreMaxFiles = file.restoreMaxFiles;
|
||||
pub const allocTmpDir = file.allocTmpDir;
|
||||
pub const freeTmpDir = file.freeTmpDir;
|
||||
pub const isFlatpak = flatpak.isFlatpak;
|
||||
pub const FlatpakHostCommand = flatpak.FlatpakHostCommand;
|
||||
pub const home = homedir.home;
|
||||
pub const expandHome = homedir.expandHome;
|
||||
pub const ensureLocale = locale.ensureLocale;
|
||||
pub const clickInterval = mouse.clickInterval;
|
||||
pub const open = openpkg.open;
|
||||
|
@ -58,6 +58,15 @@ fn dir(
|
||||
opts: Options,
|
||||
internal_opts: InternalOptions,
|
||||
) ![]u8 {
|
||||
// If we have a cached home dir, use that.
|
||||
if (opts.home) |home| {
|
||||
return try std.fs.path.join(alloc, &[_][]const u8{
|
||||
home,
|
||||
internal_opts.default_subdir,
|
||||
opts.subdir orelse "",
|
||||
});
|
||||
}
|
||||
|
||||
// First check the env var. On Windows we have to allocate so this tracks
|
||||
// both whether we have the env var and whether we own it.
|
||||
// on Windows we treat `LOCALAPPDATA` as a fallback for `XDG_CONFIG_HOME`
|
||||
@ -93,15 +102,6 @@ fn dir(
|
||||
return try alloc.dupe(u8, env);
|
||||
}
|
||||
|
||||
// If we have a cached home dir, use that.
|
||||
if (opts.home) |home| {
|
||||
return try std.fs.path.join(alloc, &[_][]const u8{
|
||||
home,
|
||||
internal_opts.default_subdir,
|
||||
opts.subdir orelse "",
|
||||
});
|
||||
}
|
||||
|
||||
// Get our home dir
|
||||
var buf: [1024]u8 = undefined;
|
||||
if (try homedir.home(&buf)) |home| {
|
||||
@ -143,6 +143,32 @@ test {
|
||||
}
|
||||
}
|
||||
|
||||
test "cache directory paths" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const mock_home = "/Users/test";
|
||||
|
||||
// Test when XDG_CACHE_HOME is not set
|
||||
{
|
||||
// Test base path
|
||||
{
|
||||
const cache_path = try cache(alloc, .{ .home = mock_home });
|
||||
defer alloc.free(cache_path);
|
||||
try testing.expectEqualStrings("/Users/test/.cache", cache_path);
|
||||
}
|
||||
|
||||
// Test with subdir
|
||||
{
|
||||
const cache_path = try cache(alloc, .{
|
||||
.home = mock_home,
|
||||
.subdir = "ghostty",
|
||||
});
|
||||
defer alloc.free(cache_path);
|
||||
try testing.expectEqualStrings("/Users/test/.cache/ghostty", cache_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test parseTerminalExec {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -360,6 +360,7 @@ pub const DerivedConfig = struct {
|
||||
arena: ArenaAllocator,
|
||||
|
||||
font_thicken: bool,
|
||||
font_thicken_strength: u8,
|
||||
font_features: std.ArrayListUnmanaged([:0]const u8),
|
||||
font_styles: font.CodepointResolver.StyleStatus,
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
@ -410,6 +411,7 @@ pub const DerivedConfig = struct {
|
||||
return .{
|
||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||
.font_thicken = config.@"font-thicken",
|
||||
.font_thicken_strength = config.@"font-thicken-strength",
|
||||
.font_features = font_features.list,
|
||||
.font_styles = font_styles,
|
||||
|
||||
@ -2837,6 +2839,7 @@ fn addGlyph(
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
.thicken_strength = self.config.font_thicken_strength,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -146,7 +146,7 @@ image_bg_end: u32 = 0,
|
||||
image_text_end: u32 = 0,
|
||||
image_virtual: bool = false,
|
||||
|
||||
/// Defererred OpenGL operation to update the screen size.
|
||||
/// Deferred OpenGL operation to update the screen size.
|
||||
const SetScreenSize = struct {
|
||||
size: renderer.Size,
|
||||
|
||||
@ -272,6 +272,7 @@ pub const DerivedConfig = struct {
|
||||
arena: ArenaAllocator,
|
||||
|
||||
font_thicken: bool,
|
||||
font_thicken_strength: u8,
|
||||
font_features: std.ArrayListUnmanaged([:0]const u8),
|
||||
font_styles: font.CodepointResolver.StyleStatus,
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
@ -321,6 +322,7 @@ pub const DerivedConfig = struct {
|
||||
return .{
|
||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||
.font_thicken = config.@"font-thicken",
|
||||
.font_thicken_strength = config.@"font-thicken-strength",
|
||||
.font_features = font_features.list,
|
||||
.font_styles = font_styles,
|
||||
|
||||
@ -764,7 +766,7 @@ pub fn updateFrame(
|
||||
|
||||
// We used to share terminal state, but we've since learned through
|
||||
// analysis that it is faster to copy the terminal state than to
|
||||
// hold the lock wile rebuilding GPU cells.
|
||||
// hold the lock while rebuilding GPU cells.
|
||||
var screen_copy = try state.terminal.screen.clone(
|
||||
self.alloc,
|
||||
.{ .viewport = .{} },
|
||||
@ -2093,6 +2095,7 @@ fn addGlyph(
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
.thicken_strength = self.config.font_thicken_strength,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -3281,7 +3281,7 @@ fn markDirty(self: *PageList, pt: point.Point) void {
|
||||
/// point remains valid even through scrolling without any additional work.
|
||||
///
|
||||
/// A downside is that the pin is only valid until the pagelist is modified
|
||||
/// in a way that may invalid page pointers or shuffle rows, such as resizing,
|
||||
/// in a way that may invalidate page pointers or shuffle rows, such as resizing,
|
||||
/// erasing rows, etc.
|
||||
///
|
||||
/// A pin can also be "tracked" which means that it will be updated as the
|
||||
@ -3389,9 +3389,9 @@ pub const Pin = struct {
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Never extend cell that has a default background.
|
||||
// A default background is if there is no background
|
||||
// on the style OR the explicitly set background
|
||||
// Never extend a cell that has a default background.
|
||||
// A default background is applied if there is no background
|
||||
// on the style or the explicitly set background
|
||||
// matches our default background.
|
||||
const s = self.style(cell);
|
||||
const bg = s.bg(cell, palette) orelse return true;
|
||||
@ -3486,7 +3486,7 @@ pub const Pin = struct {
|
||||
|
||||
// If our y is after the top y but we're on the same page
|
||||
// then we're between the top and bottom if our y is less
|
||||
// than or equal to the bottom y IF its the same page. If the
|
||||
// than or equal to the bottom y if its the same page. If the
|
||||
// bottom is another page then it means that the range is
|
||||
// at least the full top page and since we're the same page
|
||||
// we're in the range.
|
||||
@ -3508,7 +3508,7 @@ pub const Pin = struct {
|
||||
if (self.y > bottom.y) return false;
|
||||
if (self.y < bottom.y) return true;
|
||||
|
||||
// If our y is the same then we're between if we're before
|
||||
// If our y is the same, then we're between if we're before
|
||||
// or equal to the bottom x.
|
||||
assert(self.y == bottom.y);
|
||||
return self.x <= bottom.x;
|
||||
|
@ -382,6 +382,7 @@ fn encodeError(r: *Response, err: EncodeableError) void {
|
||||
error.DecompressionFailed => r.message = "EINVAL: decompression failed",
|
||||
error.FilePathTooLong => r.message = "EINVAL: file path too long",
|
||||
error.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir",
|
||||
error.TemporaryFileNotNamedCorrectly => r.message = "EINVAL: temporary file not named correctly",
|
||||
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
|
||||
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
|
||||
error.UnsupportedDepth => r.message = "EINVAL: unsupported pixel depth",
|
||||
|
@ -220,6 +220,9 @@ pub const LoadingImage = struct {
|
||||
// Temporary file logic
|
||||
if (medium == .temporary_file) {
|
||||
if (!isPathInTempDir(path)) return error.TemporaryFileNotInTempDir;
|
||||
if (std.mem.indexOf(u8, path, "tty-graphics-protocol") == null) {
|
||||
return error.TemporaryFileNotNamedCorrectly;
|
||||
}
|
||||
}
|
||||
defer if (medium == .temporary_file) {
|
||||
posix.unlink(path) catch |err| {
|
||||
@ -469,6 +472,7 @@ pub const Image = struct {
|
||||
DimensionsTooLarge,
|
||||
FilePathTooLong,
|
||||
TemporaryFileNotInTempDir,
|
||||
TemporaryFileNotNamedCorrectly,
|
||||
UnsupportedFormat,
|
||||
UnsupportedMedium,
|
||||
UnsupportedDepth,
|
||||
@ -682,7 +686,7 @@ test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk"
|
||||
try testing.expect(img.compression == .none);
|
||||
}
|
||||
|
||||
test "image load: rgb, not compressed, temporary file" {
|
||||
test "image load: temporary file without correct path" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -697,6 +701,39 @@ test "image load: rgb, not compressed, temporary file" {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = try tmp_dir.dir.realpath("image.data", &buf);
|
||||
|
||||
var cmd: command.Command = .{
|
||||
.control = .{ .transmit = .{
|
||||
.format = .rgb,
|
||||
.medium = .temporary_file,
|
||||
.compression = .none,
|
||||
.width = 20,
|
||||
.height = 15,
|
||||
.image_id = 31,
|
||||
} },
|
||||
.data = try alloc.dupe(u8, path),
|
||||
};
|
||||
defer cmd.deinit(alloc);
|
||||
try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd));
|
||||
|
||||
// Temporary file should still be there
|
||||
try tmp_dir.dir.access(path, .{});
|
||||
}
|
||||
|
||||
test "image load: rgb, not compressed, temporary file" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var tmp_dir = try internal_os.TempDir.init();
|
||||
defer tmp_dir.deinit();
|
||||
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
|
||||
try tmp_dir.dir.writeFile(.{
|
||||
.sub_path = "tty-graphics-protocol-image.data",
|
||||
.data = data,
|
||||
});
|
||||
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
|
||||
|
||||
var cmd: command.Command = .{
|
||||
.control = .{ .transmit = .{
|
||||
.format = .rgb,
|
||||
@ -762,12 +799,12 @@ test "image load: png, not compressed, regular file" {
|
||||
defer tmp_dir.deinit();
|
||||
const data = @embedFile("testdata/image-png-none-50x76-2147483647-raw.data");
|
||||
try tmp_dir.dir.writeFile(.{
|
||||
.sub_path = "image.data",
|
||||
.sub_path = "tty-graphics-protocol-image.data",
|
||||
.data = data,
|
||||
});
|
||||
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = try tmp_dir.dir.realpath("image.data", &buf);
|
||||
const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
|
||||
|
||||
var cmd: command.Command = .{
|
||||
.control = .{ .transmit = .{
|
||||
|
@ -163,6 +163,15 @@ pub const Command = union(enum) {
|
||||
duration_ms: u16,
|
||||
},
|
||||
|
||||
/// Show GUI message Box (OSC 9;2)
|
||||
show_message_box: []const u8,
|
||||
|
||||
/// Change ConEmu tab (OSC 9;3)
|
||||
change_conemu_tab_title: union(enum) {
|
||||
reset: void,
|
||||
value: []const u8,
|
||||
},
|
||||
|
||||
/// Set progress state (OSC 9;4)
|
||||
progress: struct {
|
||||
state: ProgressState,
|
||||
@ -360,6 +369,9 @@ pub const Parser = struct {
|
||||
// ConEmu specific substates
|
||||
conemu_sleep,
|
||||
conemu_sleep_value,
|
||||
conemu_message_box,
|
||||
conemu_tab,
|
||||
conemu_tab_txt,
|
||||
conemu_progress_prestate,
|
||||
conemu_progress_state,
|
||||
conemu_progress_prevalue,
|
||||
@ -787,6 +799,12 @@ pub const Parser = struct {
|
||||
'1' => {
|
||||
self.state = .conemu_sleep;
|
||||
},
|
||||
'2' => {
|
||||
self.state = .conemu_message_box;
|
||||
},
|
||||
'3' => {
|
||||
self.state = .conemu_tab;
|
||||
},
|
||||
'4' => {
|
||||
self.state = .conemu_progress_prestate;
|
||||
},
|
||||
@ -804,6 +822,17 @@ pub const Parser = struct {
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
self.state = .conemu_sleep_value;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.conemu_message_box => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .show_message_box = undefined };
|
||||
self.temp_state = .{ .str = &self.command.show_message_box };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
self.prepAllocableString();
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
@ -812,6 +841,23 @@ pub const Parser = struct {
|
||||
else => self.complete = true,
|
||||
},
|
||||
|
||||
.conemu_tab => switch (c) {
|
||||
';' => {
|
||||
self.state = .conemu_tab_txt;
|
||||
self.command = .{ .change_conemu_tab_title = .{ .reset = {} } };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.conemu_tab_txt => {
|
||||
self.command = .{ .change_conemu_tab_title = .{ .value = undefined } };
|
||||
self.temp_state = .{ .str = &self.command.change_conemu_tab_title.value };
|
||||
self.complete = true;
|
||||
self.prepAllocableString();
|
||||
},
|
||||
|
||||
.conemu_progress_prestate => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .progress = .{
|
||||
@ -1759,6 +1805,110 @@ test "OSC: show desktop notification with title" {
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
|
||||
}
|
||||
|
||||
test "OSC: conemu message box" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;2;hello world";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_message_box);
|
||||
try testing.expectEqualStrings("hello world", cmd.show_message_box);
|
||||
}
|
||||
|
||||
test "OSC: conemu message box invalid input" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;2";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b');
|
||||
try testing.expect(cmd == null);
|
||||
}
|
||||
|
||||
test "OSC: conemu message box empty message" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;2;";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_message_box);
|
||||
try testing.expectEqualStrings("", cmd.show_message_box);
|
||||
}
|
||||
|
||||
test "OSC: conemu message box spaces only message" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;2; ";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_message_box);
|
||||
try testing.expectEqualStrings(" ", cmd.show_message_box);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab title" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;3;foo bar";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .change_conemu_tab_title);
|
||||
try testing.expectEqualStrings("foo bar", cmd.change_conemu_tab_title.value);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab reset title" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;3;";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
const expected_command: Command = .{ .change_conemu_tab_title = .{ .reset = {} } };
|
||||
try testing.expectEqual(expected_command, cmd);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab spaces only title" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;3; ";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .change_conemu_tab_title);
|
||||
try testing.expectEqualStrings(" ", cmd.change_conemu_tab_title.value);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab invalid input" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;3";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b');
|
||||
try testing.expect(cmd == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -380,109 +380,172 @@ pub fn Stream(comptime Handler: type) type {
|
||||
fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void {
|
||||
switch (input.final) {
|
||||
// CUU - Cursor Up
|
||||
'A', 'k' => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor up command: {}", .{input});
|
||||
return;
|
||||
'A', 'k' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor up command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
false,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI A with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CUD - Cursor Down
|
||||
'B' => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor down command: {}", .{input});
|
||||
return;
|
||||
'B' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor down command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
false,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI B with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CUF - Cursor Right
|
||||
'C' => if (@hasDecl(T, "setCursorRight")) try self.handler.setCursorRight(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor right command: {}", .{input});
|
||||
return;
|
||||
'C' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorRight")) try self.handler.setCursorRight(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor right command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI C with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CUB - Cursor Left
|
||||
'D', 'j' => if (@hasDecl(T, "setCursorLeft")) try self.handler.setCursorLeft(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor left command: {}", .{input});
|
||||
return;
|
||||
'D', 'j' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorLeft")) try self.handler.setCursorLeft(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor left command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI D with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CNL - Cursor Next Line
|
||||
'E' => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor up command: {}", .{input});
|
||||
return;
|
||||
'E' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor up command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
true,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI E with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CPL - Cursor Previous Line
|
||||
'F' => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor down command: {}", .{input});
|
||||
return;
|
||||
'F' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid cursor down command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
true,
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI F with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// HPA - Cursor Horizontal Position Absolute
|
||||
// TODO: test
|
||||
'G', '`' => if (@hasDecl(T, "setCursorCol")) switch (input.params.len) {
|
||||
0 => try self.handler.setCursorCol(1),
|
||||
1 => try self.handler.setCursorCol(input.params[0]),
|
||||
else => log.warn("invalid HPA command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
'G', '`' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorCol")) switch (input.params.len) {
|
||||
0 => try self.handler.setCursorCol(1),
|
||||
1 => try self.handler.setCursorCol(input.params[0]),
|
||||
else => log.warn("invalid HPA command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI G with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CUP - Set Cursor Position.
|
||||
// TODO: test
|
||||
'H', 'f' => if (@hasDecl(T, "setCursorPos")) switch (input.params.len) {
|
||||
0 => try self.handler.setCursorPos(1, 1),
|
||||
1 => try self.handler.setCursorPos(input.params[0], 1),
|
||||
2 => try self.handler.setCursorPos(input.params[0], input.params[1]),
|
||||
else => log.warn("invalid CUP command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
'H', 'f' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorPos")) switch (input.params.len) {
|
||||
0 => try self.handler.setCursorPos(1, 1),
|
||||
1 => try self.handler.setCursorPos(input.params[0], 1),
|
||||
2 => try self.handler.setCursorPos(input.params[0], input.params[1]),
|
||||
else => log.warn("invalid CUP command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI H with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CHT - Cursor Horizontal Tabulation
|
||||
'I' => if (@hasDecl(T, "horizontalTab")) try self.handler.horizontalTab(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid horizontal tab command: {}", .{input});
|
||||
return;
|
||||
'I' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "horizontalTab")) try self.handler.horizontalTab(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid horizontal tab command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI I with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// Erase Display
|
||||
'J' => if (@hasDecl(T, "eraseDisplay")) {
|
||||
@ -540,31 +603,52 @@ pub fn Stream(comptime Handler: type) type {
|
||||
|
||||
// IL - Insert Lines
|
||||
// TODO: test
|
||||
'L' => if (@hasDecl(T, "insertLines")) switch (input.params.len) {
|
||||
0 => try self.handler.insertLines(1),
|
||||
1 => try self.handler.insertLines(input.params[0]),
|
||||
else => log.warn("invalid IL command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
'L' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "insertLines")) switch (input.params.len) {
|
||||
0 => try self.handler.insertLines(1),
|
||||
1 => try self.handler.insertLines(input.params[0]),
|
||||
else => log.warn("invalid IL command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI L with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// DL - Delete Lines
|
||||
// TODO: test
|
||||
'M' => if (@hasDecl(T, "deleteLines")) switch (input.params.len) {
|
||||
0 => try self.handler.deleteLines(1),
|
||||
1 => try self.handler.deleteLines(input.params[0]),
|
||||
else => log.warn("invalid DL command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
'M' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "deleteLines")) switch (input.params.len) {
|
||||
0 => try self.handler.deleteLines(1),
|
||||
1 => try self.handler.deleteLines(input.params[0]),
|
||||
else => log.warn("invalid DL command: {}", .{input}),
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI M with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// Delete Character (DCH)
|
||||
'P' => if (@hasDecl(T, "deleteChars")) try self.handler.deleteChars(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid delete characters command: {}", .{input});
|
||||
return;
|
||||
'P' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "deleteChars")) try self.handler.deleteChars(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid delete characters command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI P with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// Scroll Up (SD)
|
||||
|
||||
@ -587,38 +671,43 @@ pub fn Stream(comptime Handler: type) type {
|
||||
},
|
||||
|
||||
// Scroll Down (SD)
|
||||
'T' => if (@hasDecl(T, "scrollDown")) try self.handler.scrollDown(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid scroll down command: {}", .{input});
|
||||
return;
|
||||
'T' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "scrollDown")) try self.handler.scrollDown(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid scroll down command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI T with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// Cursor Tabulation Control
|
||||
'W' => {
|
||||
switch (input.params.len) {
|
||||
0 => if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{input}),
|
||||
'W' => switch (input.intermediates.len) {
|
||||
0 => {
|
||||
if (input.params.len == 0 or
|
||||
(input.params.len == 1 and input.params[0] == 0))
|
||||
{
|
||||
if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{input});
|
||||
|
||||
1 => if (input.intermediates.len == 1 and input.intermediates[0] == '?') {
|
||||
if (input.params[0] == 5) {
|
||||
if (@hasDecl(T, "tabReset"))
|
||||
try self.handler.tabReset()
|
||||
else
|
||||
log.warn("unimplemented tab reset callback: {}", .{input});
|
||||
} else log.warn("invalid cursor tabulation control: {}", .{input});
|
||||
} else {
|
||||
switch (input.params[0]) {
|
||||
0 => if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{input}),
|
||||
return;
|
||||
}
|
||||
|
||||
switch (input.params.len) {
|
||||
0 => unreachable,
|
||||
|
||||
1 => switch (input.params[0]) {
|
||||
0 => unreachable,
|
||||
|
||||
2 => if (@hasDecl(T, "tabClear"))
|
||||
try self.handler.tabClear(.current)
|
||||
@ -631,63 +720,103 @@ pub fn Stream(comptime Handler: type) type {
|
||||
log.warn("unimplemented tab clear callback: {}", .{input}),
|
||||
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
else => {},
|
||||
}
|
||||
|
||||
log.warn("invalid cursor tabulation control: {}", .{input});
|
||||
return;
|
||||
log.warn("invalid cursor tabulation control: {}", .{input});
|
||||
return;
|
||||
},
|
||||
|
||||
1 => if (input.intermediates[0] == '?' and input.params[0] == 5) {
|
||||
if (@hasDecl(T, "tabReset"))
|
||||
try self.handler.tabReset()
|
||||
else
|
||||
log.warn("unimplemented tab reset callback: {}", .{input});
|
||||
} else log.warn("invalid cursor tabulation control: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI W with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// Erase Characters (ECH)
|
||||
'X' => if (@hasDecl(T, "eraseChars")) try self.handler.eraseChars(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid erase characters command: {}", .{input});
|
||||
return;
|
||||
'X' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "eraseChars")) try self.handler.eraseChars(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid erase characters command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI X with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// CHT - Cursor Horizontal Tabulation Back
|
||||
'Z' => if (@hasDecl(T, "horizontalTabBack")) try self.handler.horizontalTabBack(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid horizontal tab back command: {}", .{input});
|
||||
return;
|
||||
'Z' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "horizontalTabBack")) try self.handler.horizontalTabBack(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid horizontal tab back command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI Z with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// HPR - Cursor Horizontal Position Relative
|
||||
'a' => if (@hasDecl(T, "setCursorColRelative")) try self.handler.setCursorColRelative(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid HPR command: {}", .{input});
|
||||
return;
|
||||
'a' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorColRelative")) try self.handler.setCursorColRelative(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid HPR command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI a with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// Repeat Previous Char (REP)
|
||||
'b' => if (@hasDecl(T, "printRepeat")) try self.handler.printRepeat(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid print repeat command: {}", .{input});
|
||||
return;
|
||||
'b' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "printRepeat")) try self.handler.printRepeat(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid print repeat command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI b with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// c - Device Attributes (DA1)
|
||||
'c' => if (@hasDecl(T, "deviceAttributes")) {
|
||||
@ -708,40 +837,61 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
// VPA - Cursor Vertical Position Absolute
|
||||
'd' => if (@hasDecl(T, "setCursorRow")) try self.handler.setCursorRow(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid VPA command: {}", .{input});
|
||||
return;
|
||||
'd' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorRow")) try self.handler.setCursorRow(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid VPA command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI d with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// VPR - Cursor Vertical Position Relative
|
||||
'e' => if (@hasDecl(T, "setCursorRowRelative")) try self.handler.setCursorRowRelative(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid VPR command: {}", .{input});
|
||||
return;
|
||||
'e' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setCursorRowRelative")) try self.handler.setCursorRowRelative(
|
||||
switch (input.params.len) {
|
||||
0 => 1,
|
||||
1 => input.params[0],
|
||||
else => {
|
||||
log.warn("invalid VPR command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI e with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// TBC - Tab Clear
|
||||
// TODO: test
|
||||
'g' => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(
|
||||
switch (input.params.len) {
|
||||
1 => @enumFromInt(input.params[0]),
|
||||
else => {
|
||||
log.warn("invalid tab clear command: {}", .{input});
|
||||
return;
|
||||
'g' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(
|
||||
switch (input.params.len) {
|
||||
1 => @enumFromInt(input.params[0]),
|
||||
else => {
|
||||
log.warn("invalid tab clear command: {}", .{input});
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
) else log.warn("unimplemented CSI callback: {}", .{input}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI g with intermediates: {s}",
|
||||
.{input.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// SM - Set Mode
|
||||
'h' => if (@hasDecl(T, "setMode")) mode: {
|
||||
@ -1455,7 +1605,7 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.progress, .sleep => {
|
||||
.progress, .sleep, .show_message_box, .change_conemu_tab_title => {
|
||||
log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
}
|
||||
@ -1564,10 +1714,13 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
||||
|
||||
// HTS - Horizontal Tab Set
|
||||
'H' => if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{action}),
|
||||
'H' => if (@hasDecl(T, "tabSet")) switch (action.intermediates.len) {
|
||||
0 => try self.handler.tabSet(),
|
||||
else => {
|
||||
log.warn("invalid tab set command: {}", .{action});
|
||||
return;
|
||||
},
|
||||
} else log.warn("unimplemented tab set callback: {}", .{action}),
|
||||
|
||||
// RI - Reverse Index
|
||||
'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) {
|
||||
@ -1597,17 +1750,17 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
||||
|
||||
// SPA - Start of Guarded Area
|
||||
'V' => if (@hasDecl(T, "setProtectedMode")) {
|
||||
'V' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) {
|
||||
try self.handler.setProtectedMode(ansi.ProtectedMode.iso);
|
||||
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
||||
|
||||
// EPA - End of Guarded Area
|
||||
'W' => if (@hasDecl(T, "setProtectedMode")) {
|
||||
'W' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) {
|
||||
try self.handler.setProtectedMode(ansi.ProtectedMode.off);
|
||||
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
||||
|
||||
// DECID
|
||||
'Z' => if (@hasDecl(T, "deviceAttributes")) {
|
||||
'Z' => if (@hasDecl(T, "deviceAttributes") and action.intermediates.len == 0) {
|
||||
try self.handler.deviceAttributes(.primary, &.{});
|
||||
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
||||
|
||||
@ -1666,12 +1819,12 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
||||
|
||||
// Set application keypad mode
|
||||
'=' => if (@hasDecl(T, "setMode")) {
|
||||
'=' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) {
|
||||
try self.handler.setMode(.keypad_keys, true);
|
||||
} else log.warn("unimplemented setMode: {}", .{action}),
|
||||
|
||||
// Reset application keypad mode
|
||||
'>' => if (@hasDecl(T, "setMode")) {
|
||||
'>' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) {
|
||||
try self.handler.setMode(.keypad_keys, false);
|
||||
} else log.warn("unimplemented setMode: {}", .{action}),
|
||||
|
||||
@ -1753,6 +1906,10 @@ test "stream: cursor right (CUF)" {
|
||||
s.handler.amount = 0;
|
||||
try s.nextSlice("\x1B[5;4C");
|
||||
try testing.expectEqual(@as(u16, 0), s.handler.amount);
|
||||
|
||||
s.handler.amount = 0;
|
||||
try s.nextSlice("\x1b[?3C");
|
||||
try testing.expectEqual(@as(u16, 0), s.handler.amount);
|
||||
}
|
||||
|
||||
test "stream: dec set mode (SM) and reset mode (RM)" {
|
||||
@ -1770,6 +1927,10 @@ test "stream: dec set mode (SM) and reset mode (RM)" {
|
||||
|
||||
try s.nextSlice("\x1B[?6l");
|
||||
try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);
|
||||
|
||||
s.handler.mode = @as(modes.Mode, @enumFromInt(1));
|
||||
try s.nextSlice("\x1B[6 h");
|
||||
try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);
|
||||
}
|
||||
|
||||
test "stream: ansi set mode (SM) and reset mode (RM)" {
|
||||
@ -1788,6 +1949,10 @@ test "stream: ansi set mode (SM) and reset mode (RM)" {
|
||||
|
||||
try s.nextSlice("\x1B[4l");
|
||||
try testing.expect(s.handler.mode == null);
|
||||
|
||||
s.handler.mode = null;
|
||||
try s.nextSlice("\x1B[>5h");
|
||||
try testing.expect(s.handler.mode == null);
|
||||
}
|
||||
|
||||
test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" {
|
||||
@ -1937,6 +2102,12 @@ test "stream: DECED, DECSED" {
|
||||
try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?);
|
||||
try testing.expect(!s.handler.protected.?);
|
||||
}
|
||||
{
|
||||
// Invalid and ignored by the handler
|
||||
for ("\x1B[>0J") |c| try s.next(c);
|
||||
try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?);
|
||||
try testing.expect(!s.handler.protected.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "stream: DECEL, DECSEL" {
|
||||
@ -1997,6 +2168,12 @@ test "stream: DECEL, DECSEL" {
|
||||
try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?);
|
||||
try testing.expect(!s.handler.protected.?);
|
||||
}
|
||||
{
|
||||
// Invalid and ignored by the handler
|
||||
for ("\x1B[<1K") |c| try s.next(c);
|
||||
try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?);
|
||||
try testing.expect(!s.handler.protected.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "stream: DECSCUSR" {
|
||||
@ -2014,6 +2191,10 @@ test "stream: DECSCUSR" {
|
||||
|
||||
try s.nextSlice("\x1B[1 q");
|
||||
try testing.expect(s.handler.style.? == .blinking_block);
|
||||
|
||||
// Invalid and ignored by the handler
|
||||
try s.nextSlice("\x1B[?0 q");
|
||||
try testing.expect(s.handler.style.? == .blinking_block);
|
||||
}
|
||||
|
||||
test "stream: DECSCUSR without space" {
|
||||
@ -2054,6 +2235,10 @@ test "stream: XTSHIFTESCAPE" {
|
||||
|
||||
try s.nextSlice("\x1B[>1s");
|
||||
try testing.expect(s.handler.escape.? == true);
|
||||
|
||||
// Invalid and ignored by the handler
|
||||
try s.nextSlice("\x1B[1 s");
|
||||
try testing.expect(s.handler.escape.? == true);
|
||||
}
|
||||
|
||||
test "stream: change window title with invalid utf-8" {
|
||||
@ -2374,6 +2559,14 @@ test "stream CSI W tab set" {
|
||||
s.handler.called = false;
|
||||
try s.nextSlice("\x1b[0W");
|
||||
try testing.expect(s.handler.called);
|
||||
|
||||
s.handler.called = false;
|
||||
try s.nextSlice("\x1b[>W");
|
||||
try testing.expect(!s.handler.called);
|
||||
|
||||
s.handler.called = false;
|
||||
try s.nextSlice("\x1b[99W");
|
||||
try testing.expect(!s.handler.called);
|
||||
}
|
||||
|
||||
test "stream CSI ? W reset tab stops" {
|
||||
@ -2392,4 +2585,8 @@ test "stream CSI ? W reset tab stops" {
|
||||
|
||||
try s.nextSlice("\x1b[?5W");
|
||||
try testing.expect(s.handler.reset);
|
||||
|
||||
// Invalid and ignored by the handler
|
||||
try s.nextSlice("\x1b[?1;2;3W");
|
||||
try testing.expect(s.handler.reset);
|
||||
}
|
||||
|
@ -466,6 +466,9 @@ pub fn clearScreen(self: *Termio, td: *ThreadData, history: bool) !void {
|
||||
// for alt screen, we do nothing.
|
||||
if (self.terminal.active_screen == .alternate) return;
|
||||
|
||||
// Clear our selection
|
||||
self.terminal.screen.clearSelection();
|
||||
|
||||
// Clear our scrollback
|
||||
if (history) self.terminal.eraseDisplay(.scrollback, false);
|
||||
|
||||
|
@ -42,6 +42,7 @@ wdth = "wdth"
|
||||
Strat = "Strat"
|
||||
grey = "gray"
|
||||
greyscale = "grayscale"
|
||||
DECID = "DECID"
|
||||
|
||||
[type.swift.extend-words]
|
||||
inout = "inout"
|
||||
|
Reference in New Issue
Block a user