mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge branch 'main' of github.com:ghostty-org/ghostty
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Mitchell Hashimoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -43,7 +43,7 @@ comptime {
|
||||
}
|
||||
|
||||
/// The version of the next release.
|
||||
const app_version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 };
|
||||
const app_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 1 };
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
@ -1,6 +1,6 @@
|
||||
.{
|
||||
.name = "ghostty",
|
||||
.version = "0.1.0",
|
||||
.version = "1.0.1",
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
|
17
flake.lock
generated
17
flake.lock
generated
@ -1,5 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@ -52,6 +68,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"zig": "zig"
|
||||
|
@ -9,6 +9,12 @@
|
||||
# system glibc that the user is building for.
|
||||
nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11";
|
||||
|
||||
# Used for shell.nix
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
zig = {
|
||||
url = "github:mitchellh/zig-overlay";
|
||||
inputs = {
|
||||
|
@ -5,6 +5,7 @@ enum QuickTerminalPosition : String {
|
||||
case bottom
|
||||
case left
|
||||
case right
|
||||
case center
|
||||
|
||||
/// Set the loaded state for a window.
|
||||
func setLoaded(_ window: NSWindow) {
|
||||
@ -25,6 +26,14 @@ enum QuickTerminalPosition : String {
|
||||
width: screen.frame.width / 4,
|
||||
height: screen.frame.height)
|
||||
), display: false)
|
||||
|
||||
case .center:
|
||||
window.setFrame(.init(
|
||||
origin: window.frame.origin,
|
||||
size: .init(
|
||||
width: screen.frame.width / 2,
|
||||
height: screen.frame.height / 3)
|
||||
), display: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +70,10 @@ enum QuickTerminalPosition : String {
|
||||
|
||||
case .left, .right:
|
||||
finalSize.height = screen.frame.height
|
||||
|
||||
case .center:
|
||||
finalSize.width = screen.frame.width / 2
|
||||
finalSize.height = screen.frame.height / 3
|
||||
}
|
||||
|
||||
return finalSize
|
||||
@ -80,6 +93,9 @@ enum QuickTerminalPosition : String {
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.frame.maxX, y: 0)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.width)
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,6 +113,9 @@ enum QuickTerminalPosition : String {
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: (screen.visibleFrame.maxY - window.frame.height) / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -409,6 +409,11 @@ extension Ghostty {
|
||||
// ID. If vsync is enabled, this will be used with the CVDisplayLink to ensure
|
||||
// the proper refresh rate is going.
|
||||
ghostty_surface_set_display_id(surface, screen.displayID ?? 0)
|
||||
|
||||
// We also just trigger a backing property change. Just in case the screen has
|
||||
// a different scaling factor, this ensures that we update our content scale.
|
||||
// Issue: https://github.com/ghostty-org/ghostty/issues/2731
|
||||
viewDidChangeBackingProperties()
|
||||
}
|
||||
|
||||
// MARK: - NSView
|
||||
@ -570,6 +575,20 @@ extension Ghostty {
|
||||
super.rightMouseUp(with: event)
|
||||
}
|
||||
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
super.mouseEntered(with: event)
|
||||
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
// On mouse enter we need to reset our cursor position. This is
|
||||
// super important because we set it to -1/-1 on mouseExit and
|
||||
// lots of mouse logic (i.e. whether to send mouse reports) depend
|
||||
// on the position being in the viewport if it is.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
|
@ -110,7 +110,7 @@
|
||||
in
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "ghostty";
|
||||
version = "0.1.0";
|
||||
version = "1.0.1";
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
|
@ -221,6 +221,9 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
switch (config.@"window-theme") {
|
||||
.system, .light => {},
|
||||
.dark => {
|
||||
const settings = c.gtk_settings_get_default();
|
||||
c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
|
||||
|
||||
c.gtk_css_provider_load_from_resource(
|
||||
provider,
|
||||
"/com/mitchellh/ghostty/style-dark.css",
|
||||
@ -234,6 +237,9 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
.auto, .ghostty => {
|
||||
const lum = config.background.toTerminalRGB().perceivedLuminance();
|
||||
if (lum <= 0.5) {
|
||||
const settings = c.gtk_settings_get_default();
|
||||
c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
|
||||
|
||||
c.gtk_css_provider_load_from_resource(
|
||||
provider,
|
||||
"/com/mitchellh/ghostty/style-dark.css",
|
||||
|
@ -386,7 +386,8 @@ fn keyEvent(
|
||||
|
||||
// Try to process the event as text
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
||||
_ = c.gtk_im_context_filter_keypress(self.im_context, event);
|
||||
if (event != null)
|
||||
_ = c.gtk_im_context_filter_keypress(self.im_context, event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1341,8 +1341,9 @@ fn gtkMouseDown(
|
||||
y: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(gesture)) orelse return;
|
||||
|
||||
const self = userdataSelf(ud.?);
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(gesture));
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
|
||||
const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture)));
|
||||
@ -1374,7 +1375,8 @@ fn gtkMouseUp(
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(gesture));
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(gesture)) orelse return;
|
||||
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
|
||||
const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture)));
|
||||
@ -1393,6 +1395,8 @@ fn gtkMouseMotion(
|
||||
y: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)) orelse return;
|
||||
|
||||
const self = userdataSelf(ud.?);
|
||||
const scaled = self.scaledCoordinates(x, y);
|
||||
|
||||
@ -1401,13 +1405,6 @@ fn gtkMouseMotion(
|
||||
.y = @floatCast(scaled.y),
|
||||
};
|
||||
|
||||
// GTK can send spurious mouse movement events. Ignore them
|
||||
// because this can cause actual issues:
|
||||
// https://github.com/ghostty-org/ghostty/issues/2022
|
||||
if (pos.x == self.cursor_pos.x and pos.y == self.cursor_pos.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Our pos changed, update
|
||||
self.cursor_pos = pos;
|
||||
|
||||
@ -1418,7 +1415,6 @@ fn gtkMouseMotion(
|
||||
}
|
||||
|
||||
// Get our modifiers
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
|
||||
@ -1432,10 +1428,11 @@ fn gtkMouseLeave(
|
||||
ec: *c.GtkEventControllerMotion,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)) orelse return;
|
||||
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
// Get our modifiers
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| {
|
||||
@ -1536,11 +1533,12 @@ pub fn keyEvent(
|
||||
keycode: c.guint,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) bool {
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
const event = c.gtk_event_controller_get_current_event(
|
||||
@ptrCast(ec_key),
|
||||
) orelse return false;
|
||||
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||
|
@ -200,7 +200,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
||||
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
||||
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
header.packEnd(btn);
|
||||
header.packStart(btn);
|
||||
}
|
||||
|
||||
self.header = header;
|
||||
|
@ -22,9 +22,19 @@ pub fn genKeybindActions(writer: anytype) !void {
|
||||
|
||||
@setEvalBranchQuota(5_000);
|
||||
const fields = @typeInfo(KeybindAction).Union.fields;
|
||||
|
||||
var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
|
||||
inline for (fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
|
||||
// Write previously stored doc comment below all related actions
|
||||
if (@hasDecl(help_strings.KeybindAction, field.name)) {
|
||||
try writer.writeAll(buffer.items);
|
||||
try writer.writeAll("\n");
|
||||
|
||||
buffer.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// Write the field name.
|
||||
try writer.writeAll("## `");
|
||||
try writer.writeAll(field.name);
|
||||
@ -37,10 +47,9 @@ pub fn genKeybindActions(writer: anytype) !void {
|
||||
'\n',
|
||||
);
|
||||
while (iter.next()) |s| {
|
||||
try writer.writeAll(s);
|
||||
try writer.writeAll("\n");
|
||||
try buffer.appendSlice(s);
|
||||
try buffer.appendSlice("\n");
|
||||
}
|
||||
try writer.writeAll("\n\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,11 +73,11 @@ const ThemeListElement = struct {
|
||||
///
|
||||
/// The second directory is the `themes` subdirectory of the Ghostty resources
|
||||
/// directory. Ghostty ships with a multitude of themes that will be installed
|
||||
/// into this directory. On macOS, this directory is the `Ghostty.app/Contents/
|
||||
/// Resources/ghostty/themes`. On Linux, this directory is the `share/ghostty/
|
||||
/// themes` (wherever you installed the Ghostty "share" directory). If you're
|
||||
/// running Ghostty from the source, this is the `zig-out/share/ghostty/themes`
|
||||
/// directory.
|
||||
/// into this directory. On macOS, this directory is the
|
||||
/// `Ghostty.app/Contents/Resources/ghostty/themes`. On Linux, this directory
|
||||
/// is the `share/ghostty/themes` (wherever you installed the Ghostty "share"
|
||||
/// directory). If you're running Ghostty from the source, this is the
|
||||
/// `zig-out/share/ghostty/themes` directory.
|
||||
///
|
||||
/// You can also set the `GHOSTTY_RESOURCES_DIR` environment variable to point
|
||||
/// to the resources directory.
|
||||
|
@ -351,10 +351,10 @@ const c = @cImport({
|
||||
///
|
||||
/// The second directory is the `themes` subdirectory of the Ghostty resources
|
||||
/// directory. Ghostty ships with a multitude of themes that will be installed
|
||||
/// into this directory. On macOS, this list is in the `Ghostty.app/Contents/
|
||||
/// Resources/ghostty/themes` directory. On Linux, this list is in the `share/
|
||||
/// ghostty/themes` directory (wherever you installed the Ghostty "share"
|
||||
/// directory.
|
||||
/// into this directory. On macOS, this list is in the
|
||||
/// `Ghostty.app/Contents/Resources/ghostty/themes` directory. On Linux, this
|
||||
/// list is in the `share/ghostty/themes` directory (wherever you installed the
|
||||
/// Ghostty "share" directory.
|
||||
///
|
||||
/// To see a list of available themes, run `ghostty +list-themes`.
|
||||
///
|
||||
@ -1189,12 +1189,12 @@ keybind: Keybinds = .{},
|
||||
/// value larger than this will be clamped to the maximum value.
|
||||
@"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
|
||||
|
||||
// If true, when there are multiple split panes, the mouse selects the pane
|
||||
// that is focused. This only applies to the currently focused window; i.e.
|
||||
// mousing over a split in an unfocused window will now focus that split
|
||||
// and bring the window to front.
|
||||
//
|
||||
// Default is false.
|
||||
/// If true, when there are multiple split panes, the mouse selects the pane
|
||||
/// that is focused. This only applies to the currently focused window; i.e.
|
||||
/// mousing over a split in an unfocused window will now focus that split
|
||||
/// and bring the window to front.
|
||||
///
|
||||
/// Default is false.
|
||||
@"focus-follows-mouse": bool = false,
|
||||
|
||||
/// Whether to allow programs running in the terminal to read/write to the
|
||||
@ -1377,6 +1377,7 @@ keybind: Keybinds = .{},
|
||||
/// * `bottom` - Terminal appears at the bottom of the screen.
|
||||
/// * `left` - Terminal appears at the left of the screen.
|
||||
/// * `right` - Terminal appears at the right of the screen.
|
||||
/// * `center` - Terminal appears at the center of the screen.
|
||||
///
|
||||
/// Changing this configuration requires restarting Ghostty completely.
|
||||
@"quick-terminal-position": QuickTerminalPosition = .top,
|
||||
@ -2532,6 +2533,32 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
.{ .key = .{ .translated = .f }, .mods = .{ .super = true, .ctrl = true } },
|
||||
.{ .toggle_fullscreen = {} },
|
||||
);
|
||||
|
||||
// "Natural text editing" keybinds. This forces these keys to go back
|
||||
// to legacy encoding (not fixterms). It seems macOS users more than
|
||||
// others are used to these keys so we set them as defaults. If
|
||||
// people want to get back to the fixterm encoding they can set
|
||||
// the keybinds to `unbind`.
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .right }, .mods = .{ .super = true } },
|
||||
.{ .text = "\\x05" },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .left }, .mods = .{ .super = true } },
|
||||
.{ .text = "\\x01" },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .left }, .mods = .{ .alt = true } },
|
||||
.{ .esc = "b" },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .right }, .mods = .{ .alt = true } },
|
||||
.{ .esc = "f" },
|
||||
);
|
||||
}
|
||||
|
||||
// Add our default link for URL detection
|
||||
@ -5257,6 +5284,7 @@ pub const QuickTerminalPosition = enum {
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
center,
|
||||
};
|
||||
|
||||
/// See quick-terminal-screen
|
||||
|
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
@ -6,7 +7,24 @@ const internal_os = @import("../os/main.zig");
|
||||
/// paths the main config file could be in.
|
||||
pub fn open(alloc_gpa: Allocator) !void {
|
||||
// default location
|
||||
const config_path = try internal_os.xdg.config(alloc_gpa, .{ .subdir = "ghostty/config" });
|
||||
const config_path = config_path: {
|
||||
const xdg_config_path = try internal_os.xdg.config(alloc_gpa, .{ .subdir = "ghostty/config" });
|
||||
|
||||
if (comptime builtin.os.tag == .macos) macos: {
|
||||
// On macOS, use the application support path if the XDG path doesn't exists.
|
||||
if (std.fs.accessAbsolute(xdg_config_path, .{})) {
|
||||
break :macos;
|
||||
} else |err| switch (err) {
|
||||
error.BadPathName, error.FileNotFound => {},
|
||||
else => break :macos,
|
||||
}
|
||||
|
||||
alloc_gpa.free(xdg_config_path);
|
||||
break :config_path try internal_os.macos.appSupportDir(alloc_gpa, "config");
|
||||
}
|
||||
|
||||
break :config_path xdg_config_path;
|
||||
};
|
||||
defer alloc_gpa.free(config_path);
|
||||
|
||||
// Create config directory recursively.
|
||||
|
@ -2515,19 +2515,19 @@ fn draw_smooth_mosaic(
|
||||
const right: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
|
||||
var path: z2d.StaticPath(12) = .{};
|
||||
path.init();
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
if (mosaic.tl) path.lineTo(left, top);
|
||||
if (mosaic.ul) path.lineTo(left, upper);
|
||||
if (mosaic.ll) path.lineTo(left, lower);
|
||||
if (mosaic.bl) path.lineTo(left, bottom);
|
||||
if (mosaic.bc) path.lineTo(center, bottom);
|
||||
if (mosaic.br) path.lineTo(right, bottom);
|
||||
if (mosaic.lr) path.lineTo(right, lower);
|
||||
if (mosaic.ur) path.lineTo(right, upper);
|
||||
if (mosaic.tr) path.lineTo(right, top);
|
||||
if (mosaic.tc) path.lineTo(center, top);
|
||||
path.close();
|
||||
if (mosaic.tl) path.lineTo(left, top); // +1, nodes.len = 1
|
||||
if (mosaic.ul) path.lineTo(left, upper); // +1, nodes.len = 2
|
||||
if (mosaic.ll) path.lineTo(left, lower); // +1, nodes.len = 3
|
||||
if (mosaic.bl) path.lineTo(left, bottom); // +1, nodes.len = 4
|
||||
if (mosaic.bc) path.lineTo(center, bottom); // +1, nodes.len = 5
|
||||
if (mosaic.br) path.lineTo(right, bottom); // +1, nodes.len = 6
|
||||
if (mosaic.lr) path.lineTo(right, lower); // +1, nodes.len = 7
|
||||
if (mosaic.ur) path.lineTo(right, upper); // +1, nodes.len = 8
|
||||
if (mosaic.tr) path.lineTo(right, top); // +1, nodes.len = 9
|
||||
if (mosaic.tc) path.lineTo(center, top); // +1, nodes.len = 10
|
||||
path.close(); // +2, nodes.len = 12
|
||||
|
||||
try z2d.painter.fill(
|
||||
canvas.alloc,
|
||||
@ -2535,7 +2535,7 @@ fn draw_smooth_mosaic(
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
|
||||
} },
|
||||
&path.nodes,
|
||||
path.wrapped_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
@ -2560,12 +2560,12 @@ fn draw_edge_triangle(
|
||||
};
|
||||
|
||||
var path: z2d.StaticPath(5) = .{};
|
||||
path.init();
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(center, middle);
|
||||
path.lineTo(x0, y0);
|
||||
path.lineTo(x1, y1);
|
||||
path.close();
|
||||
path.moveTo(center, middle); // +1, nodes.len = 1
|
||||
path.lineTo(x0, y0); // +1, nodes.len = 2
|
||||
path.lineTo(x1, y1); // +1, nodes.len = 3
|
||||
path.close(); // +2, nodes.len = 5
|
||||
|
||||
try z2d.painter.fill(
|
||||
canvas.alloc,
|
||||
@ -2573,7 +2573,7 @@ fn draw_edge_triangle(
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
|
||||
} },
|
||||
&path.nodes,
|
||||
path.wrapped_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32)
|
||||
fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
const width = self.width;
|
||||
const height = self.height;
|
||||
|
||||
|
||||
var p1_x: u32 = 0;
|
||||
var p1_y: u32 = 0;
|
||||
var p2_x: u32 = 0;
|
||||
@ -123,7 +123,6 @@ fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
var p3_x: u32 = 0;
|
||||
var p3_y: u32 = 0;
|
||||
|
||||
|
||||
switch (cp) {
|
||||
0xE0B1 => {
|
||||
p1_x = 0;
|
||||
@ -141,19 +140,15 @@ fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
p3_x = width;
|
||||
p3_y = height;
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
try canvas.triangle_outline(.{
|
||||
.p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) },
|
||||
.p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) },
|
||||
.p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) },
|
||||
},
|
||||
@floatFromInt(Thickness.light.height(self.thickness)),
|
||||
.on);
|
||||
|
||||
}, @floatFromInt(Thickness.light.height(self.thickness)), .on);
|
||||
}
|
||||
|
||||
fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
|
@ -184,13 +184,13 @@ pub const Canvas = struct {
|
||||
/// Draw and fill a quad.
|
||||
pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void {
|
||||
var path: z2d.StaticPath(6) = .{};
|
||||
path.init();
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(q.p0.x, q.p0.y);
|
||||
path.lineTo(q.p1.x, q.p1.y);
|
||||
path.lineTo(q.p2.x, q.p2.y);
|
||||
path.lineTo(q.p3.x, q.p3.y);
|
||||
path.close();
|
||||
path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2
|
||||
path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3
|
||||
path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4
|
||||
path.close(); // +2, nodes.len = 6
|
||||
|
||||
try z2d.painter.fill(
|
||||
self.alloc,
|
||||
@ -198,7 +198,7 @@ pub const Canvas = struct {
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
&path.nodes,
|
||||
path.wrapped_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
@ -206,12 +206,12 @@ pub const Canvas = struct {
|
||||
/// Draw and fill a triangle.
|
||||
pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void {
|
||||
var path: z2d.StaticPath(5) = .{};
|
||||
path.init();
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(t.p0.x, t.p0.y);
|
||||
path.lineTo(t.p1.x, t.p1.y);
|
||||
path.lineTo(t.p2.x, t.p2.y);
|
||||
path.close();
|
||||
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2
|
||||
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
|
||||
path.close(); // +2, nodes.len = 5
|
||||
|
||||
try z2d.painter.fill(
|
||||
self.alloc,
|
||||
@ -219,18 +219,18 @@ pub const Canvas = struct {
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
&path.nodes,
|
||||
path.wrapped_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void {
|
||||
var path: z2d.StaticPath(5) = .{};
|
||||
path.init();
|
||||
var path: z2d.StaticPath(3) = .{};
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(t.p0.x, t.p0.y);
|
||||
path.lineTo(t.p1.x, t.p1.y);
|
||||
path.lineTo(t.p2.x, t.p2.y);
|
||||
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2
|
||||
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
|
||||
|
||||
try z2d.painter.stroke(
|
||||
self.alloc,
|
||||
@ -238,7 +238,7 @@ pub const Canvas = struct {
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
&path.nodes,
|
||||
path.wrapped_path.nodes.items,
|
||||
.{
|
||||
.line_cap_mode = .round,
|
||||
.line_width = thickness,
|
||||
@ -248,11 +248,11 @@ pub const Canvas = struct {
|
||||
|
||||
/// Stroke a line.
|
||||
pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void {
|
||||
var path: z2d.StaticPath(3) = .{};
|
||||
path.init();
|
||||
var path: z2d.StaticPath(2) = .{};
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(l.p0.x, l.p0.y);
|
||||
path.lineTo(l.p1.x, l.p1.y);
|
||||
path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2
|
||||
|
||||
try z2d.painter.stroke(
|
||||
self.alloc,
|
||||
@ -260,7 +260,7 @@ pub const Canvas = struct {
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
&path.nodes,
|
||||
path.wrapped_path.nodes.items,
|
||||
.{
|
||||
.line_cap_mode = .round,
|
||||
.line_width = thickness,
|
||||
|
@ -308,7 +308,7 @@ pub const Key = enum(c_int) {
|
||||
equal,
|
||||
left_bracket, // [
|
||||
right_bracket, // ]
|
||||
backslash, // /
|
||||
backslash, // \
|
||||
|
||||
// control
|
||||
up,
|
||||
|
@ -1721,21 +1721,22 @@ fn prepKittyGraphics(
|
||||
}.lessThan,
|
||||
);
|
||||
|
||||
// Find our indices
|
||||
self.image_bg_end = 0;
|
||||
self.image_text_end = 0;
|
||||
// Find our indices. The values are sorted by z so we can find the
|
||||
// first placement out of bounds to find the limits.
|
||||
var bg_end: ?u32 = null;
|
||||
var text_end: ?u32 = null;
|
||||
const bg_limit = std.math.minInt(i32) / 2;
|
||||
for (self.image_placements.items, 0..) |p, i| {
|
||||
if (self.image_bg_end == 0 and p.z >= bg_limit) {
|
||||
self.image_bg_end = @intCast(i);
|
||||
if (bg_end == null and p.z >= bg_limit) {
|
||||
bg_end = @intCast(i);
|
||||
}
|
||||
if (self.image_text_end == 0 and p.z >= 0) {
|
||||
self.image_text_end = @intCast(i);
|
||||
if (text_end == null and p.z >= 0) {
|
||||
text_end = @intCast(i);
|
||||
}
|
||||
}
|
||||
if (self.image_text_end == 0) {
|
||||
self.image_text_end = @intCast(self.image_placements.items.len);
|
||||
}
|
||||
|
||||
self.image_bg_end = bg_end orelse 0;
|
||||
self.image_text_end = text_end orelse self.image_bg_end;
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
@ -1820,6 +1821,21 @@ fn prepKittyPlacement(
|
||||
break :offset_y @intCast(offset_pixels);
|
||||
} else 0;
|
||||
|
||||
// If we specify `rows` then our offset above is in viewport space
|
||||
// and not in the coordinate space of the source image. Without `rows`
|
||||
// that's one and the same.
|
||||
const source_offset_y: u32 = if (p.rows > 0) source_offset_y: {
|
||||
// Determine the scale factor to apply for this row height.
|
||||
const image_height: f64 = @floatFromInt(image.height);
|
||||
const viewport_height: f64 = @floatFromInt(p.rows * self.grid_metrics.cell_height);
|
||||
const scale: f64 = image_height / viewport_height;
|
||||
|
||||
// Apply the scale to the offset
|
||||
const offset_y_f64: f64 = @floatFromInt(offset_y);
|
||||
const source_offset_y_f64: f64 = offset_y_f64 * scale;
|
||||
break :source_offset_y @intFromFloat(@round(source_offset_y_f64));
|
||||
} else offset_y;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
@ -1833,7 +1849,7 @@ fn prepKittyPlacement(
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y + offset_y);
|
||||
const source_y = @min(image.height, p.source_y + source_offset_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
@ -1845,7 +1861,11 @@ fn prepKittyPlacement(
|
||||
|
||||
// Calculate the width/height of our image.
|
||||
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
|
||||
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
|
||||
const dest_height = if (p.rows > 0) rows: {
|
||||
// Clip to the viewport to handle scrolling. offset_y is already in
|
||||
// viewport scale so we can subtract it directly.
|
||||
break :rows (p.rows * self.grid_metrics.cell_height) - offset_y;
|
||||
} else source_height;
|
||||
|
||||
// Accumulate the placement
|
||||
if (image.width > 0 and image.height > 0) {
|
||||
|
@ -952,21 +952,22 @@ fn prepKittyGraphics(
|
||||
}.lessThan,
|
||||
);
|
||||
|
||||
// Find our indices
|
||||
self.image_bg_end = 0;
|
||||
self.image_text_end = 0;
|
||||
// Find our indices. The values are sorted by z so we can find the
|
||||
// first placement out of bounds to find the limits.
|
||||
var bg_end: ?u32 = null;
|
||||
var text_end: ?u32 = null;
|
||||
const bg_limit = std.math.minInt(i32) / 2;
|
||||
for (self.image_placements.items, 0..) |p, i| {
|
||||
if (self.image_bg_end == 0 and p.z >= bg_limit) {
|
||||
self.image_bg_end = @intCast(i);
|
||||
if (bg_end == null and p.z >= bg_limit) {
|
||||
bg_end = @intCast(i);
|
||||
}
|
||||
if (self.image_text_end == 0 and p.z >= 0) {
|
||||
self.image_text_end = @intCast(i);
|
||||
if (text_end == null and p.z >= 0) {
|
||||
text_end = @intCast(i);
|
||||
}
|
||||
}
|
||||
if (self.image_text_end == 0) {
|
||||
self.image_text_end = @intCast(self.image_placements.items.len);
|
||||
}
|
||||
|
||||
self.image_bg_end = bg_end orelse 0;
|
||||
self.image_text_end = text_end orelse self.image_bg_end;
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
@ -1051,6 +1052,21 @@ fn prepKittyPlacement(
|
||||
break :offset_y @intCast(offset_pixels);
|
||||
} else 0;
|
||||
|
||||
// If we specify `rows` then our offset above is in viewport space
|
||||
// and not in the coordinate space of the source image. Without `rows`
|
||||
// that's one and the same.
|
||||
const source_offset_y: u32 = if (p.rows > 0) source_offset_y: {
|
||||
// Determine the scale factor to apply for this row height.
|
||||
const image_height: f64 = @floatFromInt(image.height);
|
||||
const viewport_height: f64 = @floatFromInt(p.rows * self.grid_metrics.cell_height);
|
||||
const scale: f64 = image_height / viewport_height;
|
||||
|
||||
// Apply the scale to the offset
|
||||
const offset_y_f64: f64 = @floatFromInt(offset_y);
|
||||
const source_offset_y_f64: f64 = offset_y_f64 * scale;
|
||||
break :source_offset_y @intFromFloat(@round(source_offset_y_f64));
|
||||
} else offset_y;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
@ -1064,7 +1080,7 @@ fn prepKittyPlacement(
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y + offset_y);
|
||||
const source_y = @min(image.height, p.source_y + source_offset_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
@ -1076,7 +1092,11 @@ fn prepKittyPlacement(
|
||||
|
||||
// Calculate the width/height of our image.
|
||||
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
|
||||
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
|
||||
const dest_height = if (p.rows > 0) rows: {
|
||||
// Clip to the viewport to handle scrolling. offset_y is already in
|
||||
// viewport scale so we can subtract it directly.
|
||||
break :rows (p.rows * self.grid_metrics.cell_height) - offset_y;
|
||||
} else source_height;
|
||||
|
||||
// Accumulate the placement
|
||||
if (image.width > 0 and image.height > 0) {
|
||||
|
@ -2406,9 +2406,19 @@ fn erasePage(self: *PageList, node: *List.Node) void {
|
||||
|
||||
/// Returns the pin for the given point. The pin is NOT tracked so it
|
||||
/// is only valid as long as the pagelist isn't modified.
|
||||
///
|
||||
/// This will return null if the point is out of bounds. The caller
|
||||
/// should clamp the point to the bounds of the coordinate space if
|
||||
/// necessary.
|
||||
pub fn pin(self: *const PageList, pt: point.Point) ?Pin {
|
||||
// getTopLeft is much more expensive than checking the cols bounds
|
||||
// so we do this first.
|
||||
const x = pt.coord().x;
|
||||
if (x >= self.cols) return null;
|
||||
|
||||
// Grab the top left and move to the point.
|
||||
var p = self.getTopLeft(pt).down(pt.coord().y) orelse return null;
|
||||
p.x = pt.coord().x;
|
||||
p.x = x;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
@ -620,7 +620,7 @@ pub fn cursorAbsolute(self: *Screen, x: size.CellCountInt, y: size.CellCountInt)
|
||||
self.cursor.x = x; // Must be set before cursorChangePin
|
||||
self.cursor.y = y;
|
||||
self.cursorChangePin(page_pin);
|
||||
const page_rac = page_pin.rowAndCell();
|
||||
const page_rac = self.cursor.page_pin.rowAndCell();
|
||||
self.cursor.page_row = page_rac.row;
|
||||
self.cursor.page_cell = page_rac.cell;
|
||||
}
|
||||
@ -779,9 +779,15 @@ pub fn cursorDownScroll(self: *Screen) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// This scrolls the active area at and above the cursor. The lines below
|
||||
/// the cursor are not scrolled.
|
||||
/// This scrolls the active area at and above the cursor.
|
||||
/// The lines below the cursor are not scrolled.
|
||||
pub fn cursorScrollAbove(self: *Screen) !void {
|
||||
// We unconditionally mark the cursor row as dirty here because
|
||||
// the cursor always changes page rows inside this function, and
|
||||
// when that happens it can mean the text in the old row needs to
|
||||
// be re-shaped because the cursor splits runs to break ligatures.
|
||||
self.cursor.page_pin.markDirty();
|
||||
|
||||
// If the cursor is on the bottom of the screen, its faster to use
|
||||
// our specialized function for that case.
|
||||
if (self.cursor.y == self.pages.rows - 1) {
|
||||
@ -793,6 +799,14 @@ pub fn cursorScrollAbove(self: *Screen) !void {
|
||||
// Logic below assumes we always have at least one row that isn't moving
|
||||
assert(self.cursor.y < self.pages.rows - 1);
|
||||
|
||||
// Explanation:
|
||||
// We don't actually move everything that's at or above the cursor row,
|
||||
// since this would require us to shift up our ENTIRE scrollback, which
|
||||
// would be ridiculously expensive. Instead, we insert a new row at the
|
||||
// end of the pagelist (`grow()`), and move everything BELOW the cursor
|
||||
// DOWN by one row. This has the same practical result but it's a whole
|
||||
// lot cheaper in 99% of cases.
|
||||
|
||||
const old_pin = self.cursor.page_pin.*;
|
||||
if (try self.pages.grow()) |_| {
|
||||
try self.cursorScrollAboveRotate();
|
||||
@ -803,6 +817,9 @@ pub fn cursorScrollAbove(self: *Screen) !void {
|
||||
// If we're on the last page we can do a very fast path because
|
||||
// all the rows we need to move around are within a single page.
|
||||
|
||||
// Note: we don't need to call cursorChangePin here because
|
||||
// the pin page is the same so there is no accounting to do
|
||||
// for styles or any of that.
|
||||
assert(old_pin.node == self.cursor.page_pin.node);
|
||||
self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
|
||||
|
||||
@ -823,10 +840,6 @@ pub fn cursorScrollAbove(self: *Screen) !void {
|
||||
const page_rac = self.cursor.page_pin.rowAndCell();
|
||||
self.cursor.page_row = page_rac.row;
|
||||
self.cursor.page_cell = page_rac.cell;
|
||||
|
||||
// Note: we don't need to call cursorChangePin here because
|
||||
// the pin page is the same so there is no accounting to do for
|
||||
// styles or any of that.
|
||||
} else {
|
||||
// We didn't grow pages but our cursor isn't on the last page.
|
||||
// In this case we need to do more work because we need to copy
|
||||
@ -863,7 +876,7 @@ pub fn cursorScrollAbove(self: *Screen) !void {
|
||||
}
|
||||
|
||||
fn cursorScrollAboveRotate(self: *Screen) !void {
|
||||
self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
|
||||
self.cursorChangePin(self.cursor.page_pin.down(1).?);
|
||||
|
||||
// Go through each of the pages following our pin, shift all rows
|
||||
// down by one, and copy the last row of the previous page.
|
||||
@ -1763,10 +1776,15 @@ pub fn manualStyleUpdate(self: *Screen) !void {
|
||||
|
||||
// If our new style is the default, just reset to that
|
||||
if (self.cursor.style.default()) {
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_id = style.default_id;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the cursor style ID to prevent weird things from happening
|
||||
// if the page capacity has to be adjusted which would end up calling
|
||||
// manualStyleUpdate again.
|
||||
self.cursor.style_id = style.default_id;
|
||||
|
||||
// After setting the style, we need to update our style map.
|
||||
// Note that we COULD lazily do this in print. We should look into
|
||||
// if that makes a meaningful difference. Our priority is to keep print
|
||||
@ -2068,17 +2086,18 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
|
||||
};
|
||||
|
||||
var page_it = sel_start.pageIterator(.right_down, sel_end);
|
||||
var row_count: usize = 0;
|
||||
while (page_it.next()) |chunk| {
|
||||
const rows = chunk.rows();
|
||||
for (rows, chunk.start..) |row, y| {
|
||||
for (rows, chunk.start.., 0..) |row, y, row_i| {
|
||||
const cells_ptr = row.cells.ptr(chunk.node.data.memory);
|
||||
|
||||
const start_x = if (row_count == 0 or sel_ordered.rectangle)
|
||||
const start_x = if ((row_i == 0 or sel_ordered.rectangle) and
|
||||
sel_start.node == chunk.node)
|
||||
sel_start.x
|
||||
else
|
||||
0;
|
||||
const end_x = if (row_count == rows.len - 1 or sel_ordered.rectangle)
|
||||
const end_x = if ((row_i == rows.len - 1 or sel_ordered.rectangle) and
|
||||
sel_end.node == chunk.node)
|
||||
sel_end.x + 1
|
||||
else
|
||||
self.pages.cols;
|
||||
@ -2133,8 +2152,6 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
|
||||
.x = chunk.node.data.size.cols - 1,
|
||||
});
|
||||
}
|
||||
|
||||
row_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3760,6 +3777,81 @@ test "Screen: cursorAbsolute across pages preserves style" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: cursorAbsolute to page with insufficient capacity" {
|
||||
// This test checks for a very specific edge case
|
||||
// which previously resulted in memory corruption.
|
||||
//
|
||||
// The conditions for this edge case are as such:
|
||||
// - The cursor has an associated style or other managed memory.
|
||||
// - The cursor moves to a different page.
|
||||
// - The new page is at capacity and must have its capacity adjusted.
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 3, 1);
|
||||
defer s.deinit();
|
||||
|
||||
// Scroll down enough to go to another page
|
||||
const start_page = &s.pages.pages.last.?.data;
|
||||
const rem = start_page.capacity.rows;
|
||||
start_page.pauseIntegrityChecks(true);
|
||||
for (0..rem) |_| try s.cursorDownOrScroll();
|
||||
start_page.pauseIntegrityChecks(false);
|
||||
|
||||
const new_page = &s.cursor.page_pin.node.data;
|
||||
|
||||
// We need our page to change for this test to make sense. If this
|
||||
// assertion fails then the bug is in the test: we should be scrolling
|
||||
// above enough for a new page to show up.
|
||||
try testing.expect(start_page != new_page);
|
||||
|
||||
// Add styles to the start page until it reaches capacity.
|
||||
{
|
||||
// Pause integrity checks because they're slow and
|
||||
// we're not testing this, this is just setup.
|
||||
start_page.pauseIntegrityChecks(true);
|
||||
defer start_page.pauseIntegrityChecks(false);
|
||||
defer start_page.assertIntegrity();
|
||||
|
||||
var n: u24 = 1;
|
||||
while (start_page.styles.add(
|
||||
start_page.memory,
|
||||
.{ .bg_color = .{ .rgb = @bitCast(n) } },
|
||||
)) |_| n += 1 else |_| {}
|
||||
}
|
||||
|
||||
// Set a style on the cursor.
|
||||
try s.setAttribute(.{ .bold = {} });
|
||||
{
|
||||
const styleval = new_page.styles.get(
|
||||
new_page.memory,
|
||||
s.cursor.style_id,
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
|
||||
// Go back up into the start page and we should still have that style.
|
||||
s.cursorAbsolute(1, 1);
|
||||
{
|
||||
const cur_page = &s.cursor.page_pin.node.data;
|
||||
// The page we're on now should NOT equal start_page, since its
|
||||
// capacity should have been adjusted, which invalidates our ptr.
|
||||
try testing.expect(start_page != cur_page);
|
||||
// To make sure we DID change pages we check we're not on new_page.
|
||||
try testing.expect(new_page != cur_page);
|
||||
|
||||
const styleval = cur_page.styles.get(
|
||||
cur_page.memory,
|
||||
s.cursor.style_id,
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
|
||||
s.cursor.page_pin.node.data.assertIntegrity();
|
||||
new_page.assertIntegrity();
|
||||
}
|
||||
|
||||
test "Screen: scrolling" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@ -4313,8 +4405,31 @@ test "Screen: scroll above same page" {
|
||||
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
s.cursorAbsolute(0, 1);
|
||||
s.pages.clearDirty();
|
||||
|
||||
// At this point:
|
||||
// +-------------+ ACTIVE
|
||||
// +----------+ : = PAGE 0
|
||||
// 0 |1ABCD00000| | 0
|
||||
// 1 |2EFGH00000| | 1
|
||||
// :^ : : = PIN 0
|
||||
// 2 |3IJKL00000| | 2
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
try s.cursorScrollAbove();
|
||||
|
||||
// +----------+ = PAGE 0
|
||||
// 0 |1ABCD00000|
|
||||
// +-------------+ ACTIVE
|
||||
// 1 |2EFGH00000| | 0
|
||||
// 2 | | | 1
|
||||
// :^ : : = PIN 0
|
||||
// 3 |3IJKL00000| | 2
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
// try s.pages.diagram(std.io.getStdErr().writer());
|
||||
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
@ -4331,10 +4446,11 @@ test "Screen: scroll above same page" {
|
||||
}, cell.content.color_rgb);
|
||||
}
|
||||
|
||||
// Only y=1,2 are dirty because they are the ones that CHANGED contents
|
||||
// (not just scroll).
|
||||
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0 row 1 (active row 0) is dirty because the cursor moved off of it.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0 row 2 (active row 1) is dirty because it was cleared.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
// Page 0 row 3 (active row 2) is dirty because it's new.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
}
|
||||
|
||||
@ -4364,8 +4480,25 @@ test "Screen: scroll above same page but cursor on previous page" {
|
||||
// +----------+ = PAGE 0
|
||||
// ... : :
|
||||
// +-------------+ ACTIVE
|
||||
// 4303 |1A00000000| | 0
|
||||
// 4304 |2B00000000| | 1
|
||||
// 4305 |1A00000000| | 0
|
||||
// 4306 |2B00000000| | 1
|
||||
// :^ : : = PIN 0
|
||||
// 4307 |3C00000000| | 2
|
||||
// +----------+ :
|
||||
// +----------+ : = PAGE 1
|
||||
// 0 |4D00000000| | 3
|
||||
// 1 |5E00000000| | 4
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
try s.cursorScrollAbove();
|
||||
|
||||
// +----------+ = PAGE 0
|
||||
// ... : :
|
||||
// 4305 |1A00000000|
|
||||
// +-------------+ ACTIVE
|
||||
// 4306 |2B00000000| | 0
|
||||
// 4307 | | | 1
|
||||
// :^ : : = PIN 0
|
||||
// +----------+ :
|
||||
// +----------+ : = PAGE 1
|
||||
@ -4375,7 +4508,7 @@ test "Screen: scroll above same page but cursor on previous page" {
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
try s.cursorScrollAbove();
|
||||
// try s.pages.diagram(std.io.getStdErr().writer());
|
||||
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
@ -4393,9 +4526,13 @@ test "Screen: scroll above same page but cursor on previous page" {
|
||||
}, cell.content.color_rgb);
|
||||
}
|
||||
|
||||
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0's penultimate row is dirty because the cursor moved off of it.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// The rest of the rows are dirty because they've been modified or are new.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
|
||||
}
|
||||
|
||||
test "Screen: scroll above same page but cursor on previous page last row" {
|
||||
@ -4424,8 +4561,8 @@ test "Screen: scroll above same page but cursor on previous page last row" {
|
||||
// +----------+ = PAGE 0
|
||||
// ... : :
|
||||
// +-------------+ ACTIVE
|
||||
// 4303 |1A00000000| | 0
|
||||
// 4304 |2B00000000| | 1
|
||||
// 4306 |1A00000000| | 0
|
||||
// 4307 |2B00000000| | 1
|
||||
// :^ : : = PIN 0
|
||||
// +----------+ :
|
||||
// +----------+ : = PAGE 1
|
||||
@ -4439,9 +4576,9 @@ test "Screen: scroll above same page but cursor on previous page last row" {
|
||||
|
||||
// +----------+ = PAGE 0
|
||||
// ... : :
|
||||
// 4303 |1A00000000|
|
||||
// 4306 |1A00000000|
|
||||
// +-------------+ ACTIVE
|
||||
// 4304 |2B00000000| | 0
|
||||
// 4307 |2B00000000| | 0
|
||||
// +----------+ :
|
||||
// +----------+ : = PAGE 1
|
||||
// 0 | | | 1
|
||||
@ -4470,9 +4607,22 @@ test "Screen: scroll above same page but cursor on previous page last row" {
|
||||
}, cell.content.color_rgb);
|
||||
}
|
||||
|
||||
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0's final row is dirty because the cursor moved off of it.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 1's rows are all dirty because every row was moved.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
|
||||
|
||||
// Attempt to clear the style from the cursor and
|
||||
// then assert the integrity of both of our pages.
|
||||
//
|
||||
// This catches a case of memory corruption where the cursor
|
||||
// is moved between pages without accounting for style refs.
|
||||
try s.setAttribute(.{ .reset_bg = {} });
|
||||
s.pages.pages.first.?.data.assertIntegrity();
|
||||
s.pages.pages.last.?.data.assertIntegrity();
|
||||
}
|
||||
|
||||
test "Screen: scroll above creates new page" {
|
||||
@ -4495,8 +4645,34 @@ test "Screen: scroll above creates new page" {
|
||||
|
||||
// Ensure we're still on the first page
|
||||
try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
|
||||
|
||||
// At this point:
|
||||
// +----------+ = PAGE 0
|
||||
// ... : :
|
||||
// +-------------+ ACTIVE
|
||||
// 4305 |1ABCD00000| | 0
|
||||
// 4306 |2EFGH00000| | 1
|
||||
// :^ : : = PIN 0
|
||||
// 4307 |3IJKL00000| | 2
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
try s.cursorScrollAbove();
|
||||
|
||||
// +----------+ = PAGE 0
|
||||
// ... : :
|
||||
// 4305 |1ABCD00000|
|
||||
// +-------------+ ACTIVE
|
||||
// 4306 |2EFGH00000| | 0
|
||||
// 4307 | | | 1
|
||||
// :^ : : = PIN 0
|
||||
// +----------+ :
|
||||
// +----------+ : = PAGE 1
|
||||
// 0 |3IJKL00000| | 2
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
// try s.pages.diagram(std.io.getStdErr().writer());
|
||||
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
@ -4513,9 +4689,11 @@ test "Screen: scroll above creates new page" {
|
||||
}, cell.content.color_rgb);
|
||||
}
|
||||
|
||||
// Only y=1 is dirty because they are the ones that CHANGED contents
|
||||
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0's penultimate row is dirty because the cursor moved off of it.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0's final row is dirty because it was cleared.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
// Page 1's row is dirty because it's new.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
}
|
||||
|
||||
@ -4535,8 +4713,31 @@ test "Screen: scroll above no scrollback bottom of page" {
|
||||
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
s.cursorAbsolute(0, 1);
|
||||
s.pages.clearDirty();
|
||||
|
||||
// At this point:
|
||||
// +-------------+ ACTIVE
|
||||
// +----------+ : = PAGE 0
|
||||
// 0 |1ABCD00000| | 0
|
||||
// 1 |2EFGH00000| | 1
|
||||
// :^ : : = PIN 0
|
||||
// 2 |3IJKL00000| | 2
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
try s.cursorScrollAbove();
|
||||
|
||||
// +----------+ = PAGE 0
|
||||
// 0 |1ABCD00000|
|
||||
// +-------------+ ACTIVE
|
||||
// 1 |2EFGH00000| | 0
|
||||
// 2 | | | 1
|
||||
// :^ : : = PIN 0
|
||||
// 3 |3IJKL00000| | 2
|
||||
// +----------+ :
|
||||
// +-------------+
|
||||
|
||||
//try s.pages.diagram(std.io.getStdErr().writer());
|
||||
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
@ -4553,10 +4754,11 @@ test "Screen: scroll above no scrollback bottom of page" {
|
||||
}, cell.content.color_rgb);
|
||||
}
|
||||
|
||||
// Only y=1,2 are dirty because they are the ones that CHANGED contents
|
||||
// (not just scroll).
|
||||
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0 row 1 (active row 0) is dirty because the cursor moved off of it.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
// Page 0 row 2 (active row 1) is dirty because it was cleared.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
// Page 0 row 3 (active row 2) is dirty because it is new.
|
||||
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
}
|
||||
|
||||
@ -8301,7 +8503,7 @@ test "Screen: selectionString multi-page" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 1, 3, 2048);
|
||||
var s = try init(alloc, 10, 3, 2048);
|
||||
defer s.deinit();
|
||||
|
||||
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||
@ -8313,20 +8515,20 @@ test "Screen: selectionString multi-page" {
|
||||
}
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s.testWriteString("y\ny\ny");
|
||||
try s.testWriteString("123456789\n!@#$%^&*(\n123456789");
|
||||
|
||||
{
|
||||
const sel = Selection.init(
|
||||
s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
|
||||
s.pages.pin(.{ .active = .{ .x = 0, .y = 2 } }).?,
|
||||
s.pages.pin(.{ .active = .{ .x = 2, .y = 2 } }).?,
|
||||
false,
|
||||
);
|
||||
const contents = try s.selectionString(alloc, .{
|
||||
.sel = sel,
|
||||
.trim = false,
|
||||
.trim = true,
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = "y\ny\ny";
|
||||
const expected = "123456789\n!@#$%^&*(\n123";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +451,7 @@ pub fn adjust(
|
||||
|
||||
test "Selection: adjust right" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A1234\nB5678\nC1234\nD5678");
|
||||
|
||||
@ -518,7 +518,7 @@ test "Selection: adjust right" {
|
||||
|
||||
test "Selection: adjust left" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A1234\nB5678\nC1234\nD5678");
|
||||
|
||||
@ -567,7 +567,7 @@ test "Selection: adjust left" {
|
||||
|
||||
test "Selection: adjust left skips blanks" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A1234\nB5678\nC12\nD56");
|
||||
|
||||
@ -616,7 +616,7 @@ test "Selection: adjust left skips blanks" {
|
||||
|
||||
test "Selection: adjust up" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A\nB\nC\nD\nE");
|
||||
|
||||
@ -663,7 +663,7 @@ test "Selection: adjust up" {
|
||||
|
||||
test "Selection: adjust down" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A\nB\nC\nD\nE");
|
||||
|
||||
@ -702,7 +702,7 @@ test "Selection: adjust down" {
|
||||
.y = 1,
|
||||
} }, s.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 4,
|
||||
.x = 9,
|
||||
.y = 4,
|
||||
} }, s.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
@ -710,7 +710,7 @@ test "Selection: adjust down" {
|
||||
|
||||
test "Selection: adjust down with not full screen" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A\nB\nC");
|
||||
|
||||
@ -730,7 +730,7 @@ test "Selection: adjust down with not full screen" {
|
||||
.y = 1,
|
||||
} }, s.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 4,
|
||||
.x = 9,
|
||||
.y = 2,
|
||||
} }, s.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
@ -738,7 +738,7 @@ test "Selection: adjust down with not full screen" {
|
||||
|
||||
test "Selection: adjust home" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A\nB\nC");
|
||||
|
||||
@ -766,7 +766,7 @@ test "Selection: adjust home" {
|
||||
|
||||
test "Selection: adjust end with not full screen" {
|
||||
const testing = std.testing;
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A\nB\nC");
|
||||
|
||||
@ -786,7 +786,7 @@ test "Selection: adjust end with not full screen" {
|
||||
.y = 0,
|
||||
} }, s.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 4,
|
||||
.x = 9,
|
||||
.y = 2,
|
||||
} }, s.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
@ -1110,7 +1110,7 @@ test "Selection: order, rectangle" {
|
||||
test "topLeft" {
|
||||
const testing = std.testing;
|
||||
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
// forward
|
||||
@ -1173,7 +1173,7 @@ test "topLeft" {
|
||||
test "bottomRight" {
|
||||
const testing = std.testing;
|
||||
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
// forward
|
||||
@ -1236,7 +1236,7 @@ test "bottomRight" {
|
||||
test "ordered" {
|
||||
const testing = std.testing;
|
||||
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
// forward
|
||||
@ -1317,7 +1317,7 @@ test "ordered" {
|
||||
test "Selection: contains" {
|
||||
const testing = std.testing;
|
||||
|
||||
var s = try Screen.init(testing.allocator, 5, 10, 0);
|
||||
var s = try Screen.init(testing.allocator, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
const sel = Selection.init(
|
||||
@ -1350,13 +1350,13 @@ test "Selection: contains" {
|
||||
{
|
||||
const sel = Selection.init(
|
||||
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
|
||||
s.pages.pin(.{ .screen = .{ .x = 10, .y = 1 } }).?,
|
||||
s.pages.pin(.{ .screen = .{ .x = 8, .y = 1 } }).?,
|
||||
false,
|
||||
);
|
||||
|
||||
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
|
||||
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?));
|
||||
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 12, .y = 1 } }).?));
|
||||
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 9, .y = 1 } }).?));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ pub const Name = enum(u8) {
|
||||
};
|
||||
|
||||
/// RGB
|
||||
pub const RGB = struct {
|
||||
pub const RGB = packed struct(u24) {
|
||||
r: u8 = 0,
|
||||
g: u8 = 0,
|
||||
b: u8 = 0,
|
||||
@ -155,9 +155,9 @@ pub const RGB = struct {
|
||||
return 0.299 * (r_f64 / 255) + 0.587 * (g_f64 / 255) + 0.114 * (b_f64 / 255);
|
||||
}
|
||||
|
||||
test "size" {
|
||||
try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB));
|
||||
try std.testing.expectEqual(@as(usize, 3), @sizeOf(RGB));
|
||||
comptime {
|
||||
assert(@bitSizeOf(RGB) == 24);
|
||||
assert(@sizeOf(RGB) == 4);
|
||||
}
|
||||
|
||||
/// Parse a color from a floating point intensity value.
|
||||
|
@ -158,6 +158,12 @@ pub const Command = union(enum) {
|
||||
/// End a hyperlink (OSC 8)
|
||||
hyperlink_end: void,
|
||||
|
||||
/// Set progress state (OSC 9;4)
|
||||
progress: struct {
|
||||
state: ProgressState,
|
||||
progress: ?u8 = null,
|
||||
},
|
||||
|
||||
pub const ColorKind = union(enum) {
|
||||
palette: u8,
|
||||
foreground,
|
||||
@ -173,6 +179,14 @@ pub const Command = union(enum) {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ProgressState = enum {
|
||||
remove,
|
||||
set,
|
||||
@"error",
|
||||
indeterminate,
|
||||
pause,
|
||||
};
|
||||
};
|
||||
|
||||
/// The terminator used to end an OSC command. For OSC commands that demand
|
||||
@ -322,6 +336,27 @@ pub const Parser = struct {
|
||||
// https://sw.kovidgoyal.net/kitty/color-stack/#id1
|
||||
kitty_color_protocol_key,
|
||||
kitty_color_protocol_value,
|
||||
|
||||
// OSC 9 is used by ConEmu and iTerm2 for different things.
|
||||
// iTerm2 uses it to post a notification[1].
|
||||
// ConEmu uses it to implement many custom functions[2].
|
||||
//
|
||||
// Some Linux applications (namely systemd and flatpak) have
|
||||
// adopted the ConEmu implementation but this causes bogus
|
||||
// notifications on iTerm2 compatible terminal emulators.
|
||||
//
|
||||
// Ghostty supports both by disallowing ConEmu-specific commands
|
||||
// from being shown as desktop notifications.
|
||||
//
|
||||
// [1]: https://iterm2.com/documentation-escape-codes.html
|
||||
// [2]: https://conemu.github.io/en/AnsiEscapeCodes.html#OSC_Operating_system_commands
|
||||
osc_9,
|
||||
|
||||
// ConEmu specific substates
|
||||
conemu_progress_prestate,
|
||||
conemu_progress_state,
|
||||
conemu_progress_prevalue,
|
||||
conemu_progress_value,
|
||||
};
|
||||
|
||||
/// This must be called to clean up any allocated memory.
|
||||
@ -735,18 +770,103 @@ pub const Parser = struct {
|
||||
|
||||
.@"9" => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .show_desktop_notification = .{
|
||||
.title = "",
|
||||
.body = undefined,
|
||||
} };
|
||||
|
||||
self.temp_state = .{ .str = &self.command.show_desktop_notification.body };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.state = .string;
|
||||
self.state = .osc_9;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.osc_9 => switch (c) {
|
||||
'4' => {
|
||||
self.state = .conemu_progress_prestate;
|
||||
},
|
||||
|
||||
// Todo: parse out other ConEmu operating system commands.
|
||||
// Even if we don't support them we probably don't want
|
||||
// them showing up as desktop notifications.
|
||||
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_prestate => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .progress = .{
|
||||
.state = undefined,
|
||||
} };
|
||||
self.state = .conemu_progress_state;
|
||||
},
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_state => switch (c) {
|
||||
'0' => {
|
||||
self.command.progress.state = .remove;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
self.complete = true;
|
||||
},
|
||||
'1' => {
|
||||
self.command.progress.state = .set;
|
||||
self.command.progress.progress = 0;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
'2' => {
|
||||
self.command.progress.state = .@"error";
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
'3' => {
|
||||
self.command.progress.state = .indeterminate;
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
'4' => {
|
||||
self.command.progress.state = .pause;
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_prevalue => switch (c) {
|
||||
';' => {
|
||||
self.state = .conemu_progress_value;
|
||||
},
|
||||
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_value => switch (c) {
|
||||
'0'...'9' => value: {
|
||||
// No matter what substate we're in, a number indicates
|
||||
// a completed ConEmu progress command.
|
||||
self.complete = true;
|
||||
|
||||
// If we aren't a set substate, then we don't care
|
||||
// about the value.
|
||||
const p = &self.command.progress;
|
||||
if (p.state != .set and p.state != .@"error" and p.state != .pause) break :value;
|
||||
|
||||
if (p.state == .set)
|
||||
assert(p.progress != null)
|
||||
else if (p.progress == null)
|
||||
p.progress = 0;
|
||||
|
||||
// If we're over 100% we're done.
|
||||
if (p.progress.? >= 100) break :value;
|
||||
|
||||
// If we're over 10 then any new digit forces us to
|
||||
// be 100.
|
||||
if (p.progress.? >= 10)
|
||||
p.progress = 100
|
||||
else {
|
||||
const d = std.fmt.charToDigit(c, 10) catch 0;
|
||||
p.progress = @min(100, (p.progress.? * 10) + d);
|
||||
}
|
||||
},
|
||||
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.query_fg_color => switch (c) {
|
||||
'?' => {
|
||||
self.command = .{ .report_color = .{ .kind = .foreground } };
|
||||
@ -901,6 +1021,16 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn showDesktopNotification(self: *Parser) void {
|
||||
self.command = .{ .show_desktop_notification = .{
|
||||
.title = "",
|
||||
.body = undefined,
|
||||
} };
|
||||
|
||||
self.temp_state = .{ .str = &self.command.show_desktop_notification.body };
|
||||
self.state = .string;
|
||||
}
|
||||
|
||||
fn prepAllocableString(self: *Parser) void {
|
||||
assert(self.buf_dynamic == null);
|
||||
|
||||
@ -1532,6 +1662,174 @@ test "OSC: show desktop notification with title" {
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;1;100";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .set);
|
||||
try testing.expect(cmd.progress.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set overflow" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;1;900";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .set);
|
||||
try testing.expect(cmd.progress.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set single digit" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;1;9";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .set);
|
||||
try testing.expect(cmd.progress.progress == 9);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set double digit" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;1;94";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .set);
|
||||
try testing.expect(cmd.progress.progress == 94);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set extra semicolon triggers desktop notification" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;1;100;";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.title, "");
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "4;1;100;");
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove with no progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;0;";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .remove);
|
||||
try testing.expect(cmd.progress.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove ignores progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;0;100";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .remove);
|
||||
try testing.expect(cmd.progress.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove extra semicolon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;0;100;";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.title, "");
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "4;0;100;");
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress error" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;2";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .@"error");
|
||||
try testing.expect(cmd.progress.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress error with progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;2;100";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .@"error");
|
||||
try testing.expect(cmd.progress.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress pause" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;4";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .pause);
|
||||
try testing.expect(cmd.progress.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress pause with progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;4;100";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .pause);
|
||||
try testing.expect(cmd.progress.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: empty param" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -103,6 +103,12 @@ pub fn RefCountedSet(
|
||||
/// unlikely. Roughly a (1/table_cap)^32 -- with any normal
|
||||
/// table capacity that is so unlikely that it's not worth
|
||||
/// handling.
|
||||
///
|
||||
/// However, that assumes a uniform hash function, which
|
||||
/// is not guaranteed and can be subverted with a crafted
|
||||
/// input. We handle this gracefully by returning an error
|
||||
/// anywhere where we're about to insert if there's any
|
||||
/// item with a PSL in the last slot of the stats array.
|
||||
psl_stats: [32]Id = [_]Id{0} ** 32,
|
||||
|
||||
/// The backing store of items
|
||||
@ -237,6 +243,16 @@ pub fn RefCountedSet(
|
||||
return id;
|
||||
}
|
||||
|
||||
// While it should be statistically impossible to exceed the
|
||||
// bounds of `psl_stats`, the hash function is not perfect and
|
||||
// in such a case we want to remain stable. If we're about to
|
||||
// insert an item and there's something with a PSL of `len - 1`,
|
||||
// we may end up with a PSL of `len` which would exceed the bounds.
|
||||
// In such a case, we claim to be out of memory.
|
||||
if (self.psl_stats[self.psl_stats.len - 1] > 0) {
|
||||
return AddError.OutOfMemory;
|
||||
}
|
||||
|
||||
// If the item doesn't exist, we need an available ID.
|
||||
if (self.next_id >= self.layout.cap) {
|
||||
// Arbitrarily chosen, threshold for rehashing.
|
||||
@ -284,6 +300,11 @@ pub fn RefCountedSet(
|
||||
|
||||
if (id < self.next_id) {
|
||||
if (items[id].meta.ref == 0) {
|
||||
// See comment in `addContext` for details.
|
||||
if (self.psl_stats[self.psl_stats.len - 1] > 0) {
|
||||
return AddError.OutOfMemory;
|
||||
}
|
||||
|
||||
self.deleteItem(base, id, ctx);
|
||||
|
||||
const added_id = self.upsert(base, value, id, ctx);
|
||||
@ -419,7 +440,7 @@ pub fn RefCountedSet(
|
||||
|
||||
if (item.meta.bucket > self.layout.table_cap) return;
|
||||
|
||||
if (table[item.meta.bucket] != id) return;
|
||||
assert(table[item.meta.bucket] == id);
|
||||
|
||||
if (comptime @hasDecl(Context, "deleted")) {
|
||||
// Inform the context struct that we're
|
||||
@ -449,6 +470,8 @@ pub fn RefCountedSet(
|
||||
}
|
||||
|
||||
table[p] = 0;
|
||||
|
||||
self.assertIntegrity(base, ctx);
|
||||
}
|
||||
|
||||
/// Find an item in the table and return its ID.
|
||||
@ -463,7 +486,7 @@ pub fn RefCountedSet(
|
||||
const hash: u64 = ctx.hash(value);
|
||||
|
||||
for (0..self.max_psl + 1) |i| {
|
||||
const p: usize = @intCast((hash + i) & self.layout.table_mask);
|
||||
const p: usize = @intCast((hash +% i) & self.layout.table_mask);
|
||||
const id = table[p];
|
||||
|
||||
// Empty bucket, our item cannot have probed to
|
||||
@ -538,11 +561,10 @@ pub fn RefCountedSet(
|
||||
var held_id: Id = new_id;
|
||||
var held_item: *Item = &new_item;
|
||||
|
||||
var chosen_p: ?Id = null;
|
||||
var chosen_id: Id = new_id;
|
||||
|
||||
for (0..self.layout.table_cap - 1) |i| {
|
||||
const p: Id = @intCast((hash + i) & self.layout.table_mask);
|
||||
const p: Id = @intCast((hash +% i) & self.layout.table_mask);
|
||||
const id = table[p];
|
||||
|
||||
// Empty bucket, put our held item in to it and break.
|
||||
@ -557,7 +579,9 @@ pub fn RefCountedSet(
|
||||
const item = &items[id];
|
||||
|
||||
// If there's a dead item then we resurrect it
|
||||
// for our value so that we can re-use its ID.
|
||||
// for our value so that we can re-use its ID,
|
||||
// unless its ID is greater than the one we're
|
||||
// given (i.e. prefer smaller IDs).
|
||||
if (item.meta.ref == 0) {
|
||||
if (comptime @hasDecl(Context, "deleted")) {
|
||||
// Inform the context struct that we're
|
||||
@ -565,40 +589,41 @@ pub fn RefCountedSet(
|
||||
ctx.deleted(item.value);
|
||||
}
|
||||
|
||||
chosen_id = id;
|
||||
|
||||
held_item.meta.bucket = p;
|
||||
// Reap the dead item.
|
||||
self.psl_stats[item.meta.psl] -= 1;
|
||||
item.* = .{};
|
||||
|
||||
// Only resurrect this item if it has a
|
||||
// smaller id than the one we were given.
|
||||
if (id < new_id) chosen_id = id;
|
||||
|
||||
// Put the currently held item in to the
|
||||
// bucket of the item that we just reaped.
|
||||
table[p] = held_id;
|
||||
held_item.meta.bucket = p;
|
||||
self.psl_stats[held_item.meta.psl] += 1;
|
||||
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||
|
||||
// If we're not still holding our new item then we
|
||||
// need to make sure that we put the re-used ID in
|
||||
// the right place, where we previously put new_id.
|
||||
if (chosen_p) |c| {
|
||||
table[c] = id;
|
||||
table[p] = held_id;
|
||||
} else {
|
||||
// If we're still holding our new item then we
|
||||
// don't actually have to do anything, because
|
||||
// the table already has the correct ID here.
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// This item has a lower PSL, swap it out with our held item.
|
||||
if (item.meta.psl < held_item.meta.psl) {
|
||||
if (held_id == new_id) {
|
||||
chosen_p = p;
|
||||
new_item.meta.bucket = p;
|
||||
}
|
||||
|
||||
// If this item has a lower PSL, or has equal PSL and lower ref
|
||||
// count, then we swap it out with our held item. By doing this,
|
||||
// items with high reference counts are prioritized for earlier
|
||||
// placement. The assumption is that an item which has a higher
|
||||
// reference count will be accessed more frequently, so we want
|
||||
// to minimize the time it takes to find it.
|
||||
if (item.meta.psl < held_item.meta.psl or
|
||||
item.meta.psl == held_item.meta.psl and
|
||||
item.meta.ref < held_item.meta.ref)
|
||||
{
|
||||
// Put our held item in the bucket.
|
||||
table[p] = held_id;
|
||||
items[held_id].meta.bucket = p;
|
||||
held_item.meta.bucket = p;
|
||||
self.psl_stats[held_item.meta.psl] += 1;
|
||||
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||
|
||||
// Pick up the item that has a lower PSL.
|
||||
held_id = id;
|
||||
held_item = item;
|
||||
self.psl_stats[item.meta.psl] -= 1;
|
||||
@ -608,8 +633,60 @@ pub fn RefCountedSet(
|
||||
held_item.meta.psl += 1;
|
||||
}
|
||||
|
||||
// Our chosen ID may have changed if we decided
|
||||
// to re-use a dead item's ID, so we make sure
|
||||
// the chosen bucket contains the correct ID.
|
||||
table[new_item.meta.bucket] = chosen_id;
|
||||
|
||||
// Finally place our new item in to our array.
|
||||
items[chosen_id] = new_item;
|
||||
|
||||
self.assertIntegrity(base, ctx);
|
||||
|
||||
return chosen_id;
|
||||
}
|
||||
|
||||
fn assertIntegrity(
|
||||
self: *const Self,
|
||||
base: anytype,
|
||||
ctx: Context,
|
||||
) void {
|
||||
// Disabled because this is excessively slow, only enable
|
||||
// if debugging a RefCountedSet issue or modifying its logic.
|
||||
if (false and std.debug.runtime_safety) {
|
||||
const table = self.table.ptr(base);
|
||||
const items = self.items.ptr(base);
|
||||
|
||||
var psl_stats: [32]Id = [_]Id{0} ** 32;
|
||||
|
||||
for (items[0..self.layout.cap], 0..) |item, id| {
|
||||
if (item.meta.bucket < std.math.maxInt(Id)) {
|
||||
assert(table[item.meta.bucket] == id);
|
||||
psl_stats[item.meta.psl] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
std.testing.expectEqualSlices(Id, &psl_stats, &self.psl_stats) catch assert(false);
|
||||
|
||||
assert(std.mem.eql(Id, &psl_stats, &self.psl_stats));
|
||||
|
||||
psl_stats = [_]Id{0} ** 32;
|
||||
|
||||
for (table[0..self.layout.table_cap], 0..) |id, bucket| {
|
||||
const item = items[id];
|
||||
if (item.meta.bucket < std.math.maxInt(Id)) {
|
||||
assert(item.meta.bucket == bucket);
|
||||
|
||||
const hash: u64 = ctx.hash(item.value);
|
||||
const p: usize = @intCast((hash +% item.meta.psl) & self.layout.table_mask);
|
||||
assert(p == bucket);
|
||||
|
||||
psl_stats[item.meta.psl] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
std.testing.expectEqualSlices(Id, &psl_stats, &self.psl_stats) catch assert(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -601,30 +601,37 @@ pub fn Stream(comptime Handler: type) type {
|
||||
// Cursor Tabulation Control
|
||||
'W' => {
|
||||
switch (input.params.len) {
|
||||
0 => if (input.intermediates.len == 1 and input.intermediates[0] == '?') {
|
||||
if (@hasDecl(T, "tabReset"))
|
||||
try self.handler.tabReset()
|
||||
else
|
||||
log.warn("unimplemented tab reset callback: {}", .{input});
|
||||
},
|
||||
0 => if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{input}),
|
||||
|
||||
1 => switch (input.params[0]) {
|
||||
0 => if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{input}),
|
||||
1 => if (input.intermediates.len == 1 and input.intermediates[0] == '?') {
|
||||
if (input.params[0] == 5) {
|
||||
if (@hasDecl(T, "tabReset"))
|
||||
try self.handler.tabReset()
|
||||
else
|
||||
log.warn("unimplemented tab reset callback: {}", .{input});
|
||||
} else log.warn("invalid cursor tabulation control: {}", .{input});
|
||||
} else {
|
||||
switch (input.params[0]) {
|
||||
0 => if (@hasDecl(T, "tabSet"))
|
||||
try self.handler.tabSet()
|
||||
else
|
||||
log.warn("unimplemented tab set callback: {}", .{input}),
|
||||
|
||||
2 => if (@hasDecl(T, "tabClear"))
|
||||
try self.handler.tabClear(.current)
|
||||
else
|
||||
log.warn("unimplemented tab clear callback: {}", .{input}),
|
||||
2 => if (@hasDecl(T, "tabClear"))
|
||||
try self.handler.tabClear(.current)
|
||||
else
|
||||
log.warn("unimplemented tab clear callback: {}", .{input}),
|
||||
|
||||
5 => if (@hasDecl(T, "tabClear"))
|
||||
try self.handler.tabClear(.all)
|
||||
else
|
||||
log.warn("unimplemented tab clear callback: {}", .{input}),
|
||||
5 => if (@hasDecl(T, "tabClear"))
|
||||
try self.handler.tabClear(.all)
|
||||
else
|
||||
log.warn("unimplemented tab clear callback: {}", .{input}),
|
||||
|
||||
else => {},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
|
||||
else => {},
|
||||
@ -1447,6 +1454,10 @@ pub fn Stream(comptime Handler: type) type {
|
||||
return;
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.progress => {
|
||||
log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
}
|
||||
|
||||
// Fall through for when we don't have a handler.
|
||||
@ -2327,3 +2338,58 @@ test "stream: CSI t pop title with index" {
|
||||
.index = 5,
|
||||
}, s.handler.op.?);
|
||||
}
|
||||
|
||||
test "stream CSI W clear tab stops" {
|
||||
const H = struct {
|
||||
op: ?csi.TabClear = null,
|
||||
|
||||
pub fn tabClear(self: *@This(), op: csi.TabClear) !void {
|
||||
self.op = op;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Stream(H) = .{ .handler = .{} };
|
||||
|
||||
try s.nextSlice("\x1b[2W");
|
||||
try testing.expectEqual(csi.TabClear.current, s.handler.op.?);
|
||||
|
||||
try s.nextSlice("\x1b[5W");
|
||||
try testing.expectEqual(csi.TabClear.all, s.handler.op.?);
|
||||
}
|
||||
|
||||
test "stream CSI W tab set" {
|
||||
const H = struct {
|
||||
called: bool = false,
|
||||
|
||||
pub fn tabSet(self: *@This()) !void {
|
||||
self.called = true;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Stream(H) = .{ .handler = .{} };
|
||||
|
||||
try s.nextSlice("\x1b[W");
|
||||
try testing.expect(s.handler.called);
|
||||
|
||||
s.handler.called = false;
|
||||
try s.nextSlice("\x1b[0W");
|
||||
try testing.expect(s.handler.called);
|
||||
}
|
||||
|
||||
test "stream CSI ? W reset tab stops" {
|
||||
const H = struct {
|
||||
reset: bool = false,
|
||||
|
||||
pub fn tabReset(self: *@This()) !void {
|
||||
self.reset = true;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Stream(H) = .{ .handler = .{} };
|
||||
|
||||
try s.nextSlice("\x1b[?2W");
|
||||
try testing.expect(!s.handler.reset);
|
||||
|
||||
try s.nextSlice("\x1b[?5W");
|
||||
try testing.expect(s.handler.reset);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const Offset = size.Offset;
|
||||
const OffsetBuf = size.OffsetBuf;
|
||||
const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet;
|
||||
|
||||
const Wyhash = std.hash.Wyhash;
|
||||
const XxHash3 = std.hash.XxHash3;
|
||||
const autoHash = std.hash.autoHash;
|
||||
|
||||
/// The unique identifier for a style. This is at most the number of cells
|
||||
@ -27,7 +27,9 @@ pub const Style = struct {
|
||||
|
||||
/// On/off attributes that don't require much bit width so we use
|
||||
/// a packed struct to make this take up significantly less space.
|
||||
flags: packed struct {
|
||||
flags: Flags = .{},
|
||||
|
||||
const Flags = packed struct(u16) {
|
||||
bold: bool = false,
|
||||
italic: bool = false,
|
||||
faint: bool = false,
|
||||
@ -37,16 +39,23 @@ pub const Style = struct {
|
||||
strikethrough: bool = false,
|
||||
overline: bool = false,
|
||||
underline: sgr.Attribute.Underline = .none,
|
||||
} = .{},
|
||||
_padding: u5 = 0,
|
||||
};
|
||||
|
||||
/// The color for an SGR attribute. A color can come from multiple
|
||||
/// sources so we use this to track the source plus color value so that
|
||||
/// we can properly react to things like palette changes.
|
||||
pub const Color = union(enum) {
|
||||
pub const Color = union(Tag) {
|
||||
none: void,
|
||||
palette: u8,
|
||||
rgb: color.RGB,
|
||||
|
||||
const Tag = enum(u8) {
|
||||
none,
|
||||
palette,
|
||||
rgb,
|
||||
};
|
||||
|
||||
/// Formatting to make debug logs easier to read
|
||||
/// by only including non-default attributes.
|
||||
pub fn format(
|
||||
@ -78,7 +87,10 @@ pub const Style = struct {
|
||||
|
||||
/// True if the style is equal to another style.
|
||||
pub fn eql(self: Style, other: Style) bool {
|
||||
return std.meta.eql(self, other);
|
||||
const packed_self = PackedStyle.fromStyle(self);
|
||||
const packed_other = PackedStyle.fromStyle(other);
|
||||
// TODO: in Zig 0.14, equating packed structs is allowed. Remove this work around.
|
||||
return @as(u128, @bitCast(packed_self)) == @as(u128, @bitCast(packed_other));
|
||||
}
|
||||
|
||||
/// Returns the bg color for a cell with this style given the cell
|
||||
@ -230,16 +242,84 @@ pub const Style = struct {
|
||||
_ = try writer.write(" }");
|
||||
}
|
||||
|
||||
/// `PackedStyle` represents the same data as `Style` but without padding,
|
||||
/// which is necessary for hashing via re-interpretation of the underlying
|
||||
/// bytes.
|
||||
///
|
||||
/// `Style` is still preferred for everything else as it has type-safety
|
||||
/// when using the `Color` tagged union.
|
||||
///
|
||||
/// Empirical testing shows that storing all of the tags first and then the
|
||||
/// data provides a better layout for serializing into and is faster on
|
||||
/// benchmarks.
|
||||
const PackedStyle = packed struct(u128) {
|
||||
tags: packed struct {
|
||||
fg: Color.Tag,
|
||||
bg: Color.Tag,
|
||||
underline: Color.Tag,
|
||||
},
|
||||
data: packed struct {
|
||||
fg: Data,
|
||||
bg: Data,
|
||||
underline: Data,
|
||||
},
|
||||
flags: Flags,
|
||||
_padding: u16 = 0,
|
||||
|
||||
/// After https://github.com/ziglang/zig/issues/19754 is implemented,
|
||||
/// it will be an compiler-error to have packed union fields of
|
||||
/// differing size.
|
||||
///
|
||||
/// For now we just need to be careful not to accidentally introduce
|
||||
/// padding.
|
||||
const Data = packed union {
|
||||
none: u24,
|
||||
palette: packed struct(u24) {
|
||||
idx: u8,
|
||||
_padding: u16 = 0,
|
||||
},
|
||||
rgb: color.RGB,
|
||||
|
||||
fn fromColor(c: Color) Data {
|
||||
return switch (c) {
|
||||
inline else => |v, t| @unionInit(
|
||||
Data,
|
||||
@tagName(t),
|
||||
switch (t) {
|
||||
.none => 0,
|
||||
.palette => .{ .idx = v },
|
||||
.rgb => v,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn fromStyle(style: Style) PackedStyle {
|
||||
return .{
|
||||
.tags = .{
|
||||
.fg = std.meta.activeTag(style.fg_color),
|
||||
.bg = std.meta.activeTag(style.bg_color),
|
||||
.underline = std.meta.activeTag(style.underline_color),
|
||||
},
|
||||
.data = .{
|
||||
.fg = Data.fromColor(style.fg_color),
|
||||
.bg = Data.fromColor(style.bg_color),
|
||||
.underline = Data.fromColor(style.underline_color),
|
||||
},
|
||||
.flags = style.flags,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn hash(self: *const Style) u64 {
|
||||
var hasher = Wyhash.init(0);
|
||||
autoHash(&hasher, self.*);
|
||||
return hasher.final();
|
||||
const packed_style = PackedStyle.fromStyle(self.*);
|
||||
return XxHash3.hash(0, std.mem.asBytes(&packed_style));
|
||||
}
|
||||
|
||||
test {
|
||||
// The size of the struct so we can be aware of changes.
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(@as(usize, 14), @sizeOf(Style));
|
||||
comptime {
|
||||
assert(@sizeOf(PackedStyle) == 16);
|
||||
assert(std.meta.hasUniqueRepresentation(PackedStyle));
|
||||
}
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user