From 10e37a3deefcb3e433f4dfa77336cafdd363edc2 Mon Sep 17 00:00:00 2001 From: Kyaw Date: Sun, 24 Nov 2024 17:08:07 +0700 Subject: [PATCH 01/62] config: support loading from "Application Support" directory on macOS --- src/apprt/glfw.zig | 2 +- src/config/Config.zig | 43 ++++++++++++++++++++++---------- src/os/macos.zig | 54 ++++++++++++++++++++++++++++++++++++++++ src/os/macos_version.zig | 21 ---------------- src/os/main.zig | 3 +-- 5 files changed, 86 insertions(+), 37 deletions(-) create mode 100644 src/os/macos.zig delete mode 100644 src/os/macos_version.zig diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index e793615d5..bf4c44ad0 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -724,7 +724,7 @@ pub const Surface = struct { /// Set the shape of the cursor. fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void { if ((comptime builtin.target.isDarwin()) and - !internal_os.macosVersionAtLeast(13, 0, 0)) + !internal_os.macos.isAtLeastVersion(13, 0, 0)) { // We only set our cursor if we're NOT on Mac, or if we are then the // macOS version is >= 13 (Ventura). On prior versions, glfw crashes diff --git a/src/config/Config.zig b/src/config/Config.zig index 55cd55606..e2843508a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1809,10 +1809,15 @@ pub fn deinit(self: *Config) void { /// Load the configuration according to the default rules: /// /// 1. Defaults -/// 2. XDG Config File +/// 2. Configuration Files /// 3. CLI flags /// 4. Recursively defined configuration files /// +/// Configuration files are loaded in the follow order: +/// +/// 1. XDG Config File +/// 2. "Application Support" Config File on macOS +/// pub fn load(alloc_gpa: Allocator) !Config { var result = try default(alloc_gpa); errdefer result.deinit(); @@ -2394,25 +2399,37 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { try self.expandPaths(std.fs.path.dirname(path).?); } -/// Load the configuration from the default configuration file. The default -/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. -pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { - const config_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); - defer alloc.free(config_path); - - self.loadFile(alloc, config_path) catch |err| switch (err) { +/// Load optional configuration file from `path`. All errors are ignored. +pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void { + self.loadFile(alloc, path) catch |err| switch (err) { error.FileNotFound => std.log.info( - "homedir config not found, not loading path={s}", - .{config_path}, + "optional config file not found, not loading path={s}", + .{path}, ), - else => std.log.warn( - "error reading config file, not loading err={} path={s}", - .{ err, config_path }, + "error reading optional config file, not loading err={} path={s}", + .{ err, path }, ), }; } +/// Load configurations from the default configuration files. The default +/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. +/// +/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` +/// is also loaded. +pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { + const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); + defer alloc.free(xdg_path); + self.loadOptionalFile(alloc, xdg_path); + + if (builtin.os.tag == .macos) { + const app_support_path = try internal_os.macos.getAppSupportDir(alloc, "config"); + defer alloc.free(app_support_path); + self.loadOptionalFile(alloc, app_support_path); + } +} + /// Load and parse the CLI args. pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { diff --git a/src/os/macos.zig b/src/os/macos.zig new file mode 100644 index 000000000..62ba87c5a --- /dev/null +++ b/src/os/macos.zig @@ -0,0 +1,54 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const objc = @import("objc"); +const Allocator = std.mem.Allocator; + +pub const NSOperatingSystemVersion = extern struct { + major: i64, + minor: i64, + patch: i64, +}; + +/// Verifies that the running macOS system version is at least the given version. +pub fn isAtLeastVersion(major: i64, minor: i64, patch: i64) bool { + assert(builtin.target.isDarwin()); + + const NSProcessInfo = objc.getClass("NSProcessInfo").?; + const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); + return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{ + NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch }, + }); +} + +pub const NSSearchPathDirectory = enum(c_ulong) { + NSApplicationSupportDirectory = 14, +}; + +pub const NSSearchPathDomainMask = enum(c_ulong) { + NSUserDomainMask = 1, +}; + +pub fn getAppSupportDir(alloc: Allocator, sub_path: []const u8) ![]u8 { + assert(builtin.target.isDarwin()); + + const err: ?*anyopaque = undefined; + const NSFileManager = objc.getClass("NSFileManager").?; + const manager = NSFileManager.msgSend(objc.Object, objc.sel("defaultManager"), .{}); + const url = manager.msgSend( + objc.Object, + objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"), + .{ + NSSearchPathDirectory.NSApplicationSupportDirectory, + NSSearchPathDomainMask.NSUserDomainMask, + @as(?*anyopaque, null), + true, + &err, + }, + ); + const path = url.getProperty(objc.Object, "path"); + const c_str = path.getProperty([*:0]const u8, "UTF8String"); + const app_support_dir = std.mem.sliceTo(c_str, 0); + + return try std.fs.path.join(alloc, &.{ app_support_dir, "com.mitchellh.ghostty", sub_path }); +} diff --git a/src/os/macos_version.zig b/src/os/macos_version.zig deleted file mode 100644 index e0b21560e..000000000 --- a/src/os/macos_version.zig +++ /dev/null @@ -1,21 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const objc = @import("objc"); - -/// Verifies that the running macOS system version is at least the given version. -pub fn macosVersionAtLeast(major: i64, minor: i64, patch: i64) bool { - assert(builtin.target.isDarwin()); - - const NSProcessInfo = objc.getClass("NSProcessInfo").?; - const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); - return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{ - NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch }, - }); -} - -pub const NSOperatingSystemVersion = extern struct { - major: i64, - minor: i64, - patch: i64, -}; diff --git a/src/os/main.zig b/src/os/main.zig index 22765f546..073129300 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -8,7 +8,6 @@ const file = @import("file.zig"); const flatpak = @import("flatpak.zig"); const homedir = @import("homedir.zig"); const locale = @import("locale.zig"); -const macos_version = @import("macos_version.zig"); const mouse = @import("mouse.zig"); const openpkg = @import("open.zig"); const pipepkg = @import("pipe.zig"); @@ -21,6 +20,7 @@ pub const hostname = @import("hostname.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); +pub const macos = @import("macos.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); @@ -37,7 +37,6 @@ pub const freeTmpDir = file.freeTmpDir; pub const isFlatpak = flatpak.isFlatpak; pub const home = homedir.home; pub const ensureLocale = locale.ensureLocale; -pub const macosVersionAtLeast = macos_version.macosVersionAtLeast; pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open; pub const pipe = pipepkg.pipe; From 6c615046ba8d164d99712808104120f968453e09 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Nov 2024 13:26:21 -0800 Subject: [PATCH 02/62] config: only change conditional state if there are relevant changes Related to #2775 This makes it so that `changeConditionalState` only does something if the conditional state (1) has changes and (2) those changes are relevant to the current conditional state. By "relevant" we mean that the conditional state being changed is state that is actually used by the configuration. --- src/config/Config.zig | 133 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 9 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 55cd55606..0f8be9662 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1793,6 +1793,10 @@ _diagnostics: cli.DiagnosticList = .{}, /// determine if a conditional configuration matches or not. _conditional_state: conditional.State = .{}, +/// The conditional keys that are used at any point during the configuration +/// loading. This is used to speed up the conditional evaluation process. +_conditional_set: std.EnumSet(conditional.Key) = .{}, + /// The steps we can use to reload the configuration after it has been loaded /// without reopening the files. This is used in very specific cases such /// as loadTheme which has more details on why. @@ -2610,6 +2614,10 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { /// on the new state. The caller must free the old configuration if they /// wish. /// +/// This returns null if the conditional state would result in no changes +/// to the configuration. In this case, the caller can continue to use +/// the existing configuration or clone if they want a copy. +/// /// This doesn't re-read any files, it just re-applies the same /// configuration with the new conditional state. Importantly, this means /// that if you change the conditional state and the user in the interim @@ -2618,7 +2626,30 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { pub fn changeConditionalState( self: *const Config, new: conditional.State, -) !Config { +) !?Config { + // If the conditional state between the old and new is the same, + // then we don't need to do anything. + relevant: { + inline for (@typeInfo(conditional.Key).Enum.fields) |field| { + const key: conditional.Key = @field(conditional.Key, field.name); + + // Conditional set contains the keys that this config uses. So we + // only continue if we use this key. + if (self._conditional_set.contains(key) and !equalField( + @TypeOf(@field(self._conditional_state, field.name)), + @field(self._conditional_state, field.name), + @field(new, field.name), + )) { + break :relevant; + } + } + + // If we got here, then we didn't find any differences between + // the old and new conditional state that would affect the + // configuration. + return null; + } + // Create our new configuration const alloc_gpa = self._arena.?.child_allocator; var new_config = try self.cloneEmpty(alloc_gpa); @@ -2765,6 +2796,9 @@ pub fn finalize(self: *Config) !void { // This setting doesn't make sense with different light/dark themes // because it'll force the theme based on the Ghostty theme. if (self.@"window-theme" == .auto) self.@"window-theme" = .system; + + // Mark that we use a conditional theme + self._conditional_set.insert(.theme); } } @@ -3029,6 +3063,9 @@ pub fn clone( } assert(result._replay_steps.items.len == self._replay_steps.items.len); + // Copy the conditional set + result._conditional_set = self._conditional_set; + return result; } @@ -5258,14 +5295,13 @@ test "clone preserves conditional state" { var a = try Config.default(alloc); defer a.deinit(); - var b = try a.changeConditionalState(.{ .theme = .dark }); - defer b.deinit(); - try testing.expectEqual(.dark, b._conditional_state.theme); - var dest = try b.clone(alloc); + a._conditional_state.theme = .dark; + try testing.expectEqual(.dark, a._conditional_state.theme); + var dest = try a.clone(alloc); defer dest.deinit(); // Should have no changes - var it = b.changeIterator(&dest); + var it = a.changeIterator(&dest); try testing.expectEqual(@as(?Key, null), it.next()); // Should have the same conditional state @@ -5315,7 +5351,7 @@ test "clone can then change conditional state" { try cfg_light.loadIter(alloc, &it); try cfg_light.finalize(); - var cfg_dark = try cfg_light.changeConditionalState(.{ .theme = .dark }); + var cfg_dark = (try cfg_light.changeConditionalState(.{ .theme = .dark })).?; defer cfg_dark.deinit(); try testing.expectEqual(Color{ @@ -5332,7 +5368,7 @@ test "clone can then change conditional state" { .b = 0xEE, }, cfg_clone.background); - var cfg_light2 = try cfg_clone.changeConditionalState(.{ .theme = .light }); + var cfg_light2 = (try cfg_clone.changeConditionalState(.{ .theme = .light })).?; defer cfg_light2.deinit(); try testing.expectEqual(Color{ .r = 0xFF, @@ -5341,6 +5377,25 @@ test "clone can then change conditional state" { }, cfg_light2.background); } +test "clone preserves conditional set" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--theme=light:foo,dark:bar", + "--window-theme=auto", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + var clone1 = try cfg.clone(alloc); + defer clone1.deinit(); + + try testing.expect(clone1._conditional_set.contains(.theme)); +} + test "changed" { const testing = std.testing; const alloc = testing.allocator; @@ -5355,6 +5410,44 @@ test "changed" { try testing.expect(!source.changed(&dest, .@"font-size")); } +test "changeConditionalState ignores irrelevant changes" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--theme=foo", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + try testing.expect(try cfg.changeConditionalState( + .{ .theme = .dark }, + ) == null); + } +} + +test "changeConditionalState applies relevant changes" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--theme=light:foo,dark:bar", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + var cfg2 = (try cfg.changeConditionalState(.{ .theme = .dark })).?; + defer cfg2.deinit(); + + try testing.expect(cfg2._conditional_set.contains(.theme)); + } +} test "theme loading" { const testing = std.testing; const alloc = testing.allocator; @@ -5386,6 +5479,9 @@ test "theme loading" { .g = 0x3A, .b = 0xBC, }, cfg.background); + + // Not a conditional theme + try testing.expect(!cfg._conditional_set.contains(.theme)); } test "theme loading preserves conditional state" { @@ -5534,7 +5630,7 @@ test "theme loading correct light/dark" { try cfg.loadIter(alloc, &it); try cfg.finalize(); - var new = try cfg.changeConditionalState(.{ .theme = .dark }); + var new = (try cfg.changeConditionalState(.{ .theme = .dark })).?; defer new.deinit(); try testing.expectEqual(Color{ .r = 0xEE, @@ -5561,3 +5657,22 @@ test "theme specifying light/dark changes window-theme from auto" { try testing.expect(cfg.@"window-theme" == .system); } } + +test "theme specifying light/dark sets theme usage in conditional state" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--theme=light:foo,dark:bar", + "--window-theme=auto", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + try testing.expect(cfg.@"window-theme" == .system); + try testing.expect(cfg._conditional_set.contains(.theme)); + } +} From a39aa7e89daf9eb734be631520b7d1e1c1b1f06e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Nov 2024 15:12:14 -0800 Subject: [PATCH 03/62] apprt/gtk: only show config reload toast if app config changes This resolves the toast showing up every time the surface config changes which can be relatively frequent under certain circumstances such as theme changes. --- src/apprt/gtk/App.zig | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 0cee1938e..bb5309333 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -462,7 +462,7 @@ pub fn performAction( .equalize_splits => self.equalizeSplits(target), .goto_split => self.gotoSplit(target, value), .open_config => try configpkg.edit.open(self.core_app.alloc), - .config_change => self.configChange(value.config), + .config_change => self.configChange(target, value.config), .reload_config => try self.reloadConfig(target, value), .inspector => self.controlInspector(target, value), .desktop_notification => self.showDesktopNotification(target, value), @@ -818,18 +818,32 @@ fn showDesktopNotification( c.g_application_send_notification(g_app, n.body.ptr, notification); } -fn configChange(self: *App, new_config: *const Config) void { +fn configChange( + self: *App, + target: apprt.Target, + new_config: *const Config, +) void { _ = new_config; - self.syncConfigChanges() catch |err| { - log.warn("error handling configuration changes err={}", .{err}); - }; + switch (target) { + // We don't do anything for surface config change events. There + // is nothing to sync with regards to a surface today. + .surface => {}, - if (adwaita.enabled(&self.config)) { - if (self.core_app.focusedSurface()) |core_surface| { - const surface = core_surface.rt_surface; - if (surface.container.window()) |window| window.onConfigReloaded(); - } + .app => { + self.syncConfigChanges() catch |err| { + log.warn("error handling configuration changes err={}", .{err}); + }; + + // App changes needs to show a toast that our configuration + // has reloaded. + if (adwaita.enabled(&self.config)) { + if (self.core_app.focusedSurface()) |core_surface| { + const surface = core_surface.rt_surface; + if (surface.container.window()) |window| window.onConfigReloaded(); + } + } + }, } } From adc59be9776373d128ee599d76af03b5c77300fd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Nov 2024 16:04:16 -0800 Subject: [PATCH 04/62] os: more error handling on reading the app support dir --- src/config/Config.zig | 16 ++++------- src/os/macos.zig | 64 +++++++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e2843508a..d88347e2e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1809,14 +1809,10 @@ pub fn deinit(self: *Config) void { /// Load the configuration according to the default rules: /// /// 1. Defaults -/// 2. Configuration Files -/// 3. CLI flags -/// 4. Recursively defined configuration files -/// -/// Configuration files are loaded in the follow order: -/// -/// 1. XDG Config File -/// 2. "Application Support" Config File on macOS +/// 2. XDG config dir +/// 3. "Application Support" directory (macOS only) +/// 4. CLI flags +/// 5. Recursively defined configuration files /// pub fn load(alloc_gpa: Allocator) !Config { var result = try default(alloc_gpa); @@ -2423,8 +2419,8 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { defer alloc.free(xdg_path); self.loadOptionalFile(alloc, xdg_path); - if (builtin.os.tag == .macos) { - const app_support_path = try internal_os.macos.getAppSupportDir(alloc, "config"); + if (comptime builtin.os.tag == .macos) { + const app_support_path = try internal_os.macos.appSupportDir(alloc, "config"); defer alloc.free(app_support_path); self.loadOptionalFile(alloc, app_support_path); } diff --git a/src/os/macos.zig b/src/os/macos.zig index 62ba87c5a..fe312c6e1 100644 --- a/src/os/macos.zig +++ b/src/os/macos.zig @@ -4,15 +4,9 @@ const assert = std.debug.assert; const objc = @import("objc"); const Allocator = std.mem.Allocator; -pub const NSOperatingSystemVersion = extern struct { - major: i64, - minor: i64, - patch: i64, -}; - /// Verifies that the running macOS system version is at least the given version. pub fn isAtLeastVersion(major: i64, minor: i64, patch: i64) bool { - assert(builtin.target.isDarwin()); + comptime assert(builtin.target.isDarwin()); const NSProcessInfo = objc.getClass("NSProcessInfo").?; const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); @@ -21,20 +15,24 @@ pub fn isAtLeastVersion(major: i64, minor: i64, patch: i64) bool { }); } -pub const NSSearchPathDirectory = enum(c_ulong) { - NSApplicationSupportDirectory = 14, -}; +pub const AppSupportDirError = Allocator.Error || error{AppleAPIFailed}; -pub const NSSearchPathDomainMask = enum(c_ulong) { - NSUserDomainMask = 1, -}; +/// Return the path to the application support directory for Ghostty +/// with the given sub path joined. This allocates the result using the +/// given allocator. +pub fn appSupportDir( + alloc: Allocator, + sub_path: []const u8, +) AppSupportDirError![]u8 { + comptime assert(builtin.target.isDarwin()); -pub fn getAppSupportDir(alloc: Allocator, sub_path: []const u8) ![]u8 { - assert(builtin.target.isDarwin()); - - const err: ?*anyopaque = undefined; const NSFileManager = objc.getClass("NSFileManager").?; - const manager = NSFileManager.msgSend(objc.Object, objc.sel("defaultManager"), .{}); + const manager = NSFileManager.msgSend( + objc.Object, + objc.sel("defaultManager"), + .{}, + ); + const url = manager.msgSend( objc.Object, objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"), @@ -43,12 +41,36 @@ pub fn getAppSupportDir(alloc: Allocator, sub_path: []const u8) ![]u8 { NSSearchPathDomainMask.NSUserDomainMask, @as(?*anyopaque, null), true, - &err, + @as(?*anyopaque, null), }, ); + + // I don't think this is possible but just in case. + if (url.value == null) return error.AppleAPIFailed; + + // Get the UTF-8 string from the URL. const path = url.getProperty(objc.Object, "path"); - const c_str = path.getProperty([*:0]const u8, "UTF8String"); + const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse + return error.AppleAPIFailed; const app_support_dir = std.mem.sliceTo(c_str, 0); - return try std.fs.path.join(alloc, &.{ app_support_dir, "com.mitchellh.ghostty", sub_path }); + return try std.fs.path.join(alloc, &.{ + app_support_dir, + "com.mitchellh.ghostty", + sub_path, + }); } + +pub const NSOperatingSystemVersion = extern struct { + major: i64, + minor: i64, + patch: i64, +}; + +pub const NSSearchPathDirectory = enum(c_ulong) { + NSApplicationSupportDirectory = 14, +}; + +pub const NSSearchPathDomainMask = enum(c_ulong) { + NSUserDomainMask = 1, +}; From b9345e8d6abbbca506fd18061af1f2c1c7c9906b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Nov 2024 16:10:55 -0800 Subject: [PATCH 05/62] try to abstract bundle ID to a zig file --- src/apprt/gtk/App.zig | 3 ++- src/apprt/gtk/ConfigErrorsWindow.zig | 3 ++- src/apprt/gtk/Surface.zig | 3 ++- src/apprt/gtk/Window.zig | 2 +- src/apprt/gtk/inspector.zig | 3 ++- src/build_config.zig | 14 ++++++++++++++ src/main_ghostty.zig | 2 +- src/os/macos.zig | 3 ++- 8 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 0cee1938e..5e41fcdae 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -14,6 +14,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const builtin = @import("builtin"); +const build_config = @import("../../build_config.zig"); const apprt = @import("../../apprt.zig"); const configpkg = @import("../../config.zig"); const input = @import("../../input.zig"); @@ -181,7 +182,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { } } - const default_id = "com.mitchellh.ghostty"; + const default_id = comptime build_config.bundle_id; break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id; }; diff --git a/src/apprt/gtk/ConfigErrorsWindow.zig b/src/apprt/gtk/ConfigErrorsWindow.zig index 6d4cda21b..3ff52908e 100644 --- a/src/apprt/gtk/ConfigErrorsWindow.zig +++ b/src/apprt/gtk/ConfigErrorsWindow.zig @@ -3,6 +3,7 @@ const ConfigErrors = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const build_config = @import("../../build_config.zig"); const configpkg = @import("../../config.zig"); const Config = configpkg.Config; @@ -53,7 +54,7 @@ fn init(self: *ConfigErrors, app: *App) !void { c.gtk_window_set_title(gtk_window, "Configuration Errors"); c.gtk_window_set_default_size(gtk_window, 600, 275); c.gtk_window_set_resizable(gtk_window, 0); - c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); + c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); // Set some state diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index aef67b308..9a361c228 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -5,6 +5,7 @@ const Surface = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const build_config = @import("../../build_config.zig"); const configpkg = @import("../../config.zig"); const apprt = @import("../../apprt.zig"); const font = @import("../../font/main.zig"); @@ -1149,7 +1150,7 @@ pub fn showDesktopNotification( defer c.g_object_unref(notification); c.g_notification_set_body(notification, body.ptr); - const icon = c.g_themed_icon_new("com.mitchellh.ghostty"); + const icon = c.g_themed_icon_new(build_config.bundle_id); defer c.g_object_unref(icon); c.g_notification_set_icon(notification, icon); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index e220ac03b..23265c101 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -103,7 +103,7 @@ pub fn init(self: *Window, app: *App) !void { // 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_icon_name(gtk_window, "com.mitchellh.ghostty"); + c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id); // Apply class to color headerbar if window-theme is set to `ghostty` and // GTK version is before 4.16. The conditional is because above 4.16 diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig index f5bdf8a24..119e20a6c 100644 --- a/src/apprt/gtk/inspector.zig +++ b/src/apprt/gtk/inspector.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const build_config = @import("../../build_config.zig"); const App = @import("App.zig"); const Surface = @import("Surface.zig"); const TerminalWindow = @import("Window.zig"); @@ -141,7 +142,7 @@ const Window = struct { self.window = gtk_window; c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector"); c.gtk_window_set_default_size(gtk_window, 1000, 600); - c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); + c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id); // Initialize our imgui widget try self.imgui_widget.init(); diff --git a/src/build_config.zig b/src/build_config.zig index 715552e03..1448f9de5 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -103,6 +103,20 @@ pub const app_runtime: apprt.Runtime = config.app_runtime; pub const font_backend: font.Backend = config.font_backend; pub const renderer: rendererpkg.Impl = config.renderer; +/// The bundle ID for the app. This is used in many places and is currently +/// hardcoded here. We could make this configurable in the future if there +/// is a reason to do so. +/// +/// On macOS, this must match the App bundle ID. We can get that dynamically +/// via an API but I don't want to pay the cost of that at runtime. +/// +/// On GTK, this should match the various folders with resources. +/// +/// There are many places that don't use this variable so simply swapping +/// this variable is NOT ENOUGH to change the bundle ID. I just wanted to +/// avoid it in Zig coe as much as possible. +pub const bundle_id = "com.mitchellh.ghostty"; + /// True if we should have "slow" runtime safety checks. The initial motivation /// for this was terminal page/pagelist integrity checks. These were VERY /// slow but very thorough. But they made it so slow that the terminal couldn't diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 071d4d530..b3df80538 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -141,7 +141,7 @@ fn logFn( // Initialize a logger. This is slow to do on every operation // but we shouldn't be logging too much. - const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope)); + const logger = macos.os.Log.create(build_config.bundle_id, @tagName(scope)); defer logger.release(); logger.log(std.heap.c_allocator, mac_level, format, args); } diff --git a/src/os/macos.zig b/src/os/macos.zig index fe312c6e1..d405cd161 100644 --- a/src/os/macos.zig +++ b/src/os/macos.zig @@ -1,5 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("../build_config.zig"); const assert = std.debug.assert; const objc = @import("objc"); const Allocator = std.mem.Allocator; @@ -56,7 +57,7 @@ pub fn appSupportDir( return try std.fs.path.join(alloc, &.{ app_support_dir, - "com.mitchellh.ghostty", + build_config.bundle_id, sub_path, }); } From 2cbc2833d10046333b698d1caa601bc7ef85de98 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Tue, 26 Nov 2024 08:59:51 -0600 Subject: [PATCH 06/62] termio: fixes to kitty color reporting The kitty color report response is an OSC, not a CSI, so change `[` to `]`. Colors which are unset should be reported with `{key}=`. --- src/termio/stream_handler.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2fc9e92af..64915f704 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1418,7 +1418,7 @@ pub const StreamHandler = struct { var buf = std.ArrayList(u8).init(self.alloc); defer buf.deinit(); const writer = buf.writer(); - try writer.writeAll("\x1b[21"); + try writer.writeAll("\x1b]21"); for (request.list.items) |item| { switch (item) { @@ -1435,7 +1435,7 @@ pub const StreamHandler = struct { }, }, } orelse { - log.warn("no color configured for {}", .{key}); + try writer.print(";{}=", .{key}); continue; }; From 39fbd7db4baa36db8366379b1955df8c533e5bd9 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Tue, 26 Nov 2024 11:10:00 -0700 Subject: [PATCH 07/62] Prevent GTK from initializing Vulkan. This improves startup time --- src/apprt/gtk/App.zig | 7 ++++++- src/termio/Exec.zig | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 0cee1938e..95942bc9a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -99,9 +99,11 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { c.gtk_get_micro_version(), }); + // Disabling Vulkan can improve startup times by hundreds of + // milliseconds on some systems if (version.atLeast(4, 16, 0)) { // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE - _ = internal_os.setenv("GDK_DISABLE", "gles-api"); + _ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan"); _ = internal_os.setenv("GDK_DEBUG", "opengl"); } else if (version.atLeast(4, 14, 0)) { // We need to export GDK_DEBUG to run on Wayland after GTK 4.14. @@ -110,7 +112,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // reassess... // // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589 + _ = internal_os.setenv("GDK_DISABLE", "vulkan"); _ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles"); + } else { + _ = internal_os.setenv("GDK_DISABLE", "vulkan"); } if (version.atLeast(4, 14, 0)) { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 07aa43c42..41f86958e 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -843,6 +843,7 @@ const Subprocess = struct { // Don't leak these environment variables to child processes. if (comptime build_config.app_runtime == .gtk) { env.remove("GDK_DEBUG"); + env.remove("GDK_DISABLE"); env.remove("GSK_RENDERER"); } From 9171cb5c295614a71d50f399cee6eb048f37ca0d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Nov 2024 10:49:45 -0800 Subject: [PATCH 08/62] config: implement `clone` for RepeatableLink Fixes #2819 --- src/config/Config.zig | 30 +++++++++++++++++++++++------- src/input/Link.zig | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 253e96420..bf29994c3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4610,17 +4610,33 @@ pub const RepeatableLink = struct { } /// Deep copy of the struct. Required by Config. - pub fn clone(self: *const Self, alloc: Allocator) error{}!Self { - _ = self; - _ = alloc; - return .{}; + pub fn clone( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!Self { + // Note: we don't do any errdefers below since the allocation + // is expected to be arena allocated. + + var list = try std.ArrayListUnmanaged(inputpkg.Link).initCapacity( + alloc, + self.links.items.len, + ); + for (self.links.items) |item| { + const copy = try item.clone(alloc); + list.appendAssumeCapacity(copy); + } + + return .{ .links = list }; } /// Compare if two of our value are requal. Required by Config. pub fn equal(self: Self, other: Self) bool { - _ = self; - _ = other; - return true; + const itemsA = self.links.items; + const itemsB = other.links.items; + if (itemsA.len != itemsB.len) return false; + for (itemsA, itemsB) |*a, *b| { + if (!a.equal(b)) return false; + } else return true; } /// Used by Formatter diff --git a/src/input/Link.zig b/src/input/Link.zig index adc52a270..37b45dbd1 100644 --- a/src/input/Link.zig +++ b/src/input/Link.zig @@ -4,6 +4,8 @@ //! action types. const Link = @This(); +const std = @import("std"); +const Allocator = std.mem.Allocator; const oni = @import("oniguruma"); const Mods = @import("key.zig").Mods; @@ -59,3 +61,19 @@ pub fn oniRegex(self: *const Link) !oni.Regex { null, ); } + +/// Deep clone the link. +pub fn clone(self: *const Link, alloc: Allocator) Allocator.Error!Link { + return .{ + .regex = try alloc.dupe(u8, self.regex), + .action = self.action, + .highlight = self.highlight, + }; +} + +/// Check if two links are equal. +pub fn equal(self: *const Link, other: *const Link) bool { + return std.meta.eql(self.action, other.action) and + std.meta.eql(self.highlight, other.highlight) and + std.mem.eql(u8, self.regex, other.regex); +} From e3621e81b74327bffee11d59823e6fa0e2855a4c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Nov 2024 13:17:35 -0800 Subject: [PATCH 09/62] apprt/gtk: use proper env var for vulkan disable on <= 4.14 --- src/apprt/gtk/App.zig | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 95942bc9a..93f08c459 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -100,9 +100,11 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { }); // Disabling Vulkan can improve startup times by hundreds of - // milliseconds on some systems + // milliseconds on some systems. We don't use Vulkan so we can just + // disable it. if (version.atLeast(4, 16, 0)) { - // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE + // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE. + // For the remainder of "why" see the 4.14 comment below. _ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan"); _ = internal_os.setenv("GDK_DEBUG", "opengl"); } else if (version.atLeast(4, 14, 0)) { @@ -112,14 +114,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // reassess... // // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589 - _ = internal_os.setenv("GDK_DISABLE", "vulkan"); - _ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles"); + _ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable"); } else { - _ = internal_os.setenv("GDK_DISABLE", "vulkan"); + _ = internal_os.setenv("GDK_DEBUG", "vulkan-disable"); } if (version.atLeast(4, 14, 0)) { - // We need to export GSK_RENDERER to opengl because GTK uses ngl by default after 4.14 + // We need to export GSK_RENDERER to opengl because GTK uses ngl by + // default after 4.14 _ = internal_os.setenv("GSK_RENDERER", "opengl"); } From e20b27de848afaa167c70d838a2f98f28c6ac0b5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Nov 2024 14:27:44 -0800 Subject: [PATCH 10/62] terminal: eraseChars was checking wide char split boundary on wrong cell Fixes #2817 The test is pretty explanatory. I also renamed `end` to `count` since I think this poor naming was the reason for the bug. In `eraseChars`, the `count` (nee `end`) is the number of cells to erase, not the index of the last cell to erase. --- src/terminal/Terminal.zig | 44 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index f5784b6ab..8de914a3e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1955,13 +1955,9 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void { } pub fn eraseChars(self: *Terminal, count_req: usize) void { - const count = @max(count_req, 1); - - // Our last index is at most the end of the number of chars we have - // in the current line. - const end = end: { + const count = end: { const remaining = self.cols - self.screen.cursor.x; - var end = @min(remaining, count); + var end = @min(remaining, @max(count_req, 1)); // If our last cell is a wide char then we need to also clear the // cell beyond it since we can't just split a wide char. @@ -1979,7 +1975,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { // protected modes. We need to figure out how to make `clearCells` or at // least `clearUnprotectedCells` handle boundary conditions... self.screen.splitCellBoundary(self.screen.cursor.x); - self.screen.splitCellBoundary(end); + self.screen.splitCellBoundary(self.screen.cursor.x + count); // Reset our row's soft-wrap. self.screen.cursorResetWrap(); @@ -1997,7 +1993,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { self.screen.clearCells( &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, - cells[0..end], + cells[0..count], ); return; } @@ -2005,7 +2001,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { self.screen.clearUnprotectedCells( &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, - cells[0..end], + cells[0..count], ); } @@ -6104,6 +6100,36 @@ test "Terminal: eraseChars wide char boundary conditions" { } } +test "Terminal: eraseChars wide char splits proper cell boundaries" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 1, .cols = 30 }); + defer t.deinit(alloc); + + // This is a test for a bug: https://github.com/ghostty-org/ghostty/issues/2817 + // To explain the setup: + // (1) We need our wide characters starting on an even (1-based) column. + // (2) We need our cursor to be in the middle somewhere. + // (3) We need our count to be less than our cursor X and on a split cell. + // The bug was that we split the wrong cell boundaries. + + try t.printString("x食べて下さい"); + { + const str = try t.plainString(alloc); + defer testing.allocator.free(str); + try testing.expectEqualStrings("x食べて下さい", str); + } + + t.setCursorPos(1, 6); // At: て + t.eraseChars(4); // Delete: て下 + t.screen.cursor.page_pin.node.data.assertIntegrity(); + + { + const str = try t.plainString(alloc); + defer testing.allocator.free(str); + try testing.expectEqualStrings("x食べ さい", str); + } +} + test "Terminal: eraseChars wide char wrap boundary conditions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 3, .cols = 8 }); From 2e939b617e8e20652e25067b041595441ff6b380 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Nov 2024 15:10:29 -0800 Subject: [PATCH 11/62] config: clone should copy diagnostics Fixes #2800 --- src/cli/diagnostics.zig | 55 +++++++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 3 +++ 2 files changed, 58 insertions(+) diff --git a/src/cli/diagnostics.zig b/src/cli/diagnostics.zig index e4d390c03..8090684fd 100644 --- a/src/cli/diagnostics.zig +++ b/src/cli/diagnostics.zig @@ -34,6 +34,14 @@ pub const Diagnostic = struct { try writer.print("{s}", .{self.message}); } + + pub fn clone(self: *const Diagnostic, alloc: Allocator) Allocator.Error!Diagnostic { + return .{ + .location = try self.location.clone(alloc), + .key = try alloc.dupeZ(u8, self.key), + .message = try alloc.dupeZ(u8, self.message), + }; + } }; /// The possible locations for a diagnostic message. This is used @@ -61,6 +69,19 @@ pub const Location = union(enum) { if (!@hasDecl(Iter, "location")) return .none; return iter.location() orelse .none; } + + pub fn clone(self: *const Location, alloc: Allocator) Allocator.Error!Location { + return switch (self.*) { + .none, + .cli, + => self.*, + + .file => |v| .{ .file = .{ + .path = try alloc.dupe(u8, v.path), + .line = v.line, + } }, + }; + } }; /// A list of diagnostics. The "_diagnostics" field must be this type @@ -88,11 +109,45 @@ pub const DiagnosticList = struct { // We specifically want precompute for libghostty. .lib => true, }; + const Precompute = if (precompute_enabled) struct { messages: std.ArrayListUnmanaged([:0]const u8) = .{}, + + pub fn clone( + self: *const Precompute, + alloc: Allocator, + ) Allocator.Error!Precompute { + var result: Precompute = .{}; + try result.messages.ensureTotalCapacity(alloc, self.messages.items.len); + for (self.messages.items) |msg| { + result.messages.appendAssumeCapacity( + try alloc.dupeZ(u8, msg), + ); + } + return result; + } } else void; + const precompute_init: Precompute = if (precompute_enabled) .{} else {}; + pub fn clone( + self: *const DiagnosticList, + alloc: Allocator, + ) Allocator.Error!DiagnosticList { + var result: DiagnosticList = .{}; + + try result.list.ensureTotalCapacity(alloc, self.list.items.len); + for (self.list.items) |*diag| result.list.appendAssumeCapacity( + try diag.clone(alloc), + ); + + if (comptime precompute_enabled) { + result.precompute = try self.precompute.clone(alloc); + } + + return result; + } + pub fn append( self: *DiagnosticList, alloc: Allocator, diff --git a/src/config/Config.zig b/src/config/Config.zig index bf29994c3..2dc732752 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3063,6 +3063,9 @@ pub fn clone( ); } + // Copy our diagnostics + result._diagnostics = try self._diagnostics.clone(alloc_arena); + // Preserve our replay steps. We copy them exactly to also preserve // the exact conditionals required for some steps. try result._replay_steps.ensureTotalCapacity( From a482224da8577246d4a31f72d298a25352e0df9d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Nov 2024 19:57:29 -0800 Subject: [PATCH 12/62] renderer: set QoS class of the renderer thread on macOS This sets the macOS QoS class of the renderer thread. Apple recommends[1] that all threads should have a QoS class set, and there are many benefits[2] to that, mainly around power management moreso than performance I'd expect. In this commit, I start by setting the QoS class of the renderer thread. By default, the renderer thread is set to user interactive, because it is a UI thread after all. But under some conditions we downgrade: - If the surface is not visible at all (i.e. another window is fully covering it or its minimized), we set the QoS class to utility. This is lower than the default, previous QoS and should help macOS unschedule the workload or move it to a different core. - If the surface is visible but not focused, we set the QoS class to user initiated. This is lower than user interactive but higher than default. The renderer should remain responsive but not consume as much time as it would if it was user interactive. I'm unable to see any noticable difference in anything from these changes. Unfortunately it doesn't seem like Apple provides good tools to play around with this. We should continue to apply QoS classes to our other threads on macOS. [1]: https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occl [2]: https://blog.xoria.org/macos-tips-threading/ --- src/os/macos.zig | 41 +++++++++++++++++++++++++++++ src/renderer/Thread.zig | 58 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/os/macos.zig b/src/os/macos.zig index d405cd161..53dfd1719 100644 --- a/src/os/macos.zig +++ b/src/os/macos.zig @@ -62,6 +62,47 @@ pub fn appSupportDir( }); } +pub const SetQosClassError = error{ + // The thread can't have its QoS class changed usually because + // a different pthread API was called that makes it an invalid + // target. + ThreadIncompatible, +}; + +/// Set the QoS class of the running thread. +/// +/// https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occ +pub fn setQosClass(class: QosClass) !void { + return switch (std.posix.errno(pthread_set_qos_class_self_np( + class, + 0, + ))) { + .SUCCESS => {}, + .PERM => error.ThreadIncompatible, + + // EPERM is the only known error that can happen based on + // the man pages for pthread_set_qos_class_self_np. I haven't + // checked the XNU source code to see if there are other + // possible errors. + else => @panic("unexpected pthread_set_qos_class_self_np error"), + }; +} + +/// https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW1 +pub const QosClass = enum(c_uint) { + user_interactive = 0x21, + user_initiated = 0x19, + default = 0x15, + utility = 0x11, + background = 0x09, + unspecified = 0x00, +}; + +extern "c" fn pthread_set_qos_class_self_np( + qos_class: QosClass, + relative_priority: c_int, +) c_int; + pub const NSOperatingSystemVersion = extern struct { major: i64, minor: i64, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 91e355480..cc63889fa 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -4,8 +4,10 @@ pub const Thread = @This(); const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; const xev = @import("xev"); const crash = @import("../crash/main.zig"); +const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -92,6 +94,10 @@ flags: packed struct { /// This is true when the view is visible. This is used to determine /// if we should be rendering or not. visible: bool = true, + + /// This is true when the view is focused. This defaults to true + /// and it is up to the apprt to set the correct value. + focused: bool = true, } = .{}, pub const DerivedConfig = struct { @@ -199,6 +205,9 @@ fn threadMain_(self: *Thread) !void { }; defer crash.sentry.thread_state = null; + // Setup our thread QoS + self.setQosClass(); + // Run our loop start/end callbacks if the renderer cares. const has_loop = @hasDecl(renderer.Renderer, "loopEnter"); if (has_loop) try self.renderer.loopEnter(self); @@ -237,6 +246,36 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.until_done); } +fn setQosClass(self: *const Thread) void { + // Thread QoS classes are only relevant on macOS. + if (comptime !builtin.target.isDarwin()) return; + + const class: internal_os.macos.QosClass = class: { + // If we aren't visible (our view is fully occluded) then we + // always drop our rendering priority down because it's just + // mostly wasted work. + // + // The renderer itself should be doing this as well (for example + // Metal will stop our DisplayLink) but this also helps with + // general forced updates and CPU usage i.e. a rebuild cells call. + if (!self.flags.visible) break :class .utility; + + // If we're not focused, but we're visible, then we set a higher + // than default priority because framerates still matter but it isn't + // as important as when we're focused. + if (!self.flags.focused) break :class .user_initiated; + + // We are focused and visible, we are the definition of user interactive. + break :class .user_interactive; + }; + + if (internal_os.macos.setQosClass(class)) { + log.debug("thread QoS class set class={}", .{class}); + } else |err| { + log.warn("error setting QoS class err={}", .{err}); + } +} + fn startDrawTimer(self: *Thread) void { // If our renderer doesn't support animations then we never run this. if (!@hasDecl(renderer.Renderer, "hasAnimations")) return; @@ -273,10 +312,16 @@ fn drainMailbox(self: *Thread) !void { switch (message) { .crash => @panic("crash request, crashing intentionally"), - .visible => |v| { + .visible => |v| visible: { + // If our state didn't change we do nothing. + if (self.flags.visible == v) break :visible; + // Set our visible state self.flags.visible = v; + // Visibility affects our QoS class + self.setQosClass(); + // If we became visible then we immediately trigger a draw. // We don't need to update frame data because that should // still be happening. @@ -293,7 +338,16 @@ fn drainMailbox(self: *Thread) !void { // check the visible state themselves to control their behavior. }, - .focus => |v| { + .focus => |v| focus: { + // If our state didn't change we do nothing. + if (self.flags.focused == v) break :focus; + + // Set our state + self.flags.focused = v; + + // Focus affects our QoS class + self.setQosClass(); + // Set it on the renderer try self.renderer.setFocus(v); From f12ac32c973091f698fc521d9f293ae477875dce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Nov 2024 16:35:27 -0800 Subject: [PATCH 13/62] README: clarify config docs location --- README.md | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3c3a2460d..861e0937a 100644 --- a/README.md +++ b/README.md @@ -107,25 +107,40 @@ palette = 7=#a89984 palette = 15=#fbf1c7 ``` -You can view all available configuration options and their documentation -by executing the command `ghostty +show-config --default --docs`. Note that -this will output the full default configuration with docs to stdout, so -you may want to pipe that through a pager, an editor, etc. +#### Configuration Documentation + +There are multiple places to find documentation on the configuration options. +All locations are identical (they're all generated from the same source): + +1. There are HTML and Markdown formatted docs in the + `$prefix/share/ghostty/docs` directory. This directory is created + when you build or install Ghostty. The `$prefix` is `zig-out` if you're + building from source (or the specified `--prefix` flag). On macOS, + `$prefix` is the `Contents/Resources` subdirectory of the `.app` bundle. + +2. There are man pages in the `$prefix/share/man` directory. This directory + is created when you build or install Ghostty. + +3. In the CLI, you can run `ghostty +show-config --default --docs`. + Note that this will output the full default configuration with docs to + stdout, so you may want to pipe that through a pager, an editor, etc. + +4. In the source code, you can find the configuration structure in the + [Config structure](https://github.com/ghostty-org/ghostty/blob/main/src/config/Config.zig). + The available keys are the keys verbatim, and their possible values are typically + documented in the comments. + +5. Not documentation per se, but you can search for the + [public config files](https://github.com/search?q=path%3Aghostty%2Fconfig&type=code) + of many Ghostty users for examples and inspiration. > [!NOTE] > -> You'll see a lot of weird blank configurations like `font-family =`. This +> You may see strange looking blank configurations like `font-family =`. This > is a valid syntax to specify the default behavior (no value). The > `+show-config` outputs it so it's clear that key is defaulting and also > to have something to attach the doc comment to. -You can also see and read all available configuration options in the source -[Config structure](https://github.com/ghostty-org/ghostty/blob/main/src/config/Config.zig). -The available keys are the keys verbatim, and their possible values are typically -documented in the comments. You also can search for the -[public config files](https://github.com/search?q=path%3Aghostty%2Fconfig&type=code) -of many Ghostty users for examples and inspiration. - > [!NOTE] > > Configuration can be reloaded on the fly with the `reload_config` From abafb81a1b5180d8cf009f7eb68bbea5b6141b8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Nov 2024 16:50:04 -0800 Subject: [PATCH 14/62] apprt/gtk: update app color scheme state Fixes #2781 This commit contains two separate changes but very related: 1. We update the color scheme state of the app on app start. This is necessary so that the configuration properly reflects the conditional state of the theme at the app level (i.e. the window headerbar). 2. We take ownership of the new config when it changes, matching macOS. This ensures that things like our GTK headerbar update when the theme changes but more generally whenever any config changes. And some housekeeping: - We remove runtime CSS setup from init. We can do it on the first tick of `run` instead. This will probably save some CPU cycles especially when we're just notifying a single instance to create a new window. - I moved dbus event setup to `run` as well. We don't need to know these events unless we're actually running the app. Similar to the above, should save some CPU time on single instance runs. --- src/apprt/gtk/App.zig | 88 ++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index f81c1a76a..ead41de7c 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -385,22 +385,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { if (config.@"initial-window") c.g_application_activate(gapp); - // Register for dbus events - if (c.g_application_get_dbus_connection(gapp)) |dbus_connection| { - _ = c.g_dbus_connection_signal_subscribe( - dbus_connection, - null, - "org.freedesktop.portal.Settings", - "SettingChanged", - "/org/freedesktop/portal/desktop", - "org.freedesktop.appearance", - c.G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, - >kNotifyColorScheme, - core_app, - null, - ); - } - // Internally, GTK ensures that only one instance of this provider exists in the provider list // for the display. const css_provider = c.gtk_css_provider_new(); @@ -409,12 +393,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { @ptrCast(css_provider), c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 3, ); - loadRuntimeCss(core_app.alloc, &config, css_provider) catch |err| switch (err) { - error.OutOfMemory => log.warn( - "out of memory loading runtime CSS, no runtime CSS applied", - .{}, - ), - }; return .{ .core_app = core_app, @@ -831,14 +809,20 @@ fn configChange( target: apprt.Target, new_config: *const Config, ) void { - _ = new_config; - switch (target) { // We don't do anything for surface config change events. There // is nothing to sync with regards to a surface today. .surface => {}, .app => { + // We clone (to take ownership) and update our configuration. + if (new_config.clone(self.core_app.alloc)) |config_clone| { + self.config.deinit(); + self.config = config_clone; + } else |err| { + log.warn("error cloning configuration err={}", .{err}); + } + self.syncConfigChanges() catch |err| { log.warn("error handling configuration changes err={}", .{err}); }; @@ -892,7 +876,7 @@ fn syncConfigChanges(self: *App) !void { // Load our runtime CSS. If this fails then our window is just stuck // with the old CSS but we don't want to fail the entire sync operation. - loadRuntimeCss(self.core_app.alloc, &self.config, self.css_provider) catch |err| switch (err) { + self.loadRuntimeCss() catch |err| switch (err) { error.OutOfMemory => log.warn( "out of memory loading runtime CSS, no runtime CSS applied", .{}, @@ -956,15 +940,14 @@ fn syncActionAccelerator( } fn loadRuntimeCss( - alloc: Allocator, - config: *const Config, - provider: *c.GtkCssProvider, + self: *const App, ) Allocator.Error!void { - var stack_alloc = std.heap.stackFallback(4096, alloc); + var stack_alloc = std.heap.stackFallback(4096, self.core_app.alloc); var buf = std.ArrayList(u8).init(stack_alloc.get()); defer buf.deinit(); const writer = buf.writer(); + const config: *const Config = &self.config; const window_theme = config.@"window-theme"; const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background; const headerbar_background = config.background; @@ -1027,7 +1010,7 @@ fn loadRuntimeCss( // Clears any previously loaded CSS from this provider c.gtk_css_provider_load_from_data( - provider, + self.css_provider, buf.items.ptr, @intCast(buf.items.len), ); @@ -1076,11 +1059,17 @@ pub fn run(self: *App) !void { self.transient_cgroup_base = path; } else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"}); + // Setup our D-Bus connection for listening to settings changes. + self.initDbus(); + // Setup our menu items self.initActions(); self.initMenu(); self.initContextMenu(); + // Setup our initial color scheme + self.colorSchemeEvent(self.getColorScheme()); + // On startup, we want to check for configuration errors right away // so we can show our error window. We also need to setup other initial // state. @@ -1114,6 +1103,26 @@ pub fn run(self: *App) !void { } } +fn initDbus(self: *App) void { + const dbus = c.g_application_get_dbus_connection(@ptrCast(self.app)) orelse { + log.warn("unable to get dbus connection, not setting up events", .{}); + return; + }; + + _ = c.g_dbus_connection_signal_subscribe( + dbus, + null, + "org.freedesktop.portal.Settings", + "SettingChanged", + "/org/freedesktop/portal/desktop", + "org.freedesktop.appearance", + c.G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, + >kNotifyColorScheme, + self, + null, + ); +} + // This timeout function is started when no surfaces are open. It can be // cancelled if a new surface is opened before the timer expires. pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean { @@ -1394,7 +1403,7 @@ fn gtkNotifyColorScheme( parameters: ?*c.GVariant, user_data: ?*anyopaque, ) callconv(.C) void { - const core_app: *CoreApp = @ptrCast(@alignCast(user_data orelse { + const self: *App = @ptrCast(@alignCast(user_data orelse { log.err("style change notification: userdata is null", .{}); return; })); @@ -1426,9 +1435,20 @@ fn gtkNotifyColorScheme( else .light; - for (core_app.surfaces.items) |surface| { - surface.core_surface.colorSchemeCallback(color_scheme) catch |err| { - log.err("unable to tell surface about color scheme change: {}", .{err}); + self.colorSchemeEvent(color_scheme); +} + +fn colorSchemeEvent( + self: *App, + scheme: apprt.ColorScheme, +) void { + self.core_app.colorSchemeEvent(self, scheme) catch |err| { + log.err("error updating app color scheme err={}", .{err}); + }; + + for (self.core_app.surfaces.items) |surface| { + surface.core_surface.colorSchemeCallback(scheme) catch |err| { + log.err("unable to tell surface about color scheme change err={}", .{err}); }; } } From ba4185f6b78124da5b91f0675f62f8e7e31deb4d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Nov 2024 08:31:49 -0800 Subject: [PATCH 15/62] macos: disable background opacity/blur in native fullscreen See #2840 --- .../Features/Terminal/TerminalController.swift | 18 +++++++++++++++++- src/config/Config.zig | 4 ++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 81c74987b..698551f3e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -94,6 +94,16 @@ class TerminalController: BaseTerminalController { } } + + override func fullscreenDidChange() { + super.fullscreenDidChange() + + // When our fullscreen state changes, we resync our appearance because some + // properties change when fullscreen or not. + guard let focusedSurface else { return } + syncAppearance(focusedSurface.derivedConfig) + } + //MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -204,7 +214,13 @@ class TerminalController: BaseTerminalController { } // If we have window transparency then set it transparent. Otherwise set it opaque. - if (surfaceConfig.backgroundOpacity < 1) { + + // Window transparency only takes effect if our window is not native fullscreen. + // In native fullscreen we disable transparency/opacity because the background + // becomes gray and widgets show through. + if (!window.styleMask.contains(.fullScreen) && + surfaceConfig.backgroundOpacity < 1 + ) { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that diff --git a/src/config/Config.zig b/src/config/Config.zig index 2dc732752..97ac19226 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -527,6 +527,10 @@ palette: Palette = .{}, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 /// or greater than 1 will be clamped to the nearest valid value. +/// +/// On macOS, background opacity is disabled when the terminal enters native +/// fullscreen. This is because the background becomes gray and it can cause +/// widgets to show through which isn't generally desirable. @"background-opacity": f64 = 1.0, /// A positive value enables blurring of the background when background-opacity From 5b01cb353de47a0053c313e3bc20170cbece679e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Nov 2024 08:46:03 -0800 Subject: [PATCH 16/62] config: need to dupe filepath for diagnostics Fixes #2800 The source string with the filepath is not guaranteed to exist beyond the lifetime of the parse operation. We must copy it. --- src/cli/args.zig | 13 ++++++++----- src/cli/diagnostics.zig | 4 ++-- src/config/Config.zig | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cli/args.zig b/src/cli/args.zig index 3e378f347..454ca360e 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -104,7 +104,7 @@ pub fn parse( try dst._diagnostics.append(arena_alloc, .{ .key = try arena_alloc.dupeZ(u8, arg), .message = "invalid field", - .location = diags.Location.fromIter(iter), + .location = try diags.Location.fromIter(iter, arena_alloc), }); continue; @@ -145,7 +145,7 @@ pub fn parse( try dst._diagnostics.append(arena_alloc, .{ .key = try arena_alloc.dupeZ(u8, key), .message = message, - .location = diags.Location.fromIter(iter), + .location = try diags.Location.fromIter(iter, arena_alloc), }); }; } @@ -1140,7 +1140,7 @@ pub fn ArgsIterator(comptime Iterator: type) type { } /// Returns a location for a diagnostic message. - pub fn location(self: *const Self) ?diags.Location { + pub fn location(self: *const Self, _: Allocator) error{}!?diags.Location { return .{ .cli = self.index }; } }; @@ -1262,12 +1262,15 @@ pub fn LineIterator(comptime ReaderType: type) type { } /// Returns a location for a diagnostic message. - pub fn location(self: *const Self) ?diags.Location { + pub fn location( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!?diags.Location { // If we have no filepath then we have no location. if (self.filepath.len == 0) return null; return .{ .file = .{ - .path = self.filepath, + .path = try alloc.dupe(u8, self.filepath), .line = self.line, } }; } diff --git a/src/cli/diagnostics.zig b/src/cli/diagnostics.zig index 8090684fd..40fed3001 100644 --- a/src/cli/diagnostics.zig +++ b/src/cli/diagnostics.zig @@ -56,7 +56,7 @@ pub const Location = union(enum) { pub const Key = @typeInfo(Location).Union.tag_type.?; - pub fn fromIter(iter: anytype) Location { + pub fn fromIter(iter: anytype, alloc: Allocator) Allocator.Error!Location { const Iter = t: { const T = @TypeOf(iter); break :t switch (@typeInfo(T)) { @@ -67,7 +67,7 @@ pub const Location = union(enum) { }; if (!@hasDecl(Iter, "location")) return .none; - return iter.location() orelse .none; + return (try iter.location(alloc)) orelse .none; } pub fn clone(self: *const Location, alloc: Allocator) Allocator.Error!Location { diff --git a/src/config/Config.zig b/src/config/Config.zig index 2dc732752..a3bc79099 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2988,7 +2988,7 @@ pub fn parseManuallyHook( if (command.items.len == 0) { try self._diagnostics.append(alloc, .{ - .location = cli.Location.fromIter(iter), + .location = try cli.Location.fromIter(iter, alloc), .message = try std.fmt.allocPrintZ( alloc, "missing command after {s}", From 62fe3eb652f99437aa12448cbe24b0db149c924b Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Thu, 28 Nov 2024 23:33:35 +0100 Subject: [PATCH 17/62] added right click action for plasma --- build.zig | 7 +++++++ dist/linux/ghostty_dolphin.desktop | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 dist/linux/ghostty_dolphin.desktop diff --git a/build.zig b/build.zig index 15fed7ed6..2e36b7861 100644 --- a/build.zig +++ b/build.zig @@ -573,6 +573,13 @@ pub fn build(b: *std.Build) !void { b.installFile("dist/linux/app.desktop", "share/applications/com.mitchellh.ghostty.desktop"); } + // Right click menu action in case of Plasma desktop + if (env.get("DESKTOP_SESSION")) |desktop| { + if (std.mem.eql(u8, desktop, "plasma")) { + b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/ghostty_dolphin.desktop"); + } + } + // Various icons that our application can use, including the icon // that will be used for the desktop. b.installFile("images/icons/icon_16x16.png", "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png"); diff --git a/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop new file mode 100644 index 000000000..da46f6286 --- /dev/null +++ b/dist/linux/ghostty_dolphin.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Service +ServiceTypes=KonqPopupMenu/Plugin +MimeType=inode/directory +Actions=RunGhosttyDir + +[Desktop Action RunGhosttyDir] +Name=Open Ghostty Here +Icon=com.mitchellh.ghostty +Exec=cd %F && ghostty + From adfbf9d7f45f559bec08b261fef8ebaf40d7fd72 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Thu, 28 Nov 2024 23:40:21 +0100 Subject: [PATCH 18/62] update readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 3c3a2460d..a3921eb90 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,13 @@ support](https://www.openssh.com/txt/release-8.7) for setting `TERM` via > `xterm-256color` does not include all of Ghostty's capabilities, terminal > features beyond xterm's like colored and styled underlines will not work. +### Desktop Integration (Plasma only) +The build process detects if you are running plasma through an environment variable ```DESKTOP_SESSION``` and will install a ```.desktop``` file for a right-click menu action to open Ghostty in the working directory. To enable this, you need to give the file executable permissions: +```bash +chmod +x ~/.local/share/kio/servicemenus/ghostty_dolphin.desktop +``` +You should see an action called "Open Ghostty Here" when you right click in any directory in Dolphin. + ## Roadmap and Status The high-level ambitious plan for the project, in order: From 4be06d1c112be7bd37843bac32d302275fb14592 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Fri, 29 Nov 2024 00:27:51 +0100 Subject: [PATCH 19/62] rename file to match package --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 2e36b7861..0f58fa6c4 100644 --- a/build.zig +++ b/build.zig @@ -576,7 +576,7 @@ pub fn build(b: *std.Build) !void { // Right click menu action in case of Plasma desktop if (env.get("DESKTOP_SESSION")) |desktop| { if (std.mem.eql(u8, desktop, "plasma")) { - b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/ghostty_dolphin.desktop"); + b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/com.mitchellh.ghostty.desktop"); } } From 074312c5effcfff8a68cc524e810a2bb0d1e719d Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Fri, 29 Nov 2024 11:14:17 +0100 Subject: [PATCH 20/62] updated permissions and build process Removed check for plasma in build.zig, it installs it anyways now Added executable permissions for ghostty_dolphin.desktop since Plasma requires them for context menu items --- build.zig | 8 ++------ dist/linux/ghostty_dolphin.desktop | 0 2 files changed, 2 insertions(+), 6 deletions(-) mode change 100644 => 100755 dist/linux/ghostty_dolphin.desktop diff --git a/build.zig b/build.zig index 0f58fa6c4..d233bff1f 100644 --- a/build.zig +++ b/build.zig @@ -573,12 +573,8 @@ pub fn build(b: *std.Build) !void { b.installFile("dist/linux/app.desktop", "share/applications/com.mitchellh.ghostty.desktop"); } - // Right click menu action in case of Plasma desktop - if (env.get("DESKTOP_SESSION")) |desktop| { - if (std.mem.eql(u8, desktop, "plasma")) { - b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/com.mitchellh.ghostty.desktop"); - } - } + // Right click menu action for Plasma desktop + b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/com.mitchellh.ghostty.desktop"); // Various icons that our application can use, including the icon // that will be used for the desktop. diff --git a/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop old mode 100644 new mode 100755 From 46adc2fb4366a12d2f24d6fc774049eb2126f4d6 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Fri, 29 Nov 2024 11:14:36 +0100 Subject: [PATCH 21/62] Revert "update readme" This reverts commit adfbf9d7f45f559bec08b261fef8ebaf40d7fd72. --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 83e3437f1..861e0937a 100644 --- a/README.md +++ b/README.md @@ -363,13 +363,6 @@ support](https://www.openssh.com/txt/release-8.7) for setting `TERM` via > `xterm-256color` does not include all of Ghostty's capabilities, terminal > features beyond xterm's like colored and styled underlines will not work. -### Desktop Integration (Plasma only) -The build process detects if you are running plasma through an environment variable ```DESKTOP_SESSION``` and will install a ```.desktop``` file for a right-click menu action to open Ghostty in the working directory. To enable this, you need to give the file executable permissions: -```bash -chmod +x ~/.local/share/kio/servicemenus/ghostty_dolphin.desktop -``` -You should see an action called "Open Ghostty Here" when you right click in any directory in Dolphin. - ## Roadmap and Status The high-level ambitious plan for the project, in order: From 46afa9c4e19ed2cc969b957e4ce8decf31f523f8 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Fri, 29 Nov 2024 11:51:10 +0100 Subject: [PATCH 22/62] use cli argument instead of cd --- dist/linux/ghostty_dolphin.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop index da46f6286..3f2730c4e 100755 --- a/dist/linux/ghostty_dolphin.desktop +++ b/dist/linux/ghostty_dolphin.desktop @@ -7,5 +7,5 @@ Actions=RunGhosttyDir [Desktop Action RunGhosttyDir] Name=Open Ghostty Here Icon=com.mitchellh.ghostty -Exec=cd %F && ghostty +Exec=ghostty --working-directory=%F From 3bf1fdd7dee69f0bb0d382db38c6ad76fab75148 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 29 Nov 2024 10:32:46 -0800 Subject: [PATCH 23/62] macos: titlebar tabs can find titlebar container in fullscreen Fixes #2850 In native fullscreen, the titlebar container is no longer part of our NSWindow and is instead a separate window called NSToolbarFullScreenWindow. We now search for this window when we are in native fullscreen. --- .../Features/Terminal/TerminalWindow.swift | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 496d19700..503e76791 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -5,10 +5,7 @@ class TerminalWindow: NSWindow { lazy var titlebarColor: NSColor = backgroundColor { didSet { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - + guard let titlebarContainer else { return } titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = titlebarColor.cgColor } @@ -68,6 +65,48 @@ class TerminalWindow: NSWindow { bindings.forEach() { $0.invalidate() } } + // MARK: Titlebar Helpers + // These helpers are generic to what we're trying to achieve (i.e. titlebar + // style tabs, titlebar styling, etc.). They're just here to make it easier. + + private var titlebarContainer: NSView? { + // If we aren't fullscreen then the titlebar container is part of our window. + if !styleMask.contains(.fullScreen) { + guard let view = contentView?.superview ?? contentView else { return nil } + return titlebarContainerView(in: view) + } + + // If we are fullscreen, the titlebar container view is part of a separate + // "fullscreen window", we need to find the window and then get the view. + for window in NSApplication.shared.windows { + // This is the private window class that contains the toolbar + guard window.className == "NSToolbarFullScreenWindow" else { continue } + + // The parent will match our window. This is used to filter the correct + // fullscreen window if we have multiple. + guard window.parent == self else { continue } + + guard let view = window.contentView else { continue } + return titlebarContainerView(in: view) + } + + return nil + } + + private func titlebarContainerView(in view: NSView) -> NSView? { + if view.className == "NSTitlebarContainerView" { + return view + } + + for subview in view.subviews { + if let found = titlebarContainerView(in: subview) { + return found + } + } + + return nil + } + // MARK: - NSWindow override var title: String { @@ -152,9 +191,8 @@ class TerminalWindow: NSWindow { // would be an opaque color. When the titlebar isn't transparent, however, the system applies // a compositing effect to the unselected tab backgrounds, which makes them blend with the // titlebar's/window's background. - if let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first { + if let effectView = titlebarContainer?.descendants( + withClassName: "NSVisualEffectView").first { effectView.isHidden = titlebarTabs || !titlebarTabs && !hasVeryDarkBackground } @@ -223,10 +261,7 @@ class TerminalWindow: NSWindow { // window's key status changes in terms of becoming less prominent visually, // so we need to do it manually. private func updateNewTabButtonOpacity() { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } + guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { $0 as? NSImageView != nil }) as? NSImageView else { return } @@ -237,10 +272,7 @@ class TerminalWindow: NSWindow { // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, // just as it does in the stock tab bar. private func updateNewTabButtonImage() { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } + guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { $0 as? NSImageView != nil }) as? NSImageView else { return } @@ -272,10 +304,7 @@ class TerminalWindow: NSWindow { private func updateTabsForVeryDarkBackgrounds() { guard hasVeryDarkBackground else { return } - - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } + guard let titlebarContainer else { return } if let tabGroup = tabGroup, tabGroup.isTabBarVisible { guard let activeTabBackgroundView = titlebarContainer.firstDescendant(withClassName: "NSTabButton")?.superview?.subviews.last?.firstDescendant(withID: "_backgroundView") @@ -301,8 +330,7 @@ class TerminalWindow: NSWindow { }() private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } - + guard let titlebarContainer else { return nil } let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) let view = NSView(frame: NSRect(origin: .zero, size: size)) @@ -390,9 +418,7 @@ class TerminalWindow: NSWindow { // 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 + guard let titlebarView = titlebarContainer?.subviews .first(where: { $0.className == "NSTitlebarView" }) else { return nil } return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField } @@ -450,10 +476,7 @@ class TerminalWindow: NSWindow { // For titlebar tabs, we want to hide the separator view so that we get rid // of an aesthetically unpleasing shadow. private func hideTitleBarSeparators() { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - + guard let titlebarContainer else { return } for v in titlebarContainer.descendants(withClassName: "NSTitlebarSeparatorView") { v.isHidden = true } From df4a6f2161eed1dfc4d2652f3cc6fca54d158e84 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Fri, 29 Nov 2024 14:23:28 -0500 Subject: [PATCH 24/62] font/sprite: add missing chevron powerline fonts --- src/font/sprite/Face.zig | 2 ++ src/font/sprite/Powerline.zig | 51 +++++++++++++++++++++++++++++++++++ src/font/sprite/canvas.zig | 27 ++++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index ca0ed96e8..e1cd12f00 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -280,6 +280,8 @@ const Kind = enum { // Powerline fonts 0xE0B0, + 0xE0B1, + 0xE0B3, 0xE0B4, 0xE0B6, 0xE0B2, diff --git a/src/font/sprite/Powerline.zig b/src/font/sprite/Powerline.zig index fdb13870b..8a435a3e8 100644 --- a/src/font/sprite/Powerline.zig +++ b/src/font/sprite/Powerline.zig @@ -93,6 +93,11 @@ fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) 0xE0BE, => try self.draw_wedge_triangle(canvas, cp), + // Soft Dividers + 0xE0B1, + 0xE0B3, + => try self.draw_chevron(canvas, cp), + // Half-circles 0xE0B4, 0xE0B6, @@ -107,6 +112,50 @@ 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; + var p2_y: u32 = 0; + var p3_x: u32 = 0; + var p3_y: u32 = 0; + + + switch (cp) { + 0xE0B1 => { + p1_x = 0; + p1_y = 0; + p2_x = width; + p2_y = height / 2; + p3_x = 0; + p3_y = height; + }, + 0xE0B3 => { + p1_x = width; + p1_y = 0; + p2_x = 0; + p2_y = height / 2; + p3_x = width; + p3_y = height; + }, + + 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); + +} + fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { const width = self.width; const height = self.height; @@ -501,6 +550,8 @@ test "all" { 0xE0B6, 0xE0D2, 0xE0D4, + 0xE0B1, + 0xE0B3, }; for (cps) |cp| { var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index 81f9095b3..3d472538c 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -231,10 +231,35 @@ pub const Canvas = struct { try path.lineTo(t.p1.x, t.p1.y); try path.lineTo(t.p2.x, t.p2.y); try path.close(); - + try ctx.fill(self.alloc, path); } + pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void { + var ctx: z2d.Context = .{ + .surface = self.sfc, + .pattern = .{ + .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, + }, + }, + .line_width = thickness, + .line_cap_mode = .round, + }; + + var path = z2d.Path.init(self.alloc); + defer path.deinit(); + + try path.moveTo(t.p0.x, t.p0.y); + try path.lineTo(t.p1.x, t.p1.y); + try path.lineTo(t.p2.x, t.p2.y); + // try path.close(); + + try ctx.stroke(self.alloc, path); + // try ctx.fill(self.alloc, path); + + } + /// Stroke a line. pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void { var ctx: z2d.Context = .{ From 9ac929ef8e2b4c885f059fcc23291f0d9d203b0f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 29 Nov 2024 14:20:50 -0800 Subject: [PATCH 25/62] macos: unicode keybindings must convert to string properly Fixes #2848 The proper way to convert a unicode scalar in Swift is to use the `String` initializer that takes a `UnicodeScalar` as an argument. We were converting a number to a string before, which is incorrect. --- macos/Sources/Ghostty/Ghostty.Input.swift | 3 +- src/input/Binding.zig | 37 ++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 43bf8d096..0a279ea1f 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -50,7 +50,8 @@ extension Ghostty { } case GHOSTTY_TRIGGER_UNICODE: - equiv = String(trigger.key.unicode) + guard let scalar = UnicodeScalar(trigger.key.unicode) else { return nil } + equiv = String(scalar) default: return nil diff --git a/src/input/Binding.zig b/src/input/Binding.zig index fa719d981..a467bfc2b 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1454,21 +1454,30 @@ pub const Set = struct { }; // If we have any leaders we need to clone them. - var it = result.bindings.iterator(); - while (it.next()) |entry| switch (entry.value_ptr.*) { - // Leaves could have data to clone (i.e. text actions - // contain allocated strings). - .leaf => |*s| s.* = try s.clone(alloc), + { + var it = result.bindings.iterator(); + while (it.next()) |entry| switch (entry.value_ptr.*) { + // Leaves could have data to clone (i.e. text actions + // contain allocated strings). + .leaf => |*s| s.* = try s.clone(alloc), - // Must be deep cloned. - .leader => |*s| { - const ptr = try alloc.create(Set); - errdefer alloc.destroy(ptr); - ptr.* = try s.*.clone(alloc); - errdefer ptr.deinit(alloc); - s.* = ptr; - }, - }; + // Must be deep cloned. + .leader => |*s| { + const ptr = try alloc.create(Set); + errdefer alloc.destroy(ptr); + ptr.* = try s.*.clone(alloc); + errdefer ptr.deinit(alloc); + s.* = ptr; + }, + }; + } + + // We need to clone the action keys in the reverse map since + // they may contain allocated values. + { + var it = result.reverse.keyIterator(); + while (it.next()) |action| action.* = try action.clone(alloc); + } return result; } From 853ba9e3c7c6fc5a3cdb533a3047e10a6af49019 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 29 Nov 2024 14:39:22 -0800 Subject: [PATCH 26/62] terminal: reset should preserve desired default mode values Fixes #2857 Some terminal modes always reset, but there are others that should be conditional based on how the terminal's default state is configured. Primarily from #2857 is the grapheme clustering mode (mode 2027) which was always resetting to false but should be conditional based on the the `grapheme-width-method` configuration. --- src/terminal/Terminal.zig | 22 +++++++++++++++++++++- src/terminal/main.zig | 1 + src/terminal/modes.zig | 11 +++++++++++ src/termio/Termio.zig | 31 ++++++++++++++++++------------- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 8de914a3e..a2cf510b1 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -193,6 +193,10 @@ pub const Options = struct { cols: size.CellCountInt, rows: size.CellCountInt, max_scrollback: usize = 10_000, + + /// The default mode state. When the terminal gets a reset, it + /// will revert back to this state. + default_modes: modes.ModePacked = .{}, }; /// Initialize a new terminal. @@ -216,6 +220,10 @@ pub fn init( .right = cols - 1, }, .pwd = std.ArrayList(u8).init(alloc), + .modes = .{ + .values = opts.default_modes, + .default = opts.default_modes, + }, }; } @@ -2680,7 +2688,7 @@ fn resetCommonState(self: *Terminal) void { self.screen.endHyperlink(); self.screen.charset = .{}; - self.modes = .{}; + self.modes.reset(); self.flags = .{}; self.tabstops.reset(TABSTOP_INTERVAL); self.screen.kitty_keyboard = .{}; @@ -10555,6 +10563,18 @@ test "Terminal: fullReset clears alt screen kitty keyboard state" { try testing.expectEqual(0, t.secondary_screen.kitty_keyboard.current().int()); } +test "Terminal: fullReset default modes" { + var t = try init(testing.allocator, .{ + .cols = 10, + .rows = 10, + .default_modes = .{ .grapheme_cluster = true }, + }); + defer t.deinit(testing.allocator); + try testing.expect(t.modes.get(.grapheme_cluster)); + t.fullReset(); + try testing.expect(t.modes.get(.grapheme_cluster)); +} + // https://github.com/mitchellh/ghostty/issues/272 // This is also tested in depth in screen resize tests but I want to keep // this test around to ensure we don't regress at multiple layers. diff --git a/src/terminal/main.zig b/src/terminal/main.zig index d295ea1ba..3fc7d2600 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -47,6 +47,7 @@ pub const CursorStyle = Screen.CursorStyle; pub const CursorStyleReq = ansi.CursorStyle; pub const DeviceAttributeReq = ansi.DeviceAttributeReq; pub const Mode = modes.Mode; +pub const ModePacked = modes.ModePacked; pub const ModifyKeyFormat = ansi.ModifyKeyFormat; pub const ProtectedMode = ansi.ProtectedMode; pub const StatusLineType = ansi.StatusLineType; diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index c4dbb1cd6..89d352e4a 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -21,6 +21,17 @@ pub const ModeState = struct { /// a real-world issue but we need to be aware of a DoS vector. saved: ModePacked = .{}, + /// The default values for the modes. This is used to reset + /// the modes to their default values during reset. + default: ModePacked = .{}, + + /// Reset the modes to their default values. This also clears the + /// saved state. + pub fn reset(self: *ModeState) void { + self.values = self.default; + self.saved = .{}; + } + /// Set a mode to a value. pub fn set(self: *ModeState, mode: Mode, value: bool) void { switch (mode) { diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index e7b391419..1ebe84541 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -127,6 +127,23 @@ pub const DerivedConfig = struct { /// This will also start the child process if the termio is configured /// to run a child process. pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { + // The default terminal modes based on our config. + const default_modes: terminal.ModePacked = modes: { + var modes: terminal.ModePacked = .{}; + + // Setup our initial grapheme cluster support if enabled. We use a + // switch to ensure we get a compiler error if more cases are added. + switch (opts.full_config.@"grapheme-width-method") { + .unicode => modes.grapheme_cluster = true, + .legacy => {}, + } + + // Set default cursor blink settings + modes.cursor_blinking = opts.config.cursor_blink orelse true; + + break :modes modes; + }; + // Create our terminal var term = try terminal.Terminal.init(alloc, opts: { const grid_size = opts.size.grid(); @@ -134,19 +151,13 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .cols = grid_size.columns, .rows = grid_size.rows, .max_scrollback = opts.full_config.@"scrollback-limit", + .default_modes = default_modes, }; }); errdefer term.deinit(alloc); term.default_palette = opts.config.palette; term.color_palette.colors = opts.config.palette; - // Setup our initial grapheme cluster support if enabled. We use a - // switch to ensure we get a compiler error if more cases are added. - switch (opts.full_config.@"grapheme-width-method") { - .unicode => term.modes.set(.grapheme_cluster, true), - .legacy => {}, - } - // Set the image size limits try term.screen.kitty_images.setLimit( alloc, @@ -159,12 +170,6 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { opts.config.image_storage_limit, ); - // Set default cursor blink settings - term.modes.set( - .cursor_blinking, - opts.config.cursor_blink orelse true, - ); - // Set our default cursor style term.screen.cursor.cursor_style = opts.config.cursor_style; From 3048d71537a5a6f3ce512d6c36e0604c34d164ef Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Sun, 1 Dec 2024 13:16:56 +0100 Subject: [PATCH 27/62] added gtk-single-instance argument --- dist/linux/ghostty_dolphin.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop index 3f2730c4e..5e8351390 100755 --- a/dist/linux/ghostty_dolphin.desktop +++ b/dist/linux/ghostty_dolphin.desktop @@ -7,5 +7,5 @@ Actions=RunGhosttyDir [Desktop Action RunGhosttyDir] Name=Open Ghostty Here Icon=com.mitchellh.ghostty -Exec=ghostty --working-directory=%F +Exec=ghostty --working-directory=%F --gtk-single-instance=false From f384fd038b642cae3b4f537de894c39c0539c59a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 1 Dec 2024 11:31:55 -0800 Subject: [PATCH 28/62] macos: trigger fullscreenDidChange on any fullscreen event Fixes #2840 Related to #2842 This builds on #2842 by missing a key situation: when native fullscreen is toggled using the menu bar items it doesn't go through our `FullscreenStyle` machinery so we don't trigger fullscreen change events. This commit makes it so that our FullscreenStyle always listens for native fullscreen change (even in non-native modes) to fire a fullscreen did change event. This way we can always rely on the event to be fired when fullscreen changes no matter what. --- .../QuickTerminalController.swift | 1 + .../Terminal/BaseTerminalController.swift | 16 ++++- .../Terminal/TerminalController.swift | 1 + macos/Sources/Helpers/Fullscreen.swift | 72 +++++++++++++------ 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 18549eea1..b5e65d76e 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -57,6 +57,7 @@ class QuickTerminalController: BaseTerminalController { // MARK: NSWindowController override func windowDidLoad() { + super.windowDidLoad() guard let window = self.window else { return } // The controller is the window delegate so we can detect events such as diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 721248013..68c243004 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -404,7 +404,21 @@ class BaseTerminalController: NSWindowController, } } - //MARK: - NSWindowDelegate + // MARK: NSWindowController + + override func windowDidLoad() { + guard let window else { return } + + // We always initialize our fullscreen style to native if we can because + // initialization sets up some state (i.e. observers). If its set already + // somehow we don't do this. + if fullscreenStyle == nil { + fullscreenStyle = NativeFullscreen(window) + fullscreenStyle?.delegate = self + } + } + + // MARK: NSWindowDelegate // This is called when performClose is called on a window (NOT when close() // is called directly). performClose is called primarily when UI elements such diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 698551f3e..67e7259f3 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -275,6 +275,7 @@ class TerminalController: BaseTerminalController { } override func windowDidLoad() { + super.windowDidLoad() guard let window = window as? TerminalWindow else { return } // I copy this because we may change the source in the future but also because diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index f5df43ec2..bb3859e07 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -45,20 +45,53 @@ extension FullscreenDelegate { func fullscreenDidChange() {} } +/// The base class for fullscreen implementations, cannot be used as a FullscreenStyle on its own. +class FullscreenBase { + let window: NSWindow + weak var delegate: FullscreenDelegate? + + required init?(_ window: NSWindow) { + self.window = window + + // We want to trigger delegate methods on window native fullscreen + // changes (didEnterFullScreenNotification, etc.) no matter what our + // fullscreen style is. + let center = NotificationCenter.default + center.addObserver( + self, + selector: #selector(didEnterFullScreenNotification), + name: NSWindow.didEnterFullScreenNotification, + object: window) + center.addObserver( + self, + selector: #selector(didExitFullScreenNotification), + name: NSWindow.didExitFullScreenNotification, + object: window) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func didEnterFullScreenNotification(_ notification: Notification) { + delegate?.fullscreenDidChange() + } + + @objc private func didExitFullScreenNotification(_ notification: Notification) { + delegate?.fullscreenDidChange() + } +} + /// macOS native fullscreen. This is the typical behavior you get by pressing the green fullscreen /// button on regular titlebars. -class NativeFullscreen: FullscreenStyle { - private let window: NSWindow - - weak var delegate: FullscreenDelegate? +class NativeFullscreen: FullscreenBase, FullscreenStyle { var isFullscreen: Bool { window.styleMask.contains(.fullScreen) } var supportsTabs: Bool { true } required init?(_ window: NSWindow) { // TODO: There are many requirements for native fullscreen we should // check here such as the stylemask. - - self.window = window + super.init(window) } func enter() { @@ -72,8 +105,9 @@ class NativeFullscreen: FullscreenStyle { // Enter fullscreen window.toggleFullScreen(self) - // Notify the delegate - delegate?.fullscreenDidChange() + // Note: we don't call our delegate here because the base class + // will always trigger the delegate on native fullscreen notifications + // and we don't want to double notify. } func exit() { @@ -84,14 +118,13 @@ class NativeFullscreen: FullscreenStyle { window.toggleFullScreen(nil) - // Notify the delegate - delegate?.fullscreenDidChange() + // Note: we don't call our delegate here because the base class + // will always trigger the delegate on native fullscreen notifications + // and we don't want to double notify. } } -class NonNativeFullscreen: FullscreenStyle { - weak var delegate: FullscreenDelegate? - +class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Non-native fullscreen never supports tabs because tabs require // the "titled" style and we don't have it for non-native fullscreen. var supportsTabs: Bool { false } @@ -110,13 +143,8 @@ class NonNativeFullscreen: FullscreenStyle { var hideMenu: Bool = true } - private let window: NSWindow private var savedState: SavedState? - required init?(_ window: NSWindow) { - self.window = window - } - func enter() { // If we are in fullscreen we don't do it again. guard !isFullscreen else { return } @@ -187,8 +215,12 @@ class NonNativeFullscreen: FullscreenStyle { guard isFullscreen else { return } guard let savedState else { return } - // Remove all our notifications - NotificationCenter.default.removeObserver(self) + // Remove all our notifications. We remove them one by one because + // we don't want to remove the observers that our superclass sets. + let center = NotificationCenter.default + center.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window) + center.removeObserver(self, name: NSWindow.didResignMainNotification, object: window) + center.removeObserver(self, name: NSWindow.didChangeScreenNotification, object: window) // Unhide our elements if savedState.dock { From e7bfc17318effbe48c9bcdf471b2af805e7d8e33 Mon Sep 17 00:00:00 2001 From: Valentin Shinkarev Date: Sun, 1 Dec 2024 22:58:46 +0300 Subject: [PATCH 29/62] fix slow scroll in mouseReport --- src/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index eef2eb8b3..78a842673 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2336,7 +2336,7 @@ pub fn scrollCallback( // If we're scrolling up or down, then send a mouse event. if (self.io.terminal.flags.mouse_event != .none) { - if (y.delta != 0) { + for (0..@abs(y.delta)) |_| { const pos = try self.rt_surface.getCursorPos(); try self.mouseReport(switch (y.direction()) { .up_right => .four, @@ -2344,7 +2344,7 @@ pub fn scrollCallback( }, .press, self.mouse.mods, pos); } - if (x.delta != 0) { + for (0..@abs(x.delta)) |_| { const pos = try self.rt_surface.getCursorPos(); try self.mouseReport(switch (x.direction()) { .up_right => .six, From d57d1d2395c9f80ecdb7be4cd58858b299f95c42 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Dec 2024 09:39:21 -0500 Subject: [PATCH 30/62] terminal: failing tracked pin test on fullReset --- src/terminal/PageList.zig | 6 +++++- src/terminal/Terminal.zig | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 70f972ebe..3d67278ba 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2356,7 +2356,11 @@ pub fn countTrackedPins(self: *const PageList) usize { /// Checks if a pin is valid for this pagelist. This is a very slow and /// expensive operation since we traverse the entire linked list in the /// worst case. Only for runtime safety/debug. -fn pinIsValid(self: *const PageList, p: Pin) bool { +pub fn pinIsValid(self: *const PageList, p: Pin) bool { + // This is very slow so we want to ensure we only ever + // call this during slow runtime safety builds. + comptime assert(build_config.slow_runtime_safety); + var it = self.pages.first; while (it) |node| : (it = node.next) { if (node != p.node) continue; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index a2cf510b1..0a2914f10 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -10575,6 +10575,16 @@ test "Terminal: fullReset default modes" { try testing.expect(t.modes.get(.grapheme_cluster)); } +test "Terminal: fullReset tracked pins" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Create a tracked pin + const p = try t.screen.pages.trackPin(t.screen.cursor.page_pin.*); + t.fullReset(); + try testing.expect(t.screen.pages.pinIsValid(p.*)); +} + // https://github.com/mitchellh/ghostty/issues/272 // This is also tested in depth in screen resize tests but I want to keep // this test around to ensure we don't regress at multiple layers. From d7fcaefdf3622a3aefadbf788c062065576ff928 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Dec 2024 17:25:17 -0500 Subject: [PATCH 31/62] terminal: PageList.reset --- src/terminal/PageList.zig | 134 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 3d67278ba..aec0af278 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -330,6 +330,77 @@ pub fn deinit(self: *PageList) void { } } +/// Reset the PageList back to an empty state. This is similar to +/// deinit and reinit but it importantly preserves the pointer +/// stability of tracked pins (they're moved to the top-left since +/// all contents are cleared). +/// +/// This can't fail because we always retain at least enough allocated +/// memory to fit the active area. +pub fn reset(self: *PageList) void { + // We need enough pages/nodes to keep our active area. This should + // never fail since we by definition have allocated a page already + // that fits our size but I'm not confident to make that assertion. + const cap = std_capacity.adjust( + .{ .cols = self.cols }, + ) catch @panic("reset: std_capacity.adjust failed"); + assert(cap.rows > 0); // adjust should never return 0 rows + + // The number of pages we need is the number of rows in the active + // area divided by the row capacity of a page. + const page_count = std.math.divCeil( + usize, + self.rows, + cap.rows, + ) catch unreachable; + + // Before resetting our pools we need to free any pages that + // are non-standard size since those were allocated outside + // the pool. + { + const page_alloc = self.pool.pages.arena.child_allocator; + var it = self.pages.first; + while (it) |node| : (it = node.next) { + if (node.data.memory.len > std_size) { + page_alloc.free(node.data.memory); + } + } + } + + // Reset our pools to free as much memory as possible while retaining + // the capacity for at least the minimum number of pages we need. + // The return value is whether memory was reclaimed or not, but in + // either case the pool is left in a valid state. + _ = self.pool.pages.reset(.{ + .retain_with_limit = page_count * PagePool.item_size, + }); + _ = self.pool.nodes.reset(.{ + .retain_with_limit = page_count * NodePool.item_size, + }); + + // Initialize our pages. This should not be able to fail since + // we retained the capacity for the minimum number of pages we need. + self.pages, self.page_size = initPages( + &self.pool, + self.cols, + self.rows, + ) catch @panic("initPages failed"); + + // Update all our tracked pins to point to our first page top-left + { + var it = self.tracked_pins.iterator(); + while (it.next()) |entry| { + const p: *Pin = entry.key_ptr.*; + p.node = self.pages.first.?; + p.x = 0; + p.y = 0; + } + } + + // Move our viewport back to the active area since everything is gone. + self.viewport = .active; +} + pub const Clone = struct { /// The top and bottom (inclusive) points of the region to clone. /// The x coordinate is ignored; the full row is always cloned. @@ -8195,3 +8266,66 @@ test "PageList resize reflow wrap moves kitty placeholder" { } try testing.expect(it.next() == null); } + +test "PageList reset" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + s.reset(); + try testing.expect(s.viewport == .active); + try testing.expect(s.pages.first != null); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // Active area should be the top + try testing.expectEqual(Pin{ + .node = s.pages.first.?, + .y = 0, + .x = 0, + }, s.getTopLeft(.active)); +} + +test "PageList reset across two pages" { + const testing = std.testing; + const alloc = testing.allocator; + + // Find a cap that makes it so that rows don't fit on one page. + const rows = 100; + const cap = cap: { + var cap = try std_capacity.adjust(.{ .cols = 50 }); + while (cap.rows >= rows) cap = try std_capacity.adjust(.{ + .cols = cap.cols + 50, + }); + + break :cap cap; + }; + + // Init + var s = try init(alloc, cap.cols, rows, null); + defer s.deinit(); + s.reset(); + try testing.expect(s.viewport == .active); + try testing.expect(s.pages.first != null); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); +} + +test "PageList clears history" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(30); + s.reset(); + try testing.expect(s.viewport == .active); + try testing.expect(s.pages.first != null); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // Active area should be the top + try testing.expectEqual(Pin{ + .node = s.pages.first.?, + .y = 0, + .x = 0, + }, s.getTopLeft(.active)); +} From 212bd3d5fb5ced7866f459fdc80f757811f14b3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Dec 2024 17:43:56 -0500 Subject: [PATCH 32/62] terminal: fullReset uses the new screen reset methods --- src/terminal/Screen.zig | 48 +++++++++++++++++++++- src/terminal/Terminal.zig | 84 ++++++++++----------------------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 4f3fe270e..d8787487f 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -83,8 +83,8 @@ pub const Dirty = packed struct { /// The cursor position and style. pub const Cursor = struct { // The x/y position within the viewport. - x: size.CellCountInt, - y: size.CellCountInt, + x: size.CellCountInt = 0, + y: size.CellCountInt = 0, /// The visual style of the cursor. This defaults to block because /// it has to default to something, but users of this struct are @@ -249,6 +249,50 @@ pub fn assertIntegrity(self: *const Screen) void { } } +/// Reset the screen according to the logic of a DEC RIS sequence. +/// +/// - Clears the screen and attempts to reclaim memory. +/// - Moves the cursor to the top-left. +/// - Clears any cursor state: style, hyperlink, etc. +/// - Resets the charset +/// - Clears the selection +/// - Deletes all Kitty graphics +/// - Resets Kitty Keyboard settings +/// - Disables protection mode +/// +pub fn reset(self: *Screen) void { + // Reset our pages + self.pages.reset(); + + // The above reset preserves tracked pins so we can still use + // our cursor pin, which should be at the top-left already. + const cursor_pin: *PageList.Pin = self.cursor.page_pin; + assert(cursor_pin.node == self.pages.pages.first.?); + assert(cursor_pin.x == 0); + assert(cursor_pin.y == 0); + const cursor_rac = cursor_pin.rowAndCell(); + self.cursor.deinit(self.alloc); + self.cursor = .{ + .page_pin = cursor_pin, + .page_row = cursor_rac.row, + .page_cell = cursor_rac.cell, + }; + + // Clear kitty graphics + self.kitty_images.delete( + self.alloc, + undefined, // All image deletion doesn't need the terminal + .{ .all = true }, + ); + + // Reset our basic state + self.saved_cursor = null; + self.charset = .{}; + self.kitty_keyboard = .{}; + self.protected_mode = .off; + self.clearSelection(); +} + /// Clone the screen. /// /// This will copy: diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 0a2914f10..a11028304 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2627,82 +2627,38 @@ pub fn plainStringUnwrapped(self: *Terminal, alloc: Allocator) ![]const u8 { /// Full reset. /// -/// This will attempt to free the existing screen memory and allocate -/// new screens but if that fails this will reuse the existing memory -/// from the prior screens. In the latter case, memory may be wasted -/// (since its unused) but it isn't leaked. +/// This will attempt to free the existing screen memory but if that fails +/// this will reuse the existing memory. In the latter case, memory may +/// be wasted (since its unused) but it isn't leaked. pub fn fullReset(self: *Terminal) void { - // Attempt to initialize new screens. - var new_primary = Screen.init( - self.screen.alloc, - self.cols, - self.rows, - self.screen.pages.explicit_max_size, - ) catch |err| { - log.warn("failed to allocate new primary screen, reusing old memory err={}", .{err}); - self.fallbackReset(); - return; - }; - const new_secondary = Screen.init( - self.secondary_screen.alloc, - self.cols, - self.rows, - 0, - ) catch |err| { - log.warn("failed to allocate new secondary screen, reusing old memory err={}", .{err}); - new_primary.deinit(); - self.fallbackReset(); - return; - }; + // Reset our screens + self.screen.reset(); + self.secondary_screen.reset(); - // If we got here, both new screens were successfully allocated - // and we can deinitialize the old screens. - self.screen.deinit(); - self.secondary_screen.deinit(); + // Ensure we're back on primary screen + if (self.active_screen != .primary) { + const old = self.screen; + self.screen = self.secondary_screen; + self.secondary_screen = old; + self.active_screen = .primary; + } - // Replace with the newly allocated screens. - self.screen = new_primary; - self.secondary_screen = new_secondary; - - self.resetCommonState(); -} - -fn fallbackReset(self: *Terminal) void { - // Clear existing screens without reallocation - self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = false }); - self.screen.clearSelection(); - self.eraseDisplay(.scrollback, false); - self.eraseDisplay(.complete, false); - self.screen.cursorAbsolute(0, 0); - self.resetCommonState(); -} - -fn resetCommonState(self: *Terminal) void { - // We set the saved cursor to null and then restore. This will force - // our cursor to go back to the default which will also move the cursor - // to the top-left. - self.screen.saved_cursor = null; - self.restoreCursor() catch |err| { - log.warn("restore cursor on primary screen failed err={}", .{err}); - }; - - self.screen.endHyperlink(); - self.screen.charset = .{}; + // Rest our basic state self.modes.reset(); self.flags = .{}; self.tabstops.reset(TABSTOP_INTERVAL); - self.screen.kitty_keyboard = .{}; - self.secondary_screen.kitty_keyboard = .{}; - self.screen.protected_mode = .off; + self.previous_char = null; + self.pwd.clearRetainingCapacity(); + self.status_display = .main; self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1, .left = 0, .right = self.cols - 1, }; - self.previous_char = null; - self.pwd.clearRetainingCapacity(); - self.status_display = .main; + + // Always mark dirty so we redraw everything + self.flags.dirty.clear = true; } /// Returns true if the point is dirty, used for testing. From e712314f31b55d84b689732cc6def8015e74c514 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Dec 2024 08:57:42 -0500 Subject: [PATCH 33/62] terminal: PageList.reset has to zero arena memory to avoid reuse Fixes #2877 As the comment in the diff states, we rely on `mmap` to zero our memory. When we reset we are reusing previously allocated memory so we won't hit an `mmap`. We need to zero the memory ourselves. This is pretty slow if there is a lot of memory but in every case except allocation failures, we expect there to be only a few pages allocated. --- src/terminal/PageList.zig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index aec0af278..01e7ed71d 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -378,6 +378,25 @@ pub fn reset(self: *PageList) void { .retain_with_limit = page_count * NodePool.item_size, }); + // Our page pool relies on mmap to zero our page memory. Since we're + // retaining a certain amount of memory, it won't use mmap and won't + // be zeroed. This block zeroes out all the memory in the pool arena. + { + // Note: we only have to do this for the page pool because the + // nodes are always fully overwritten on each allocation. + const page_arena = &self.pool.pages.arena; + var it = page_arena.state.buffer_list.first; + while (it) |node| : (it = node.next) { + // The fully allocated buffer + const alloc_buf = @as([*]u8, @ptrCast(node))[0..node.data]; + + // The buffer minus our header + const BufNode = @TypeOf(page_arena.state.buffer_list).Node; + const data_buf = alloc_buf[@sizeOf(BufNode)..]; + @memset(data_buf, 0); + } + } + // Initialize our pages. This should not be able to fail since // we retained the capacity for the minimum number of pages we need. self.pages, self.page_size = initPages( From bcefbfd7b42dcb6389c738e0ebad95b0fc3c16cc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2024 09:37:03 -0800 Subject: [PATCH 34/62] terminal: move UTF8 encoding to Page and wrap around it --- src/terminal/PageList.zig | 38 ++++++++++++++ src/terminal/Screen.zig | 86 ++----------------------------- src/terminal/page.zig | 106 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 83 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 01e7ed71d..175e3f64f 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2544,6 +2544,44 @@ pub fn getCell(self: *const PageList, pt: point.Point) ?Cell { }; } +pub const EncodeUtf8Options = struct { + /// The start and end points of the dump, both inclusive. The x will + /// be ignored and the full row will always be dumped. + tl: Pin, + br: ?Pin = null, + + /// If true, this will unwrap soft-wrapped lines. If false, this will + /// dump the screen as it is visually seen in a rendered window. + unwrap: bool = true, +}; + +/// Encode the pagelist to utf8 to the given writer. +/// +/// The writer should be buffered; this function does not attempt to +/// efficiently write and often writes one byte at a time. +/// +/// Note: this is tested using Screen.dumpString. This is a function that +/// predates this and is a thin wrapper around it so the tests all live there. +pub fn encodeUtf8( + self: *const PageList, + writer: anytype, + opts: EncodeUtf8Options, +) anyerror!void { + // We don't currently use self at all. There is an argument that this + // function should live on Pin instead but there is some future we might + // need state on here so... letting it go. + _ = self; + + var page_opts: Page.EncodeUtf8Options = .{ .unwrap = opts.unwrap }; + var iter = opts.tl.pageIterator(.right_down, opts.br); + while (iter.next()) |chunk| { + const page: *const Page = &chunk.node.data; + page_opts.start_y = chunk.start; + page_opts.end_y = chunk.end; + page_opts.preceding = try page.encodeUtf8(writer, page_opts); + } +} + /// Log a debug diagram of the page list to the provided writer. /// /// EXAMPLE: diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index d8787487f..bf63e7e05 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2731,95 +2731,15 @@ pub fn promptPath( return .{ .x = to_x - from_x, .y = to_y - from_y }; } -pub const DumpString = struct { - /// The start and end points of the dump, both inclusive. The x will - /// be ignored and the full row will always be dumped. - tl: Pin, - br: ?Pin = null, - - /// If true, this will unwrap soft-wrapped lines. If false, this will - /// dump the screen as it is visually seen in a rendered window. - unwrap: bool = true, -}; - /// Dump the screen to a string. The writer given should be buffered; /// this function does not attempt to efficiently write and generally writes /// one byte at a time. pub fn dumpString( self: *const Screen, writer: anytype, - opts: DumpString, -) !void { - var blank_rows: usize = 0; - var blank_cells: usize = 0; - - var iter = opts.tl.rowIterator(.right_down, opts.br); - while (iter.next()) |row_offset| { - const rac = row_offset.rowAndCell(); - const row = rac.row; - const cells = cells: { - const cells: [*]pagepkg.Cell = @ptrCast(rac.cell); - break :cells cells[0..self.pages.cols]; - }; - - if (!pagepkg.Cell.hasTextAny(cells)) { - blank_rows += 1; - continue; - } - if (blank_rows > 0) { - for (0..blank_rows) |_| try writer.writeByte('\n'); - blank_rows = 0; - } - - if (!row.wrap or !opts.unwrap) { - // If we're not wrapped, we always add a newline. - // If we are wrapped, we only add a new line if we're unwrapping - // soft-wrapped lines. - blank_rows += 1; - } - - if (!row.wrap_continuation or !opts.unwrap) { - // We should also reset blank cell counts at the start of each row - // unless we're unwrapping and this row is a wrap continuation. - blank_cells = 0; - } - - for (cells) |*cell| { - // Skip spacers - switch (cell.wide) { - .narrow, .wide => {}, - .spacer_head, .spacer_tail => continue, - } - - // If we have a zero value, then we accumulate a counter. We - // only want to turn zero values into spaces if we have a non-zero - // char sometime later. - if (!cell.hasText()) { - blank_cells += 1; - continue; - } - if (blank_cells > 0) { - try writer.writeByteNTimes(' ', blank_cells); - blank_cells = 0; - } - - switch (cell.content_tag) { - .codepoint => { - try writer.print("{u}", .{cell.content.codepoint}); - }, - - .codepoint_grapheme => { - try writer.print("{u}", .{cell.content.codepoint}); - const cps = row_offset.node.data.lookupGrapheme(cell).?; - for (cps) |cp| { - try writer.print("{u}", .{cp}); - } - }, - - else => unreachable, - } - } - } + opts: PageList.EncodeUtf8Options, +) anyerror!void { + try self.pages.encodeUtf8(writer, opts); } /// You should use dumpString, this is a restricted version mostly for diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 8c470d726..d41f37e8d 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1481,6 +1481,112 @@ pub const Page = struct { return self.grapheme_map.map(self.memory).capacity(); } + /// Options for encoding the page as UTF-8. + pub const EncodeUtf8Options = struct { + /// The range of rows to encode. If end_y is null, then it will + /// encode to the end of the page. + start_y: size.CellCountInt = 0, + end_y: ?size.CellCountInt = null, + + /// If true, this will unwrap soft-wrapped lines. If false, this will + /// dump the screen as it is visually seen in a rendered window. + unwrap: bool = true, + + /// Preceding state from encoding the prior page. Used to preserve + /// blanks properly across multiple pages. + preceding: TrailingUtf8State = .{}, + + /// Trailing state for UTF-8 encoding. + pub const TrailingUtf8State = struct { + rows: usize = 0, + cells: usize = 0, + }; + }; + + /// Encode the page contents as UTF-8. + /// + /// If preceding is non-null, then it will be used to initialize our + /// blank rows/cells count so that we can accumulate blanks across + /// multiple pages. + /// + /// Note: The tests for this function are done via Screen.dumpString + /// tests since that function is a thin wrapper around this one and + /// it makes it easier to test input contents. + pub fn encodeUtf8( + self: *const Page, + writer: anytype, + opts: EncodeUtf8Options, + ) anyerror!EncodeUtf8Options.TrailingUtf8State { + var blank_rows: usize = opts.preceding.rows; + var blank_cells: usize = opts.preceding.cells; + + const start_y: size.CellCountInt = opts.start_y; + const end_y: size.CellCountInt = opts.end_y orelse self.size.rows; + for (start_y..end_y) |y| { + const row: *Row = self.getRow(y); + const cells: []const Cell = self.getCells(row); + + // If this row is blank, accumulate to avoid a bunch of extra + // work later. If it isn't blank, make sure we dump all our + // blanks. + if (!Cell.hasTextAny(cells)) { + blank_rows += 1; + continue; + } + for (0..blank_rows) |_| try writer.writeByte('\n'); + blank_rows = 0; + + // If we're not wrapped, we always add a newline so after + // the row is printed we can add a newline. + if (!row.wrap or !opts.unwrap) blank_rows += 1; + + // If the row doesn't continue a wrap then we need to reset + // our blank cell count. + if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0; + + // Go through each cell and print it + for (cells) |*cell| { + // Skip spacers + switch (cell.wide) { + .narrow, .wide => {}, + .spacer_head, .spacer_tail => continue, + } + + // If we have a zero value, then we accumulate a counter. We + // only want to turn zero values into spaces if we have a non-zero + // char sometime later. + if (!cell.hasText()) { + blank_cells += 1; + continue; + } + if (blank_cells > 0) { + try writer.writeByteNTimes(' ', blank_cells); + blank_cells = 0; + } + + switch (cell.content_tag) { + .codepoint => { + try writer.print("{u}", .{cell.content.codepoint}); + }, + + .codepoint_grapheme => { + try writer.print("{u}", .{cell.content.codepoint}); + for (self.lookupGrapheme(cell).?) |cp| { + try writer.print("{u}", .{cp}); + } + }, + + // Unreachable since we do hasText() above + .bg_color_palette, + .bg_color_rgb, + => unreachable, + } + } + } + + return .{ .rows = blank_rows, .cells = blank_cells }; + } + /// Returns the bitset for the dirty bits on this page. /// /// The returned value is a DynamicBitSetUnmanaged but it is NOT From 204e4f86634451422e4ba3a6e3d0f1f855af480d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2024 09:37:03 -0800 Subject: [PATCH 35/62] terminal: support cell_map for encodeUtf8 --- src/terminal/PageList.zig | 8 +++- src/terminal/Screen.zig | 78 +++++++++++++++++++++++++++++++++++++++ src/terminal/page.zig | 75 +++++++++++++++++++++++++++++++++++-- 3 files changed, 156 insertions(+), 5 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 175e3f64f..f8afc801a 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2553,6 +2553,9 @@ pub const EncodeUtf8Options = struct { /// If true, this will unwrap soft-wrapped lines. If false, this will /// dump the screen as it is visually seen in a rendered window. unwrap: bool = true, + + /// See Page.EncodeUtf8Options. + cell_map: ?*Page.CellMap = null, }; /// Encode the pagelist to utf8 to the given writer. @@ -2572,7 +2575,10 @@ pub fn encodeUtf8( // need state on here so... letting it go. _ = self; - var page_opts: Page.EncodeUtf8Options = .{ .unwrap = opts.unwrap }; + var page_opts: Page.EncodeUtf8Options = .{ + .unwrap = opts.unwrap, + .cell_map = opts.cell_map, + }; var iter = opts.tl.pageIterator(.right_down, opts.br); while (iter.next()) |chunk| { const page: *const Page = &chunk.node.data; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index bf63e7e05..ac9483742 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -8468,3 +8468,81 @@ test "Screen: adjustCapacity cursor style ref count" { ); } } + +test "Screen UTF8 cell map with newlines" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("A\n\nB\n\nC"); + + var cell_map = Page.CellMap.init(alloc); + defer cell_map.deinit(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + try s.dumpString(builder.writer(), .{ + .tl = s.pages.getTopLeft(.screen), + .br = s.pages.getBottomRight(.screen), + .cell_map = &cell_map, + }); + + try testing.expectEqual(7, builder.items.len); + try testing.expectEqualStrings("A\n\nB\n\nC", builder.items); + try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 0, + }, cell_map.items[0]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 1, + .y = 0, + }, cell_map.items[1]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 1, + }, cell_map.items[2]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 2, + }, cell_map.items[3]); +} + +test "Screen UTF8 cell map with blank prefix" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + s.cursorAbsolute(2, 1); + try s.testWriteString("B"); + + var cell_map = Page.CellMap.init(alloc); + defer cell_map.deinit(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + try s.dumpString(builder.writer(), .{ + .tl = s.pages.getTopLeft(.screen), + .br = s.pages.getBottomRight(.screen), + .cell_map = &cell_map, + }); + + try testing.expectEqualStrings("\n B", builder.items); + try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 0, + }, cell_map.items[0]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 1, + }, cell_map.items[1]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 1, + .y = 1, + }, cell_map.items[2]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 2, + .y = 1, + }, cell_map.items[3]); +} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index d41f37e8d..83164e163 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1496,6 +1496,13 @@ pub const Page = struct { /// blanks properly across multiple pages. preceding: TrailingUtf8State = .{}, + /// If non-null, this will be cleared and filled with the x/y + /// coordinates of each byte in the UTF-8 encoded output. + /// The index in the array is the byte offset in the output + /// where 0 is the cursor of the writer when the function is + /// called. + cell_map: ?*CellMap = null, + /// Trailing state for UTF-8 encoding. pub const TrailingUtf8State = struct { rows: usize = 0, @@ -1503,13 +1510,22 @@ pub const Page = struct { }; }; + /// See cell_map + pub const CellMap = std.ArrayList(CellMapEntry); + + /// The x/y coordinate of a single cell in the cell map. + pub const CellMapEntry = struct { + y: size.CellCountInt, + x: size.CellCountInt, + }; + /// Encode the page contents as UTF-8. /// /// If preceding is non-null, then it will be used to initialize our /// blank rows/cells count so that we can accumulate blanks across /// multiple pages. /// - /// Note: The tests for this function are done via Screen.dumpString + /// Note: Many tests for this function are done via Screen.dumpString /// tests since that function is a thin wrapper around this one and /// it makes it easier to test input contents. pub fn encodeUtf8( @@ -1522,7 +1538,18 @@ pub const Page = struct { const start_y: size.CellCountInt = opts.start_y; const end_y: size.CellCountInt = opts.end_y orelse self.size.rows; - for (start_y..end_y) |y| { + + // We can probably avoid this by doing the logic below in a different + // way. The reason this exists is so that when we end a non-blank + // line with a newline, we can correctly map the cell map over to + // the correct x value. + // + // For example "A\nB". The cell map for "\n" should be (1, 0). + // This is tested in Screen.zig so feel free to refactor this. + var last_x: size.CellCountInt = 0; + + for (start_y..end_y) |y_usize| { + const y: size.CellCountInt = @intCast(y_usize); const row: *Row = self.getRow(y); const cells: []const Cell = self.getCells(row); @@ -1533,7 +1560,19 @@ pub const Page = struct { blank_rows += 1; continue; } - for (0..blank_rows) |_| try writer.writeByte('\n'); + for (1..blank_rows + 1) |i| { + try writer.writeByte('\n'); + + // This is tested in Screen.zig, i.e. one test is + // "cell map with newlines" + if (opts.cell_map) |cell_map| { + try cell_map.append(.{ + .x = last_x, + .y = @intCast(y - blank_rows + i - 1), + }); + last_x = 0; + } + } blank_rows = 0; // If we're not wrapped, we always add a newline so after @@ -1545,7 +1584,9 @@ pub const Page = struct { if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0; // Go through each cell and print it - for (cells) |*cell| { + for (cells, 0..) |*cell, x_usize| { + const x: size.CellCountInt = @intCast(x_usize); + // Skip spacers switch (cell.wide) { .narrow, .wide => {}, @@ -1561,18 +1602,44 @@ pub const Page = struct { } if (blank_cells > 0) { try writer.writeByteNTimes(' ', blank_cells); + if (opts.cell_map) |cell_map| { + for (0..blank_cells) |i| try cell_map.append(.{ + .x = @intCast(x - blank_cells + i), + .y = y, + }); + } + blank_cells = 0; } switch (cell.content_tag) { .codepoint => { try writer.print("{u}", .{cell.content.codepoint}); + if (opts.cell_map) |cell_map| { + last_x = x + 1; + try cell_map.append(.{ + .x = x, + .y = y, + }); + } }, .codepoint_grapheme => { try writer.print("{u}", .{cell.content.codepoint}); + if (opts.cell_map) |cell_map| { + last_x = x + 1; + try cell_map.append(.{ + .x = x, + .y = y, + }); + } + for (self.lookupGrapheme(cell).?) |cp| { try writer.print("{u}", .{cp}); + if (opts.cell_map) |cell_map| try cell_map.append(.{ + .x = x, + .y = y, + }); } }, From 61c5fb81150a7924e1cba399dbe526a8ef254285 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2024 09:37:03 -0800 Subject: [PATCH 36/62] terminal: single pagelist node search --- src/terminal/main.zig | 1 + src/terminal/search.zig | 148 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/terminal/search.zig diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 3fc7d2600..df3788d30 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -18,6 +18,7 @@ pub const kitty = @import("kitty.zig"); pub const modes = @import("modes.zig"); pub const page = @import("page.zig"); pub const parse_table = @import("parse_table.zig"); +pub const search = @import("search.zig"); pub const size = @import("size.zig"); pub const tmux = @import("tmux.zig"); pub const x11_color = @import("x11_color.zig"); diff --git a/src/terminal/search.zig b/src/terminal/search.zig new file mode 100644 index 000000000..96a7b56a7 --- /dev/null +++ b/src/terminal/search.zig @@ -0,0 +1,148 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const terminal = @import("main.zig"); +const point = terminal.point; +const Page = terminal.Page; +const PageList = terminal.PageList; +const Selection = terminal.Selection; +const Screen = terminal.Screen; + +pub const PageSearch = struct { + alloc: Allocator, + node: *PageList.List.Node, + needle: []const u8, + cell_map: Page.CellMap, + encoded: std.ArrayListUnmanaged(u8) = .{}, + i: usize = 0, + + pub fn init( + alloc: Allocator, + node: *PageList.List.Node, + needle: []const u8, + ) !PageSearch { + var result: PageSearch = .{ + .alloc = alloc, + .node = node, + .needle = needle, + .cell_map = Page.CellMap.init(alloc), + }; + + const page: *const Page = &node.data; + _ = try page.encodeUtf8(result.encoded.writer(alloc), .{ + .cell_map = &result.cell_map, + }); + + return result; + } + + pub fn deinit(self: *PageSearch) void { + self.encoded.deinit(self.alloc); + self.cell_map.deinit(); + } + + pub fn next(self: *PageSearch) ?Selection { + // Search our haystack for the needle. The resulting index is + // the offset from self.i not the absolute index. + const haystack: []const u8 = self.encoded.items[self.i..]; + const i_offset = std.mem.indexOf(u8, haystack, self.needle) orelse { + self.i = self.encoded.items.len; + return null; + }; + + // Get our full index into the encoded buffer. + const idx = self.i + i_offset; + + // We found our search term. Move the cursor forward one beyond + // the match. This lets us find every repeated match. + self.i = idx + 1; + + const tl: PageList.Pin = tl: { + const map = self.cell_map.items[idx]; + break :tl .{ + .node = self.node, + .y = map.y, + .x = map.x, + }; + }; + const br: PageList.Pin = br: { + const map = self.cell_map.items[idx + self.needle.len - 1]; + break :br .{ + .node = self.node, + .y = map.y, + .x = map.x, + }; + }; + + return Selection.init(tl, br, false); + } +}; + +test "search single page one match" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello, world"); + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + + var it = try PageSearch.init(alloc, node, "world"); + defer it.deinit(); + + const sel = it.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 11, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + + try testing.expect(it.next() == null); +} + +test "search single page multiple match" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. boo!"); + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + + var it = try PageSearch.init(alloc, node, "boo!"); + defer it.deinit(); + + { + const sel = it.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + { + const sel = it.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + + try testing.expect(it.next() == null); +} From eaddb695009e94f6c28fec22b63720506cc7ed4c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Nov 2024 09:48:26 -0800 Subject: [PATCH 37/62] datastruct: CircBuf can be initialized empty --- src/datastruct/circ_buf.zig | 53 +++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index ccee41801..c0c658447 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -48,7 +48,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { }; /// Initialize a new circular buffer that can store size elements. - pub fn init(alloc: Allocator, size: usize) !Self { + pub fn init(alloc: Allocator, size: usize) Allocator.Error!Self { const buf = try alloc.alloc(T, size); @memset(buf, default); @@ -56,7 +56,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { .storage = buf, .head = 0, .tail = 0, - .full = false, + .full = size == 0, }; } @@ -67,7 +67,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { /// Append a single value to the buffer. If the buffer is full, /// an error will be returned. - pub fn append(self: *Self, v: T) !void { + pub fn append(self: *Self, v: T) Allocator.Error!void { if (self.full) return error.OutOfMemory; self.storage[self.head] = v; self.head += 1; @@ -256,7 +256,7 @@ test { try testing.expectEqual(@as(usize, 0), buf.len()); } -test "append" { +test "CircBuf append" { const testing = std.testing; const alloc = testing.allocator; @@ -273,7 +273,7 @@ test "append" { try testing.expectError(error.OutOfMemory, buf.append(5)); } -test "forward iterator" { +test "CircBuf forward iterator" { const testing = std.testing; const alloc = testing.allocator; @@ -319,7 +319,7 @@ test "forward iterator" { } } -test "reverse iterator" { +test "CircBuf reverse iterator" { const testing = std.testing; const alloc = testing.allocator; @@ -365,7 +365,7 @@ test "reverse iterator" { } } -test "getPtrSlice fits" { +test "CircBuf getPtrSlice fits" { const testing = std.testing; const alloc = testing.allocator; @@ -379,7 +379,7 @@ test "getPtrSlice fits" { try testing.expectEqual(@as(usize, 11), buf.len()); } -test "getPtrSlice wraps" { +test "CircBuf getPtrSlice wraps" { const testing = std.testing; const alloc = testing.allocator; @@ -435,7 +435,7 @@ test "getPtrSlice wraps" { } } -test "rotateToZero" { +test "CircBuf rotateToZero" { const testing = std.testing; const alloc = testing.allocator; @@ -447,7 +447,7 @@ test "rotateToZero" { try buf.rotateToZero(alloc); } -test "rotateToZero offset" { +test "CircBuf rotateToZero offset" { const testing = std.testing; const alloc = testing.allocator; @@ -471,7 +471,7 @@ test "rotateToZero offset" { try testing.expectEqual(@as(usize, 1), buf.head); } -test "rotateToZero wraps" { +test "CircBuf rotateToZero wraps" { const testing = std.testing; const alloc = testing.allocator; @@ -511,7 +511,7 @@ test "rotateToZero wraps" { } } -test "rotateToZero full no wrap" { +test "CircBuf rotateToZero full no wrap" { const testing = std.testing; const alloc = testing.allocator; @@ -549,7 +549,32 @@ test "rotateToZero full no wrap" { } } -test "resize grow" { +test "CircBuf resize grow from zero" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 0); + defer buf.deinit(alloc); + try testing.expect(buf.full); + + // Resize + try buf.resize(alloc, 2); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 0), buf.len()); + try testing.expectEqual(@as(usize, 2), buf.capacity()); + + try buf.append(1); + try buf.append(2); + + { + const slices = buf.getPtrSlice(0, 2); + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + } +} + +test "CircBuf resize grow" { const testing = std.testing; const alloc = testing.allocator; @@ -582,7 +607,7 @@ test "resize grow" { } } -test "resize shrink" { +test "CircBuf resize shrink" { const testing = std.testing; const alloc = testing.allocator; From 8abbd80e06c9fb07fc265cb54214c10b7d4b5eb8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Nov 2024 09:48:26 -0800 Subject: [PATCH 38/62] CircBuf: add ensureUnusedCapacity, appendSlice --- src/datastruct/circ_buf.zig | 129 ++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index c0c658447..e6378c855 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -75,6 +75,19 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { self.full = self.head == self.tail; } + /// Append a slice to the buffer. If the buffer cannot fit the + /// entire slice then an error will be returned. It is up to the + /// caller to rotate the circular buffer if they want to overwrite + /// the oldest data. + pub fn appendSlice( + self: *Self, + slice: []const T, + ) Allocator.Error!void { + const storage = self.getPtrSlice(self.len(), slice.len); + fastmem.copy(T, storage[0], slice[0..storage[0].len]); + fastmem.copy(T, storage[1], slice[storage[0].len..]); + } + /// Clear the buffer. pub fn clear(self: *Self) void { self.head = 0; @@ -91,6 +104,34 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { }; } + /// Get the first (oldest) value in the buffer. + pub fn first(self: Self) ?*T { + // Note: this can be more efficient by not using the + // iterator, but this was an easy way to implement it. + var it = self.iterator(.forward); + return it.next(); + } + + /// Get the last (newest) value in the buffer. + pub fn last(self: Self) ?*T { + // Note: this can be more efficient by not using the + // iterator, but this was an easy way to implement it. + var it = self.iterator(.reverse); + return it.next(); + } + + /// Ensures that there is enough capacity to store amount more + /// items via append. + pub fn ensureUnusedCapacity( + self: *Self, + alloc: Allocator, + amount: usize, + ) Allocator.Error!void { + const new_cap = self.len() + amount; + if (new_cap <= self.capacity()) return; + try self.resize(alloc, new_cap); + } + /// Resize the buffer to the given size (larger or smaller). /// If larger, new values will be set to the default value. pub fn resize(self: *Self, alloc: Allocator, size: usize) Allocator.Error!void { @@ -365,6 +406,94 @@ test "CircBuf reverse iterator" { } } +test "CircBuf first/last" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + try buf.append(1); + try buf.append(2); + try buf.append(3); + try testing.expectEqual(3, buf.last().?.*); + try testing.expectEqual(1, buf.first().?.*); +} + +test "CircBuf first/last empty" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 0); + defer buf.deinit(alloc); + + try testing.expect(buf.first() == null); + try testing.expect(buf.last() == null); +} + +test "CircBuf first/last empty with cap" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + try testing.expect(buf.first() == null); + try testing.expect(buf.last() == null); +} + +test "CircBuf append slice" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + try buf.appendSlice("hello"); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 'h'); + try testing.expect(it.next().?.* == 'e'); + try testing.expect(it.next().?.* == 'l'); + try testing.expect(it.next().?.* == 'l'); + try testing.expect(it.next().?.* == 'o'); + try testing.expect(it.next() == null); + } +} + +test "CircBuf append slice with wrap" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill the buffer + _ = buf.getPtrSlice(0, buf.capacity()); + try testing.expect(buf.full); + try testing.expectEqual(@as(usize, 4), buf.len()); + + // Delete + buf.deleteOldest(2); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 2), buf.len()); + + try buf.appendSlice("AB"); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 0); + try testing.expect(it.next().?.* == 0); + try testing.expect(it.next().?.* == 'A'); + try testing.expect(it.next().?.* == 'B'); + try testing.expect(it.next() == null); + } +} + test "CircBuf getPtrSlice fits" { const testing = std.testing; const alloc = testing.allocator; From 2a13c6b6a35a1689d3642aeaf123485dc0a0e66c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Nov 2024 09:48:26 -0800 Subject: [PATCH 39/62] terminal: working on a pagelist sliding window for search --- src/terminal/search.zig | 206 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 96a7b56a7..f35249ab2 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const CircBuf = @import("../datastruct/main.zig").CircBuf; const terminal = @import("main.zig"); const point = terminal.point; const Page = terminal.Page; @@ -8,6 +9,211 @@ const PageList = terminal.PageList; const Selection = terminal.Selection; const Screen = terminal.Screen; +pub const PageListSearch = struct { + alloc: Allocator, + + /// The list we're searching. + list: *PageList, + + /// The search term we're searching for. + needle: []const u8, + + /// The window is our sliding window of pages that we're searching so + /// we can handle boundary cases where a needle is partially on the end + /// of one page and the beginning of the next. + /// + /// Note that we're not guaranteed to straddle exactly two pages. If + /// the needle is large enough and/or the pages are small enough then + /// the needle can straddle N pages. Additionally, pages aren't guaranteed + /// to be equal size so we can't precompute the window size. + window: SlidingWindow, + + pub fn init( + alloc: Allocator, + list: *PageList, + needle: []const u8, + ) !PageListSearch { + var window = try CircBuf.init(alloc, 0); + errdefer window.deinit(); + + return .{ + .alloc = alloc, + .list = list, + .current = list.pages.first, + .needle = needle, + .window = window, + }; + } + + pub fn deinit(self: *PageListSearch) void { + _ = self; + + // TODO: deinit window + } +}; + +/// The sliding window of the pages we're searching. The window is always +/// big enough so that the needle can fit in it. +const SlidingWindow = struct { + /// The data buffer is a circular buffer of u8 that contains the + /// encoded page text that we can use to search for the needle. + data: DataBuf, + + /// The meta buffer is a circular buffer that contains the metadata + /// about the pages we're searching. This usually isn't that large + /// so callers must iterate through it to find the offset to map + /// data to meta. + meta: MetaBuf, + + const DataBuf = CircBuf(u8, 0); + const MetaBuf = CircBuf(Meta, undefined); + const Meta = struct { + node: *PageList.List.Node, + cell_map: Page.CellMap, + + pub fn deinit(self: *Meta) void { + self.cell_map.deinit(); + } + }; + + pub fn initEmpty(alloc: Allocator) Allocator.Error!SlidingWindow { + var data = try DataBuf.init(alloc, 0); + errdefer data.deinit(alloc); + + var meta = try MetaBuf.init(alloc, 0); + errdefer meta.deinit(alloc); + + return .{ + .data = data, + .meta = meta, + }; + } + + pub fn deinit(self: *SlidingWindow, alloc: Allocator) void { + self.data.deinit(alloc); + + var meta_it = self.meta.iterator(.forward); + while (meta_it.next()) |meta| meta.deinit(); + self.meta.deinit(alloc); + } + + /// Add a new node to the sliding window. + /// + /// The window will prune itself if it can while always maintaining + /// the invariant that the `fixed_size` always fits within the window. + /// + /// Note it is possible for the window to be smaller than `fixed_size` + /// if not enough nodes have been added yet or the screen is just + /// smaller than the needle. + pub fn append( + self: *SlidingWindow, + alloc: Allocator, + node: *PageList.List.Node, + required_size: usize, + ) Allocator.Error!void { + // Initialize our metadata for the node. + var meta: Meta = .{ + .node = node, + .cell_map = Page.CellMap.init(alloc), + }; + errdefer meta.deinit(); + + // This is suboptimal but we need to encode the page once to + // temporary memory, and then copy it into our circular buffer. + // In the future, we should benchmark and see if we can encode + // directly into the circular buffer. + var encoded: std.ArrayListUnmanaged(u8) = .{}; + defer encoded.deinit(alloc); + + // Encode the page into the buffer. + const page: *const Page = &meta.node.data; + _ = page.encodeUtf8( + encoded.writer(alloc), + .{ .cell_map = &meta.cell_map }, + ) catch { + // writer uses anyerror but the only realistic error on + // an ArrayList is out of memory. + return error.OutOfMemory; + }; + assert(meta.cell_map.items.len == encoded.items.len); + + // Now that we know our buffer length, we can consider if we can + // prune our circular buffer or if we need to grow it. + prune: { + // Our buffer size after adding the new node. + const before_size: usize = self.data.len() + encoded.items.len; + + // Prune as long as removing the first (oldest) node retains + // our required size invariant. + var after_size: usize = before_size; + while (self.meta.first()) |oldest_meta| { + const new_size = after_size - oldest_meta.cell_map.items.len; + if (new_size < required_size) break :prune; + + // We can prune this node and retain our invariant. + // Update our new size, deinitialize the memory, and + // remove from the circular buffer. + after_size = new_size; + oldest_meta.deinit(); + self.meta.deleteOldest(1); + } + assert(after_size <= before_size); + + // If we didn't prune anything then we're done. + if (after_size == before_size) break :prune; + + // We need to prune our data buffer as well. + self.data.deleteOldest(before_size - after_size); + } + + // Ensure our buffers are big enough to store what we need. + try self.data.ensureUnusedCapacity(alloc, encoded.items.len); + try self.meta.ensureUnusedCapacity(alloc, 1); + + // Append our new node to the circular buffer. + try self.data.appendSlice(encoded.items); + try self.meta.append(meta); + + // Integrity check: verify our data matches our metadata exactly. + if (comptime std.debug.runtime_safety) { + var meta_it = self.meta.iterator(.forward); + var data_len: usize = 0; + while (meta_it.next()) |m| data_len += m.cell_map.items.len; + assert(data_len == self.data.len()); + } + } +}; + +test "SlidingWindow empty on init" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + try testing.expectEqual(0, w.data.len()); + try testing.expectEqual(0, w.meta.len()); +} + +test "SlidingWindow single append" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. boo!"); + + // Imaginary needle for search + const needle = "boo!"; + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node, needle.len); +} + pub const PageSearch = struct { alloc: Allocator, node: *PageList.List.Node, From 6ed298c9c1a46689440d4db2d690d0d7618bd156 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Dec 2024 08:04:36 -0500 Subject: [PATCH 40/62] terminal: sliding window search starts working --- ' | 555 ++++++++++++++++++++++++++++++++++++ src/datastruct/circ_buf.zig | 11 + src/terminal/search.zig | 136 +++++++++ 3 files changed, 702 insertions(+) create mode 100644 ' diff --git a/' b/' new file mode 100644 index 000000000..0b79f1879 --- /dev/null +++ b/' @@ -0,0 +1,555 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const CircBuf = @import("../datastruct/main.zig").CircBuf; +const terminal = @import("main.zig"); +const point = terminal.point; +const Page = terminal.Page; +const PageList = terminal.PageList; +const Pin = PageList.Pin; +const Selection = terminal.Selection; +const Screen = terminal.Screen; + +pub const PageListSearch = struct { + alloc: Allocator, + + /// The list we're searching. + list: *PageList, + + /// The search term we're searching for. + needle: []const u8, + + /// The window is our sliding window of pages that we're searching so + /// we can handle boundary cases where a needle is partially on the end + /// of one page and the beginning of the next. + /// + /// Note that we're not guaranteed to straddle exactly two pages. If + /// the needle is large enough and/or the pages are small enough then + /// the needle can straddle N pages. Additionally, pages aren't guaranteed + /// to be equal size so we can't precompute the window size. + window: SlidingWindow, + + pub fn init( + alloc: Allocator, + list: *PageList, + needle: []const u8, + ) !PageListSearch { + var window = try CircBuf.init(alloc, 0); + errdefer window.deinit(); + + return .{ + .alloc = alloc, + .list = list, + .current = list.pages.first, + .needle = needle, + .window = window, + }; + } + + pub fn deinit(self: *PageListSearch) void { + _ = self; + + // TODO: deinit window + } +}; + +/// The sliding window of the pages we're searching. The window is always +/// big enough so that the needle can fit in it. +const SlidingWindow = struct { + /// The data buffer is a circular buffer of u8 that contains the + /// encoded page text that we can use to search for the needle. + data: DataBuf, + + /// The meta buffer is a circular buffer that contains the metadata + /// about the pages we're searching. This usually isn't that large + /// so callers must iterate through it to find the offset to map + /// data to meta. + meta: MetaBuf, + + /// The cursor into the data buffer for our current search. + i: usize = 0, + + const DataBuf = CircBuf(u8, 0); + const MetaBuf = CircBuf(Meta, undefined); + const Meta = struct { + node: *PageList.List.Node, + cell_map: Page.CellMap, + + pub fn deinit(self: *Meta) void { + self.cell_map.deinit(); + } + }; + + pub fn initEmpty(alloc: Allocator) Allocator.Error!SlidingWindow { + var data = try DataBuf.init(alloc, 0); + errdefer data.deinit(alloc); + + var meta = try MetaBuf.init(alloc, 0); + errdefer meta.deinit(alloc); + + return .{ + .data = data, + .meta = meta, + }; + } + + pub fn deinit(self: *SlidingWindow, alloc: Allocator) void { + self.data.deinit(alloc); + + var meta_it = self.meta.iterator(.forward); + while (meta_it.next()) |meta| meta.deinit(); + self.meta.deinit(alloc); + } + + /// Search the window for the next occurrence of the needle. + pub fn next(self: *SlidingWindow, needle: []const u8) void { + const slices = self.data.getPtrSlice(0, self.data.len()); + + // Search the first slice for the needle. + if (std.mem.indexOf(u8, slices[0][self.i..], needle)) |idx| { + // Found, map the match to a selection. + var meta_it = self.meta.iterator(.forward); + var i: usize = 0; + while (meta_it.next()) |meta| { + const meta_idx = idx - i; + if (meta.cell_map.items.len < meta_idx) { + // This meta doesn't contain the match. + i += meta.cell_map.items.len; + continue; + } + + // We found the meta that contains the start of the match. + const tl: PageList.Pin = tl: { + const map = meta.cell_map.items[meta_idx]; + break :tl .{ + .node = meta.node, + .y = map.y, + .x = map.x, + }; + }; + + _ = tl; + } + + // Found, we can move our index to the next character + // after the match. This let's us find all matches even if + // they overlap. + + self.i = idx + 1; + + @panic("TODO"); + } + } + + /// Return a selection for the given start and length into the data + /// buffer and also prune the data/meta buffers if possible up to + /// this start index. + fn selectAndPrune( + self: *SlidingWindow, + start: usize, + len: usize, + ) Selection { + assert(start < self.data.len()); + assert(start + len < self.data.len()); + + var meta_it = self.meta.iterator(.forward); + var meta_: ?Meta = meta_it.next(); + + // Find the start of the match + var offset: usize = 0; + var skip_nodes: usize = 0; + const tl: PageList.Pin = tl: { + while (meta_) |meta| : (meta_ = meta_it.next()) { + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = start - offset; + if (meta_i >= meta.cell_map.items.len) { + // This meta doesn't contain the match. This means we + // can also prune this set of data because we only look + // forward. + offset += meta.cell_map.items.len; + skip_nodes += 1; + continue; + } + + // We found the meta that contains the start of the match. + const map = meta.cell_map.items[start]; + break :tl .{ + .node = meta.node, + .y = map.y, + .x = map.x, + }; + } + + // We never found the top-left. This is unreachable because + // we assert that the start index is within the data buffer, + // and when building the data buffer we assert the cell map + // length exactly matches the data buffer length. + unreachable; + }; + + // Keep track of the number of nodes we skipped for the tl. + const tl_skip_nodes = skip_nodes; + skip_nodes = 0; + + // Find the end of the match + const br: PageList.Pin = br: { + const end_idx = start + len - 1; + while (meta_) |meta| : (meta_ = meta_it.next()) { + const meta_i = end_idx - offset; + if (meta_i >= meta.cell_map.items.len) { + offset += meta.cell_map.items.len; + skip_nodes += 1; + continue; + } + + // We found the meta that contains the start of the match. + const map = meta.cell_map.items[end_idx]; + break :br .{ + .node = meta.node, + .y = map.y, + .x = map.x, + }; + } + }; + + // If we skipped any nodes for the bottom-right then we can prune + // all the way up to the total. If we didn't, it means we found + // the bottom-right in the same node as the top-left and we can't + // prune the node that the match is on because there may be + // more matches. + if (skip_nodes > 0) skip_nodes += tl_skip_nodes; + + _ = tl; + _ = br; + } + + /// Convert a data index into a pin. + fn pin( + self: *const SlidingWindow, + idx: usize, + it: ?*MetaBuf.Iterator, + ) struct { + /// The pin for the data index. + pin: Pin, + + /// The offset into the meta buffer that the pin was found. + /// This can be used to prune the meta buffer (its safe to prune + /// before this i). + meta_i: usize, + } { + _ = self; + _ = idx; + _ = start; + + while (it.next()) |meta| { + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = start - offset; + if (meta_i >= meta.cell_map.items.len) { + // This meta doesn't contain the match. This means we + // can also prune this set of data because we only look + // forward. + offset += meta.cell_map.items.len; + skip_nodes += 1; + continue; + } + + // We found the meta that contains the start of the match. + const map = meta.cell_map.items[start]; + break :tl .{ + .node = meta.node, + .y = map.y, + .x = map.x, + }; + } + + } + + /// Add a new node to the sliding window. + /// + /// The window will prune itself if it can while always maintaining + /// the invariant that the `fixed_size` always fits within the window. + /// + /// Note it is possible for the window to be smaller than `fixed_size` + /// if not enough nodes have been added yet or the screen is just + /// smaller than the needle. + pub fn append( + self: *SlidingWindow, + alloc: Allocator, + node: *PageList.List.Node, + required_size: usize, + ) Allocator.Error!void { + // Initialize our metadata for the node. + var meta: Meta = .{ + .node = node, + .cell_map = Page.CellMap.init(alloc), + }; + errdefer meta.deinit(); + + // This is suboptimal but we need to encode the page once to + // temporary memory, and then copy it into our circular buffer. + // In the future, we should benchmark and see if we can encode + // directly into the circular buffer. + var encoded: std.ArrayListUnmanaged(u8) = .{}; + defer encoded.deinit(alloc); + + // Encode the page into the buffer. + const page: *const Page = &meta.node.data; + _ = page.encodeUtf8( + encoded.writer(alloc), + .{ .cell_map = &meta.cell_map }, + ) catch { + // writer uses anyerror but the only realistic error on + // an ArrayList is out of memory. + return error.OutOfMemory; + }; + assert(meta.cell_map.items.len == encoded.items.len); + + // Now that we know our buffer length, we can consider if we can + // prune our circular buffer or if we need to grow it. + prune: { + // Our buffer size after adding the new node. + const before_size: usize = self.data.len() + encoded.items.len; + + // Prune as long as removing the first (oldest) node retains + // our required size invariant. + var after_size: usize = before_size; + while (self.meta.first()) |oldest_meta| { + const new_size = after_size - oldest_meta.cell_map.items.len; + if (new_size < required_size) break :prune; + + // We can prune this node and retain our invariant. + // Update our new size, deinitialize the memory, and + // remove from the circular buffer. + after_size = new_size; + oldest_meta.deinit(); + self.meta.deleteOldest(1); + } + assert(after_size <= before_size); + + // If we didn't prune anything then we're done. + if (after_size == before_size) break :prune; + + // We need to prune our data buffer as well. + self.data.deleteOldest(before_size - after_size); + } + + // Ensure our buffers are big enough to store what we need. + try self.data.ensureUnusedCapacity(alloc, encoded.items.len); + try self.meta.ensureUnusedCapacity(alloc, 1); + + // Append our new node to the circular buffer. + try self.data.appendSlice(encoded.items); + try self.meta.append(meta); + + // Integrity check: verify our data matches our metadata exactly. + if (comptime std.debug.runtime_safety) { + var meta_it = self.meta.iterator(.forward); + var data_len: usize = 0; + while (meta_it.next()) |m| data_len += m.cell_map.items.len; + assert(data_len == self.data.len()); + } + } +}; + +test "SlidingWindow empty on init" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + try testing.expectEqual(0, w.data.len()); + try testing.expectEqual(0, w.meta.len()); +} + +test "SlidingWindow single append" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. boo!"); + + // Imaginary needle for search + const needle = "boo!"; + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node, needle.len); +} + +test "SlidingWindow two pages" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Imaginary needle for search + const needle = "boo!"; + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node, needle.len); + try w.append(alloc, node.next.?, needle.len); + + // Ensure our data is correct +} + +pub const PageSearch = struct { + alloc: Allocator, + node: *PageList.List.Node, + needle: []const u8, + cell_map: Page.CellMap, + encoded: std.ArrayListUnmanaged(u8) = .{}, + i: usize = 0, + + pub fn init( + alloc: Allocator, + node: *PageList.List.Node, + needle: []const u8, + ) !PageSearch { + var result: PageSearch = .{ + .alloc = alloc, + .node = node, + .needle = needle, + .cell_map = Page.CellMap.init(alloc), + }; + + const page: *const Page = &node.data; + _ = try page.encodeUtf8(result.encoded.writer(alloc), .{ + .cell_map = &result.cell_map, + }); + + return result; + } + + pub fn deinit(self: *PageSearch) void { + self.encoded.deinit(self.alloc); + self.cell_map.deinit(); + } + + pub fn next(self: *PageSearch) ?Selection { + // Search our haystack for the needle. The resulting index is + // the offset from self.i not the absolute index. + const haystack: []const u8 = self.encoded.items[self.i..]; + const i_offset = std.mem.indexOf(u8, haystack, self.needle) orelse { + self.i = self.encoded.items.len; + return null; + }; + + // Get our full index into the encoded buffer. + const idx = self.i + i_offset; + + // We found our search term. Move the cursor forward one beyond + // the match. This lets us find every repeated match. + self.i = idx + 1; + + const tl: PageList.Pin = tl: { + const map = self.cell_map.items[idx]; + break :tl .{ + .node = self.node, + .y = map.y, + .x = map.x, + }; + }; + const br: PageList.Pin = br: { + const map = self.cell_map.items[idx + self.needle.len - 1]; + break :br .{ + .node = self.node, + .y = map.y, + .x = map.x, + }; + }; + + return Selection.init(tl, br, false); + } +}; + +test "search single page one match" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello, world"); + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + + var it = try PageSearch.init(alloc, node, "world"); + defer it.deinit(); + + const sel = it.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 11, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + + try testing.expect(it.next() == null); +} + +test "search single page multiple match" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. boo!"); + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + + var it = try PageSearch.init(alloc, node, "boo!"); + defer it.deinit(); + + { + const sel = it.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + { + const sel = it.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + + try testing.expect(it.next() == null); +} diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index e6378c855..c13bcc192 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -45,6 +45,17 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { self.idx += 1; return &self.buf.storage[storage_idx]; } + + /// Seek the iterator by a given amount. This will clamp + /// the values to the bounds of the buffer so overflows are + /// not possible. + pub fn seekBy(self: *Iterator, amount: isize) void { + if (amount > 0) { + self.idx +|= @intCast(amount); + } else { + self.idx -|= @intCast(@abs(amount)); + } + } }; /// Initialize a new circular buffer that can store size elements. diff --git a/src/terminal/search.zig b/src/terminal/search.zig index f35249ab2..05b2919e0 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -6,6 +6,7 @@ const terminal = @import("main.zig"); const point = terminal.point; const Page = terminal.Page; const PageList = terminal.PageList; +const Pin = PageList.Pin; const Selection = terminal.Selection; const Screen = terminal.Screen; @@ -97,6 +98,85 @@ const SlidingWindow = struct { self.meta.deinit(alloc); } + /// Search the window for the next occurrence of the needle. As + /// the window moves, the window will prune itself while maintaining + /// the invariant that the window is always big enough to contain + /// the needle. + pub fn next(self: *SlidingWindow, needle: []const u8) ?Selection { + const slices = self.data.getPtrSlice(0, self.data.len()); + + // Search the first slice for the needle. + if (std.mem.indexOf(u8, slices[0], needle)) |idx| { + return self.selection(idx, needle.len); + } + + @panic("TODO"); + } + + /// Return a selection for the given start and length into the data + /// buffer and also prune the data/meta buffers if possible up to + /// this start index. + fn selection( + self: *SlidingWindow, + start: usize, + len: usize, + ) Selection { + assert(start < self.data.len()); + assert(start + len < self.data.len()); + + var meta_it = self.meta.iterator(.forward); + const tl: Pin = pin(&meta_it, start); + + // We have to seek back so that we reinspect our current + // iterator value again in case the start and end are in the + // same segment. + meta_it.seekBy(-1); + const br: Pin = pin(&meta_it, start + len - 1); + + // TODO: prune based on meta_it.idx + + return Selection.init(tl, br, false); + } + + /// Convert a data index into a pin. + /// + /// Tip: you can get the offset into the meta buffer we searched + /// by inspecting the iterator index after this function returns. + /// I note this because this is useful if you want to prune the + /// meta buffer after you find a match. + /// + /// Precondition: the index must be within the data buffer. + fn pin( + it: *MetaBuf.Iterator, + idx: usize, + ) Pin { + var offset: usize = 0; + while (it.next()) |meta| { + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = idx - offset; + if (meta_i >= meta.cell_map.items.len) { + // This meta doesn't contain the match. This means we + // can also prune this set of data because we only look + // forward. + offset += meta.cell_map.items.len; + continue; + } + + // We found the meta that contains the start of the match. + const map = meta.cell_map.items[meta_i]; + return .{ + .node = meta.node, + .y = map.y, + .x = map.x, + }; + } + + // Unreachable because it is a precondition that the index is + // within the data buffer. + unreachable; + } + /// Add a new node to the sliding window. /// /// The window will prune itself if it can while always maintaining @@ -212,6 +292,62 @@ test "SlidingWindow single append" { try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; try w.append(alloc, node, needle.len); + + // We should be able to find two matches. + { + const sel = w.next(needle).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + { + const sel = w.next(needle).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } +} + +test "SlidingWindow two pages" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Imaginary needle for search + const needle = "boo!"; + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node, needle.len); + try w.append(alloc, node.next.?, needle.len); + + // Ensure our data is correct } pub const PageSearch = struct { From d307b02e40de5816e0ee8e49d70e0ce555e13c18 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Dec 2024 13:06:52 -0500 Subject: [PATCH 41/62] terminal: sliding window search can move the cursor --- src/datastruct/circ_buf.zig | 5 +++ src/terminal/search.zig | 75 ++++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index c13bcc192..065bf6a1d 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -56,6 +56,11 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { self.idx -|= @intCast(@abs(amount)); } } + + /// Reset the iterator back to the first value. + pub fn reset(self: *Iterator) void { + self.idx = 0; + } }; /// Initialize a new circular buffer that can store size elements. diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 05b2919e0..e217f649e 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -66,6 +66,11 @@ const SlidingWindow = struct { /// data to meta. meta: MetaBuf, + /// Offset into data for our current state. This handles the + /// situation where our search moved through meta[0] but didn't + /// do enough to prune it. + data_offset: usize = 0, + const DataBuf = CircBuf(u8, 0); const MetaBuf = CircBuf(Meta, undefined); const Meta = struct { @@ -98,31 +103,60 @@ const SlidingWindow = struct { self.meta.deinit(alloc); } + /// Clear all data but retain allocated capacity. + pub fn clearAndRetainCapacity(self: *SlidingWindow) void { + var meta_it = self.meta.iterator(.forward); + while (meta_it.next()) |meta| meta.deinit(); + self.meta.clear(); + self.data.clear(); + self.data_offset = 0; + } + /// Search the window for the next occurrence of the needle. As /// the window moves, the window will prune itself while maintaining /// the invariant that the window is always big enough to contain /// the needle. pub fn next(self: *SlidingWindow, needle: []const u8) ?Selection { - const slices = self.data.getPtrSlice(0, self.data.len()); + const data_len = self.data.len(); + if (data_len == 0) return null; + const slices = self.data.getPtrSlice( + self.data_offset, + data_len - self.data_offset, + ); // Search the first slice for the needle. if (std.mem.indexOf(u8, slices[0], needle)) |idx| { return self.selection(idx, needle.len); } - @panic("TODO"); + // TODO: search overlap + + // Search the last slice for the needle. + if (std.mem.indexOf(u8, slices[1], needle)) |idx| { + if (true) @panic("TODO: test"); + return self.selection(slices[0].len + idx, needle.len); + } + + // No match. Clear everything. + self.clearAndRetainCapacity(); + return null; } /// Return a selection for the given start and length into the data /// buffer and also prune the data/meta buffers if possible up to /// this start index. + /// + /// The start index is assumed to be relative to the offset. i.e. + /// index zero is actually at `self.data[self.data_offset]`. The + /// selection will account for the offset. fn selection( self: *SlidingWindow, - start: usize, + start_offset: usize, len: usize, ) Selection { + const start = start_offset + self.data_offset; assert(start < self.data.len()); - assert(start + len < self.data.len()); + assert(start + len <= self.data.len()); var meta_it = self.meta.iterator(.forward); const tl: Pin = pin(&meta_it, start); @@ -132,8 +166,37 @@ const SlidingWindow = struct { // same segment. meta_it.seekBy(-1); const br: Pin = pin(&meta_it, start + len - 1); + assert(meta_it.idx >= 1); - // TODO: prune based on meta_it.idx + // meta_it.idx is now the index after the br pin. We can + // safely prune our data up to this index. (It is after + // because next() is called at least once). + const br_meta_idx: usize = meta_it.idx - 1; + meta_it.reset(); + var offset: usize = 0; + while (meta_it.next()) |meta| { + const meta_idx = start - offset; + if (meta_idx >= meta.cell_map.items.len) { + // Prior to our matches, we can prune it. + offset += meta.cell_map.items.len; + meta.deinit(); + } + + assert(meta_it.idx == br_meta_idx + 1); + break; + } + + // If we have metas to prune, then prune them. They should be + // deinitialized already from the while loop above. + if (br_meta_idx > 0) { + assert(offset > 0); + self.meta.deleteOldest(br_meta_idx); + self.data.deleteOldest(offset); + @panic("TODO: TEST"); + } + + // Move our data one beyond so we don't rematch. + self.data_offset = start - offset + 1; return Selection.init(tl, br, false); } @@ -316,6 +379,8 @@ test "SlidingWindow single append" { .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); } + try testing.expect(w.next(needle) == null); + try testing.expect(w.next(needle) == null); } test "SlidingWindow two pages" { From b487aa8e1fbd1981103a00464333467068661006 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Dec 2024 13:40:48 -0500 Subject: [PATCH 42/62] terminal: search across two pages and pruning appears to be working --- src/terminal/search.zig | 208 +++++++++++++++++++++++----------------- 1 file changed, 121 insertions(+), 87 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index e217f649e..35f79ed23 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -53,8 +53,9 @@ pub const PageListSearch = struct { } }; -/// The sliding window of the pages we're searching. The window is always -/// big enough so that the needle can fit in it. +/// Search pages via a sliding window. The sliding window always maintains +/// the invariant that data isn't pruned until we've searched it and +/// accounted for overlaps across pages. const SlidingWindow = struct { /// The data buffer is a circular buffer of u8 that contains the /// encoded page text that we can use to search for the needle. @@ -137,8 +138,42 @@ const SlidingWindow = struct { return self.selection(slices[0].len + idx, needle.len); } - // No match. Clear everything. - self.clearAndRetainCapacity(); + // No match. We keep `needle.len - 1` bytes available to + // handle the future overlap case. + var meta_it = self.meta.iterator(.reverse); + prune: { + var saved: usize = 0; + while (meta_it.next()) |meta| { + const needed = needle.len - 1 - saved; + if (meta.cell_map.items.len >= needed) { + // We save up to this meta. We set our data offset + // to exactly where it needs to be to continue + // searching. + self.data_offset = meta.cell_map.items.len - needed; + break; + } + + saved += meta.cell_map.items.len; + } else { + // If we exited the while loop naturally then we + // never got the amount we needed and so there is + // nothing to prune. + assert(saved < needle.len - 1); + break :prune; + } + + const prune_count = self.meta.len() - meta_it.idx; + if (prune_count == 0) { + // This can happen if we need to save up to the first + // meta value to retain our window. + break :prune; + } + + // We can now delete all the metas up to but NOT including + // the meta we found through meta_it. + @panic("TODO: test"); + } + return null; } @@ -158,71 +193,74 @@ const SlidingWindow = struct { assert(start < self.data.len()); assert(start + len <= self.data.len()); + // meta_consumed is the number of bytes we've consumed in the + // data buffer up to and NOT including the meta where we've + // found our pin. This is important because it tells us the + // amount of data we can safely deleted from self.data since + // we can't partially delete a meta block's data. (The partial + // amount is represented by self.data_offset). var meta_it = self.meta.iterator(.forward); - const tl: Pin = pin(&meta_it, start); + var meta_consumed: usize = 0; + const tl: Pin = pin(&meta_it, &meta_consumed, start); // We have to seek back so that we reinspect our current // iterator value again in case the start and end are in the // same segment. meta_it.seekBy(-1); - const br: Pin = pin(&meta_it, start + len - 1); + const br: Pin = pin(&meta_it, &meta_consumed, start + len - 1); assert(meta_it.idx >= 1); - // meta_it.idx is now the index after the br pin. We can - // safely prune our data up to this index. (It is after - // because next() is called at least once). - const br_meta_idx: usize = meta_it.idx - 1; - meta_it.reset(); - var offset: usize = 0; - while (meta_it.next()) |meta| { - const meta_idx = start - offset; - if (meta_idx >= meta.cell_map.items.len) { - // Prior to our matches, we can prune it. - offset += meta.cell_map.items.len; - meta.deinit(); + // Our offset into the current meta block is the start index + // minus the amount of data fully consumed. We then add one + // to move one past the match so we don't repeat it. + self.data_offset = start - meta_consumed + 1; + + // meta_it.idx is br's meta index plus one (because the iterator + // moves one past the end; we call next() one last time). So + // we compare against one to check that the meta that we matched + // in has prior meta blocks we can prune. + if (meta_it.idx > 1) { + // Deinit all our memory in the meta blocks prior to our + // match. + const meta_count = meta_it.idx - 1; + meta_it.reset(); + for (0..meta_count) |_| meta_it.next().?.deinit(); + if (comptime std.debug.runtime_safety) { + assert(meta_it.idx == meta_count); + assert(meta_it.next().?.node == br.node); } + self.meta.deleteOldest(meta_count); - assert(meta_it.idx == br_meta_idx + 1); - break; + // Delete all the data up to our current index. + assert(meta_consumed > 0); + self.data.deleteOldest(meta_consumed); } - // If we have metas to prune, then prune them. They should be - // deinitialized already from the while loop above. - if (br_meta_idx > 0) { - assert(offset > 0); - self.meta.deleteOldest(br_meta_idx); - self.data.deleteOldest(offset); - @panic("TODO: TEST"); - } - - // Move our data one beyond so we don't rematch. - self.data_offset = start - offset + 1; - + self.assertIntegrity(); return Selection.init(tl, br, false); } /// Convert a data index into a pin. /// - /// Tip: you can get the offset into the meta buffer we searched - /// by inspecting the iterator index after this function returns. - /// I note this because this is useful if you want to prune the - /// meta buffer after you find a match. + /// The iterator and offset are both expected to be passed by + /// pointer so that the pin can be efficiently called for multiple + /// indexes (in order). See selection() for an example. /// /// Precondition: the index must be within the data buffer. fn pin( it: *MetaBuf.Iterator, + offset: *usize, idx: usize, ) Pin { - var offset: usize = 0; while (it.next()) |meta| { // meta_i is the index we expect to find the match in the // cell map within this meta if it contains it. - const meta_i = idx - offset; + const meta_i = idx - offset.*; if (meta_i >= meta.cell_map.items.len) { // This meta doesn't contain the match. This means we // can also prune this set of data because we only look // forward. - offset += meta.cell_map.items.len; + offset.* += meta.cell_map.items.len; continue; } @@ -240,19 +278,13 @@ const SlidingWindow = struct { unreachable; } - /// Add a new node to the sliding window. - /// - /// The window will prune itself if it can while always maintaining - /// the invariant that the `fixed_size` always fits within the window. - /// - /// Note it is possible for the window to be smaller than `fixed_size` - /// if not enough nodes have been added yet or the screen is just - /// smaller than the needle. + /// Add a new node to the sliding window. This will always grow + /// the sliding window; data isn't pruned until it is consumed + /// via a search (via next()). pub fn append( self: *SlidingWindow, alloc: Allocator, node: *PageList.List.Node, - required_size: usize, ) Allocator.Error!void { // Initialize our metadata for the node. var meta: Meta = .{ @@ -280,35 +312,6 @@ const SlidingWindow = struct { }; assert(meta.cell_map.items.len == encoded.items.len); - // Now that we know our buffer length, we can consider if we can - // prune our circular buffer or if we need to grow it. - prune: { - // Our buffer size after adding the new node. - const before_size: usize = self.data.len() + encoded.items.len; - - // Prune as long as removing the first (oldest) node retains - // our required size invariant. - var after_size: usize = before_size; - while (self.meta.first()) |oldest_meta| { - const new_size = after_size - oldest_meta.cell_map.items.len; - if (new_size < required_size) break :prune; - - // We can prune this node and retain our invariant. - // Update our new size, deinitialize the memory, and - // remove from the circular buffer. - after_size = new_size; - oldest_meta.deinit(); - self.meta.deleteOldest(1); - } - assert(after_size <= before_size); - - // If we didn't prune anything then we're done. - if (after_size == before_size) break :prune; - - // We need to prune our data buffer as well. - self.data.deleteOldest(before_size - after_size); - } - // Ensure our buffers are big enough to store what we need. try self.data.ensureUnusedCapacity(alloc, encoded.items.len); try self.meta.ensureUnusedCapacity(alloc, 1); @@ -317,13 +320,20 @@ const SlidingWindow = struct { try self.data.appendSlice(encoded.items); try self.meta.append(meta); + self.assertIntegrity(); + } + + fn assertIntegrity(self: *const SlidingWindow) void { + if (comptime !std.debug.runtime_safety) return; + // Integrity check: verify our data matches our metadata exactly. - if (comptime std.debug.runtime_safety) { - var meta_it = self.meta.iterator(.forward); - var data_len: usize = 0; - while (meta_it.next()) |m| data_len += m.cell_map.items.len; - assert(data_len == self.data.len()); - } + var meta_it = self.meta.iterator(.forward); + var data_len: usize = 0; + while (meta_it.next()) |m| data_len += m.cell_map.items.len; + assert(data_len == self.data.len()); + + // Integrity check: verify our data offset is within bounds. + assert(self.data_offset < self.data.len()); } }; @@ -354,7 +364,7 @@ test "SlidingWindow single append" { // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node, needle.len); + try w.append(alloc, node); // We should be able to find two matches. { @@ -409,10 +419,34 @@ test "SlidingWindow two pages" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node, needle.len); - try w.append(alloc, node.next.?, needle.len); + try w.append(alloc, node); + try w.append(alloc, node.next.?); - // Ensure our data is correct + // Search should find two matches + { + const sel = w.next(needle).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 76, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 79, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + { + const sel = w.next(needle).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + try testing.expect(w.next(needle) == null); + try testing.expect(w.next(needle) == null); } pub const PageSearch = struct { From 09e4cccd2c891d3a2e242fce486f8ff8fd83db02 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Dec 2024 15:39:36 -0800 Subject: [PATCH 43/62] terminal: remove unused pagesearch --- src/terminal/search.zig | 139 ---------------------------------------- 1 file changed, 139 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 35f79ed23..40462491a 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -448,142 +448,3 @@ test "SlidingWindow two pages" { try testing.expect(w.next(needle) == null); try testing.expect(w.next(needle) == null); } - -pub const PageSearch = struct { - alloc: Allocator, - node: *PageList.List.Node, - needle: []const u8, - cell_map: Page.CellMap, - encoded: std.ArrayListUnmanaged(u8) = .{}, - i: usize = 0, - - pub fn init( - alloc: Allocator, - node: *PageList.List.Node, - needle: []const u8, - ) !PageSearch { - var result: PageSearch = .{ - .alloc = alloc, - .node = node, - .needle = needle, - .cell_map = Page.CellMap.init(alloc), - }; - - const page: *const Page = &node.data; - _ = try page.encodeUtf8(result.encoded.writer(alloc), .{ - .cell_map = &result.cell_map, - }); - - return result; - } - - pub fn deinit(self: *PageSearch) void { - self.encoded.deinit(self.alloc); - self.cell_map.deinit(); - } - - pub fn next(self: *PageSearch) ?Selection { - // Search our haystack for the needle. The resulting index is - // the offset from self.i not the absolute index. - const haystack: []const u8 = self.encoded.items[self.i..]; - const i_offset = std.mem.indexOf(u8, haystack, self.needle) orelse { - self.i = self.encoded.items.len; - return null; - }; - - // Get our full index into the encoded buffer. - const idx = self.i + i_offset; - - // We found our search term. Move the cursor forward one beyond - // the match. This lets us find every repeated match. - self.i = idx + 1; - - const tl: PageList.Pin = tl: { - const map = self.cell_map.items[idx]; - break :tl .{ - .node = self.node, - .y = map.y, - .x = map.x, - }; - }; - const br: PageList.Pin = br: { - const map = self.cell_map.items[idx + self.needle.len - 1]; - break :br .{ - .node = self.node, - .y = map.y, - .x = map.x, - }; - }; - - return Selection.init(tl, br, false); - } -}; - -test "search single page one match" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 80, 24, 0); - defer s.deinit(); - try s.testWriteString("hello, world"); - - // We want to test single-page cases. - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - - var it = try PageSearch.init(alloc, node, "world"); - defer it.deinit(); - - const sel = it.next().?; - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 11, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); - - try testing.expect(it.next() == null); -} - -test "search single page multiple match" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 80, 24, 0); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. boo!"); - - // We want to test single-page cases. - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - - var it = try PageSearch.init(alloc, node, "boo!"); - defer it.deinit(); - - { - const sel = it.next().?; - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 10, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); - } - { - const sel = it.next().?; - try testing.expectEqual(point.Point{ .active = .{ - .x = 19, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 22, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); - } - - try testing.expect(it.next() == null); -} From 79026a114837ba0945d0f6948c7c2efc9f549516 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Dec 2024 15:52:48 -0800 Subject: [PATCH 44/62] terminal: test no match pruning --- src/terminal/search.zig | 116 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 40462491a..71ac6aea4 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -171,9 +171,18 @@ const SlidingWindow = struct { // We can now delete all the metas up to but NOT including // the meta we found through meta_it. - @panic("TODO: test"); + meta_it = self.meta.iterator(.forward); + var prune_data_len: usize = 0; + for (0..prune_count) |_| { + const meta = meta_it.next().?; + prune_data_len += meta.cell_map.items.len; + meta.deinit(); + } + self.meta.deleteOldest(prune_count); + self.data.deleteOldest(prune_data_len); } + self.assertIntegrity(); return null; } @@ -393,6 +402,33 @@ test "SlidingWindow single append" { try testing.expect(w.next(needle) == null); } +test "SlidingWindow single append no match" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. boo!"); + + // Imaginary needle for search + const needle = "nope!"; + + // We want to test single-page cases. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + + // No matches + try testing.expect(w.next(needle) == null); + try testing.expect(w.next(needle) == null); + + // Should still keep the page + try testing.expectEqual(1, w.meta.len()); +} + test "SlidingWindow two pages" { const testing = std.testing; const alloc = testing.allocator; @@ -448,3 +484,81 @@ test "SlidingWindow two pages" { try testing.expect(w.next(needle) == null); try testing.expect(w.next(needle) == null); } + +test "SlidingWindow two pages no match prunes first page" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + try w.append(alloc, node.next.?); + + // Imaginary needle for search. Doesn't match! + const needle = "nope!"; + + // Search should find nothing + try testing.expect(w.next(needle) == null); + try testing.expect(w.next(needle) == null); + + // We should've pruned our page because the second page + // has enough text to contain our needle. + try testing.expectEqual(1, w.meta.len()); +} + +test "SlidingWindow two pages no match keeps both pages" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + try w.append(alloc, node.next.?); + + // Imaginary needle for search. Doesn't match! + var needle_list = std.ArrayList(u8).init(alloc); + defer needle_list.deinit(); + try needle_list.appendNTimes('x', first_page_rows * s.pages.cols); + const needle: []const u8 = needle_list.items; + + // Search should find nothing + try testing.expect(w.next(needle) == null); + try testing.expect(w.next(needle) == null); + + // No pruning because both pages are needed to fit needle. + try testing.expectEqual(2, w.meta.len()); +} From af1ee4d95f645d9a33841dcb5b77a93c8bdc9745 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 10:36:14 -0800 Subject: [PATCH 45/62] terminal: search match across page boundary --- src/terminal/search.zig | 65 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 71ac6aea4..88da8304d 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -212,6 +212,12 @@ const SlidingWindow = struct { var meta_consumed: usize = 0; const tl: Pin = pin(&meta_it, &meta_consumed, start); + // Store the information required to prune later. We store this + // now because we only want to prune up to our START so we can + // find overlapping matches. + const tl_meta_idx = meta_it.idx - 1; + const tl_meta_consumed = meta_consumed; + // We have to seek back so that we reinspect our current // iterator value again in case the start and end are in the // same segment. @@ -222,27 +228,27 @@ const SlidingWindow = struct { // Our offset into the current meta block is the start index // minus the amount of data fully consumed. We then add one // to move one past the match so we don't repeat it. - self.data_offset = start - meta_consumed + 1; + self.data_offset = start - tl_meta_consumed + 1; // meta_it.idx is br's meta index plus one (because the iterator // moves one past the end; we call next() one last time). So // we compare against one to check that the meta that we matched // in has prior meta blocks we can prune. - if (meta_it.idx > 1) { + if (tl_meta_idx > 0) { // Deinit all our memory in the meta blocks prior to our // match. - const meta_count = meta_it.idx - 1; + const meta_count = tl_meta_idx; meta_it.reset(); for (0..meta_count) |_| meta_it.next().?.deinit(); if (comptime std.debug.runtime_safety) { assert(meta_it.idx == meta_count); - assert(meta_it.next().?.node == br.node); + assert(meta_it.next().?.node == tl.node); } self.meta.deleteOldest(meta_count); // Delete all the data up to our current index. - assert(meta_consumed > 0); - self.data.deleteOldest(meta_consumed); + assert(tl_meta_consumed > 0); + self.data.deleteOldest(tl_meta_consumed); } self.assertIntegrity(); @@ -485,6 +491,53 @@ test "SlidingWindow two pages" { try testing.expect(w.next(needle) == null); } +test "SlidingWindow two pages match across boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("hell"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("o, world!"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + + // Imaginary needle for search + const needle = "hello, world"; + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + try w.append(alloc, node.next.?); + + // Search should find a match + { + const sel = w.next(needle).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 76, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + try testing.expect(w.next(needle) == null); + try testing.expect(w.next(needle) == null); + + // We shouldn't prune because we don't have enough space + try testing.expectEqual(2, w.meta.len()); +} + test "SlidingWindow two pages no match prunes first page" { const testing = std.testing; const alloc = testing.allocator; From 852e04fa009eca31727762f2d72d2fec4fcea273 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 10:58:22 -0800 Subject: [PATCH 46/62] terminal: test for match in second slice of circ buf --- src/terminal/search.zig | 73 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 88da8304d..7b6486429 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -118,12 +118,16 @@ const SlidingWindow = struct { /// the invariant that the window is always big enough to contain /// the needle. pub fn next(self: *SlidingWindow, needle: []const u8) ?Selection { - const data_len = self.data.len(); - if (data_len == 0) return null; - const slices = self.data.getPtrSlice( - self.data_offset, - data_len - self.data_offset, - ); + const slices = slices: { + // If we have less data then the needle then we can't possibly match + const data_len = self.data.len(); + if (data_len < needle.len) return null; + + break :slices self.data.getPtrSlice( + self.data_offset, + data_len - self.data_offset, + ); + }; // Search the first slice for the needle. if (std.mem.indexOf(u8, slices[0], needle)) |idx| { @@ -134,7 +138,6 @@ const SlidingWindow = struct { // Search the last slice for the needle. if (std.mem.indexOf(u8, slices[1], needle)) |idx| { - if (true) @panic("TODO: test"); return self.selection(slices[0].len + idx, needle.len); } @@ -182,6 +185,10 @@ const SlidingWindow = struct { self.data.deleteOldest(prune_data_len); } + // Our data offset now moves to needle.len - 1 from the end so + // that we can handle the overlap case. + self.data_offset = self.data.len() - needle.len + 1; + self.assertIntegrity(); return null; } @@ -615,3 +622,55 @@ test "SlidingWindow two pages no match keeps both pages" { // No pruning because both pages are needed to fit needle. try testing.expectEqual(2, w.meta.len()); } + +test "SlidingWindow single append across circular buffer boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.initEmpty(alloc); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("XXXXXXXXXXXXXXXXXXXboo!XXXXX"); + + // We are trying to break a circular buffer boundary so the way we + // do this is to duplicate the data then do a failing search. This + // will cause the first page to be pruned. The next time we append we'll + // put it in the middle of the circ buffer. We assert this so that if + // our implementation changes our test will fail. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + try w.append(alloc, node); + { + // No wrap around yet + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len == 0); + } + + // Search non-match, prunes page + try testing.expect(w.next("abc") == null); + try testing.expectEqual(1, w.meta.len()); + + // Add new page, now wraps + try w.append(alloc, node); + { + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len > 0); + } + { + const sel = w.next("boo!").?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + try testing.expect(w.next("boo!") == null); +} From 34fb840cf99fdfdd970557df8b8accbbc156b3ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 11:16:36 -0800 Subject: [PATCH 47/62] terminal: search match on overlap case --- src/terminal/search.zig | 215 ++++++++++++++++++++++++++++------------ 1 file changed, 153 insertions(+), 62 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 7b6486429..304cc5a4e 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -72,6 +72,14 @@ const SlidingWindow = struct { /// do enough to prune it. data_offset: usize = 0, + /// The needle we're searching for. Does not own the memory. + needle: []const u8, + + /// A buffer to store the overlap search data. This is used to search + /// overlaps between pages where the match starts on one page and + /// ends on another. The length is always `needle.len * 2`. + overlap_buf: []u8, + const DataBuf = CircBuf(u8, 0); const MetaBuf = CircBuf(Meta, undefined); const Meta = struct { @@ -83,20 +91,29 @@ const SlidingWindow = struct { } }; - pub fn initEmpty(alloc: Allocator) Allocator.Error!SlidingWindow { + pub fn init( + alloc: Allocator, + needle: []const u8, + ) Allocator.Error!SlidingWindow { var data = try DataBuf.init(alloc, 0); errdefer data.deinit(alloc); var meta = try MetaBuf.init(alloc, 0); errdefer meta.deinit(alloc); + const overlap_buf = try alloc.alloc(u8, needle.len * 2); + errdefer alloc.free(overlap_buf); + return .{ .data = data, .meta = meta, + .needle = needle, + .overlap_buf = overlap_buf, }; } pub fn deinit(self: *SlidingWindow, alloc: Allocator) void { + alloc.free(self.overlap_buf); self.data.deinit(alloc); var meta_it = self.meta.iterator(.forward); @@ -117,11 +134,11 @@ const SlidingWindow = struct { /// the window moves, the window will prune itself while maintaining /// the invariant that the window is always big enough to contain /// the needle. - pub fn next(self: *SlidingWindow, needle: []const u8) ?Selection { + pub fn next(self: *SlidingWindow) ?Selection { const slices = slices: { // If we have less data then the needle then we can't possibly match const data_len = self.data.len(); - if (data_len < needle.len) return null; + if (data_len < self.needle.len) return null; break :slices self.data.getPtrSlice( self.data_offset, @@ -130,15 +147,46 @@ const SlidingWindow = struct { }; // Search the first slice for the needle. - if (std.mem.indexOf(u8, slices[0], needle)) |idx| { - return self.selection(idx, needle.len); + if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { + return self.selection(idx, self.needle.len); } - // TODO: search overlap + // Search the overlap buffer for the needle. + if (slices[0].len > 0 and slices[1].len > 0) overlap: { + // Get up to needle.len - 1 bytes from each side (as much as + // we can) and store it in the overlap buffer. + const prefix: []const u8 = prefix: { + const len = @min(slices[0].len, self.needle.len - 1); + const idx = slices[0].len - len; + break :prefix slices[0][idx..]; + }; + const suffix: []const u8 = suffix: { + const len = @min(slices[1].len, self.needle.len - 1); + break :suffix slices[1][0..len]; + }; + const overlap_len = prefix.len + suffix.len; + assert(overlap_len <= self.overlap_buf.len); + @memcpy(self.overlap_buf[0..prefix.len], prefix); + @memcpy(self.overlap_buf[prefix.len..overlap_len], suffix); + + // Search the overlap + const idx = std.mem.indexOf( + u8, + self.overlap_buf[0..overlap_len], + self.needle, + ) orelse break :overlap; + + // We found a match in the overlap buffer. We need to map the + // index back to the data buffer in order to get our selection. + return self.selection( + slices[0].len - prefix.len + idx, + self.needle.len, + ); + } // Search the last slice for the needle. - if (std.mem.indexOf(u8, slices[1], needle)) |idx| { - return self.selection(slices[0].len + idx, needle.len); + if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { + return self.selection(slices[0].len + idx, self.needle.len); } // No match. We keep `needle.len - 1` bytes available to @@ -147,7 +195,7 @@ const SlidingWindow = struct { prune: { var saved: usize = 0; while (meta_it.next()) |meta| { - const needed = needle.len - 1 - saved; + const needed = self.needle.len - 1 - saved; if (meta.cell_map.items.len >= needed) { // We save up to this meta. We set our data offset // to exactly where it needs to be to continue @@ -161,7 +209,7 @@ const SlidingWindow = struct { // If we exited the while loop naturally then we // never got the amount we needed and so there is // nothing to prune. - assert(saved < needle.len - 1); + assert(saved < self.needle.len - 1); break :prune; } @@ -187,7 +235,7 @@ const SlidingWindow = struct { // Our data offset now moves to needle.len - 1 from the end so // that we can handle the overlap case. - self.data_offset = self.data.len() - needle.len + 1; + self.data_offset = self.data.len() - self.needle.len + 1; self.assertIntegrity(); return null; @@ -363,7 +411,7 @@ test "SlidingWindow empty on init" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "boo!"); defer w.deinit(alloc); try testing.expectEqual(0, w.data.len()); try testing.expectEqual(0, w.meta.len()); @@ -373,16 +421,13 @@ test "SlidingWindow single append" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "boo!"); defer w.deinit(alloc); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); try s.testWriteString("hello. boo! hello. boo!"); - // Imaginary needle for search - const needle = "boo!"; - // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; @@ -390,7 +435,7 @@ test "SlidingWindow single append" { // We should be able to find two matches. { - const sel = w.next(needle).?; + const sel = w.next().?; try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 0, @@ -401,7 +446,7 @@ test "SlidingWindow single append" { } }, s.pages.pointFromPin(.active, sel.end()).?); } { - const sel = w.next(needle).?; + const sel = w.next().?; try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, @@ -411,32 +456,29 @@ test "SlidingWindow single append" { .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); } - try testing.expect(w.next(needle) == null); - try testing.expect(w.next(needle) == null); + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); } test "SlidingWindow single append no match" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "nope!"); defer w.deinit(alloc); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); try s.testWriteString("hello. boo! hello. boo!"); - // Imaginary needle for search - const needle = "nope!"; - // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; try w.append(alloc, node); // No matches - try testing.expect(w.next(needle) == null); - try testing.expect(w.next(needle) == null); + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); // Should still keep the page try testing.expectEqual(1, w.meta.len()); @@ -446,7 +488,7 @@ test "SlidingWindow two pages" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "boo!"); defer w.deinit(alloc); var s = try Screen.init(alloc, 80, 24, 1000); @@ -463,9 +505,6 @@ test "SlidingWindow two pages" { try testing.expect(s.pages.pages.first != s.pages.pages.last); try s.testWriteString("hello. boo!"); - // Imaginary needle for search - const needle = "boo!"; - // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; try w.append(alloc, node); @@ -473,7 +512,7 @@ test "SlidingWindow two pages" { // Search should find two matches { - const sel = w.next(needle).?; + const sel = w.next().?; try testing.expectEqual(point.Point{ .active = .{ .x = 76, .y = 22, @@ -484,7 +523,7 @@ test "SlidingWindow two pages" { } }, s.pages.pointFromPin(.active, sel.end()).?); } { - const sel = w.next(needle).?; + const sel = w.next().?; try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 23, @@ -494,15 +533,15 @@ test "SlidingWindow two pages" { .y = 23, } }, s.pages.pointFromPin(.active, sel.end()).?); } - try testing.expect(w.next(needle) == null); - try testing.expect(w.next(needle) == null); + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); } test "SlidingWindow two pages match across boundary" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "hello, world"); defer w.deinit(alloc); var s = try Screen.init(alloc, 80, 24, 1000); @@ -518,9 +557,6 @@ test "SlidingWindow two pages match across boundary" { try s.testWriteString("o, world!"); try testing.expect(s.pages.pages.first != s.pages.pages.last); - // Imaginary needle for search - const needle = "hello, world"; - // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; try w.append(alloc, node); @@ -528,7 +564,7 @@ test "SlidingWindow two pages match across boundary" { // Search should find a match { - const sel = w.next(needle).?; + const sel = w.next().?; try testing.expectEqual(point.Point{ .active = .{ .x = 76, .y = 22, @@ -538,8 +574,8 @@ test "SlidingWindow two pages match across boundary" { .y = 23, } }, s.pages.pointFromPin(.active, sel.end()).?); } - try testing.expect(w.next(needle) == null); - try testing.expect(w.next(needle) == null); + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); // We shouldn't prune because we don't have enough space try testing.expectEqual(2, w.meta.len()); @@ -549,7 +585,7 @@ test "SlidingWindow two pages no match prunes first page" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "nope!"); defer w.deinit(alloc); var s = try Screen.init(alloc, 80, 24, 1000); @@ -571,12 +607,9 @@ test "SlidingWindow two pages no match prunes first page" { try w.append(alloc, node); try w.append(alloc, node.next.?); - // Imaginary needle for search. Doesn't match! - const needle = "nope!"; - // Search should find nothing - try testing.expect(w.next(needle) == null); - try testing.expect(w.next(needle) == null); + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); // We should've pruned our page because the second page // has enough text to contain our needle. @@ -587,9 +620,6 @@ test "SlidingWindow two pages no match keeps both pages" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); - defer w.deinit(alloc); - var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -604,20 +634,23 @@ test "SlidingWindow two pages no match keeps both pages" { try testing.expect(s.pages.pages.first != s.pages.pages.last); try s.testWriteString("hello. boo!"); - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); - // Imaginary needle for search. Doesn't match! var needle_list = std.ArrayList(u8).init(alloc); defer needle_list.deinit(); try needle_list.appendNTimes('x', first_page_rows * s.pages.cols); const needle: []const u8 = needle_list.items; + var w = try SlidingWindow.init(alloc, needle); + defer w.deinit(alloc); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + try w.append(alloc, node.next.?); + // Search should find nothing - try testing.expect(w.next(needle) == null); - try testing.expect(w.next(needle) == null); + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); // No pruning because both pages are needed to fit needle. try testing.expectEqual(2, w.meta.len()); @@ -627,7 +660,7 @@ test "SlidingWindow single append across circular buffer boundary" { const testing = std.testing; const alloc = testing.allocator; - var w = try SlidingWindow.initEmpty(alloc); + var w = try SlidingWindow.init(alloc, "abc"); defer w.deinit(alloc); var s = try Screen.init(alloc, 80, 24, 0); @@ -651,9 +684,12 @@ test "SlidingWindow single append across circular buffer boundary" { } // Search non-match, prunes page - try testing.expect(w.next("abc") == null); + try testing.expect(w.next() == null); try testing.expectEqual(1, w.meta.len()); + // Change the needle, just needs to be the same length (not a real API) + w.needle = "boo"; + // Add new page, now wraps try w.append(alloc, node); { @@ -662,15 +698,70 @@ test "SlidingWindow single append across circular buffer boundary" { try testing.expect(slices[1].len > 0); } { - const sel = w.next("boo!").?; + const sel = w.next().?; try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ - .x = 22, + .x = 21, .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); } - try testing.expect(w.next("boo!") == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append match on boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var w = try SlidingWindow.init(alloc, "abcd"); + defer w.deinit(alloc); + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("o!XXXXXXXXXXXXXXXXXXXbo"); + + // We are trying to break a circular buffer boundary so the way we + // do this is to duplicate the data then do a failing search. This + // will cause the first page to be pruned. The next time we append we'll + // put it in the middle of the circ buffer. We assert this so that if + // our implementation changes our test will fail. + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + try w.append(alloc, node); + try w.append(alloc, node); + { + // No wrap around yet + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len == 0); + } + + // Search non-match, prunes page + try testing.expect(w.next() == null); + try testing.expectEqual(1, w.meta.len()); + + // Change the needle, just needs to be the same length (not a real API) + w.needle = "boo!"; + + // Add new page, now wraps + try w.append(alloc, node); + { + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len > 0); + } + { + const sel = w.next().?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 21, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 1, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + try testing.expect(w.next() == null); } From 6361bf47f7f8fda0993a15046829e8d42b085419 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 11:23:09 -0800 Subject: [PATCH 48/62] terminal: update comments/docs on sliding window search --- src/terminal/search.zig | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 304cc5a4e..09078ae28 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -53,9 +53,22 @@ pub const PageListSearch = struct { } }; -/// Search pages via a sliding window. The sliding window always maintains -/// the invariant that data isn't pruned until we've searched it and -/// accounted for overlaps across pages. +/// Searches page nodes via a sliding window. The sliding window maintains +/// the invariant that data isn't pruned until (1) we've searched it and +/// (2) we've accounted for overlaps across pages to fit the needle. +/// +/// The sliding window is first initialized empty. Pages are then appended +/// in the order to search them. If you're doing a reverse search then the +/// pages should be appended in reverse order and the needle should be +/// reversed. +/// +/// All appends grow the window. The window is only pruned when a searc +/// is done (positive or negative match) via `next()`. +/// +/// To avoid unnecessary memory growth, the recommended usage is to +/// call `next()` until it returns null and then `append` the next page +/// and repeat the process. This will always maintain the minimum +/// required memory to search for the needle. const SlidingWindow = struct { /// The data buffer is a circular buffer of u8 that contains the /// encoded page text that we can use to search for the needle. From b9dda6ad87fff1ec01699a0b12ad28cbbe51b856 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 12:15:32 -0800 Subject: [PATCH 49/62] terminal: PageListSearch works! --- src/terminal/search.zig | 101 +++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 09078ae28..fe5ac0c29 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -10,46 +10,64 @@ const Pin = PageList.Pin; const Selection = terminal.Selection; const Screen = terminal.Screen; +/// Searches for a term in a PageList structure. pub const PageListSearch = struct { - alloc: Allocator, - /// The list we're searching. list: *PageList, - /// The search term we're searching for. - needle: []const u8, - - /// The window is our sliding window of pages that we're searching so - /// we can handle boundary cases where a needle is partially on the end - /// of one page and the beginning of the next. - /// - /// Note that we're not guaranteed to straddle exactly two pages. If - /// the needle is large enough and/or the pages are small enough then - /// the needle can straddle N pages. Additionally, pages aren't guaranteed - /// to be equal size so we can't precompute the window size. + /// The sliding window of page contents and nodes to search. window: SlidingWindow, + /// Initialize the page list search. + /// + /// The needle is not copied and must be kept alive for the duration + /// of the search operation. pub fn init( alloc: Allocator, list: *PageList, needle: []const u8, - ) !PageListSearch { - var window = try CircBuf.init(alloc, 0); - errdefer window.deinit(); + ) Allocator.Error!PageListSearch { + var window = try SlidingWindow.init(alloc, needle); + errdefer window.deinit(alloc); return .{ - .alloc = alloc, .list = list, - .current = list.pages.first, - .needle = needle, .window = window, }; } - pub fn deinit(self: *PageListSearch) void { - _ = self; + pub fn deinit(self: *PageListSearch, alloc: Allocator) void { + self.window.deinit(alloc); + } - // TODO: deinit window + /// Find the next match for the needle in the pagelist. This returns + /// null when there are no more matches. + pub fn next( + self: *PageListSearch, + alloc: Allocator, + ) Allocator.Error!?Selection { + // Try to search for the needle in the window. If we find a match + // then we can return that and we're done. + if (self.window.next()) |sel| return sel; + + // Get our next node. If we have a value in our window then we + // can determine the next node. If we don't, we've never setup the + // window so we use our first node. + var node_: ?*PageList.List.Node = if (self.window.meta.last()) |meta| + meta.node.next + else + self.list.pages.first; + + // Add one pagelist node at a time, look for matches, and repeat + // until we find a match or we reach the end of the pagelist. + // This append then next pattern limits memory usage of the window. + while (node_) |node| : (node_ = node.next) { + try self.window.append(alloc, node); + if (self.window.next()) |sel| return sel; + } + + // We've reached the end of the pagelist, no matches. + return null; } }; @@ -420,6 +438,45 @@ const SlidingWindow = struct { } }; +test "PageListSearch single page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + + var search = try PageListSearch.init(alloc, &s.pages, "boo!"); + defer search.deinit(alloc); + + // We should be able to find two matches. + { + const sel = (try search.next(alloc)).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + { + const sel = (try search.next(alloc)).?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end()).?); + } + try testing.expect((try search.next(alloc)) == null); + try testing.expect((try search.next(alloc)) == null); +} + test "SlidingWindow empty on init" { const testing = std.testing; const alloc = testing.allocator; From 50b36c5d8606a4fd0be5fafd4e641751f2625861 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 12:38:29 -0800 Subject: [PATCH 50/62] comments --- src/terminal/search.zig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/terminal/search.zig b/src/terminal/search.zig index fe5ac0c29..56b181c48 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -1,3 +1,26 @@ +//! Search functionality for the terminal. +//! +//! At the time of writing this comment, this is a **work in progress**. +//! +//! Search at the time of writing is implemented using a simple +//! boyer-moore-horspool algorithm. The suboptimal part of the implementation +//! is that we need to encode each terminal page into a text buffer in order +//! to apply BMH to it. This is because the terminal page is not laid out +//! in a flat text form. +//! +//! To minimize memory usage, we use a sliding window to search for the +//! needle. The sliding window only keeps the minimum amount of page data +//! in memory to search for a needle (i.e. `needle.len - 1` bytes of overlap +//! between terminal pages). +//! +//! Future work: +//! +//! - PageListSearch on a PageList concurrently with another thread +//! - Handle pruned pages in a PageList to ensure we don't keep references +//! - Repeat search a changing active area of the screen +//! - Reverse search so that more recent matches are found first +//! + const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; @@ -11,6 +34,10 @@ const Selection = terminal.Selection; const Screen = terminal.Screen; /// Searches for a term in a PageList structure. +/// +/// At the time of writing, this does not support searching a pagelist +/// simultaneously as its being used by another thread. This will be resolved +/// in the future. pub const PageListSearch = struct { /// The list we're searching. list: *PageList, From 7dd8e7c43f4c1da2a310dbd5d9c96c3a9de1d6e4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Dec 2024 12:48:00 -0800 Subject: [PATCH 51/62] remove unused file --- ' | 555 -------------------------------------------------------------- 1 file changed, 555 deletions(-) delete mode 100644 ' diff --git a/' b/' deleted file mode 100644 index 0b79f1879..000000000 --- a/' +++ /dev/null @@ -1,555 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const CircBuf = @import("../datastruct/main.zig").CircBuf; -const terminal = @import("main.zig"); -const point = terminal.point; -const Page = terminal.Page; -const PageList = terminal.PageList; -const Pin = PageList.Pin; -const Selection = terminal.Selection; -const Screen = terminal.Screen; - -pub const PageListSearch = struct { - alloc: Allocator, - - /// The list we're searching. - list: *PageList, - - /// The search term we're searching for. - needle: []const u8, - - /// The window is our sliding window of pages that we're searching so - /// we can handle boundary cases where a needle is partially on the end - /// of one page and the beginning of the next. - /// - /// Note that we're not guaranteed to straddle exactly two pages. If - /// the needle is large enough and/or the pages are small enough then - /// the needle can straddle N pages. Additionally, pages aren't guaranteed - /// to be equal size so we can't precompute the window size. - window: SlidingWindow, - - pub fn init( - alloc: Allocator, - list: *PageList, - needle: []const u8, - ) !PageListSearch { - var window = try CircBuf.init(alloc, 0); - errdefer window.deinit(); - - return .{ - .alloc = alloc, - .list = list, - .current = list.pages.first, - .needle = needle, - .window = window, - }; - } - - pub fn deinit(self: *PageListSearch) void { - _ = self; - - // TODO: deinit window - } -}; - -/// The sliding window of the pages we're searching. The window is always -/// big enough so that the needle can fit in it. -const SlidingWindow = struct { - /// The data buffer is a circular buffer of u8 that contains the - /// encoded page text that we can use to search for the needle. - data: DataBuf, - - /// The meta buffer is a circular buffer that contains the metadata - /// about the pages we're searching. This usually isn't that large - /// so callers must iterate through it to find the offset to map - /// data to meta. - meta: MetaBuf, - - /// The cursor into the data buffer for our current search. - i: usize = 0, - - const DataBuf = CircBuf(u8, 0); - const MetaBuf = CircBuf(Meta, undefined); - const Meta = struct { - node: *PageList.List.Node, - cell_map: Page.CellMap, - - pub fn deinit(self: *Meta) void { - self.cell_map.deinit(); - } - }; - - pub fn initEmpty(alloc: Allocator) Allocator.Error!SlidingWindow { - var data = try DataBuf.init(alloc, 0); - errdefer data.deinit(alloc); - - var meta = try MetaBuf.init(alloc, 0); - errdefer meta.deinit(alloc); - - return .{ - .data = data, - .meta = meta, - }; - } - - pub fn deinit(self: *SlidingWindow, alloc: Allocator) void { - self.data.deinit(alloc); - - var meta_it = self.meta.iterator(.forward); - while (meta_it.next()) |meta| meta.deinit(); - self.meta.deinit(alloc); - } - - /// Search the window for the next occurrence of the needle. - pub fn next(self: *SlidingWindow, needle: []const u8) void { - const slices = self.data.getPtrSlice(0, self.data.len()); - - // Search the first slice for the needle. - if (std.mem.indexOf(u8, slices[0][self.i..], needle)) |idx| { - // Found, map the match to a selection. - var meta_it = self.meta.iterator(.forward); - var i: usize = 0; - while (meta_it.next()) |meta| { - const meta_idx = idx - i; - if (meta.cell_map.items.len < meta_idx) { - // This meta doesn't contain the match. - i += meta.cell_map.items.len; - continue; - } - - // We found the meta that contains the start of the match. - const tl: PageList.Pin = tl: { - const map = meta.cell_map.items[meta_idx]; - break :tl .{ - .node = meta.node, - .y = map.y, - .x = map.x, - }; - }; - - _ = tl; - } - - // Found, we can move our index to the next character - // after the match. This let's us find all matches even if - // they overlap. - - self.i = idx + 1; - - @panic("TODO"); - } - } - - /// Return a selection for the given start and length into the data - /// buffer and also prune the data/meta buffers if possible up to - /// this start index. - fn selectAndPrune( - self: *SlidingWindow, - start: usize, - len: usize, - ) Selection { - assert(start < self.data.len()); - assert(start + len < self.data.len()); - - var meta_it = self.meta.iterator(.forward); - var meta_: ?Meta = meta_it.next(); - - // Find the start of the match - var offset: usize = 0; - var skip_nodes: usize = 0; - const tl: PageList.Pin = tl: { - while (meta_) |meta| : (meta_ = meta_it.next()) { - // meta_i is the index we expect to find the match in the - // cell map within this meta if it contains it. - const meta_i = start - offset; - if (meta_i >= meta.cell_map.items.len) { - // This meta doesn't contain the match. This means we - // can also prune this set of data because we only look - // forward. - offset += meta.cell_map.items.len; - skip_nodes += 1; - continue; - } - - // We found the meta that contains the start of the match. - const map = meta.cell_map.items[start]; - break :tl .{ - .node = meta.node, - .y = map.y, - .x = map.x, - }; - } - - // We never found the top-left. This is unreachable because - // we assert that the start index is within the data buffer, - // and when building the data buffer we assert the cell map - // length exactly matches the data buffer length. - unreachable; - }; - - // Keep track of the number of nodes we skipped for the tl. - const tl_skip_nodes = skip_nodes; - skip_nodes = 0; - - // Find the end of the match - const br: PageList.Pin = br: { - const end_idx = start + len - 1; - while (meta_) |meta| : (meta_ = meta_it.next()) { - const meta_i = end_idx - offset; - if (meta_i >= meta.cell_map.items.len) { - offset += meta.cell_map.items.len; - skip_nodes += 1; - continue; - } - - // We found the meta that contains the start of the match. - const map = meta.cell_map.items[end_idx]; - break :br .{ - .node = meta.node, - .y = map.y, - .x = map.x, - }; - } - }; - - // If we skipped any nodes for the bottom-right then we can prune - // all the way up to the total. If we didn't, it means we found - // the bottom-right in the same node as the top-left and we can't - // prune the node that the match is on because there may be - // more matches. - if (skip_nodes > 0) skip_nodes += tl_skip_nodes; - - _ = tl; - _ = br; - } - - /// Convert a data index into a pin. - fn pin( - self: *const SlidingWindow, - idx: usize, - it: ?*MetaBuf.Iterator, - ) struct { - /// The pin for the data index. - pin: Pin, - - /// The offset into the meta buffer that the pin was found. - /// This can be used to prune the meta buffer (its safe to prune - /// before this i). - meta_i: usize, - } { - _ = self; - _ = idx; - _ = start; - - while (it.next()) |meta| { - // meta_i is the index we expect to find the match in the - // cell map within this meta if it contains it. - const meta_i = start - offset; - if (meta_i >= meta.cell_map.items.len) { - // This meta doesn't contain the match. This means we - // can also prune this set of data because we only look - // forward. - offset += meta.cell_map.items.len; - skip_nodes += 1; - continue; - } - - // We found the meta that contains the start of the match. - const map = meta.cell_map.items[start]; - break :tl .{ - .node = meta.node, - .y = map.y, - .x = map.x, - }; - } - - } - - /// Add a new node to the sliding window. - /// - /// The window will prune itself if it can while always maintaining - /// the invariant that the `fixed_size` always fits within the window. - /// - /// Note it is possible for the window to be smaller than `fixed_size` - /// if not enough nodes have been added yet or the screen is just - /// smaller than the needle. - pub fn append( - self: *SlidingWindow, - alloc: Allocator, - node: *PageList.List.Node, - required_size: usize, - ) Allocator.Error!void { - // Initialize our metadata for the node. - var meta: Meta = .{ - .node = node, - .cell_map = Page.CellMap.init(alloc), - }; - errdefer meta.deinit(); - - // This is suboptimal but we need to encode the page once to - // temporary memory, and then copy it into our circular buffer. - // In the future, we should benchmark and see if we can encode - // directly into the circular buffer. - var encoded: std.ArrayListUnmanaged(u8) = .{}; - defer encoded.deinit(alloc); - - // Encode the page into the buffer. - const page: *const Page = &meta.node.data; - _ = page.encodeUtf8( - encoded.writer(alloc), - .{ .cell_map = &meta.cell_map }, - ) catch { - // writer uses anyerror but the only realistic error on - // an ArrayList is out of memory. - return error.OutOfMemory; - }; - assert(meta.cell_map.items.len == encoded.items.len); - - // Now that we know our buffer length, we can consider if we can - // prune our circular buffer or if we need to grow it. - prune: { - // Our buffer size after adding the new node. - const before_size: usize = self.data.len() + encoded.items.len; - - // Prune as long as removing the first (oldest) node retains - // our required size invariant. - var after_size: usize = before_size; - while (self.meta.first()) |oldest_meta| { - const new_size = after_size - oldest_meta.cell_map.items.len; - if (new_size < required_size) break :prune; - - // We can prune this node and retain our invariant. - // Update our new size, deinitialize the memory, and - // remove from the circular buffer. - after_size = new_size; - oldest_meta.deinit(); - self.meta.deleteOldest(1); - } - assert(after_size <= before_size); - - // If we didn't prune anything then we're done. - if (after_size == before_size) break :prune; - - // We need to prune our data buffer as well. - self.data.deleteOldest(before_size - after_size); - } - - // Ensure our buffers are big enough to store what we need. - try self.data.ensureUnusedCapacity(alloc, encoded.items.len); - try self.meta.ensureUnusedCapacity(alloc, 1); - - // Append our new node to the circular buffer. - try self.data.appendSlice(encoded.items); - try self.meta.append(meta); - - // Integrity check: verify our data matches our metadata exactly. - if (comptime std.debug.runtime_safety) { - var meta_it = self.meta.iterator(.forward); - var data_len: usize = 0; - while (meta_it.next()) |m| data_len += m.cell_map.items.len; - assert(data_len == self.data.len()); - } - } -}; - -test "SlidingWindow empty on init" { - const testing = std.testing; - const alloc = testing.allocator; - - var w = try SlidingWindow.initEmpty(alloc); - defer w.deinit(alloc); - try testing.expectEqual(0, w.data.len()); - try testing.expectEqual(0, w.meta.len()); -} - -test "SlidingWindow single append" { - const testing = std.testing; - const alloc = testing.allocator; - - var w = try SlidingWindow.initEmpty(alloc); - defer w.deinit(alloc); - - var s = try Screen.init(alloc, 80, 24, 0); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. boo!"); - - // Imaginary needle for search - const needle = "boo!"; - - // We want to test single-page cases. - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node, needle.len); -} - -test "SlidingWindow two pages" { - const testing = std.testing; - const alloc = testing.allocator; - - var w = try SlidingWindow.initEmpty(alloc); - defer w.deinit(alloc); - - var s = try Screen.init(alloc, 80, 24, 1000); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Imaginary needle for search - const needle = "boo!"; - - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node, needle.len); - try w.append(alloc, node.next.?, needle.len); - - // Ensure our data is correct -} - -pub const PageSearch = struct { - alloc: Allocator, - node: *PageList.List.Node, - needle: []const u8, - cell_map: Page.CellMap, - encoded: std.ArrayListUnmanaged(u8) = .{}, - i: usize = 0, - - pub fn init( - alloc: Allocator, - node: *PageList.List.Node, - needle: []const u8, - ) !PageSearch { - var result: PageSearch = .{ - .alloc = alloc, - .node = node, - .needle = needle, - .cell_map = Page.CellMap.init(alloc), - }; - - const page: *const Page = &node.data; - _ = try page.encodeUtf8(result.encoded.writer(alloc), .{ - .cell_map = &result.cell_map, - }); - - return result; - } - - pub fn deinit(self: *PageSearch) void { - self.encoded.deinit(self.alloc); - self.cell_map.deinit(); - } - - pub fn next(self: *PageSearch) ?Selection { - // Search our haystack for the needle. The resulting index is - // the offset from self.i not the absolute index. - const haystack: []const u8 = self.encoded.items[self.i..]; - const i_offset = std.mem.indexOf(u8, haystack, self.needle) orelse { - self.i = self.encoded.items.len; - return null; - }; - - // Get our full index into the encoded buffer. - const idx = self.i + i_offset; - - // We found our search term. Move the cursor forward one beyond - // the match. This lets us find every repeated match. - self.i = idx + 1; - - const tl: PageList.Pin = tl: { - const map = self.cell_map.items[idx]; - break :tl .{ - .node = self.node, - .y = map.y, - .x = map.x, - }; - }; - const br: PageList.Pin = br: { - const map = self.cell_map.items[idx + self.needle.len - 1]; - break :br .{ - .node = self.node, - .y = map.y, - .x = map.x, - }; - }; - - return Selection.init(tl, br, false); - } -}; - -test "search single page one match" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 80, 24, 0); - defer s.deinit(); - try s.testWriteString("hello, world"); - - // We want to test single-page cases. - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - - var it = try PageSearch.init(alloc, node, "world"); - defer it.deinit(); - - const sel = it.next().?; - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 11, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); - - try testing.expect(it.next() == null); -} - -test "search single page multiple match" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 80, 24, 0); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. boo!"); - - // We want to test single-page cases. - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - - var it = try PageSearch.init(alloc, node, "boo!"); - defer it.deinit(); - - { - const sel = it.next().?; - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 10, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); - } - { - const sel = it.next().?; - try testing.expectEqual(point.Point{ .active = .{ - .x = 19, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 22, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); - } - - try testing.expect(it.next() == null); -} From bb185cf6b695420ce8b43b5c1cadd16ef71c481a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Dec 2024 11:02:19 -0800 Subject: [PATCH 52/62] apprt/gtk: force X11 backend on GTK 4.14 I'm unsure if this is an environmental issue just for me or if this is more widespread or what other downsides this may have. I'm more than willing to revert this if it ends up causing different issues. I found that with NixOS 24.11 and GTK 4.14 on my system, the default Wayland GDK backend fails to initialize EGL. With GTK 4.16 everything is fine. If I force X11 then everything also works fine. This commit forces X11 for GTK 4.14 specifically (4.16+ is allowed to use Wayland). --- src/apprt/gtk/App.zig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index ead41de7c..6329644be 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -115,8 +115,25 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // reassess... // // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589 + // + // Specific details about values: + // - "opengl" - output OpenGL debug information + // - "gl-disable-gles" - disable GLES, Ghostty can't use GLES + // - "vulkan-disable" - disable Vulkan, Ghostty can't use Vulkan + // and initializing a Vulkan context was causing a longer delay + // on some systems. _ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable"); + + // Wayland-EGL on GTK 4.14 causes "Failed to create EGL context" errors. + // This can be fixed by forcing the backend to prefer X11. This issue + // appears to be fixed in GTK 4.16 but I wasn't able to bisect why. + // The "*" at the end says that if X11 fails, try all remaining + // backends. + _ = internal_os.setenv("GDK_BACKEND", "x11,*"); } else { + // Versions prior to 4.14 are a bit of an unknown for Ghostty. It + // is an environment that isn't tested well and we don't have a + // good understanding of what we may need to do. _ = internal_os.setenv("GDK_DEBUG", "vulkan-disable"); } From 1ee7da174bb473e3348b30b83aaa1732edd40bc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Dec 2024 11:00:02 -0800 Subject: [PATCH 53/62] flake: update to Nix 24.11 --- flake.lock | 14 +++++++------- flake.nix | 3 +-- nix/devShell.nix | 23 ++++++++++++++++------- src/build/MetallibStep.zig | 4 ++-- src/terminal/kitty/graphics_storage.zig | 2 +- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/flake.lock b/flake.lock index b5e75bae7..f517f07e4 100644 --- a/flake.lock +++ b/flake.lock @@ -20,27 +20,27 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1726062281, - "narHash": "sha256-PyFVySdGj3enKqm8RQuo4v1KLJLmNLOq2yYOHsI6e2Q=", + "lastModified": 1733423277, + "narHash": "sha256-TxabjxEgkNbCGFRHgM/b9yZWlBj60gUOUnRT/wbVQR8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e65aa8301ba4f0ab8cb98f944c14aa9da07394f8", + "rev": "e36963a147267afc055f7cf65225958633e536bf", "type": "github" }, "original": { "owner": "nixos", - "ref": "release-24.05", + "ref": "release-24.11", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-unstable": { "locked": { - "lastModified": 1719082008, - "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "lastModified": 1733229606, + "narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 01acca063..d52f96d72 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,7 @@ # We want to stay as up to date as possible but need to be careful that the # glibc versions used by our dependencies from Nix are compatible with the # system glibc that the user is building for. - nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.05"; + nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11"; zig = { url = "github:mitchellh/zig-overlay"; @@ -36,7 +36,6 @@ packages.${system} = let mkArgs = optimize: { - inherit (pkgs-unstable) zig_0_13 stdenv; inherit optimize; revision = self.shortRev or self.dirtyShortRev or "dirty"; diff --git a/nix/devShell.nix b/nix/devShell.nix index 7f0e206b7..b2502d92d 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -159,11 +159,20 @@ in # it to be "portable" across the system. LD_LIBRARY_PATH = lib.makeLibraryPath rpathLibs; - # On Linux we need to setup the environment so that all GTK data - # is available (namely icons). - shellHook = lib.optionalString stdenv.hostPlatform.isLinux '' - # Minimal subset of env set by wrapGAppsHook4 for icons and global settings - export XDG_DATA_DIRS=$XDG_DATA_DIRS:${hicolor-icon-theme}/share:${gnome.adwaita-icon-theme}/share - export XDG_DATA_DIRS=$XDG_DATA_DIRS:$GSETTINGS_SCHEMAS_PATH # from glib setup hook - ''; + shellHook = + (lib.optionalString stdenv.hostPlatform.isLinux '' + # On Linux we need to setup the environment so that all GTK data + # is available (namely icons). + + # Minimal subset of env set by wrapGAppsHook4 for icons and global settings + export XDG_DATA_DIRS=$XDG_DATA_DIRS:${hicolor-icon-theme}/share:${gnome.adwaita-icon-theme}/share + export XDG_DATA_DIRS=$XDG_DATA_DIRS:$GSETTINGS_SCHEMAS_PATH # from glib setup hook + '') + + (lib.optionalString stdenv.hostPlatform.isDarwin '' + # On macOS, we unset the macOS SDK env vars that Nix sets up because + # we rely on a system installation. Nix only provides a macOS SDK + # and we need iOS too. + unset SDKROOT + unset DEVELOPER_DIR + ''); } diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index e576b9c3a..587d276c1 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -42,7 +42,7 @@ pub fn create(b: *std.Build, opts: Options) *MetallibStep { b, b.fmt("metal {s}", .{opts.name}), ); - run_ir.addArgs(&.{ "xcrun", "-sdk", sdk, "metal", "-o" }); + run_ir.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metal", "-o" }); const output_ir = run_ir.addOutputFileArg(b.fmt("{s}.ir", .{opts.name})); run_ir.addArgs(&.{"-c"}); for (opts.sources) |source| run_ir.addFileArg(source); @@ -62,7 +62,7 @@ pub fn create(b: *std.Build, opts: Options) *MetallibStep { b, b.fmt("metallib {s}", .{opts.name}), ); - run_lib.addArgs(&.{ "xcrun", "-sdk", sdk, "metallib", "-o" }); + run_lib.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metallib", "-o" }); const output_lib = run_lib.addOutputFileArg(b.fmt("{s}.metallib", .{opts.name})); run_lib.addFileArg(output_ir); run_lib.step.dependOn(&run_ir.step); diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index bf8633c88..ee46b2a6c 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -690,7 +690,7 @@ pub const ImageStorage = struct { br.x = @min( // We need to sub one here because the x value is // one width already. So if the image is width "1" - // then we add zero to X because X itelf is width 1. + // then we add zero to X because X itself is width 1. pin.x + (grid_size.cols - 1), t.cols - 1, ); From aa4e704d95cb28c838482fc2dfdd1e6df6bdf95d Mon Sep 17 00:00:00 2001 From: Dmitry Zhlobo Date: Sun, 8 Dec 2024 17:20:22 +0100 Subject: [PATCH 54/62] doc: remove outdated statement for fullscreen option We can use `macos-non-native-fullscreen` with `fullscreen` together since #1377 was closed. --- src/config/Config.zig | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 7fda17289..19417fff0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -668,9 +668,6 @@ link: RepeatableLink = .{}, /// does not apply to tabs, splits, etc. However, this setting will apply to all /// new windows, not just the first one. /// -/// On macOS, this always creates the window in native fullscreen. Non-native -/// fullscreen is not currently supported with this setting. -/// /// On macOS, this setting does not work if window-decoration is set to /// "false", because native fullscreen on macOS requires window decorations /// to be set. From 250bd35830f80788f4b80e5c83b34bfa6986f112 Mon Sep 17 00:00:00 2001 From: moni Date: Fri, 6 Dec 2024 09:44:20 +0800 Subject: [PATCH 55/62] termio: clear kitty images when deleting above the cursor --- src/termio/Termio.zig | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 1ebe84541..bbcee7906 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -478,6 +478,18 @@ pub fn clearScreen(self: *Termio, td: *ThreadData, history: bool) !void { ); } + // Clear all Kitty graphics state for this screen. This copies + // Kitty's behavior when Cmd+K deletes all Kitty graphics. I + // didn't spend time researching whether it only deletes Kitty + // graphics that are placed baove the cursor or if it deletes + // all of them. We delete all of them for now but if this behavior + // isn't fully correct we should fix this later. + self.terminal.screen.kitty_images.delete( + self.terminal.screen.alloc, + &self.terminal, + .{ .all = true }, + ); + return; } From eb138930e6b5b105808357ea21aefb2c7f8b3d75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Dec 2024 11:04:29 -0800 Subject: [PATCH 56/62] macos: prevent moveFocus from being an infinite loop Fixes #2900 It's possible for moveFocus to infinite loop if the surface view we're trying to move focus to NEVER gets attached to a window. This can happen if the window is destroyed. I think this issue should be more systemically fixed so it can't happen but this workaround for now prevents moveFocus from being an infinite loop source for the time being. --- .../Ghostty/Ghostty.TerminalSplit.swift | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index d4f82620c..272cdabdb 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -429,12 +429,34 @@ extension Ghostty { /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't /// figure it out so we're going to do this hacky thing to bring focus back to the terminal /// that should have it. - static func moveFocus(to: SurfaceView, from: SurfaceView? = nil) { - DispatchQueue.main.async { + static func moveFocus( + to: SurfaceView, + from: SurfaceView? = nil, + delay: TimeInterval? = nil + ) { + // The whole delay machinery is a bit of a hack to work around a + // situation where the window is destroyed and the surface view + // will never be attached to a window. Realistically, we should + // handle this upstream but we also don't want this function to be + // a source of infinite loops. + + // Our max delay before we give up + let maxDelay: TimeInterval = 0.5 + guard (delay ?? 0) < maxDelay else { return } + + // We start at a 50 millisecond delay and do a doubling backoff + let nextDelay: TimeInterval = if let delay { + delay * 2 + } else { + // 100 milliseconds + 0.05 + } + + let work: DispatchWorkItem = .init { // If the callback runs before the surface is attached to a view // then the window will be nil. We just reschedule in that case. guard let window = to.window else { - moveFocus(to: to, from: from) + moveFocus(to: to, from: from, delay: nextDelay) return } @@ -448,5 +470,12 @@ extension Ghostty { window.makeFirstResponder(to) } + + let queue = DispatchQueue.main + if let delay { + queue.asyncAfter(deadline: .now() + delay, execute: work) + } else { + queue.async(execute: work) + } } } From 43a7dece02953d90e76f057a730f4e614c4cd87e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Dec 2024 11:22:02 -0800 Subject: [PATCH 57/62] config: title can reload at runtime Related to #2898 --- src/Surface.zig | 8 ++++++++ src/config/Config.zig | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 78a842673..3e7300d08 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1182,6 +1182,14 @@ pub fn updateConfig( log.warn("failed to notify renderer of config change err={}", .{err}); }; + // If we have a title set then we update our window to have the + // newly configured title. + if (config.title) |title| try self.rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = title }, + ); + // Notify the window try self.rt_app.performAction( .{ .surface = self }, diff --git a/src/config/Config.zig b/src/config/Config.zig index 7fda17289..1bb8f48c4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -679,6 +679,12 @@ fullscreen: bool = false, /// The title Ghostty will use for the window. This will force the title of the /// window to be this title at all times and Ghostty will ignore any set title /// escape sequences programs (such as Neovim) may send. +/// +/// This configuration can be reloaded at runtime. If it is set, the title +/// will update for all windows. If it is unset, the next title change escape +/// sequence will be honored but previous changes will not retroactively +/// be set. This latter case may require you restart programs such as neovim +/// to get the new title. title: ?[:0]const u8 = null, /// The setting that will change the application class value. From 49f105cd27ecb7afd58d57a2df2ef6ae6f2e1765 Mon Sep 17 00:00:00 2001 From: Dmitry Zhlobo Date: Sun, 8 Dec 2024 16:04:51 +0100 Subject: [PATCH 58/62] macos: make non-native fullscreen windows not resizeable --- macos/Sources/Helpers/Fullscreen.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index bb3859e07..56912a28a 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -198,6 +198,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Being untitled let's our content take up the full frame. window.styleMask.remove(.titled) + // We dont' want the non-native fullscreen window to be resizable + // from the edges. + window.styleMask.remove(.resizable) + // Focus window window.makeKeyAndOrderFront(nil) From 313752dee27690e3920d54bfc1b55ad1e7413c76 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Dec 2024 16:48:15 -0800 Subject: [PATCH 59/62] update libxev This fixes a possible deadlock. This has never happened in reports by beta testers but good hygiene to get this fixed. --- build.zig.zon | 4 ++-- nix/zigCacheHash.nix | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index c5e2d8f70..35365af8a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .dependencies = .{ // Zig libs .libxev = .{ - .url = "https://github.com/mitchellh/libxev/archive/b8d1d93e5c899b27abbaa7df23b496c3e6a178c7.tar.gz", - .hash = "1220612bc023c21d75234882ec9a8c6a1cbd9d642da3dfb899297f14bb5bd7b6cd78", + .url = "https://github.com/mitchellh/libxev/archive/db6a52bafadf00360e675fefa7926e8e6c0e9931.tar.gz", + .hash = "12206029de146b685739f69b10a6f08baee86b3d0a5f9a659fa2b2b66c9602078bbf", }, .mach_glfw = .{ .url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz", diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index 55cb4a0f3..162f65500 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-D1SQIlmdP9x1PDgRVOy1qJGmu9osDbuyxGOcFj646N4=" +"sha256-c3MQJG7vwQBOaxHQ8cYP0HxdsLqlgsVmAiT1d7gq6js=" From 247409d70577364dca801f3512b0802b03599444 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Dec 2024 08:14:57 -0800 Subject: [PATCH 60/62] New Ghostty icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ❤️👻 This is the icon that we'll launch Ghostty 1.0 with. It was designed by Michael Flareup at PixelResort. It retains the style of the original Ghostty icon by Alasdair Monk, but brings in the new Ghost character and adds details that make it more Apple-like. The new Ghost character is an important evolution from the original since it separates us from looking too much like PacMan. The new Ghost is more unique and recognizable to Ghostty (or, hopefully will be!). The icon itself has more details: the aluminum around the edge has texture for the large enough sizes, there are visible scanlines, the glow of a screen emanates from the ghost. The icon itself is stylistic more Apple-like than other platforms. I think Apple icons tend to look very good in more environments than the reverse and I'm a big fan of the Apple aesthetic so I wanted to bring that to Ghostty for all platforms. --- README.md | 2 +- build.zig | 18 +++++++------- dist/macos/Ghostty.icns | Bin 501433 -> 382978 bytes dist/windows/ghostty.ico | Bin 73070 -> 86258 bytes images/icons/icon_1024.png | Bin 0 -> 464853 bytes images/icons/icon_128.png | Bin 0 -> 15177 bytes images/icons/icon_128@2x.png | Bin 0 -> 68177 bytes images/icons/icon_128x128.png | Bin 7944 -> 0 bytes images/icons/icon_128x128@2x@2x.png | Bin 18209 -> 0 bytes images/icons/icon_16.png | Bin 0 -> 666 bytes images/icons/icon_16@2x.png | Bin 0 -> 1563 bytes images/icons/icon_16x16.png | Bin 649 -> 0 bytes images/icons/icon_16x16@2x@2x.png | Bin 1499 -> 0 bytes images/icons/icon_256.png | Bin 0 -> 68189 bytes images/icons/icon_256@2x.png | Bin 0 -> 221047 bytes images/icons/icon_256x256.png | Bin 17785 -> 0 bytes images/icons/icon_256x256@2x@2x.png | Bin 40443 -> 0 bytes images/icons/icon_32.png | Bin 0 -> 1562 bytes images/icons/icon_32@2x.png | Bin 0 -> 4485 bytes images/icons/icon_32x32.png | Bin 1499 -> 0 bytes images/icons/icon_32x32@2x@2x.png | Bin 3071 -> 0 bytes images/icons/icon_512.png | Bin 0 -> 221047 bytes images/icons/icon_512x512.png | Bin 40443 -> 0 bytes images/icons/icon_512x512@2x@2x.png | Bin 94863 -> 0 bytes .../AppIcon.appiconset/Contents.json | 22 +++++++++--------- .../AppIcon.appiconset/icon_128x128.png | Bin 7944 -> 0 bytes .../AppIcon.appiconset/icon_128x128@2x@2x.png | Bin 18209 -> 0 bytes .../AppIcon.appiconset/icon_16x16.png | Bin 582 -> 0 bytes .../AppIcon.appiconset/icon_16x16@2x@2x.png | Bin 1499 -> 0 bytes .../AppIcon.appiconset/icon_256x256.png | Bin 17785 -> 0 bytes .../AppIcon.appiconset/icon_256x256@2x@2x.png | Bin 40443 -> 0 bytes .../AppIcon.appiconset/icon_32x32.png | Bin 1499 -> 0 bytes .../AppIcon.appiconset/icon_32x32@2x@2x.png | Bin 3071 -> 0 bytes .../AppIcon.appiconset/icon_512x512.png | Bin 40443 -> 0 bytes .../icon_512x512@2x@2x 1.png | Bin 94863 -> 0 bytes .../AppIcon.appiconset/icon_512x512@2x@2x.png | Bin 94863 -> 0 bytes .../macOS-AppIcon-1024px 1.png | Bin 0 -> 464853 bytes .../macOS-AppIcon-1024px.png | Bin 0 -> 464853 bytes .../macOS-AppIcon-128px-128pt@1x.png | Bin 0 -> 15177 bytes .../macOS-AppIcon-16px-16pt@1x.png | Bin 0 -> 666 bytes .../macOS-AppIcon-256px-128pt@2x 1.png | Bin 0 -> 68177 bytes .../macOS-AppIcon-256px-128pt@2x.png | Bin 0 -> 68177 bytes .../macOS-AppIcon-32px-16pt@2x.png | Bin 0 -> 1562 bytes .../macOS-AppIcon-32px-32pt@1x.png | Bin 0 -> 1564 bytes .../macOS-AppIcon-512px-256pt@2x.png | Bin 0 -> 221047 bytes .../macOS-AppIcon-512px.png | Bin 0 -> 220725 bytes .../macOS-AppIcon-64px-32pt@2x.png | Bin 0 -> 4485 bytes .../AppIconImage.imageset/Contents.json | 6 ++--- .../AppIconImage.imageset/icon_128x128.png | Bin 7944 -> 0 bytes .../icon_128x128@2x@2x.png | Bin 18209 -> 0 bytes .../icon_256x256@2x@2x.png | Bin 40443 -> 0 bytes .../macOS-AppIcon-1024px.png | Bin 0 -> 464853 bytes .../macOS-AppIcon-256px-128pt@2x.png | Bin 0 -> 68177 bytes .../macOS-AppIcon-512px.png | Bin 0 -> 221146 bytes src/apprt/gtk/gresource.zig | 18 +++++++------- 55 files changed, 33 insertions(+), 33 deletions(-) mode change 100755 => 100644 dist/macos/Ghostty.icns create mode 100644 images/icons/icon_1024.png create mode 100644 images/icons/icon_128.png create mode 100644 images/icons/icon_128@2x.png delete mode 100755 images/icons/icon_128x128.png delete mode 100755 images/icons/icon_128x128@2x@2x.png create mode 100644 images/icons/icon_16.png create mode 100644 images/icons/icon_16@2x.png delete mode 100755 images/icons/icon_16x16.png delete mode 100755 images/icons/icon_16x16@2x@2x.png create mode 100644 images/icons/icon_256.png create mode 100644 images/icons/icon_256@2x.png delete mode 100755 images/icons/icon_256x256.png delete mode 100755 images/icons/icon_256x256@2x@2x.png create mode 100644 images/icons/icon_32.png create mode 100644 images/icons/icon_32@2x.png delete mode 100755 images/icons/icon_32x32.png delete mode 100755 images/icons/icon_32x32@2x@2x.png create mode 100644 images/icons/icon_512.png delete mode 100755 images/icons/icon_512x512.png delete mode 100755 images/icons/icon_512x512@2x@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x 1.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px 1.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-128px-128pt@1x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-16px-16pt@1x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x 1.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-16pt@2x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-32pt@1x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px-256pt@2x.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px.png create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-64px-32pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIconImage.imageset/icon_128x128.png delete mode 100644 macos/Assets.xcassets/AppIconImage.imageset/icon_128x128@2x@2x.png delete mode 100644 macos/Assets.xcassets/AppIconImage.imageset/icon_256x256@2x@2x.png create mode 100644 macos/Assets.xcassets/AppIconImage.imageset/macOS-AppIcon-1024px.png create mode 100644 macos/Assets.xcassets/AppIconImage.imageset/macOS-AppIcon-256px-128pt@2x.png create mode 100644 macos/Assets.xcassets/AppIconImage.imageset/macOS-AppIcon-512px.png diff --git a/README.md b/README.md index 861e0937a..4cafc6ac1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- Logo + Logo
Ghostty

diff --git a/build.zig b/build.zig index d233bff1f..093afe481 100644 --- a/build.zig +++ b/build.zig @@ -578,15 +578,15 @@ pub fn build(b: *std.Build) !void { // Various icons that our application can use, including the icon // that will be used for the desktop. - b.installFile("images/icons/icon_16x16.png", "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_32x32.png", "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_128x128.png", "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_256x256.png", "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_512x512.png", "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_16x16@2x@2x.png", "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_32x32@2x@2x.png", "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_128x128@2x@2x.png", "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_256x256@2x@2x.png", "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_16.png", "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_32.png", "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_128.png", "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_256.png", "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_512.png", "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_16@2x.png", "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_32@2x.png", "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_128@2x.png", "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png"); + b.installFile("images/icons/icon_256@2x.png", "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png"); } // libghostty (non-Darwin) diff --git a/dist/macos/Ghostty.icns b/dist/macos/Ghostty.icns old mode 100755 new mode 100644 index 52365a4057685a4dbd22e507132bd07a7d3fbacd..44a44711aac562d23a82630e3b374ba9c5325b14 GIT binary patch literal 382978 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