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