mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge remote-tracking branch 'upstream/main' into titlebar-unzoom-button
This commit is contained in:
20
README.md
20
README.md
@ -434,7 +434,8 @@ To build Ghostty, you need [Zig](https://ziglang.org/) installed.
|
|||||||
|
|
||||||
On Linux, you may need to install additional dependencies. See
|
On Linux, you may need to install additional dependencies. See
|
||||||
[Linux Installation Tips](#linux-installation-tips). On macOS, you
|
[Linux Installation Tips](#linux-installation-tips). On macOS, you
|
||||||
need Xcode installed with the macOS and iOS SDKs enabled.
|
need Xcode installed with the macOS and iOS SDKs enabled. See
|
||||||
|
[Mac `.app`](#mac-app).
|
||||||
|
|
||||||
The official development environment is defined by Nix. You do not need
|
The official development environment is defined by Nix. You do not need
|
||||||
to use Nix to develop Ghostty, but the Nix environment is the environment
|
to use Nix to develop Ghostty, but the Nix environment is the environment
|
||||||
@ -565,7 +566,22 @@ all features of Ghostty work.
|
|||||||
### Mac `.app`
|
### Mac `.app`
|
||||||
|
|
||||||
To build the official, fully featured macOS application, you must
|
To build the official, fully featured macOS application, you must
|
||||||
build on a macOS machine with XCode installed:
|
build on a macOS machine with Xcode installed, and the active developer
|
||||||
|
directory pointing to it. If you're not sure that's the case, check the
|
||||||
|
output of `xcode-select --print-path`:
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ xcode-select --print-path
|
||||||
|
/Library/Developer/CommandLineTools # <-- BAD
|
||||||
|
$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
|
||||||
|
$ xcode-select --print-path
|
||||||
|
/Applications/Xcode.app/Contents/Developer # <-- GOOD
|
||||||
|
```
|
||||||
|
|
||||||
|
The above can happen if you install the Xcode Command Line Tools _after_ Xcode
|
||||||
|
is installed. With that out of the way, make sure you have both the macOS and
|
||||||
|
iOS SDKs installed (from inside Xcode → Settings → Platforms), and let's move
|
||||||
|
on to building Ghostty:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ zig build -Doptimize=ReleaseFast
|
$ zig build -Doptimize=ReleaseFast
|
||||||
|
61
build.zig
61
build.zig
@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
|||||||
const fs = std.fs;
|
const fs = std.fs;
|
||||||
const CompileStep = std.Build.Step.Compile;
|
const CompileStep = std.Build.Step.Compile;
|
||||||
const RunStep = std.Build.Step.Run;
|
const RunStep = std.Build.Step.Run;
|
||||||
|
const ResolvedTarget = std.Build.ResolvedTarget;
|
||||||
|
|
||||||
const apprt = @import("src/apprt.zig");
|
const apprt = @import("src/apprt.zig");
|
||||||
const font = @import("src/font/main.zig");
|
const font = @import("src/font/main.zig");
|
||||||
@ -706,10 +707,15 @@ pub fn build(b: *std.Build) !void {
|
|||||||
const test_step = b.step("test", "Run all tests");
|
const test_step = b.step("test", "Run all tests");
|
||||||
const test_filter = b.option([]const u8, "test-filter", "Filter for test");
|
const test_filter = b.option([]const u8, "test-filter", "Filter for test");
|
||||||
|
|
||||||
|
// Force all Mac builds to use a `generic` CPU. This avoids
|
||||||
|
// potential issues with `highway` compile errors due to missing
|
||||||
|
// `arm_neon` features (see for example https://github.com/mitchellh/ghostty/issues/1640).
|
||||||
|
const test_target = if (target.result.os.tag == .macos and builtin.target.isDarwin()) genericMacOSTarget(b) else target;
|
||||||
|
|
||||||
const main_test = b.addTest(.{
|
const main_test = b.addTest(.{
|
||||||
.name = "ghostty-test",
|
.name = "ghostty-test",
|
||||||
.root_source_file = .{ .path = "src/main.zig" },
|
.root_source_file = .{ .path = "src/main.zig" },
|
||||||
.target = target,
|
.target = test_target,
|
||||||
.filter = test_filter,
|
.filter = test_filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -754,6 +760,16 @@ fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a ResolvedTarget for a mac with a `target.result.cpu.model.name` of `generic`.
|
||||||
|
// `b.standardTargetOptions()` returns a more specific cpu like `apple_a15`.
|
||||||
|
fn genericMacOSTarget(b: *std.Build) ResolvedTarget {
|
||||||
|
return b.resolveTargetQuery(.{
|
||||||
|
.cpu_arch = .aarch64,
|
||||||
|
.os_tag = .macos,
|
||||||
|
.os_version_min = osVersionMin(.macos),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a universal macOS libghostty library and returns the path
|
/// Creates a universal macOS libghostty library and returns the path
|
||||||
/// to the final library.
|
/// to the final library.
|
||||||
///
|
///
|
||||||
@ -781,11 +797,7 @@ fn createMacOSLib(
|
|||||||
const lib = b.addStaticLibrary(.{
|
const lib = b.addStaticLibrary(.{
|
||||||
.name = "ghostty",
|
.name = "ghostty",
|
||||||
.root_source_file = .{ .path = "src/main_c.zig" },
|
.root_source_file = .{ .path = "src/main_c.zig" },
|
||||||
.target = b.resolveTargetQuery(.{
|
.target = genericMacOSTarget(b),
|
||||||
.cpu_arch = .aarch64,
|
|
||||||
.os_tag = .macos,
|
|
||||||
.os_version_min = osVersionMin(.macos),
|
|
||||||
}),
|
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
lib.bundle_compiler_rt = true;
|
lib.bundle_compiler_rt = true;
|
||||||
@ -1165,6 +1177,43 @@ fn addDeps(
|
|||||||
.gtk => {
|
.gtk => {
|
||||||
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||||
if (config.libadwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
|
if (config.libadwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
|
||||||
|
|
||||||
|
{
|
||||||
|
const gresource = @import("src/apprt/gtk/gresource.zig");
|
||||||
|
|
||||||
|
const wf = b.addWriteFiles();
|
||||||
|
const gresource_xml = wf.add(
|
||||||
|
"gresource.xml",
|
||||||
|
if (config.libadwaita)
|
||||||
|
gresource.gresource_xml_libadwaita
|
||||||
|
else
|
||||||
|
gresource.gresource_xml_gtk,
|
||||||
|
);
|
||||||
|
|
||||||
|
const generate_resources_c = b.addSystemCommand(&.{
|
||||||
|
"glib-compile-resources",
|
||||||
|
"--c-name",
|
||||||
|
"ghostty",
|
||||||
|
"--generate-source",
|
||||||
|
"--target",
|
||||||
|
});
|
||||||
|
const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c");
|
||||||
|
generate_resources_c.addFileArg(gresource_xml);
|
||||||
|
generate_resources_c.extra_file_dependencies = if (config.libadwaita) &gresource.dependencies_libadwaita else &gresource.dependencies_gtk;
|
||||||
|
step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} });
|
||||||
|
|
||||||
|
const generate_resources_h = b.addSystemCommand(&.{
|
||||||
|
"glib-compile-resources",
|
||||||
|
"--c-name",
|
||||||
|
"ghostty",
|
||||||
|
"--generate-header",
|
||||||
|
"--target",
|
||||||
|
});
|
||||||
|
const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h");
|
||||||
|
generate_resources_h.addFileArg(gresource_xml);
|
||||||
|
generate_resources_h.extra_file_dependencies = if (config.libadwaita) &gresource.dependencies_libadwaita else &gresource.dependencies_gtk;
|
||||||
|
step.addIncludePath(ghostty_resources_h.dirname());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
flake.lock
generated
6
flake.lock
generated
@ -147,11 +147,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-zig-0-12": {
|
"nixpkgs-zig-0-12": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1711143939,
|
"lastModified": 1711789504,
|
||||||
"narHash": "sha256-oT6a81U4NHjJH1hjaMVXKsdTZJwl2dT+MhMESKoevvA=",
|
"narHash": "sha256-1XRwW0MD9LxtHMMlPmF3rDw/Zbv4jLnpGnJEtibO+MQ=",
|
||||||
"owner": "vancluever",
|
"owner": "vancluever",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c4749393c06e52da4adf42877fdf9bac7141f0de",
|
"rev": "c9e24149cca8215b84fc3ce5bc2bdc1ca823a588",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
24
flake.nix
24
flake.nix
@ -52,23 +52,19 @@
|
|||||||
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
||||||
};
|
};
|
||||||
|
|
||||||
packages.${system} = rec {
|
packages.${system} = let
|
||||||
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix {
|
mkArgs = optimize: {
|
||||||
inherit (pkgs-zig-0-12) zig_0_12;
|
inherit (pkgs-zig-0-12) zig_0_12;
|
||||||
|
inherit optimize;
|
||||||
|
|
||||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||||
optimize = "Debug";
|
|
||||||
};
|
};
|
||||||
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix {
|
in rec {
|
||||||
inherit (pkgs-zig-0-12) zig_0_12;
|
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
|
||||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
|
||||||
optimize = "ReleaseSafe";
|
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
|
||||||
};
|
|
||||||
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix {
|
ghostty = ghostty-releasefast;
|
||||||
inherit (pkgs-zig-0-12) zig_0_12;
|
|
||||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
|
||||||
optimize = "ReleaseFast";
|
|
||||||
};
|
|
||||||
ghostty = ghostty-releasesafe;
|
|
||||||
default = ghostty;
|
default = ghostty;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -175,6 +175,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
|
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
|
||||||
window.appearance = appearance
|
window.appearance = appearance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the font for the window and tab titles.
|
||||||
|
if let titleFontName = ghostty.config.windowTitleFontFamily {
|
||||||
|
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
|
||||||
|
} else {
|
||||||
|
window.titlebarFont = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
||||||
@ -258,7 +265,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
if (ghostty.config.macosTitlebarTabs) {
|
if (ghostty.config.macosTitlebarTabs) {
|
||||||
window.tabbingMode = .preferred
|
window.tabbingMode = .preferred
|
||||||
window.titlebarTabs = true
|
window.titlebarTabs = true
|
||||||
syncAppearance()
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
window.tabbingMode = .automatic
|
window.tabbingMode = .automatic
|
||||||
}
|
}
|
||||||
@ -290,6 +296,9 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
window.tabGroup?.removeWindow(window)
|
window.tabGroup?.removeWindow(window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply any additional appearance-related properties to the new window.
|
||||||
|
syncAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shows the "+" button in the tab bar, responds to that click.
|
// Shows the "+" button in the tab bar, responds to that click.
|
||||||
|
@ -15,6 +15,16 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var titleFont: NSFont? {
|
||||||
|
get {
|
||||||
|
titleTextField.font
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
titleTextField.font = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override init(identifier: NSToolbar.Identifier) {
|
override init(identifier: NSToolbar.Identifier) {
|
||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
|
|
||||||
|
@ -97,6 +97,12 @@ class TerminalWindow: NSWindow {
|
|||||||
|
|
||||||
// MARK: - NSWindow
|
// MARK: - NSWindow
|
||||||
|
|
||||||
|
override var title: String {
|
||||||
|
didSet {
|
||||||
|
tab.attributedTitle = attributedTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func becomeKey() {
|
override func becomeKey() {
|
||||||
// This is required because the removeTitlebarAccessoryViewController hook does not
|
// This is required because the removeTitlebarAccessoryViewController hook does not
|
||||||
// catch the creation of a new window by "tearing off" a tab from a tabbed window.
|
// catch the creation of a new window by "tearing off" a tab from a tabbed window.
|
||||||
@ -109,6 +115,7 @@ class TerminalWindow: NSWindow {
|
|||||||
updateNewTabButtonOpacity()
|
updateNewTabButtonOpacity()
|
||||||
resetZoomTabButton.contentTintColor = .controlAccentColor
|
resetZoomTabButton.contentTintColor = .controlAccentColor
|
||||||
resetZoomToolbarButton.contentTintColor = .controlAccentColor
|
resetZoomToolbarButton.contentTintColor = .controlAccentColor
|
||||||
|
tab.attributedTitle = attributedTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
override func resignKey() {
|
override func resignKey() {
|
||||||
@ -117,6 +124,7 @@ class TerminalWindow: NSWindow {
|
|||||||
updateNewTabButtonOpacity()
|
updateNewTabButtonOpacity()
|
||||||
resetZoomTabButton.contentTintColor = .secondaryLabelColor
|
resetZoomTabButton.contentTintColor = .secondaryLabelColor
|
||||||
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
|
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
|
||||||
|
tab.attributedTitle = attributedTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutIfNeeded() {
|
override func layoutIfNeeded() {
|
||||||
@ -172,6 +180,38 @@ class TerminalWindow: NSWindow {
|
|||||||
updateResetZoomTitlebarButtonVisibility()
|
updateResetZoomTitlebarButtonVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used to set the titlebar font.
|
||||||
|
var titlebarFont: NSFont? {
|
||||||
|
didSet {
|
||||||
|
titlebarTextField?.font = titlebarFont
|
||||||
|
tab.attributedTitle = attributedTitle
|
||||||
|
|
||||||
|
if let toolbar = toolbar as? TerminalToolbar {
|
||||||
|
toolbar.titleFont = titlebarFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the NSTextField responsible for displaying the titlebar's title.
|
||||||
|
private var titlebarTextField: NSTextField? {
|
||||||
|
guard let titlebarContainer = contentView?.superview?.subviews
|
||||||
|
.first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil }
|
||||||
|
guard let titlebarView = titlebarContainer.subviews
|
||||||
|
.first(where: { $0.className == "NSTitlebarView" }) else { return nil }
|
||||||
|
return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a styled representation of our title property.
|
||||||
|
private var attributedTitle: NSAttributedString? {
|
||||||
|
guard let titlebarFont else { return nil }
|
||||||
|
|
||||||
|
let attributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.font: titlebarFont,
|
||||||
|
.foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor,
|
||||||
|
]
|
||||||
|
return NSAttributedString(string: title, attributes: attributes)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
private var newTabButtonImageLayer: VibrantLayer? = nil
|
private var newTabButtonImageLayer: VibrantLayer? = nil
|
||||||
|
@ -198,6 +198,15 @@ extension Ghostty {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var windowTitleFontFamily: String? {
|
||||||
|
guard let config = self.config else { return nil }
|
||||||
|
var v: UnsafePointer<Int8>? = nil
|
||||||
|
let key = "window-title-font-family"
|
||||||
|
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
|
||||||
|
guard let ptr = v else { return nil }
|
||||||
|
return String(cString: ptr)
|
||||||
|
}
|
||||||
|
|
||||||
var macosTitlebarTabs: Bool {
|
var macosTitlebarTabs: Bool {
|
||||||
guard let config = self.config else { return false }
|
guard let config = self.config else { return false }
|
||||||
var v = false;
|
var v = false;
|
||||||
|
@ -78,7 +78,7 @@ in
|
|||||||
mkShell {
|
mkShell {
|
||||||
name = "ghostty";
|
name = "ghostty";
|
||||||
|
|
||||||
nativeBuildInputs =
|
packages =
|
||||||
[
|
[
|
||||||
# For builds
|
# For builds
|
||||||
llvmPackages_latest.llvm
|
llvmPackages_latest.llvm
|
||||||
@ -120,13 +120,7 @@ in
|
|||||||
gdb
|
gdb
|
||||||
valgrind
|
valgrind
|
||||||
wraptest
|
wraptest
|
||||||
];
|
|
||||||
|
|
||||||
buildInputs =
|
|
||||||
[
|
|
||||||
# TODO: non-linux
|
|
||||||
]
|
|
||||||
++ lib.optionals stdenv.isLinux [
|
|
||||||
bzip2
|
bzip2
|
||||||
expat
|
expat
|
||||||
fontconfig
|
fontconfig
|
||||||
|
@ -151,8 +151,16 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
|
|
||||||
break :app @ptrCast(adw_app);
|
break :app @ptrCast(adw_app);
|
||||||
};
|
};
|
||||||
|
|
||||||
errdefer c.g_object_unref(app);
|
errdefer c.g_object_unref(app);
|
||||||
|
const gapp = @as(*c.GApplication, @ptrCast(app));
|
||||||
|
|
||||||
|
// force the resource path to a known value so that it doesn't depend on
|
||||||
|
// the app id and load in compiled resources
|
||||||
|
c.g_application_set_resource_base_path(gapp, "/com/mitchellh/ghostty");
|
||||||
|
c.g_resources_register(c.ghostty_get_resource());
|
||||||
|
|
||||||
|
// The `activate` signal is used when Ghostty is first launched and when a
|
||||||
|
// secondary Ghostty is launched and requests a new window.
|
||||||
_ = c.g_signal_connect_data(
|
_ = c.g_signal_connect_data(
|
||||||
app,
|
app,
|
||||||
"activate",
|
"activate",
|
||||||
@ -169,7 +177,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
|
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
|
||||||
errdefer c.g_main_context_release(ctx);
|
errdefer c.g_main_context_release(ctx);
|
||||||
|
|
||||||
const gapp = @as(*c.GApplication, @ptrCast(app));
|
|
||||||
var err_: ?*c.GError = null;
|
var err_: ?*c.GError = null;
|
||||||
if (c.g_application_register(
|
if (c.g_application_register(
|
||||||
gapp,
|
gapp,
|
||||||
|
@ -53,6 +53,7 @@ fn init(self: *ConfigErrors, app: *App) !void {
|
|||||||
c.gtk_window_set_title(gtk_window, "Configuration Errors");
|
c.gtk_window_set_title(gtk_window, "Configuration Errors");
|
||||||
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
||||||
c.gtk_window_set_resizable(gtk_window, 0);
|
c.gtk_window_set_resizable(gtk_window, 0);
|
||||||
|
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
|
||||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
// Set some state
|
// Set some state
|
||||||
|
@ -943,32 +943,14 @@ pub fn showDesktopNotification(
|
|||||||
0 => "Ghostty",
|
0 => "Ghostty",
|
||||||
else => title,
|
else => title,
|
||||||
};
|
};
|
||||||
|
|
||||||
const notif = c.g_notification_new(t.ptr);
|
const notif = c.g_notification_new(t.ptr);
|
||||||
defer c.g_object_unref(notif);
|
defer c.g_object_unref(notif);
|
||||||
c.g_notification_set_body(notif, body.ptr);
|
c.g_notification_set_body(notif, body.ptr);
|
||||||
|
|
||||||
// Find our icon in the current icon theme. Not pretty, but the builtin GIO
|
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
|
||||||
// method "g_themed_icon_new" doesn't search XDG_DATA_DIRS, so any install
|
|
||||||
// not in /usr/share will be unable to find an icon
|
|
||||||
const display = c.gdk_display_get_default();
|
|
||||||
const theme = c.gtk_icon_theme_get_for_display(display);
|
|
||||||
const icon = c.gtk_icon_theme_lookup_icon(
|
|
||||||
theme,
|
|
||||||
"com.mitchellh.ghostty",
|
|
||||||
null,
|
|
||||||
48,
|
|
||||||
1, // Window scale
|
|
||||||
c.GTK_TEXT_DIR_LTR,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
defer c.g_object_unref(icon);
|
defer c.g_object_unref(icon);
|
||||||
// Get the filepath of the icon we found
|
c.g_notification_set_icon(notif, icon);
|
||||||
const file = c.gtk_icon_paintable_get_file(icon);
|
|
||||||
defer c.g_object_unref(file);
|
|
||||||
// Create a GIO icon
|
|
||||||
const gicon = c.g_file_icon_new(file);
|
|
||||||
defer c.g_object_unref(gicon);
|
|
||||||
c.g_notification_set_icon(notif, gicon);
|
|
||||||
|
|
||||||
const g_app: *c.GApplication = @ptrCast(self.app.app);
|
const g_app: *c.GApplication = @ptrCast(self.app.app);
|
||||||
|
|
||||||
@ -984,6 +966,8 @@ fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
|||||||
c.gtk_gl_area_make_current(area);
|
c.gtk_gl_area_make_current(area);
|
||||||
if (c.gtk_gl_area_get_error(area)) |err| {
|
if (c.gtk_gl_area_get_error(area)) |err| {
|
||||||
log.err("surface failed to realize: {s}", .{err.*.message});
|
log.err("surface failed to realize: {s}", .{err.*.message});
|
||||||
|
log.warn("this error is usually due to a driver or gtk bug", .{});
|
||||||
|
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1624,6 +1608,7 @@ fn gtkInputCommit(
|
|||||||
|
|
||||||
fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
||||||
const self = userdataSelf(ud.?);
|
const self = userdataSelf(ud.?);
|
||||||
|
if (!self.realized) return;
|
||||||
|
|
||||||
// Notify our IM context
|
// Notify our IM context
|
||||||
c.gtk_im_context_focus_in(self.im_context);
|
c.gtk_im_context_focus_in(self.im_context);
|
||||||
@ -1637,6 +1622,7 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo
|
|||||||
|
|
||||||
fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
||||||
const self = userdataSelf(ud.?);
|
const self = userdataSelf(ud.?);
|
||||||
|
if (!self.realized) return;
|
||||||
|
|
||||||
// Notify our IM context
|
// Notify our IM context
|
||||||
c.gtk_im_context_focus_out(self.im_context);
|
c.gtk_im_context_focus_out(self.im_context);
|
||||||
|
@ -18,7 +18,6 @@ const CoreSurface = @import("../../Surface.zig");
|
|||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
const Tab = @import("Tab.zig");
|
const Tab = @import("Tab.zig");
|
||||||
const icon = @import("icon.zig");
|
|
||||||
const c = @import("c.zig");
|
const c = @import("c.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
const log = std.log.scoped(.gtk);
|
||||||
@ -31,10 +30,6 @@ window: *c.GtkWindow,
|
|||||||
/// The notebook (tab grouping) for this window.
|
/// The notebook (tab grouping) for this window.
|
||||||
notebook: *c.GtkNotebook,
|
notebook: *c.GtkNotebook,
|
||||||
|
|
||||||
/// The resources directory for the icon (if any). We need to retain a
|
|
||||||
/// pointer to this because GTK can use it at any time.
|
|
||||||
icon: icon.Icon,
|
|
||||||
|
|
||||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||||
// Allocate a fixed pointer for our window. We try to minimize
|
// Allocate a fixed pointer for our window. We try to minimize
|
||||||
// allocations but windows and other GUI requirements are so minimal
|
// allocations but windows and other GUI requirements are so minimal
|
||||||
@ -53,7 +48,6 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
// Set up our own state
|
// Set up our own state
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.icon = undefined,
|
|
||||||
.window = undefined,
|
.window = undefined,
|
||||||
.notebook = undefined,
|
.notebook = undefined,
|
||||||
};
|
};
|
||||||
@ -70,10 +64,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
// to disable this so that terminal programs can capture F10 (such as htop)
|
// to disable this so that terminal programs can capture F10 (such as htop)
|
||||||
c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
|
c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
|
||||||
|
|
||||||
// If we don't have the icon then we'll try to add our resources dir
|
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
|
||||||
// to the search path and see if we can find it there.
|
|
||||||
self.icon = try icon.appIcon(self.app, window);
|
|
||||||
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
|
|
||||||
|
|
||||||
// Apply background opacity if we have it
|
// Apply background opacity if we have it
|
||||||
if (app.config.@"background-opacity" < 1) {
|
if (app.config.@"background-opacity" < 1) {
|
||||||
@ -189,9 +180,7 @@ fn initActions(self: *Window) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Window) void {
|
pub fn deinit(_: *Window) void {}
|
||||||
self.icon.deinit(self.app);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new tab to this window.
|
/// Add a new tab to this window.
|
||||||
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
||||||
|
@ -7,6 +7,9 @@ const c = @cImport({
|
|||||||
@cInclude("gdk/x11/gdkx.h");
|
@cInclude("gdk/x11/gdkx.h");
|
||||||
// Xkb for X11 state handling
|
// Xkb for X11 state handling
|
||||||
@cInclude("X11/XKBlib.h");
|
@cInclude("X11/XKBlib.h");
|
||||||
|
|
||||||
|
// generated header files
|
||||||
|
@cInclude("ghostty_resources.h");
|
||||||
});
|
});
|
||||||
|
|
||||||
pub usingnamespace c;
|
pub usingnamespace c;
|
||||||
|
124
src/apprt/gtk/gresource.zig
Normal file
124
src/apprt/gtk/gresource.zig
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const css_files = [_][]const u8{
|
||||||
|
"style.css",
|
||||||
|
"style-dark.css",
|
||||||
|
"style-hc.css",
|
||||||
|
"style-hc-dark.css",
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons = [_]struct {
|
||||||
|
alias: []const u8,
|
||||||
|
source: []const u8,
|
||||||
|
}{
|
||||||
|
.{
|
||||||
|
.alias = "16x16",
|
||||||
|
.source = "16x16",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "16x16@2",
|
||||||
|
.source = "16x16@2x@2x",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "32x32",
|
||||||
|
.source = "32x32",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "32x32@2",
|
||||||
|
.source = "32x32@2x@2x",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "128x128",
|
||||||
|
.source = "128x128",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "128x128@2",
|
||||||
|
.source = "128x128@2x@2x",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "256x256",
|
||||||
|
.source = "256x256",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "256x256@2",
|
||||||
|
.source = "256x256@2x@2x",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.alias = "512x512",
|
||||||
|
.source = "512x512",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const gresource_xml_gtk = comptimeGenerateGResourceXML(false);
|
||||||
|
pub const gresource_xml_libadwaita = comptimeGenerateGResourceXML(true);
|
||||||
|
|
||||||
|
fn comptimeGenerateGResourceXML(comptime libadwaita: bool) []const u8 {
|
||||||
|
comptime {
|
||||||
|
@setEvalBranchQuota(13000);
|
||||||
|
var counter = std.io.countingWriter(std.io.null_writer);
|
||||||
|
try writeGResourceXML(libadwaita, &counter.writer());
|
||||||
|
|
||||||
|
var buf: [counter.bytes_written]u8 = undefined;
|
||||||
|
var stream = std.io.fixedBufferStream(&buf);
|
||||||
|
try writeGResourceXML(libadwaita, stream.writer());
|
||||||
|
return stream.getWritten();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeGResourceXML(libadwaita: bool, writer: anytype) !void {
|
||||||
|
try writer.writeAll(
|
||||||
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
\\<gresources>
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
if (libadwaita) {
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ <gresource prefix="/com/mitchellh/ghostty">
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
for (css_files) |css_file| {
|
||||||
|
try writer.print(
|
||||||
|
" <file compressed=\"true\" alias=\"{s}\">src/apprt/gtk/{s}</file>\n",
|
||||||
|
.{ css_file, css_file },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ </gresource>
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
for (icons) |icon| {
|
||||||
|
try writer.print(
|
||||||
|
" <file alias=\"{s}/apps/com.mitchellh.ghostty.png\">images/icons/icon_{s}.png</file>\n",
|
||||||
|
.{ icon.alias, icon.source },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ </gresource>
|
||||||
|
\\</gresources>
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const dependencies_gtk = deps: {
|
||||||
|
var deps: [icons.len][]const u8 = undefined;
|
||||||
|
for (icons, 0..) |icon, i| {
|
||||||
|
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||||
|
}
|
||||||
|
break :deps deps;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const dependencies_libadwaita = deps: {
|
||||||
|
var deps: [css_files.len + icons.len][]const u8 = undefined;
|
||||||
|
for (css_files, 0..) |css_file, i| {
|
||||||
|
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
|
||||||
|
}
|
||||||
|
for (icons, css_files.len..) |icon, i| {
|
||||||
|
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||||
|
}
|
||||||
|
break :deps deps;
|
||||||
|
};
|
@ -1,63 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const App = @import("App.zig");
|
|
||||||
const c = @import("c.zig");
|
|
||||||
const global_state = &@import("../../main.zig").state;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_icon);
|
|
||||||
|
|
||||||
/// An icon. The icon may be associated with some allocated state so when
|
|
||||||
/// the icon is no longer in use it should be deinitialized.
|
|
||||||
pub const Icon = struct {
|
|
||||||
name: [:0]const u8,
|
|
||||||
state: ?[:0]const u8 = null,
|
|
||||||
|
|
||||||
pub fn deinit(self: *const Icon, app: *App) void {
|
|
||||||
if (self.state) |v| app.core_app.alloc.free(v);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the application icon that can be used anywhere. This attempts to
|
|
||||||
/// find the icon in the theme and if it can't be found, it is loaded from
|
|
||||||
/// the resources dir. If the resources dir can't be found, we'll log a warning
|
|
||||||
/// and let GTK choose a fallback.
|
|
||||||
pub fn appIcon(app: *App, widget: *c.GtkWidget) !Icon {
|
|
||||||
const icon_name = "com.mitchellh.ghostty";
|
|
||||||
var result: Icon = .{ .name = icon_name };
|
|
||||||
|
|
||||||
// If we don't have the icon then we'll try to add our resources dir
|
|
||||||
// to the search path and see if we can find it there.
|
|
||||||
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(widget));
|
|
||||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
|
||||||
const resources_dir = global_state.resources_dir orelse {
|
|
||||||
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
|
||||||
log.info("gtk app will not have Ghostty icon", .{});
|
|
||||||
break :icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The resources dir usually is `/usr/share/ghostty` but GTK icons
|
|
||||||
// go into `/usr/share/icons`.
|
|
||||||
const base = std.fs.path.dirname(resources_dir) orelse {
|
|
||||||
log.warn(
|
|
||||||
"unexpected error getting dirname of resources dir dir={s}",
|
|
||||||
.{resources_dir},
|
|
||||||
);
|
|
||||||
break :icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Note that this method for adding the icon search path is
|
|
||||||
// a fallback mechanism. The recommended mechanism is the
|
|
||||||
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
|
|
||||||
// file in zig-out/share that should be installed to the proper
|
|
||||||
// place.
|
|
||||||
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
|
|
||||||
errdefer app.core_app.alloc.free(dir);
|
|
||||||
result.state = dir;
|
|
||||||
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
|
|
||||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
|
|
||||||
log.warn("Ghostty icon for gtk app not found", .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
@ -7,7 +7,6 @@ const Surface = @import("Surface.zig");
|
|||||||
const TerminalWindow = @import("Window.zig");
|
const TerminalWindow = @import("Window.zig");
|
||||||
const ImguiWidget = @import("ImguiWidget.zig");
|
const ImguiWidget = @import("ImguiWidget.zig");
|
||||||
const c = @import("c.zig");
|
const c = @import("c.zig");
|
||||||
const icon = @import("icon.zig");
|
|
||||||
const CoreInspector = @import("../../inspector/main.zig").Inspector;
|
const CoreInspector = @import("../../inspector/main.zig").Inspector;
|
||||||
|
|
||||||
const log = std.log.scoped(.inspector);
|
const log = std.log.scoped(.inspector);
|
||||||
@ -125,14 +124,12 @@ pub const Inspector = struct {
|
|||||||
const Window = struct {
|
const Window = struct {
|
||||||
inspector: *Inspector,
|
inspector: *Inspector,
|
||||||
window: *c.GtkWindow,
|
window: *c.GtkWindow,
|
||||||
icon: icon.Icon,
|
|
||||||
imgui_widget: ImguiWidget,
|
imgui_widget: ImguiWidget,
|
||||||
|
|
||||||
pub fn init(self: *Window, inspector: *Inspector) !void {
|
pub fn init(self: *Window, inspector: *Inspector) !void {
|
||||||
// Initialize to undefined
|
// Initialize to undefined
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.inspector = inspector,
|
.inspector = inspector,
|
||||||
.icon = undefined,
|
|
||||||
.window = undefined,
|
.window = undefined,
|
||||||
.imgui_widget = undefined,
|
.imgui_widget = undefined,
|
||||||
};
|
};
|
||||||
@ -144,8 +141,7 @@ const Window = struct {
|
|||||||
self.window = gtk_window;
|
self.window = gtk_window;
|
||||||
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
|
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
|
||||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||||
self.icon = try icon.appIcon(self.inspector.surface.app, window);
|
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
|
||||||
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
|
|
||||||
|
|
||||||
// Initialize our imgui widget
|
// Initialize our imgui widget
|
||||||
try self.imgui_widget.init();
|
try self.imgui_widget.init();
|
||||||
@ -163,7 +159,6 @@ const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Window) void {
|
pub fn deinit(self: *Window) void {
|
||||||
self.icon.deinit(self.inspector.surface.app);
|
|
||||||
self.inspector.locationDidClose();
|
self.inspector.locationDidClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
src/apprt/gtk/style-dark.css
Normal file
0
src/apprt/gtk/style-dark.css
Normal file
0
src/apprt/gtk/style-hc-dark.css
Normal file
0
src/apprt/gtk/style-hc-dark.css
Normal file
0
src/apprt/gtk/style-hc.css
Normal file
0
src/apprt/gtk/style-hc.css
Normal file
0
src/apprt/gtk/style.css
Normal file
0
src/apprt/gtk/style.css
Normal file
@ -624,6 +624,11 @@ keybind: Keybinds = .{},
|
|||||||
/// borders.
|
/// borders.
|
||||||
@"window-decoration": bool = true,
|
@"window-decoration": bool = true,
|
||||||
|
|
||||||
|
/// The font that will be used for the application's window and tab titles.
|
||||||
|
///
|
||||||
|
/// This is currently only supported on macOS.
|
||||||
|
@"window-title-font-family": ?[:0]const u8 = null,
|
||||||
|
|
||||||
/// The theme to use for the windows. Valid values:
|
/// The theme to use for the windows. Valid values:
|
||||||
///
|
///
|
||||||
/// * `auto` - Determine the theme based on the configured terminal
|
/// * `auto` - Determine the theme based on the configured terminal
|
||||||
|
@ -2431,139 +2431,147 @@ fn draw_dash_horizontal(
|
|||||||
) void {
|
) void {
|
||||||
assert(count >= 2 and count <= 4);
|
assert(count >= 2 and count <= 4);
|
||||||
|
|
||||||
// The number of gaps we have is one less than the number of dashes.
|
// +------------+
|
||||||
// "- - -" => 2 gaps
|
// | |
|
||||||
const gap_count = count - 1;
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | -- -- -- |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// +------------+
|
||||||
|
// Our dashed line should be made such that when tiled horizontally
|
||||||
|
// it creates one consistent line with no uneven gap or segment sizes.
|
||||||
|
// In order to make sure this is the case, we should have half-sized
|
||||||
|
// gaps on the left and right so that it is centered properly.
|
||||||
|
|
||||||
// Determine the width of each dash and the gap between them. We try
|
// For N dashes, there are N - 1 gaps between them, but we also have
|
||||||
// to have gap match desired_gap but if our cell is too small then we
|
// half-sized gaps on either side, adding up to N total gaps.
|
||||||
// have to bring it down.
|
const gap_count = count;
|
||||||
const adjusted: struct {
|
|
||||||
dash_width: u32,
|
|
||||||
gap: u32,
|
|
||||||
} = adjusted: {
|
|
||||||
for (0..desired_gap) |i| {
|
|
||||||
const gap_width: u32 = desired_gap - @as(u32, @intCast(i));
|
|
||||||
const total_gap_width: u32 = gap_count * gap_width;
|
|
||||||
|
|
||||||
// This would make a negative and overflow our u32. A negative
|
// We need at least 1 pixel for each gap and each dash, if we don't
|
||||||
// dash width is not allowed so we keep trying to fit it.
|
// have that then we can't draw our dashed line correctly so we just
|
||||||
if (total_gap_width >= self.width) continue;
|
// draw a solid line and return.
|
||||||
|
if (self.width < count + gap_count) {
|
||||||
break :adjusted .{
|
|
||||||
.dash_width = (self.width - total_gap_width) / count,
|
|
||||||
.gap = gap_width,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// In this case, there is no combination of gap width and dash
|
|
||||||
// width that would fit our desired number of dashes, so we just
|
|
||||||
// draw a horizontal line.
|
|
||||||
self.hline_middle(canvas, .light);
|
self.hline_middle(canvas, .light);
|
||||||
return;
|
return;
|
||||||
};
|
|
||||||
const dash_width = adjusted.dash_width;
|
|
||||||
const gap = adjusted.gap;
|
|
||||||
|
|
||||||
// Our total width should be less than our real width
|
|
||||||
assert(count * dash_width + gap_count * gap <= self.width);
|
|
||||||
const remaining = self.width - count * dash_width - gap_count * gap;
|
|
||||||
|
|
||||||
var x: [4]u32 = .{0} ** 4;
|
|
||||||
var w: [4]u32 = .{dash_width} ** 4;
|
|
||||||
x[1] = x[0] + w[0] + gap;
|
|
||||||
if (count == 2)
|
|
||||||
w[1] = self.width - x[1]
|
|
||||||
else if (count == 3)
|
|
||||||
w[1] += remaining
|
|
||||||
else
|
|
||||||
w[1] += remaining / 2;
|
|
||||||
|
|
||||||
if (count >= 3) {
|
|
||||||
x[2] = x[1] + w[1] + gap;
|
|
||||||
if (count == 3)
|
|
||||||
w[2] = self.width - x[2]
|
|
||||||
else
|
|
||||||
w[2] += remaining - remaining / 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count >= 4) {
|
// We never want the gaps to take up more than 50% of the space,
|
||||||
x[3] = x[2] + w[2] + gap;
|
// because if they do the dashes are too small and look wrong.
|
||||||
w[3] = self.width - x[3];
|
const gap_width = @min(desired_gap, self.width / (2 * count));
|
||||||
}
|
const total_gap_width = gap_count * gap_width;
|
||||||
|
const total_dash_width = self.width - total_gap_width;
|
||||||
|
const dash_width = total_dash_width / count;
|
||||||
|
const remaining = total_dash_width % count;
|
||||||
|
|
||||||
self.hline(canvas, x[0], x[0] + w[0], (self.height -| thick_px) / 2, thick_px);
|
assert(dash_width * count + gap_width * gap_count + remaining == self.width);
|
||||||
self.hline(canvas, x[1], x[1] + w[1], (self.height -| thick_px) / 2, thick_px);
|
|
||||||
if (count >= 3)
|
// Our dashes should be centered vertically.
|
||||||
self.hline(canvas, x[2], x[2] + w[2], (self.height -| thick_px) / 2, thick_px);
|
const y: u32 = (self.height -| thick_px) / 2;
|
||||||
if (count >= 4)
|
|
||||||
self.hline(canvas, x[3], x[3] + w[3], (self.height -| thick_px) / 2, thick_px);
|
// We start at half a gap from the left edge, in order to center
|
||||||
|
// our dashes properly.
|
||||||
|
var x: u32 = gap_width / 2;
|
||||||
|
|
||||||
|
// We'll distribute the extra space in to dash widths, 1px at a
|
||||||
|
// time. We prefer this to making gaps larger since that is much
|
||||||
|
// more visually obvious.
|
||||||
|
var extra: u32 = remaining;
|
||||||
|
|
||||||
|
for (0..count) |_| {
|
||||||
|
var x1 = x + dash_width;
|
||||||
|
// We distribute left-over size in to dash widths,
|
||||||
|
// since it's less obvious there than in the gaps.
|
||||||
|
if (extra > 0) {
|
||||||
|
extra -= 1;
|
||||||
|
x1 += 1;
|
||||||
|
}
|
||||||
|
self.hline(canvas, x, x1, y, thick_px);
|
||||||
|
// Advance by the width of the dash we drew and the width
|
||||||
|
// of a gap to get the the start of the next dash.
|
||||||
|
x = x1 + gap_width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_dash_vertical(
|
fn draw_dash_vertical(
|
||||||
self: Box,
|
self: Box,
|
||||||
canvas: *font.sprite.Canvas,
|
canvas: *font.sprite.Canvas,
|
||||||
count: u8,
|
comptime count: u8,
|
||||||
thick_px: u32,
|
thick_px: u32,
|
||||||
gap: u32,
|
desired_gap: u32,
|
||||||
) void {
|
) void {
|
||||||
assert(count >= 2 and count <= 4);
|
assert(count >= 2 and count <= 4);
|
||||||
|
|
||||||
// The number of gaps we have is one less than the number of dashes.
|
// +-----------+
|
||||||
// "- - -" => 2 gaps
|
// | | |
|
||||||
const gap_count = count - 1;
|
// | | |
|
||||||
|
// | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | |
|
||||||
|
// +-----------+
|
||||||
|
// Our dashed line should be made such that when tiled verically it
|
||||||
|
// it creates one consistent line with no uneven gap or segment sizes.
|
||||||
|
// In order to make sure this is the case, we should have an extra gap
|
||||||
|
// gap at the bottom.
|
||||||
|
//
|
||||||
|
// A single full-sized extra gap is preferred to two half-sized ones for
|
||||||
|
// vertical to allow better joining to solid characters without creating
|
||||||
|
// visible half-sized gaps. Unlike horizontal, centering is a lot less
|
||||||
|
// important, visually.
|
||||||
|
|
||||||
// Determine the height of our dashes
|
// Because of the extra gap at the bottom, there are as many gaps as
|
||||||
const dash_height = dash_height: {
|
// there are dashes.
|
||||||
var gap_i = gap;
|
const gap_count = count;
|
||||||
var dash_height = (self.height - (gap_count * gap_i)) / count;
|
|
||||||
while (dash_height <= 0 and gap_i > 1) {
|
|
||||||
gap_i -= 1;
|
|
||||||
dash_height = (self.height - (gap_count * gap_i)) / count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't fit any dashes then we just render a horizontal line.
|
// We need at least 1 pixel for each gap and each dash, if we don't
|
||||||
if (dash_height <= 0) {
|
// have that then we can't draw our dashed line correctly so we just
|
||||||
|
// draw a solid line and return.
|
||||||
|
if (self.height < count + gap_count) {
|
||||||
self.vline_middle(canvas, .light);
|
self.vline_middle(canvas, .light);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
break :dash_height dash_height;
|
// We never want the gaps to take up more than 50% of the space,
|
||||||
};
|
// because if they do the dashes are too small and look wrong.
|
||||||
|
const gap_height = @min(desired_gap, self.height / (2 * count));
|
||||||
|
const total_gap_height = gap_count * gap_height;
|
||||||
|
const total_dash_height = self.height - total_gap_height;
|
||||||
|
const dash_height = total_dash_height / count;
|
||||||
|
const remaining = total_dash_height % count;
|
||||||
|
|
||||||
// Our total height should be less than our real height
|
assert(dash_height * count + gap_height * gap_count + remaining == self.height);
|
||||||
assert(count * dash_height + gap_count * gap <= self.height);
|
|
||||||
const remaining = self.height - count * dash_height - gap_count * gap;
|
|
||||||
|
|
||||||
var y: [4]u32 = .{0} ** 4;
|
// Our dashes should be centered horizontally.
|
||||||
var h: [4]u32 = .{dash_height} ** 4;
|
const x: u32 = (self.width -| thick_px) / 2;
|
||||||
y[1] = y[0] + h[0] + gap;
|
|
||||||
if (count == 2)
|
|
||||||
h[1] = self.height - y[1]
|
|
||||||
else if (count == 3)
|
|
||||||
h[1] += remaining
|
|
||||||
else
|
|
||||||
h[1] += remaining / 2;
|
|
||||||
|
|
||||||
if (count >= 3) {
|
// We start at the top of the cell.
|
||||||
y[2] = y[1] + h[1] + gap;
|
var y: u32 = 0;
|
||||||
if (count == 3)
|
|
||||||
h[2] = self.height - y[2]
|
// We'll distribute the extra space in to dash heights, 1px at a
|
||||||
else
|
// time. We prefer this to making gaps larger since that is much
|
||||||
h[2] += remaining - remaining / 2;
|
// more visually obvious.
|
||||||
|
var extra: u32 = remaining;
|
||||||
|
|
||||||
|
inline for (0..count) |_| {
|
||||||
|
var y1 = y + dash_height;
|
||||||
|
// We distribute left-over size in to dash widths,
|
||||||
|
// since it's less obvious there than in the gaps.
|
||||||
|
if (extra > 0) {
|
||||||
|
extra -= 1;
|
||||||
|
y1 += 1;
|
||||||
}
|
}
|
||||||
|
self.vline(canvas, y, y1, x, thick_px);
|
||||||
if (count >= 4) {
|
// Advance by the height of the dash we drew and the height
|
||||||
y[3] = y[2] + h[2] + gap;
|
// of a gap to get the the start of the next dash.
|
||||||
h[3] = self.height - y[3];
|
y = y1 + gap_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.vline(canvas, y[0], y[0] + h[0], (self.width -| thick_px) / 2, thick_px);
|
|
||||||
self.vline(canvas, y[1], y[1] + h[1], (self.width -| thick_px) / 2, thick_px);
|
|
||||||
if (count >= 3)
|
|
||||||
self.vline(canvas, y[2], y[2] + h[2], (self.width -| thick_px) / 2, thick_px);
|
|
||||||
if (count >= 4)
|
|
||||||
self.vline(canvas, y[3], y[3] + h[3], (self.width -| thick_px) / 2, thick_px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
|
@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const passwd = @import("passwd.zig");
|
const passwd = @import("passwd.zig");
|
||||||
const posix = std.posix;
|
const posix = std.posix;
|
||||||
|
const objc = @import("objc");
|
||||||
|
|
||||||
const Error = error{
|
const Error = error{
|
||||||
/// The buffer used for output is not large enough to store the value.
|
/// The buffer used for output is not large enough to store the value.
|
||||||
@ -31,33 +32,26 @@ fn homeUnix(buf: []u8) !?[]u8 {
|
|||||||
return buf[0..result.len];
|
return buf[0..result.len];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything below here will require some allocation
|
// On macOS: [NSFileManager defaultManager].homeDirectoryForCurrentUser.path
|
||||||
var tempBuf: [1024]u8 = undefined;
|
|
||||||
var fba = std.heap.FixedBufferAllocator.init(&tempBuf);
|
|
||||||
|
|
||||||
// If we're on darwin, we try the directory service. I'm not sure if there
|
|
||||||
// is a Mac API to do this but if so we can link to that...
|
|
||||||
if (builtin.os.tag == .macos) {
|
if (builtin.os.tag == .macos) {
|
||||||
const run = try std.ChildProcess.run(.{
|
const NSFileManager = objc.getClass("NSFileManager").?;
|
||||||
.allocator = fba.allocator(),
|
const manager = NSFileManager.msgSend(objc.Object, objc.sel("defaultManager"), .{});
|
||||||
.argv = &[_][]const u8{
|
const homeURL = manager.getProperty(objc.Object, "homeDirectoryForCurrentUser");
|
||||||
"/bin/sh",
|
const homePath = homeURL.getProperty(objc.Object, "path");
|
||||||
"-c",
|
|
||||||
"dscl -q . -read /Users/\"$(whoami)\" NFSHomeDirectory | sed 's/^[^ ]*: //'",
|
const c_str = homePath.getProperty([*:0]const u8, "UTF8String");
|
||||||
},
|
const result = std.mem.sliceTo(c_str, 0);
|
||||||
.max_output_bytes = fba.buffer.len / 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (run.term == .Exited and run.term.Exited == 0) {
|
|
||||||
const result = trimSpace(run.stdout);
|
|
||||||
if (buf.len < result.len) return Error.BufferTooSmall;
|
if (buf.len < result.len) return Error.BufferTooSmall;
|
||||||
@memcpy(buf[0..result.len], result);
|
@memcpy(buf[0..result.len], result);
|
||||||
return buf[0..result.len];
|
return buf[0..result.len];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Everything below here will require some allocation
|
||||||
|
var tempBuf: [1024]u8 = undefined;
|
||||||
|
var fba = std.heap.FixedBufferAllocator.init(&tempBuf);
|
||||||
|
|
||||||
// We try passwd. This doesn't work on multi-user mac but we try it anyways.
|
// We try passwd. This doesn't work on multi-user mac but we try it anyways.
|
||||||
fba.reset();
|
|
||||||
const pw = try passwd.get(fba.allocator());
|
const pw = try passwd.get(fba.allocator());
|
||||||
if (pw.home) |result| {
|
if (pw.home) |result| {
|
||||||
if (buf.len < result.len) return Error.BufferTooSmall;
|
if (buf.len < result.len) return Error.BufferTooSmall;
|
||||||
|
@ -540,6 +540,9 @@ pub fn cursorDownScroll(self: *Screen) !void {
|
|||||||
assert(self.cursor.y == self.pages.rows - 1);
|
assert(self.cursor.y == self.pages.rows - 1);
|
||||||
defer self.assertIntegrity();
|
defer self.assertIntegrity();
|
||||||
|
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
self.kitty_images.dirty = true;
|
||||||
|
|
||||||
// If we have no scrollback, then we shift all our rows instead.
|
// If we have no scrollback, then we shift all our rows instead.
|
||||||
if (self.no_scrollback) {
|
if (self.no_scrollback) {
|
||||||
// If we have a single-row screen, we have no rows to shift
|
// If we have a single-row screen, we have no rows to shift
|
||||||
@ -891,10 +894,20 @@ pub fn clearUnprotectedCells(
|
|||||||
row: *Row,
|
row: *Row,
|
||||||
cells: []Cell,
|
cells: []Cell,
|
||||||
) void {
|
) void {
|
||||||
for (cells) |*cell| {
|
var x0: usize = 0;
|
||||||
if (cell.protected) continue;
|
var x1: usize = 0;
|
||||||
const cell_multi: [*]Cell = @ptrCast(cell);
|
|
||||||
self.clearCells(page, row, cell_multi[0..1]);
|
while (x0 < cells.len) clear: {
|
||||||
|
while (cells[x0].protected) {
|
||||||
|
x0 += 1;
|
||||||
|
if (x0 >= cells.len) break :clear;
|
||||||
|
}
|
||||||
|
x1 = x0 + 1;
|
||||||
|
while (x1 < cells.len and !cells[x1].protected) {
|
||||||
|
x1 += 1;
|
||||||
|
}
|
||||||
|
self.clearCells(page, row, cells[x0..x1]);
|
||||||
|
x0 = x1;
|
||||||
}
|
}
|
||||||
|
|
||||||
page.assertIntegrity();
|
page.assertIntegrity();
|
||||||
@ -2137,6 +2150,25 @@ pub fn dumpStringAlloc(
|
|||||||
return try builder.toOwnedSlice();
|
return try builder.toOwnedSlice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// You should use dumpString, this is a restricted version mostly for
|
||||||
|
/// legacy and convenience reasons for unit tests.
|
||||||
|
pub fn dumpStringAllocUnwrapped(
|
||||||
|
self: *const Screen,
|
||||||
|
alloc: Allocator,
|
||||||
|
tl: point.Point,
|
||||||
|
) ![]const u8 {
|
||||||
|
var builder = std.ArrayList(u8).init(alloc);
|
||||||
|
defer builder.deinit();
|
||||||
|
|
||||||
|
try self.dumpString(builder.writer(), .{
|
||||||
|
.tl = self.pages.getTopLeft(tl),
|
||||||
|
.br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint,
|
||||||
|
.unwrap = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return try builder.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
/// This is basically a really jank version of Terminal.printString. We
|
/// This is basically a really jank version of Terminal.printString. We
|
||||||
/// have to reimplement it here because we want a way to print to the screen
|
/// have to reimplement it here because we want a way to print to the screen
|
||||||
/// to test it but don't want all the features of Terminal.
|
/// to test it but don't want all the features of Terminal.
|
||||||
@ -2190,6 +2222,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
.content_tag = .codepoint,
|
.content_tag = .codepoint,
|
||||||
.content = .{ .codepoint = c },
|
.content = .{ .codepoint = c },
|
||||||
.style_id = self.cursor.style_id,
|
.style_id = self.cursor.style_id,
|
||||||
|
.protected = self.cursor.protected,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have a ref-counted style, increase.
|
// If we have a ref-counted style, increase.
|
||||||
@ -2206,6 +2239,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
.content_tag = .codepoint,
|
.content_tag = .codepoint,
|
||||||
.content = .{ .codepoint = 0 },
|
.content = .{ .codepoint = 0 },
|
||||||
.wide = .spacer_head,
|
.wide = .spacer_head,
|
||||||
|
.protected = self.cursor.protected,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.cursor.page_row.wrap = true;
|
self.cursor.page_row.wrap = true;
|
||||||
@ -2220,6 +2254,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
.content = .{ .codepoint = c },
|
.content = .{ .codepoint = c },
|
||||||
.style_id = self.cursor.style_id,
|
.style_id = self.cursor.style_id,
|
||||||
.wide = .wide,
|
.wide = .wide,
|
||||||
|
.protected = self.cursor.protected,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write our tail
|
// Write our tail
|
||||||
@ -2228,6 +2263,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
.content_tag = .codepoint,
|
.content_tag = .codepoint,
|
||||||
.content = .{ .codepoint = 0 },
|
.content = .{ .codepoint = 0 },
|
||||||
.wide = .spacer_tail,
|
.wide = .spacer_tail,
|
||||||
|
.protected = self.cursor.protected,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2508,6 +2544,34 @@ test "Screen clearRows active styled line" {
|
|||||||
try testing.expectEqualStrings("", str);
|
try testing.expectEqualStrings("", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen clearRows protected" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try Screen.init(alloc, 80, 24, 1000);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
try s.testWriteString("UNPROTECTED");
|
||||||
|
s.cursor.protected = true;
|
||||||
|
try s.testWriteString("PROTECTED");
|
||||||
|
s.cursor.protected = false;
|
||||||
|
try s.testWriteString("UNPROTECTED");
|
||||||
|
try s.testWriteString("\n");
|
||||||
|
s.cursor.protected = true;
|
||||||
|
try s.testWriteString("PROTECTED");
|
||||||
|
s.cursor.protected = false;
|
||||||
|
try s.testWriteString("UNPROTECTED");
|
||||||
|
s.cursor.protected = true;
|
||||||
|
try s.testWriteString("PROTECTED");
|
||||||
|
s.cursor.protected = false;
|
||||||
|
|
||||||
|
s.clearRows(.{ .active = .{} }, null, true);
|
||||||
|
|
||||||
|
const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
|
||||||
|
defer alloc.free(str);
|
||||||
|
try testing.expectEqualStrings(" PROTECTED\nPROTECTED PROTECTED", str);
|
||||||
|
}
|
||||||
|
|
||||||
test "Screen eraseRows history" {
|
test "Screen eraseRows history" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -1075,6 +1075,9 @@ pub fn index(self: *Terminal) !void {
|
|||||||
self.screen.cursor.x >= self.scrolling_region.left and
|
self.screen.cursor.x >= self.scrolling_region.left and
|
||||||
self.screen.cursor.x <= self.scrolling_region.right)
|
self.screen.cursor.x <= self.scrolling_region.right)
|
||||||
{
|
{
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
self.screen.kitty_images.dirty = true;
|
||||||
|
|
||||||
// If our scrolling region is the full screen, we create scrollback.
|
// If our scrolling region is the full screen, we create scrollback.
|
||||||
// Otherwise, we simply scroll the region.
|
// Otherwise, we simply scroll the region.
|
||||||
if (self.scrolling_region.top == 0 and
|
if (self.scrolling_region.top == 0 and
|
||||||
@ -1306,6 +1309,63 @@ pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// To be called before shifting a row (as in insertLines and deleteLines)
|
||||||
|
///
|
||||||
|
/// Takes care of boundary conditions such as potentially split wide chars
|
||||||
|
/// across scrolling region boundaries and orphaned spacer heads at line
|
||||||
|
/// ends.
|
||||||
|
fn rowWillBeShifted(
|
||||||
|
self: *Terminal,
|
||||||
|
page: *Page,
|
||||||
|
row: *Row,
|
||||||
|
) void {
|
||||||
|
const cells = row.cells.ptr(page.memory.ptr);
|
||||||
|
|
||||||
|
// If our scrolling region includes the rightmost column then we
|
||||||
|
// need to turn any spacer heads in to normal empty cells, since
|
||||||
|
// once we move them they no longer correspond with soft-wrapped
|
||||||
|
// wide characters.
|
||||||
|
//
|
||||||
|
// If it contains either of the 2 leftmost columns, then the wide
|
||||||
|
// characters in the first column which may be associated with a
|
||||||
|
// spacer head will be either moved or cleared, so we also need
|
||||||
|
// to turn the spacer heads in to empty cells in that case.
|
||||||
|
if (self.scrolling_region.right == self.cols - 1 or
|
||||||
|
self.scrolling_region.left < 2)
|
||||||
|
{
|
||||||
|
const end_cell: *Cell = &cells[page.size.cols - 1];
|
||||||
|
if (end_cell.wide == .spacer_head) {
|
||||||
|
end_cell.wide = .narrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the leftmost or rightmost cells of our scrolling region
|
||||||
|
// are parts of wide chars, we need to clear the cells' contents
|
||||||
|
// since they'd be split by the move.
|
||||||
|
const left_cell: *Cell = &cells[self.scrolling_region.left];
|
||||||
|
const right_cell: *Cell = &cells[self.scrolling_region.right];
|
||||||
|
|
||||||
|
if (left_cell.wide == .spacer_tail) {
|
||||||
|
const wide_cell: *Cell = &cells[self.scrolling_region.left - 1];
|
||||||
|
if (wide_cell.hasGrapheme()) {
|
||||||
|
page.clearGrapheme(row, wide_cell);
|
||||||
|
}
|
||||||
|
wide_cell.content.codepoint = 0;
|
||||||
|
wide_cell.wide = .narrow;
|
||||||
|
left_cell.wide = .narrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right_cell.wide == .wide) {
|
||||||
|
const tail_cell: *Cell = &cells[self.scrolling_region.right + 1];
|
||||||
|
if (right_cell.hasGrapheme()) {
|
||||||
|
page.clearGrapheme(row, right_cell);
|
||||||
|
}
|
||||||
|
right_cell.content.codepoint = 0;
|
||||||
|
right_cell.wide = .narrow;
|
||||||
|
tail_cell.wide = .narrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert amount lines at the current cursor row. The contents of the line
|
/// Insert amount lines at the current cursor row. The contents of the line
|
||||||
/// at the current cursor row and below (to the bottom-most line in the
|
/// at the current cursor row and below (to the bottom-most line in the
|
||||||
/// scrolling region) are shifted down by amount lines. The contents of the
|
/// scrolling region) are shifted down by amount lines. The contents of the
|
||||||
@ -1334,6 +1394,9 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
|||||||
self.screen.cursor.x < self.scrolling_region.left or
|
self.screen.cursor.x < self.scrolling_region.left or
|
||||||
self.screen.cursor.x > self.scrolling_region.right) return;
|
self.screen.cursor.x > self.scrolling_region.right) return;
|
||||||
|
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
self.screen.kitty_images.dirty = true;
|
||||||
|
|
||||||
// Remaining rows from our cursor to the bottom of the scroll region.
|
// Remaining rows from our cursor to the bottom of the scroll region.
|
||||||
const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1;
|
const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1;
|
||||||
|
|
||||||
@ -1359,8 +1422,21 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
|||||||
var it = bot.rowIterator(.left_up, top);
|
var it = bot.rowIterator(.left_up, top);
|
||||||
while (it.next()) |p| {
|
while (it.next()) |p| {
|
||||||
const dst_p = p.down(adjusted_count).?;
|
const dst_p = p.down(adjusted_count).?;
|
||||||
const src: *Row = p.rowAndCell().row;
|
const src_rac = p.rowAndCell();
|
||||||
const dst: *Row = dst_p.rowAndCell().row;
|
const dst_rac = dst_p.rowAndCell();
|
||||||
|
const src: *Row = src_rac.row;
|
||||||
|
const dst: *Row = dst_rac.row;
|
||||||
|
|
||||||
|
self.rowWillBeShifted(&p.page.data, src);
|
||||||
|
self.rowWillBeShifted(&dst_p.page.data, dst);
|
||||||
|
|
||||||
|
// If our scrolling region is full width, then we unset wrap.
|
||||||
|
if (!left_right) {
|
||||||
|
dst.wrap = false;
|
||||||
|
src.wrap = false;
|
||||||
|
dst.wrap_continuation = false;
|
||||||
|
src.wrap_continuation = false;
|
||||||
|
}
|
||||||
|
|
||||||
// If our page doesn't match, then we need to do a copy from
|
// If our page doesn't match, then we need to do a copy from
|
||||||
// one page to another. This is the slow path.
|
// one page to another. This is the slow path.
|
||||||
@ -1376,9 +1452,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
|||||||
@panic("TODO");
|
@panic("TODO");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row never is wrapped if we're full width.
|
|
||||||
if (!left_right) dst.wrap = false;
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1389,10 +1462,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
|||||||
dst.* = src.*;
|
dst.* = src.*;
|
||||||
src.* = dst_row;
|
src.* = dst_row;
|
||||||
|
|
||||||
// Row never is wrapped
|
|
||||||
dst.wrap = false;
|
|
||||||
src.wrap = false;
|
|
||||||
|
|
||||||
// Ensure what we did didn't corrupt the page
|
// Ensure what we did didn't corrupt the page
|
||||||
p.page.data.assertIntegrity();
|
p.page.data.assertIntegrity();
|
||||||
continue;
|
continue;
|
||||||
@ -1407,9 +1476,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
|||||||
self.scrolling_region.left,
|
self.scrolling_region.left,
|
||||||
(self.scrolling_region.right - self.scrolling_region.left) + 1,
|
(self.scrolling_region.right - self.scrolling_region.left) + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Row never is wrapped
|
|
||||||
dst.wrap = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The operations above can prune our cursor style so we need to
|
// The operations above can prune our cursor style so we need to
|
||||||
@ -1474,6 +1540,9 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void {
|
|||||||
self.screen.cursor.x < self.scrolling_region.left or
|
self.screen.cursor.x < self.scrolling_region.left or
|
||||||
self.screen.cursor.x > self.scrolling_region.right) return;
|
self.screen.cursor.x > self.scrolling_region.right) return;
|
||||||
|
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
self.screen.kitty_images.dirty = true;
|
||||||
|
|
||||||
// top is just the cursor position. insertLines starts at the cursor
|
// top is just the cursor position. insertLines starts at the cursor
|
||||||
// so this is our top. We want to shift lines down, down to the bottom
|
// so this is our top. We want to shift lines down, down to the bottom
|
||||||
// of the scroll region.
|
// of the scroll region.
|
||||||
@ -1498,8 +1567,21 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void {
|
|||||||
var it = top.rowIterator(.right_down, bot);
|
var it = top.rowIterator(.right_down, bot);
|
||||||
while (it.next()) |p| {
|
while (it.next()) |p| {
|
||||||
const src_p = p.down(count).?;
|
const src_p = p.down(count).?;
|
||||||
const src: *Row = src_p.rowAndCell().row;
|
const src_rac = src_p.rowAndCell();
|
||||||
const dst: *Row = p.rowAndCell().row;
|
const dst_rac = p.rowAndCell();
|
||||||
|
const src: *Row = src_rac.row;
|
||||||
|
const dst: *Row = dst_rac.row;
|
||||||
|
|
||||||
|
self.rowWillBeShifted(&src_p.page.data, src);
|
||||||
|
self.rowWillBeShifted(&p.page.data, dst);
|
||||||
|
|
||||||
|
// If our scrolling region is full width, then we unset wrap.
|
||||||
|
if (!left_right) {
|
||||||
|
dst.wrap = false;
|
||||||
|
src.wrap = false;
|
||||||
|
dst.wrap_continuation = false;
|
||||||
|
src.wrap_continuation = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (src_p.page != p.page) {
|
if (src_p.page != p.page) {
|
||||||
p.page.data.clonePartialRowFrom(
|
p.page.data.clonePartialRowFrom(
|
||||||
@ -1513,9 +1595,6 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void {
|
|||||||
@panic("TODO");
|
@panic("TODO");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row never is wrapped if we're full width.
|
|
||||||
if (!left_right) dst.wrap = false;
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1526,9 +1605,6 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void {
|
|||||||
dst.* = src.*;
|
dst.* = src.*;
|
||||||
src.* = dst_row;
|
src.* = dst_row;
|
||||||
|
|
||||||
// Row never is wrapped
|
|
||||||
dst.wrap = false;
|
|
||||||
|
|
||||||
// Ensure what we did didn't corrupt the page
|
// Ensure what we did didn't corrupt the page
|
||||||
p.page.data.assertIntegrity();
|
p.page.data.assertIntegrity();
|
||||||
continue;
|
continue;
|
||||||
@ -1543,9 +1619,6 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void {
|
|||||||
self.scrolling_region.left,
|
self.scrolling_region.left,
|
||||||
(self.scrolling_region.right - self.scrolling_region.left) + 1,
|
(self.scrolling_region.right - self.scrolling_region.left) + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Row never is wrapped
|
|
||||||
dst.wrap = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The operations above can prune our cursor style so we need to
|
// The operations above can prune our cursor style so we need to
|
||||||
@ -1793,20 +1866,12 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SLOW PATH
|
self.screen.clearUnprotectedCells(
|
||||||
// We had a protection mode at some point. We must go through each
|
|
||||||
// cell and check its protection attribute.
|
|
||||||
for (0..end) |x| {
|
|
||||||
const cell_multi: [*]Cell = @ptrCast(cells + x);
|
|
||||||
const cell: *Cell = @ptrCast(&cell_multi[0]);
|
|
||||||
if (cell.protected) continue;
|
|
||||||
self.screen.clearCells(
|
|
||||||
&self.screen.cursor.page_pin.page.data,
|
&self.screen.cursor.page_pin.page.data,
|
||||||
self.screen.cursor.page_row,
|
self.screen.cursor.page_row,
|
||||||
cell_multi[0..1],
|
cells[0..end],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Erase the line.
|
/// Erase the line.
|
||||||
pub fn eraseLine(
|
pub fn eraseLine(
|
||||||
@ -1878,17 +1943,12 @@ pub fn eraseLine(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (start..end) |x| {
|
self.screen.clearUnprotectedCells(
|
||||||
const cell_multi: [*]Cell = @ptrCast(cells + x);
|
|
||||||
const cell: *Cell = @ptrCast(&cell_multi[0]);
|
|
||||||
if (cell.protected) continue;
|
|
||||||
self.screen.clearCells(
|
|
||||||
&self.screen.cursor.page_pin.page.data,
|
&self.screen.cursor.page_pin.page.data,
|
||||||
self.screen.cursor.page_row,
|
self.screen.cursor.page_row,
|
||||||
cell_multi[0..1],
|
cells[start..end],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Erase the display.
|
/// Erase the display.
|
||||||
pub fn eraseDisplay(
|
pub fn eraseDisplay(
|
||||||
@ -2385,6 +2445,11 @@ pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 {
|
|||||||
return try self.screen.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
return try self.screen.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same as plainString, but respects row wrap state when building the string.
|
||||||
|
pub fn plainStringUnwrapped(self: *Terminal, alloc: Allocator) ![]const u8 {
|
||||||
|
return try self.screen.dumpStringAllocUnwrapped(alloc, .{ .viewport = .{} });
|
||||||
|
}
|
||||||
|
|
||||||
/// Full reset
|
/// Full reset
|
||||||
pub fn fullReset(self: *Terminal) void {
|
pub fn fullReset(self: *Terminal) void {
|
||||||
// Switch back to primary screen and clear it. We do not restore cursor
|
// Switch back to primary screen and clear it. We do not restore cursor
|
||||||
@ -6119,6 +6184,243 @@ test "Terminal: deleteLines left/right scroll region high count" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Terminal: deleteLines wide character spacer head" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
// +-----+
|
||||||
|
// |AAAAA| < Wrapped
|
||||||
|
// |BBBB*| < Wrapped (continued)
|
||||||
|
// |WWCCC| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// where * represents a spacer head cell
|
||||||
|
// and WW is the wide character.
|
||||||
|
try t.printString("AAAAABBBB\u{1F600}CCC");
|
||||||
|
|
||||||
|
// Delete the top line
|
||||||
|
// +-----+
|
||||||
|
// |BBBB | < Non-wrapped
|
||||||
|
// |WWCCC| < Non-wrapped
|
||||||
|
// | | < Non-wrapped
|
||||||
|
// +-----+
|
||||||
|
// This should convert the spacer head to
|
||||||
|
// a regular empty cell, and un-set wrap.
|
||||||
|
t.setCursorPos(1, 1);
|
||||||
|
t.deleteLines(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||||
|
defer testing.allocator.free(unwrapped_str);
|
||||||
|
try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", str);
|
||||||
|
try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", unwrapped_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: deleteLines wide character spacer head left scroll margin" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
// +-----+
|
||||||
|
// |AAAAA| < Wrapped
|
||||||
|
// |BBBB*| < Wrapped (continued)
|
||||||
|
// |WWCCC| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// where * represents a spacer head cell
|
||||||
|
// and WW is the wide character.
|
||||||
|
try t.printString("AAAAABBBB\u{1F600}CCC");
|
||||||
|
|
||||||
|
t.scrolling_region.left = 2;
|
||||||
|
|
||||||
|
// Delete the top line
|
||||||
|
// ### <- scrolling region
|
||||||
|
// +-----+
|
||||||
|
// |AABB | < Wrapped
|
||||||
|
// |BBCCC| < Wrapped (continued)
|
||||||
|
// |WW | < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// This should convert the spacer head to
|
||||||
|
// a regular empty cell, but due to the
|
||||||
|
// left scrolling margin, wrap state should
|
||||||
|
// remain.
|
||||||
|
t.setCursorPos(1, 3);
|
||||||
|
t.deleteLines(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||||
|
defer testing.allocator.free(unwrapped_str);
|
||||||
|
try testing.expectEqualStrings("AABB \nBBCCC\n\u{1F600}", str);
|
||||||
|
try testing.expectEqualStrings("AABB BBCCC\u{1F600}", unwrapped_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: deleteLines wide character spacer head right scroll margin" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
// +-----+
|
||||||
|
// |AAAAA| < Wrapped
|
||||||
|
// |BBBB*| < Wrapped (continued)
|
||||||
|
// |WWCCC| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// where * represents a spacer head cell
|
||||||
|
// and WW is the wide character.
|
||||||
|
try t.printString("AAAAABBBB\u{1F600}CCC");
|
||||||
|
|
||||||
|
t.scrolling_region.right = 3;
|
||||||
|
|
||||||
|
// Delete the top line
|
||||||
|
// #### <- scrolling region
|
||||||
|
// +-----+
|
||||||
|
// |BBBBA| < Wrapped
|
||||||
|
// |WWCC | < Wrapped (continued)
|
||||||
|
// | C| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// This should convert the spacer head to
|
||||||
|
// a regular empty cell, but due to the
|
||||||
|
// right scrolling margin, wrap state should
|
||||||
|
// remain.
|
||||||
|
t.setCursorPos(1, 1);
|
||||||
|
t.deleteLines(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||||
|
defer testing.allocator.free(unwrapped_str);
|
||||||
|
try testing.expectEqualStrings("BBBBA\n\u{1F600}CC \n C", str);
|
||||||
|
try testing.expectEqualStrings("BBBBA\u{1F600}CC C", unwrapped_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: deleteLines wide character spacer head left and right scroll margin" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
// +-----+
|
||||||
|
// |AAAAA| < Wrapped
|
||||||
|
// |BBBB*| < Wrapped (continued)
|
||||||
|
// |WWCCC| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// where * represents a spacer head cell
|
||||||
|
// and WW is the wide character.
|
||||||
|
try t.printString("AAAAABBBB\u{1F600}CCC");
|
||||||
|
|
||||||
|
t.scrolling_region.right = 3;
|
||||||
|
t.scrolling_region.left = 2;
|
||||||
|
|
||||||
|
// Delete the top line
|
||||||
|
// ## <- scrolling region
|
||||||
|
// +-----+
|
||||||
|
// |AABBA| < Wrapped
|
||||||
|
// |BBCC*| < Wrapped (continued)
|
||||||
|
// |WW C| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// Because there is both a left scrolling
|
||||||
|
// margin > 1 and a right scrolling margin
|
||||||
|
// the spacer head should remain, and the
|
||||||
|
// wrap state should be untouched.
|
||||||
|
t.setCursorPos(1, 3);
|
||||||
|
t.deleteLines(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||||
|
defer testing.allocator.free(unwrapped_str);
|
||||||
|
try testing.expectEqualStrings("AABBA\nBBCC\n\u{1F600} C", str);
|
||||||
|
try testing.expectEqualStrings("AABBABBCC\u{1F600} C", unwrapped_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: deleteLines wide character spacer head left (< 2) and right scroll margin" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
// +-----+
|
||||||
|
// |AAAAA| < Wrapped
|
||||||
|
// |BBBB*| < Wrapped (continued)
|
||||||
|
// |WWCCC| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// where * represents a spacer head cell
|
||||||
|
// and WW is the wide character.
|
||||||
|
try t.printString("AAAAABBBB\u{1F600}CCC");
|
||||||
|
|
||||||
|
t.scrolling_region.right = 3;
|
||||||
|
t.scrolling_region.left = 1;
|
||||||
|
|
||||||
|
// Delete the top line
|
||||||
|
// ### <- scrolling region
|
||||||
|
// +-----+
|
||||||
|
// |ABBBA| < Wrapped
|
||||||
|
// |B CC | < Wrapped (continued)
|
||||||
|
// | C| < Non-wrapped (continued)
|
||||||
|
// +-----+
|
||||||
|
// Because the left margin is 1, the wide
|
||||||
|
// char is split, and therefore removed,
|
||||||
|
// along with the spacer head - however,
|
||||||
|
// wrap state should be untouched.
|
||||||
|
t.setCursorPos(1, 2);
|
||||||
|
t.deleteLines(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||||
|
defer testing.allocator.free(unwrapped_str);
|
||||||
|
try testing.expectEqualStrings("ABBBA\nB CC \n C", str);
|
||||||
|
try testing.expectEqualStrings("ABBBAB CC C", unwrapped_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: deleteLines wide characters split by left/right scroll region boundaries" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .cols = 5, .rows = 2 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
// +-----+
|
||||||
|
// |AAAAA|
|
||||||
|
// |WWBWW|
|
||||||
|
// +-----+
|
||||||
|
// where WW represents a wide character
|
||||||
|
try t.printString("AAAAA\n\u{1F600}B\u{1F600}");
|
||||||
|
|
||||||
|
t.scrolling_region.right = 3;
|
||||||
|
t.scrolling_region.left = 1;
|
||||||
|
|
||||||
|
// Delete the top line
|
||||||
|
// ### <- scrolling region
|
||||||
|
// +-----+
|
||||||
|
// |A B A|
|
||||||
|
// | |
|
||||||
|
// +-----+
|
||||||
|
// The two wide chars, because they're
|
||||||
|
// split by the edge of the scrolling
|
||||||
|
// region, get removed.
|
||||||
|
t.setCursorPos(1, 2);
|
||||||
|
t.deleteLines(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("A B A\n ", str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "Terminal: deleteLines zero" {
|
test "Terminal: deleteLines zero" {
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
|
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
|
||||||
|
@ -3,6 +3,8 @@ const assert = std.debug.assert;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.kitty_gfx);
|
||||||
|
|
||||||
/// The key-value pairs for the control information for a command. The
|
/// The key-value pairs for the control information for a command. The
|
||||||
/// keys are always single characters and the values are either single
|
/// keys are always single characters and the values are either single
|
||||||
/// characters or 32-bit unsigned integers.
|
/// characters or 32-bit unsigned integers.
|
||||||
@ -27,8 +29,11 @@ pub const CommandParser = struct {
|
|||||||
kv_temp_len: u4 = 0,
|
kv_temp_len: u4 = 0,
|
||||||
kv_current: u8 = 0, // Current kv key
|
kv_current: u8 = 0, // Current kv key
|
||||||
|
|
||||||
/// This is the list of bytes that contains both KV data and final
|
/// This is the list we use to collect the bytes from the data payload.
|
||||||
/// data. You shouldn't access this directly.
|
/// The Kitty Graphics protocol specification seems to imply that the
|
||||||
|
/// payload content of a single command should never exceed 4096 bytes,
|
||||||
|
/// but Kitty itself supports larger payloads, so we use an ArrayList
|
||||||
|
/// here instead of a fixed buffer so that we can too.
|
||||||
data: std.ArrayList(u8),
|
data: std.ArrayList(u8),
|
||||||
|
|
||||||
/// Internal state for parsing.
|
/// Internal state for parsing.
|
||||||
@ -42,7 +47,7 @@ pub const CommandParser = struct {
|
|||||||
control_value,
|
control_value,
|
||||||
control_value_ignore,
|
control_value_ignore,
|
||||||
|
|
||||||
/// We're parsing the data blob.
|
/// Collecting the data payload blob.
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,9 +111,6 @@ pub const CommandParser = struct {
|
|||||||
|
|
||||||
.data => try self.data.append(c),
|
.data => try self.data.append(c),
|
||||||
}
|
}
|
||||||
|
|
||||||
// We always add to our data list because this is our stable
|
|
||||||
// array of bytes that we'll reference everywhere else.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete the parsing. This must be called after all the
|
/// Complete the parsing. This must be called after all the
|
||||||
@ -165,12 +167,45 @@ pub const CommandParser = struct {
|
|||||||
return .{
|
return .{
|
||||||
.control = control,
|
.control = control,
|
||||||
.quiet = quiet,
|
.quiet = quiet,
|
||||||
.data = if (self.data.items.len == 0) "" else data: {
|
.data = try self.decodeData(),
|
||||||
break :data try self.data.toOwnedSlice();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decodes the payload data from base64 and returns it as a slice.
|
||||||
|
/// This function will destroy the contents of self.data, it should
|
||||||
|
/// only be used once we are done collecting payload bytes.
|
||||||
|
fn decodeData(self: *CommandParser) ![]const u8 {
|
||||||
|
if (self.data.items.len == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Base64Decoder = std.base64.standard_no_pad.Decoder;
|
||||||
|
|
||||||
|
// We remove any padding, since it's optional, and decode without it.
|
||||||
|
while (self.data.items[self.data.items.len - 1] == '=') {
|
||||||
|
self.data.items.len -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = Base64Decoder.calcSizeForSlice(self.data.items) catch |err| {
|
||||||
|
log.warn("failed to calculate base64 size for payload: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is kinda cursed, but we can decode the base64 on top of
|
||||||
|
// itself, since it's guaranteed that the encoded size is larger,
|
||||||
|
// and any bytes in areas that are written to will have already
|
||||||
|
// been used (assuming scalar decoding).
|
||||||
|
Base64Decoder.decode(self.data.items[0..size], self.data.items) catch |err| {
|
||||||
|
log.warn("failed to decode base64 payload data: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the extra bytes.
|
||||||
|
self.data.items.len = size;
|
||||||
|
|
||||||
|
return try self.data.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
fn accumulateValue(self: *CommandParser, c: u8, overflow_state: State) !void {
|
fn accumulateValue(self: *CommandParser, c: u8, overflow_state: State) !void {
|
||||||
const idx = self.kv_temp_len;
|
const idx = self.kv_temp_len;
|
||||||
self.kv_temp_len += 1;
|
self.kv_temp_len += 1;
|
||||||
@ -855,7 +890,7 @@ test "query command" {
|
|||||||
var p = CommandParser.init(alloc);
|
var p = CommandParser.init(alloc);
|
||||||
defer p.deinit();
|
defer p.deinit();
|
||||||
|
|
||||||
const input = "i=31,s=1,v=1,a=q,t=d,f=24;AAAA";
|
const input = "i=31,s=1,v=1,a=q,t=d,f=24;QUFBQQ";
|
||||||
for (input) |c| try p.feed(c);
|
for (input) |c| try p.feed(c);
|
||||||
const command = try p.complete();
|
const command = try p.complete();
|
||||||
defer command.deinit(alloc);
|
defer command.deinit(alloc);
|
||||||
|
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
const posix = std.posix;
|
const posix = std.posix;
|
||||||
|
|
||||||
|
const fastmem = @import("../../fastmem.zig");
|
||||||
const command = @import("graphics_command.zig");
|
const command = @import("graphics_command.zig");
|
||||||
const point = @import("../point.zig");
|
const point = @import("../point.zig");
|
||||||
const PageList = @import("../PageList.zig");
|
const PageList = @import("../PageList.zig");
|
||||||
@ -56,30 +57,16 @@ pub const LoadingImage = struct {
|
|||||||
.display = cmd.display(),
|
.display = cmd.display(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Special case for the direct medium, we just add it directly
|
// Special case for the direct medium, we just add the chunk directly.
|
||||||
// which will handle copying the data, base64 decoding, etc.
|
|
||||||
if (t.medium == .direct) {
|
if (t.medium == .direct) {
|
||||||
try result.addData(alloc, cmd.data);
|
try result.addData(alloc, cmd.data);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For every other medium, we'll need to at least base64 decode
|
// Otherwise, the payload data is guaranteed to be a path.
|
||||||
// the data to make it useful so let's do that. Also, all the data
|
|
||||||
// has to be path data so we can put it in a stack-allocated buffer.
|
|
||||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
|
||||||
const Base64Decoder = std.base64.standard.Decoder;
|
|
||||||
const size = Base64Decoder.calcSizeForSlice(cmd.data) catch |err| {
|
|
||||||
log.warn("failed to calculate base64 size for file path: {}", .{err});
|
|
||||||
return error.InvalidData;
|
|
||||||
};
|
|
||||||
if (size > buf.len) return error.FilePathTooLong;
|
|
||||||
Base64Decoder.decode(&buf, cmd.data) catch |err| {
|
|
||||||
log.warn("failed to decode base64 data: {}", .{err});
|
|
||||||
return error.InvalidData;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (comptime builtin.os.tag != .windows) {
|
if (comptime builtin.os.tag != .windows) {
|
||||||
if (std.mem.indexOfScalar(u8, buf[0..size], 0) != null) {
|
if (std.mem.indexOfScalar(u8, cmd.data, 0) != null) {
|
||||||
// posix.realpath *asserts* that the path does not have
|
// posix.realpath *asserts* that the path does not have
|
||||||
// internal nulls instead of erroring.
|
// internal nulls instead of erroring.
|
||||||
log.warn("failed to get absolute path: BadPathName", .{});
|
log.warn("failed to get absolute path: BadPathName", .{});
|
||||||
@ -88,7 +75,7 @@ pub const LoadingImage = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var abs_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
var abs_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
const path = posix.realpath(buf[0..size], &abs_buf) catch |err| {
|
const path = posix.realpath(cmd.data, &abs_buf) catch |err| {
|
||||||
log.warn("failed to get absolute path: {}", .{err});
|
log.warn("failed to get absolute path: {}", .{err});
|
||||||
return error.InvalidData;
|
return error.InvalidData;
|
||||||
};
|
};
|
||||||
@ -229,42 +216,25 @@ pub const LoadingImage = struct {
|
|||||||
alloc.destroy(self);
|
alloc.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a chunk of base64-encoded data to the image. Use this if the
|
/// Adds a chunk of data to the image. Use this if the image
|
||||||
/// image is coming in chunks (the "m" parameter in the protocol).
|
/// is coming in chunks (the "m" parameter in the protocol).
|
||||||
pub fn addData(self: *LoadingImage, alloc: Allocator, data: []const u8) !void {
|
pub fn addData(self: *LoadingImage, alloc: Allocator, data: []const u8) !void {
|
||||||
// If no data, skip
|
// If no data, skip
|
||||||
if (data.len == 0) return;
|
if (data.len == 0) return;
|
||||||
|
|
||||||
// Grow our array list by size capacity if it needs it
|
|
||||||
const Base64Decoder = std.base64.standard.Decoder;
|
|
||||||
const size = Base64Decoder.calcSizeForSlice(data) catch |err| {
|
|
||||||
log.warn("failed to calculate size for base64 data: {}", .{err});
|
|
||||||
return error.InvalidData;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If our data would get too big, return an error
|
// If our data would get too big, return an error
|
||||||
if (self.data.items.len + size > max_size) {
|
if (self.data.items.len + data.len > max_size) {
|
||||||
log.warn("image data too large max_size={}", .{max_size});
|
log.warn("image data too large max_size={}", .{max_size});
|
||||||
return error.InvalidData;
|
return error.InvalidData;
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.data.ensureUnusedCapacity(alloc, size);
|
// Ensure we have enough room to add the data
|
||||||
|
// to the end of the ArrayList before doing so.
|
||||||
|
try self.data.ensureUnusedCapacity(alloc, data.len);
|
||||||
|
|
||||||
// We decode directly into the arraylist
|
|
||||||
const start_i = self.data.items.len;
|
const start_i = self.data.items.len;
|
||||||
self.data.items.len = start_i + size;
|
self.data.items.len = start_i + data.len;
|
||||||
const buf = self.data.items[start_i..];
|
fastmem.copy(u8, self.data.items[start_i..], data);
|
||||||
Base64Decoder.decode(buf, data) catch |err| switch (err) {
|
|
||||||
// We have to ignore invalid padding because lots of encoders
|
|
||||||
// add the wrong padding. Since we validate image data later
|
|
||||||
// (PNG decode or simple dimensions check), we can ignore this.
|
|
||||||
error.InvalidPadding => {},
|
|
||||||
|
|
||||||
else => {
|
|
||||||
log.warn("failed to decode base64 data: {}", .{err});
|
|
||||||
return error.InvalidData;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete the chunked image, returning a completed image.
|
/// Complete the chunked image, returning a completed image.
|
||||||
@ -457,23 +427,6 @@ pub const Rect = struct {
|
|||||||
bottom_right: PageList.Pin,
|
bottom_right: PageList.Pin,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Easy base64 encoding function.
|
|
||||||
fn testB64(alloc: Allocator, data: []const u8) ![]const u8 {
|
|
||||||
const B64Encoder = std.base64.standard.Encoder;
|
|
||||||
const b64 = try alloc.alloc(u8, B64Encoder.calcSize(data.len));
|
|
||||||
errdefer alloc.free(b64);
|
|
||||||
return B64Encoder.encode(b64, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Easy base64 decoding function.
|
|
||||||
fn testB64Decode(alloc: Allocator, data: []const u8) ![]const u8 {
|
|
||||||
const B64Decoder = std.base64.standard.Decoder;
|
|
||||||
const result = try alloc.alloc(u8, try B64Decoder.calcSizeForSlice(data));
|
|
||||||
errdefer alloc.free(result);
|
|
||||||
try B64Decoder.decode(result, data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This specifically tests we ALLOW invalid RGB data because Kitty
|
// This specifically tests we ALLOW invalid RGB data because Kitty
|
||||||
// documents that this should work.
|
// documents that this should work.
|
||||||
test "image load with invalid RGB data" {
|
test "image load with invalid RGB data" {
|
||||||
@ -548,7 +501,7 @@ test "image load: rgb, zlib compressed, direct" {
|
|||||||
} },
|
} },
|
||||||
.data = try alloc.dupe(
|
.data = try alloc.dupe(
|
||||||
u8,
|
u8,
|
||||||
@embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data"),
|
@embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647-raw.data"),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
defer cmd.deinit(alloc);
|
defer cmd.deinit(alloc);
|
||||||
@ -576,7 +529,7 @@ test "image load: rgb, not compressed, direct" {
|
|||||||
} },
|
} },
|
||||||
.data = try alloc.dupe(
|
.data = try alloc.dupe(
|
||||||
u8,
|
u8,
|
||||||
@embedFile("testdata/image-rgb-none-20x15-2147483647.data"),
|
@embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data"),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
defer cmd.deinit(alloc);
|
defer cmd.deinit(alloc);
|
||||||
@ -593,7 +546,7 @@ test "image load: rgb, zlib compressed, direct, chunked" {
|
|||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data");
|
const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647-raw.data");
|
||||||
|
|
||||||
// Setup our initial chunk
|
// Setup our initial chunk
|
||||||
var cmd: command.Command = .{
|
var cmd: command.Command = .{
|
||||||
@ -630,7 +583,7 @@ test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk"
|
|||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647.data");
|
const data = @embedFile("testdata/image-rgb-zlib_deflate-128x96-2147483647-raw.data");
|
||||||
|
|
||||||
// Setup our initial chunk
|
// Setup our initial chunk
|
||||||
var cmd: command.Command = .{
|
var cmd: command.Command = .{
|
||||||
@ -668,11 +621,7 @@ test "image load: rgb, not compressed, temporary file" {
|
|||||||
|
|
||||||
var tmp_dir = try internal_os.TempDir.init();
|
var tmp_dir = try internal_os.TempDir.init();
|
||||||
defer tmp_dir.deinit();
|
defer tmp_dir.deinit();
|
||||||
const data = try testB64Decode(
|
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
|
||||||
alloc,
|
|
||||||
@embedFile("testdata/image-rgb-none-20x15-2147483647.data"),
|
|
||||||
);
|
|
||||||
defer alloc.free(data);
|
|
||||||
try tmp_dir.dir.writeFile("image.data", data);
|
try tmp_dir.dir.writeFile("image.data", data);
|
||||||
|
|
||||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
@ -687,7 +636,7 @@ test "image load: rgb, not compressed, temporary file" {
|
|||||||
.height = 15,
|
.height = 15,
|
||||||
.image_id = 31,
|
.image_id = 31,
|
||||||
} },
|
} },
|
||||||
.data = try testB64(alloc, path),
|
.data = try alloc.dupe(u8, path),
|
||||||
};
|
};
|
||||||
defer cmd.deinit(alloc);
|
defer cmd.deinit(alloc);
|
||||||
var loading = try LoadingImage.init(alloc, &cmd);
|
var loading = try LoadingImage.init(alloc, &cmd);
|
||||||
@ -706,11 +655,7 @@ test "image load: rgb, not compressed, regular file" {
|
|||||||
|
|
||||||
var tmp_dir = try internal_os.TempDir.init();
|
var tmp_dir = try internal_os.TempDir.init();
|
||||||
defer tmp_dir.deinit();
|
defer tmp_dir.deinit();
|
||||||
const data = try testB64Decode(
|
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
|
||||||
alloc,
|
|
||||||
@embedFile("testdata/image-rgb-none-20x15-2147483647.data"),
|
|
||||||
);
|
|
||||||
defer alloc.free(data);
|
|
||||||
try tmp_dir.dir.writeFile("image.data", data);
|
try tmp_dir.dir.writeFile("image.data", data);
|
||||||
|
|
||||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
@ -725,7 +670,7 @@ test "image load: rgb, not compressed, regular file" {
|
|||||||
.height = 15,
|
.height = 15,
|
||||||
.image_id = 31,
|
.image_id = 31,
|
||||||
} },
|
} },
|
||||||
.data = try testB64(alloc, path),
|
.data = try alloc.dupe(u8, path),
|
||||||
};
|
};
|
||||||
defer cmd.deinit(alloc);
|
defer cmd.deinit(alloc);
|
||||||
var loading = try LoadingImage.init(alloc, &cmd);
|
var loading = try LoadingImage.init(alloc, &cmd);
|
||||||
@ -757,7 +702,7 @@ test "image load: png, not compressed, regular file" {
|
|||||||
.height = 0,
|
.height = 0,
|
||||||
.image_id = 31,
|
.image_id = 31,
|
||||||
} },
|
} },
|
||||||
.data = try testB64(alloc, path),
|
.data = try alloc.dupe(u8, path),
|
||||||
};
|
};
|
||||||
defer cmd.deinit(alloc);
|
defer cmd.deinit(alloc);
|
||||||
var loading = try LoadingImage.init(alloc, &cmd);
|
var loading = try LoadingImage.init(alloc, &cmd);
|
||||||
|
BIN
src/terminal/kitty/testdata/image-rgb-none-20x15-2147483647-raw.data
vendored
Normal file
BIN
src/terminal/kitty/testdata/image-rgb-none-20x15-2147483647-raw.data
vendored
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
DRoeCxgcCxcjEh4qDBgkCxcjChYiCxcjCRclBRMhBxIXHysvTVNRbHJwcXB2Li0zCBYXEyEiCxkaDBobChcbCBUZDxsnBBAcEBwoChYiCxcjDBgkDhwqBxUjDBccm6aqy9HP1NrYzs3UsK+2IjAxCBYXCBYXBxUWFBoaDxUVICYqIyktERcZDxUXDxUVEhgYDhUTCxIQGh8XusC4zM7FvL61q6elmZWTTVtcDBobDRscCxkaKS8vaW9vxMnOur/EiY+RaW5wICYmW2FhfYOBQEZEnqSc4ebeqauilZaOsa2rm5eVcH5/GigpChgZCBYX0NHP3d7c3tzbx8XExsTEvry8wL241dLN0tDF0tDF29nM4d/StbKpzMrAUk5DZmJXeYSGKTU3ER0fDRkb1tfVysvJ0tDPsa+tr6ytop+gmZaRqaahuritw8G2urirqKaZiYZ9paKZZmJXamZbOkZIDhocBxMVBBASxMDBtrKzqqanoZ2ejYeLeHF2eXFvhn58npePta6ml5CKgXp0W1hPaWZdZWdSYmRPFiYADR0AFCQAEyMAt7O0lJCRf3t8eHR1Zl9kY1xhYVpYbGRieXJqeHFpdW1oc2tmcG1kX1xTbW9ajY96jp55kaF8kKB7kaF8sK6rcnFtX11cXFpZW1pWWFdTXVpTXltUaGJgY11bY11da2Vla25dam1ccHtTnqmBorVtp7pypLdvobRsh4aCaGdjWFZVXFpZYWBcZ2ZiaGVeZGFaY11bYlxaV1FRZ2FhdHdmbG9egItjo66GpLdvq752rL93rsF5kpKIZ2ddWFxTW19WbnZdipJ6cnhaaW9RaGhgV1ZPY2Jga2poanFQd35dk6Vpn7B0oLFvorNxm6xqmKlnv760enpwVlpRW19Wc3til5+Hl55/k5p7iIiAcnJqd3Z0bm1rcHdWh45tipxgladrkaJglKVjkaJgkqNh09DJiYZ/YmZdY2deeYZYjJlrj51ijpxhztHClJaIdHNvdHNvanNHi5RpmaxnjKBbmqhrmadqkJ5hi5lcxsO8jImCaGtiYmZdg5Bikp9xjJpfjpxh1djJqq2eamllZ2Zid4BVmKF2kqZhh5tWlaNmlaNmjpxfjJpdw729rqiodnZ0cHBuiplij55nj6FVjJ5SzdC9t7qncW1sXlpZh45iqbCEmKllmapmmqlqnq1unaxtoK9w
|
|
BIN
src/terminal/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647-raw.data
vendored
Normal file
BIN
src/terminal/kitty/testdata/image-rgb-zlib_deflate-128x96-2147483647-raw.data
vendored
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1465,6 +1465,16 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
},
|
},
|
||||||
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
||||||
|
|
||||||
|
// SPA - Start of Guarded Area
|
||||||
|
'V' => if (@hasDecl(T, "setProtectedMode")) {
|
||||||
|
try self.handler.setProtectedMode(ansi.ProtectedMode.iso);
|
||||||
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
||||||
|
|
||||||
|
// EPA - End of Guarded Area
|
||||||
|
'W' => if (@hasDecl(T, "setProtectedMode")) {
|
||||||
|
try self.handler.setProtectedMode(ansi.ProtectedMode.off);
|
||||||
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
||||||
|
|
||||||
// DECID
|
// DECID
|
||||||
'Z' => if (@hasDecl(T, "deviceAttributes")) {
|
'Z' => if (@hasDecl(T, "deviceAttributes")) {
|
||||||
try self.handler.deviceAttributes(.primary, &.{});
|
try self.handler.deviceAttributes(.primary, &.{});
|
||||||
|
@ -1993,7 +1993,7 @@ const StreamHandler = struct {
|
|||||||
pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void {
|
pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void {
|
||||||
self.terminal.setCursorPos(
|
self.terminal.setCursorPos(
|
||||||
self.terminal.screen.cursor.y + 1,
|
self.terminal.screen.cursor.y + 1,
|
||||||
self.terminal.screen.cursor.x + 1 + offset,
|
self.terminal.screen.cursor.x + 1 +| offset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2003,7 +2003,7 @@ const StreamHandler = struct {
|
|||||||
|
|
||||||
pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void {
|
pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void {
|
||||||
self.terminal.setCursorPos(
|
self.terminal.setCursorPos(
|
||||||
self.terminal.screen.cursor.y + 1 + offset,
|
self.terminal.screen.cursor.y + 1 +| offset,
|
||||||
self.terminal.screen.cursor.x + 1,
|
self.terminal.screen.cursor.x + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user