From 892dc2789657c4161137af191b49bcb7723ea7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 28 Oct 2024 00:50:24 -0400 Subject: [PATCH 01/46] Make sure a potential port component is considered during hostname validation --- src/termio/stream_handler.zig | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 90a33e8b7..00762bc73 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1057,13 +1057,36 @@ pub const StreamHandler = struct { // Get the raw string of the URI. Its unclear to me if the various // tags of this enum guarantee no percent-encoding so we just // check all of it. This isn't a performance critical path. - const host = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, + const host = host: { + const h = switch (host_component) { + .raw => |v| v, + .percent_encoded => |v| v, + }; + if (h.len == 0 or std.mem.eql(u8, "localhost", h)) { + break :host_valid true; + } + + // When the "Private Wi-Fi address" setting is toggled on macOS the hostname + // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. + // The URI will be parsed as if the last set o digit is a port, so we need to + // make sure that part is included when it's set. + if (uri.port) |port| { + // 65_535 is considered the highest port number on Linux. + const PORT_NUMBER_MAX_DIGITS = 5; + + // Make sure there is space for a max length hostname + the max number of digits. + var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; + + const host_and_port = std.fmt.bufPrint(&host_and_port_buf, "{s}:{d}", .{ h, port }) catch |err| { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); + break :host_valid false; + }; + + break :host host_and_port; + } else { + break :host h; + } }; - if (host.len == 0 or std.mem.eql(u8, "localhost", host)) { - break :host_valid true; - } // Otherwise, it must match our hostname. var buf: [posix.HOST_NAME_MAX]u8 = undefined; From a2a1d93d5cbc2c6e00193902b4d3ac09d161b330 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 28 Oct 2024 09:48:01 +0100 Subject: [PATCH 02/46] apprt: propagate OSC10/11 (set term fore/background color) through to apprt This is to allow the running apprt to set the UI theme to match the terminal application coloring. --- include/ghostty.h | 18 ++++++++++++++++++ src/Surface.zig | 16 ++++++++++++++++ src/apprt/action.zig | 20 ++++++++++++++++++++ src/apprt/glfw.zig | 2 ++ src/apprt/gtk/App.zig | 2 ++ src/apprt/surface.zig | 6 ++++++ src/termio/stream_handler.zig | 2 ++ 7 files changed, 66 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index ca70456d8..b4735fc40 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -512,6 +512,20 @@ typedef struct { ghostty_input_trigger_s trigger; } ghostty_action_key_sequence_s; +// apprt.action.SetBackground +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} ghostty_action_set_background_s; + +// apprt.action.SetForeground +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} ghostty_action_set_foreground_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_NEW_WINDOW, @@ -545,6 +559,8 @@ typedef enum { GHOSTTY_ACTION_QUIT_TIMER, GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, + GHOSTTY_ACTION_SET_BACKGROUND, + GHOSTTY_ACTION_SET_FOREGROUND, } ghostty_action_tag_e; typedef union { @@ -567,6 +583,8 @@ typedef union { ghostty_action_quit_timer_e quit_timer; ghostty_action_secure_input_e secure_input; ghostty_action_key_sequence_s key_sequence; + ghostty_action_set_background_s set_background; + ghostty_action_set_foreground_s set_foreground; } ghostty_action_u; typedef struct { diff --git a/src/Surface.zig b/src/Surface.zig index 82d1240eb..b14ffe706 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -799,6 +799,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .unlocked); }, + .set_background => |color| { + try self.rt_app.performAction( + .{ .surface = self }, + .set_background, + .{ .r = color.r, .g = color.g, .b = color.b }, + ); + }, + + .set_foreground => |color| { + try self.rt_app.performAction( + .{ .surface = self }, + .set_background, + .{ .r = color.r, .g = color.g, .b = color.b }, + ); + }, + .set_mouse_shape => |shape| { log.debug("changing mouse shape: {}", .{shape}); try self.rt_app.performAction( diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 2c37ca270..1fc79a3a5 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -186,6 +186,12 @@ pub const Action = union(Key) { /// key mode because other input may be ignored. key_sequence: KeySequence, + /// The terminal background color was changed. + set_background: SetBackground, + + /// The terminal foreground color was changed. + set_foreground: SetForeground, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { new_window, @@ -219,6 +225,8 @@ pub const Action = union(Key) { quit_timer, secure_input, key_sequence, + set_background, + set_foreground, }; /// Sync with: ghostty_action_u @@ -448,3 +456,15 @@ pub const KeySequence = union(enum) { }; } }; + +pub const SetBackground = extern struct { + r: u8, + g: u8, + b: u8, +}; + +pub const SetForeground = extern struct { + r: u8, + g: u8, + b: u8, +}; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 1dde97c9c..f9651a934 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -223,6 +223,8 @@ pub const App = struct { .mouse_over_link, .cell_size, .renderer_health, + .set_foreground, + .set_background, => log.info("unimplemented action={}", .{action}), } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index af664d720..b444f9da8 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -485,6 +485,8 @@ pub fn performAction( .key_sequence, .render_inspector, .renderer_health, + .set_foreground, + .set_background, => log.warn("unimplemented action={}", .{action}), } } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index daa2ad547..7de83a78b 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -70,6 +70,12 @@ pub const Message = union(enum) { /// unless the surface exits. password_input: bool, + /// The terminal background color was changed. + set_background: terminal.color.RGB, + + /// The terminal foreground color was changed. + set_foreground: terminal.color.RGB, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 90a33e8b7..e53544842 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1215,12 +1215,14 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(.{ .foreground_color = color, }, .{ .forever = {} }); + self.surfaceMessageWriter(.{ .set_background = color }); }, .background => { self.background_color = color; _ = self.renderer_mailbox.push(.{ .background_color = color, }, .{ .forever = {} }); + self.surfaceMessageWriter(.{ .set_background = color }); }, .cursor => { self.cursor_color = color; From 04f752e5b8f6a6a3318f7f8898a993759bd8219e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 11:12:00 -0400 Subject: [PATCH 03/46] PACKAGING: recommend -Dtarget --- PACKAGING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PACKAGING.md b/PACKAGING.md index 5e6560c34..f31252272 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -91,3 +91,8 @@ relevant to package maintainers: - `-Dcpu=baseline`: Build for the "baseline" CPU of the target architecture. This avoids building for newer CPU features that may not be available on all target machines. + +- `-Dtarget=$arch-$os-$abi`: Build for a specific target triple. This is + often necessary for system packages to specify a specific minimum Linux + version, glibc, etc. Run `zig targets` to a get a full list of available + targets. From 7db9528aca70227944c79a5dcbd509e9a4a62c45 Mon Sep 17 00:00:00 2001 From: "Marvin A. Ruder" Date: Wed, 30 Oct 2024 19:56:01 +0100 Subject: [PATCH 04/46] fix(macOS): Fix visual glitch when switching between full-screen apps * Check whether window is on active space before clamping Fixes #2527 --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 432345627..071b7626b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -152,6 +152,7 @@ class BaseTerminalController: NSWindowController, // screen then we clamp it back to within the screen. guard let window else { return } guard window.isVisible else { return } + guard window.isOnActiveSpace else { return } guard let screen = window.screen else { return } let visibleFrame = screen.visibleFrame From 1065359b9a7b3ee597df47309854c5ee22d2c988 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 16:31:59 -0400 Subject: [PATCH 05/46] apprt: rename set_bg/fg to "color_change" to report all color changes --- include/ghostty.h | 23 +++++++++---------- macos/Sources/Ghostty/Ghostty.App.swift | 2 ++ src/Surface.zig | 30 ++++++++++++------------- src/apprt/action.zig | 26 +++++++++++---------- src/apprt/glfw.zig | 3 +-- src/apprt/gtk/App.zig | 3 +-- src/apprt/surface.zig | 10 ++++----- src/termio/stream_handler.zig | 8 +++++-- 8 files changed, 55 insertions(+), 50 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index b4735fc40..f4836f210 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -512,19 +512,20 @@ typedef struct { ghostty_input_trigger_s trigger; } ghostty_action_key_sequence_s; -// apprt.action.SetBackground -typedef struct { - uint8_t r; - uint8_t g; - uint8_t b; -} ghostty_action_set_background_s; +// apprt.action.ColorKind +typedef enum { + GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1, + GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2, + GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3, +} ghostty_action_color_kind_e; -// apprt.action.SetForeground +// apprt.action.ColorChange typedef struct { + ghostty_action_color_kind_e kind; uint8_t r; uint8_t g; uint8_t b; -} ghostty_action_set_foreground_s; +} ghostty_action_color_change_s; // apprt.Action.Key typedef enum { @@ -559,8 +560,7 @@ typedef enum { GHOSTTY_ACTION_QUIT_TIMER, GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, - GHOSTTY_ACTION_SET_BACKGROUND, - GHOSTTY_ACTION_SET_FOREGROUND, + GHOSTTY_ACTION_COLOR_CHANGE, } ghostty_action_tag_e; typedef union { @@ -583,8 +583,7 @@ typedef union { ghostty_action_quit_timer_e quit_timer; ghostty_action_secure_input_e secure_input; ghostty_action_key_sequence_s key_sequence; - ghostty_action_set_background_s set_background; - ghostty_action_set_foreground_s set_foreground; + ghostty_action_color_change_s color_change; } ghostty_action_u; typedef struct { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e5320a24a..07acf0f91 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -521,6 +521,8 @@ extension Ghostty { case GHOSTTY_ACTION_KEY_SEQUENCE: keySequence(app, target: target, v: action.action.key_sequence) + case GHOSTTY_ACTION_COLOR_CHANGE: + fallthrough case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: diff --git a/src/Surface.zig b/src/Surface.zig index b14ffe706..2797f647f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -799,21 +799,21 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .unlocked); }, - .set_background => |color| { - try self.rt_app.performAction( - .{ .surface = self }, - .set_background, - .{ .r = color.r, .g = color.g, .b = color.b }, - ); - }, - - .set_foreground => |color| { - try self.rt_app.performAction( - .{ .surface = self }, - .set_background, - .{ .r = color.r, .g = color.g, .b = color.b }, - ); - }, + .color_change => |change| try self.rt_app.performAction( + .{ .surface = self }, + .color_change, + .{ + .kind = switch (change.kind) { + .background => .background, + .foreground => .foreground, + .cursor => .cursor, + .palette => |v| @enumFromInt(v), + }, + .r = change.color.r, + .g = change.color.g, + .b = change.color.b, + }, + ), .set_mouse_shape => |shape| { log.debug("changing mouse shape: {}", .{shape}); diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1fc79a3a5..feb2e2ba4 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -186,11 +186,9 @@ pub const Action = union(Key) { /// key mode because other input may be ignored. key_sequence: KeySequence, - /// The terminal background color was changed. - set_background: SetBackground, - - /// The terminal foreground color was changed. - set_foreground: SetForeground, + /// A terminal color was changed programmatically through things + /// such as OSC 10/11. + color_change: ColorChange, /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { @@ -225,8 +223,7 @@ pub const Action = union(Key) { quit_timer, secure_input, key_sequence, - set_background, - set_foreground, + color_change, }; /// Sync with: ghostty_action_u @@ -457,14 +454,19 @@ pub const KeySequence = union(enum) { } }; -pub const SetBackground = extern struct { +pub const ColorChange = extern struct { + kind: ColorKind, r: u8, g: u8, b: u8, }; -pub const SetForeground = extern struct { - r: u8, - g: u8, - b: u8, +pub const ColorKind = enum(c_int) { + // Negative numbers indicate some named kind + foreground = -1, + background = -2, + cursor = -3, + + // 0+ values indicate a palette index + _, }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index f9651a934..638f52bab 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -223,8 +223,7 @@ pub const App = struct { .mouse_over_link, .cell_size, .renderer_health, - .set_foreground, - .set_background, + .color_change, => log.info("unimplemented action={}", .{action}), } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b444f9da8..017fc0ed4 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -485,8 +485,7 @@ pub fn performAction( .key_sequence, .render_inspector, .renderer_health, - .set_foreground, - .set_background, + .color_change, => log.warn("unimplemented action={}", .{action}), } } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 7de83a78b..07eb1d466 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -70,11 +70,11 @@ pub const Message = union(enum) { /// unless the surface exits. password_input: bool, - /// The terminal background color was changed. - set_background: terminal.color.RGB, - - /// The terminal foreground color was changed. - set_foreground: terminal.color.RGB, + /// A terminal color was changed using OSC sequences. + color_change: struct { + kind: terminal.osc.Command.ColorKind, + color: terminal.color.RGB, + }, pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index e53544842..5fe2e95e3 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1215,14 +1215,12 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(.{ .foreground_color = color, }, .{ .forever = {} }); - self.surfaceMessageWriter(.{ .set_background = color }); }, .background => { self.background_color = color; _ = self.renderer_mailbox.push(.{ .background_color = color, }, .{ .forever = {} }); - self.surfaceMessageWriter(.{ .set_background = color }); }, .cursor => { self.cursor_color = color; @@ -1231,6 +1229,12 @@ pub const StreamHandler = struct { }, .{ .forever = {} }); }, } + + // Notify the surface of the color change + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = kind, + .color = color, + } }); } pub fn resetColor( From b454f90a1a51bdb1434582fc62be8644458fd41b Mon Sep 17 00:00:00 2001 From: "Marvin A. Ruder" Date: Wed, 30 Oct 2024 21:42:25 +0100 Subject: [PATCH 06/46] Replace check * Check whether window is fullscreen before clamping --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 071b7626b..03bc40a1f 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -152,7 +152,7 @@ class BaseTerminalController: NSWindowController, // screen then we clamp it back to within the screen. guard let window else { return } guard window.isVisible else { return } - guard window.isOnActiveSpace else { return } + guard !window.styleMask.contains(.fullScreen) else { return } guard let screen = window.screen else { return } let visibleFrame = screen.visibleFrame From 7eb5563e9cfbd6a32ab1a88671f4126cb31d72f0 Mon Sep 17 00:00:00 2001 From: Nyaa97 Date: Tue, 29 Oct 2024 21:16:32 +0100 Subject: [PATCH 07/46] Fix linking freetype and glslang --- build.zig | 3 ++- pkg/cimgui/build.zig | 2 +- pkg/harfbuzz/build.zig | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index d4b0df667..4e7a072e6 100644 --- a/build.zig +++ b/build.zig @@ -975,7 +975,7 @@ fn addDeps( if (b.systemIntegrationOption("freetype", .{})) { step.linkSystemLibrary2("bzip2", dynamic_link_opts); - step.linkSystemLibrary2("freetype", dynamic_link_opts); + step.linkSystemLibrary2("freetype2", dynamic_link_opts); } else { step.linkLibrary(freetype_dep.artifact("freetype")); try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin()); @@ -1068,6 +1068,7 @@ fn addDeps( step.root_module.addImport("glslang", glslang_dep.module("glslang")); if (b.systemIntegrationOption("glslang", .{})) { step.linkSystemLibrary2("glslang", dynamic_link_opts); + step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts); } else { step.linkLibrary(glslang_dep.artifact("glslang")); try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index 09d340adb..4b5d56963 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -32,7 +32,7 @@ pub fn build(b: *std.Build) !void { }; if (b.systemIntegrationOption("freetype", .{})) { - lib.linkSystemLibrary2("freetype", dynamic_link_opts); + lib.linkSystemLibrary2("freetype2", dynamic_link_opts); } else { const freetype = b.dependency("freetype", .{ .target = target, diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index bded32172..b5c5c3c1e 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -76,7 +76,8 @@ pub fn build(b: *std.Build) !void { }); if (b.systemIntegrationOption("freetype", .{})) { - lib.linkSystemLibrary2("freetype", dynamic_link_opts); + lib.linkSystemLibrary2("freetype2", dynamic_link_opts); + module.linkSystemLibrary("freetype2", dynamic_link_opts); } else { lib.linkLibrary(freetype.artifact("freetype")); module.addIncludePath(freetype.builder.dependency("freetype", .{}).path("include")); From 756755c052f708290eed1322b43afe67f84d86f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 16:45:52 -0400 Subject: [PATCH 08/46] comment --- .../Sources/Features/Terminal/BaseTerminalController.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 03bc40a1f..8acda4ed1 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -152,9 +152,12 @@ class BaseTerminalController: NSWindowController, // screen then we clamp it back to within the screen. guard let window else { return } guard window.isVisible else { return } - guard !window.styleMask.contains(.fullScreen) else { return } - guard let screen = window.screen else { return } + // We ignore fullscreen windows because macOS automatically resizes + // those back to the fullscreen bounds. + guard !window.styleMask.contains(.fullScreen) else { return } + + guard let screen = window.screen else { return } let visibleFrame = screen.visibleFrame var newFrame = window.frame From e0c5bdc53b157a7adb08dcc48528f5af3b053ab0 Mon Sep 17 00:00:00 2001 From: Rick Calixte <10281587+rcalixte@users.noreply.github.com> Date: Sun, 20 Oct 2024 00:03:54 -0400 Subject: [PATCH 09/46] Terminal: Reinitialize screens on scrollback reset When resetting the terminal screen, the memory buffer allocated for the scrollback is now cleared by reinitializing the screen and falling back to the current method if any of the attempts to reinitialize fail. Closes #2464 --- src/terminal/Terminal.zig | 59 +++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 882ef41c0..a2bf6d50e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2621,12 +2621,59 @@ pub fn plainStringUnwrapped(self: *Terminal, alloc: Allocator) ![]const u8 { return try self.screen.dumpStringAllocUnwrapped(alloc, .{ .viewport = .{} }); } -/// Full reset +/// 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. pub fn fullReset(self: *Terminal) void { - // Switch back to primary screen and clear it. We do not restore cursor - // because see the next step... - self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = false }); + // 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; + }; + // If we got here, both new screens were successfully allocated + // and we can deinitialize the old screens. + self.screen.deinit(); + self.secondary_screen.deinit(); + + // 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. @@ -2640,7 +2687,6 @@ pub fn fullReset(self: *Terminal) void { self.modes = .{}; self.flags = .{}; self.tabstops.reset(TABSTOP_INTERVAL); - self.screen.clearSelection(); self.screen.kitty_keyboard = .{}; self.secondary_screen.kitty_keyboard = .{}; self.screen.protected_mode = .off; @@ -2651,9 +2697,6 @@ pub fn fullReset(self: *Terminal) void { .right = self.cols - 1, }; self.previous_char = null; - self.eraseDisplay(.scrollback, false); - self.eraseDisplay(.complete, false); - self.screen.cursorAbsolute(0, 0); self.pwd.clearRetainingCapacity(); self.status_display = .main; } From 569d887de809ab345316b7b8906ee79d2921dcde Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 18:03:01 -0400 Subject: [PATCH 10/46] core: show mouse whenever focus state changes on surface Related to #2525 --- src/Surface.zig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 2797f647f..5c06ed985 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -130,6 +130,11 @@ config: DerivedConfig, /// This is used to determine if we need to confirm, hold open, etc. child_exited: bool = false, +/// We maintain our focus state and assume we're focused by default. +/// If we're not initially focused then apprts can call focusCallback +/// to let us know. +focused: bool = true, + /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key /// input can be forwarded to the OS for further processing if it @@ -1988,6 +1993,10 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { crash.sentry.thread_state = self.crashThreadState(); defer crash.sentry.thread_state = null; + // If our focus state is the same we do nothing. + if (self.focused == focused) return; + self.focused = focused; + // Notify our render thread of the new state _ = self.renderer_thread.mailbox.push(.{ .focus = focused, @@ -2044,6 +2053,12 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { // Schedule render which also drains our mailbox try self.queueRender(); + // Whenever our focus changes we unhide the mouse. The mouse will be + // hidden again if the user starts typing. This helps alleviate some + // buggy behavior upstream in macOS with the mouse never becoming visible + // again when tabbing between programs (see #2525). + self.showMouse(); + // Update the focus state and notify the terminal { self.renderer_state.mutex.lock(); From e64b231248f68b2fd1e19d538d243b886d5284ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 20:25:40 -0400 Subject: [PATCH 11/46] macos: setup colorspace in base terminal controller Fixes #2519 This sets up the colorspace for terminal windows in the base controller. This also modifies some of our logic so its easier for subclasses of base controllers to specify custom logic when the configuration reloads, since that's likely to be a common thing. --- macos/Sources/App/macOS/AppDelegate.swift | 5 -- .../QuickTerminalController.swift | 43 +--------- .../Terminal/BaseTerminalController.swift | 72 ++++++++++++++++- .../Terminal/TerminalController.swift | 80 +++++++------------ macos/Sources/Ghostty/Ghostty.Config.swift | 16 ++-- 5 files changed, 111 insertions(+), 105 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 8179e1950..2d8314a7f 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -496,11 +496,6 @@ class AppDelegate: NSObject, // AppKit mutex on the appearance. DispatchQueue.main.async { self.syncAppearance() } - // Update all of our windows - terminalManager.windows.forEach { window in - window.controller.configDidReload() - } - // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance c.errors = state.config.errors diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index c382a62a0..82052eabb 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -33,11 +33,6 @@ class QuickTerminalController: BaseTerminalController { selector: #selector(onToggleFullscreen), name: Ghostty.Notification.ghosttyToggleFullscreen, object: nil) - center.addObserver( - self, - selector: #selector(ghosttyDidReloadConfig), - name: Ghostty.Notification.ghosttyDidReloadConfig, - object: nil) } required init?(coder: NSCoder) { @@ -53,6 +48,8 @@ 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 @@ -63,9 +60,6 @@ class QuickTerminalController: BaseTerminalController { // make this restorable, but it isn't currently implemented. window.isRestorable = false - // Setup our configured appearance that we support. - syncAppearance() - // Setup our initial size based on our configured position position.setLoaded(window) @@ -297,35 +291,6 @@ class QuickTerminalController: BaseTerminalController { }) } - private func syncAppearance() { - guard let window else { return } - - // If our window is not visible, then delay this. This is possible specifically - // during state restoration but probably in other scenarios as well. To delay, - // we just loop directly on the dispatch queue. We have to delay because some - // APIs such as window blur have no effect unless the window is visible. - guard window.isVisible else { - // Weak window so that if the window changes or is destroyed we aren't holding a ref - DispatchQueue.main.async { [weak self] in self?.syncAppearance() } - return - } - - // If we have window transparency then set it transparent. Otherwise set it opaque. - if (ghostty.config.backgroundOpacity < 1) { - window.isOpaque = false - - // This is weird, but we don't use ".clear" because this creates a look that - // matches Terminal.app much more closer. This lets users transition from - // Terminal.app more easily. - window.backgroundColor = .white.withAlphaComponent(0.001) - - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) - } else { - window.isOpaque = true - window.backgroundColor = .windowBackgroundColor - } - } - // MARK: First Responder @IBAction override func closeWindow(_ sender: Any) { @@ -357,10 +322,6 @@ class QuickTerminalController: BaseTerminalController { // We ignore the requested mode and always use non-native for the quick terminal toggleFullscreen(mode: .nonNative) } - - @objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) { - syncAppearance() - } } extension Notification.Name { diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 8acda4ed1..71a239710 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -93,6 +93,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(didChangeScreenParametersNotification), name: NSApplication.didChangeScreenParametersNotification, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidReloadConfigNotification), + name: Ghostty.Notification.ghosttyDidReloadConfig, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -123,7 +128,7 @@ class BaseTerminalController: NSWindowController, /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// what surface is focused. This must be called whenever a surface OR window changes focus. - func syncFocusToSurfaceTree() { + private func syncFocusToSurfaceTree() { guard let tree = self.surfaceTree else { return } for leaf in tree { @@ -136,6 +141,48 @@ class BaseTerminalController: NSWindowController, } } + // Call this whenever we want to setup our appearance parameters based on + // configuration changes. + private func syncAppearance() { + guard let window else { return } + + // If our window is not visible, then delay this. This is possible specifically + // during state restoration but probably in other scenarios as well. To delay, + // we just loop directly on the dispatch queue. We have to delay because some + // APIs such as window blur have no effect unless the window is visible. + guard window.isVisible else { + // Weak window so that if the window changes or is destroyed we aren't holding a ref + DispatchQueue.main.async { [weak self] in self?.syncAppearance() } + return + } + + // If we have window transparency then set it transparent. Otherwise set it opaque. + if (ghostty.config.backgroundOpacity < 1) { + window.isOpaque = false + + // This is weird, but we don't use ".clear" because this creates a look that + // matches Terminal.app much more closer. This lets users transition from + // Terminal.app more easily. + window.backgroundColor = .white.withAlphaComponent(0.001) + + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + } else { + window.isOpaque = true + window.backgroundColor = .windowBackgroundColor + } + + // Terminals typically operate in sRGB color space and macOS defaults + // to "native" which is typically P3. There is a lot more resources + // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 + // Ghostty defaults to sRGB but this can be overridden. + switch (ghostty.config.windowColorspace) { + case .displayP3: + window.colorSpace = .displayP3 + case .srgb: + window.colorSpace = .sRGB + } + } + // Call this whenever the frame changes private func windowFrameDidChange() { // We need to update our saved frame information in case of monitor @@ -145,6 +192,14 @@ class BaseTerminalController: NSWindowController, savedFrame = .init(window: window.frame, screen: screen.visibleFrame) } + // MARK: Overridable Callbacks + + /// Called whenever Ghostty reloads the configuration. Callers should call super. + open func ghosttyDidReloadConfig() { + // Whenever the config changes we setup our appearance. + syncAppearance() + } + // MARK: Notifications @objc private func didChangeScreenParametersNotification(_ notification: Notification) { @@ -191,6 +246,10 @@ class BaseTerminalController: NSWindowController, window.setFrame(newFrame, display: true) } + @objc private func ghosttyDidReloadConfigNotification(notification: SwiftUI.Notification) { + ghosttyDidReloadConfig() + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -381,7 +440,16 @@ class BaseTerminalController: NSWindowController, } } - //MARK: - NSWindowDelegate + // MARK: NSWindowController + + override func windowDidLoad() { + super.windowDidLoad() + + // Setup our configured appearance that we support. + syncAppearance() + } + + // 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 f31740105..f0216d520 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -78,14 +78,16 @@ class TerminalController: BaseTerminalController { } } - //MARK: - Methods + override func ghosttyDidReloadConfig() { + super.ghosttyDidReloadConfig() - func configDidReload() { guard let window = window as? TerminalWindow else { return } window.focusFollowsMouse = ghostty.config.focusFollowsMouse syncAppearance() } + //MARK: - Methods + /// Update the accessory view of each tab according to the keyboard /// shortcut that activates it (if any). This is called when the key window /// changes, when a window is closed, and when tabs are reordered @@ -164,21 +166,6 @@ class TerminalController: BaseTerminalController { window.titlebarFont = nil } - // If we have window transparency then set it transparent. Otherwise set it opaque. - if (ghostty.config.backgroundOpacity < 1) { - window.isOpaque = false - - // This is weird, but we don't use ".clear" because this creates a look that - // matches Terminal.app much more closer. This lets users transition from - // Terminal.app more easily. - window.backgroundColor = .white.withAlphaComponent(0.001) - - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) - } else { - window.isOpaque = true - window.backgroundColor = .windowBackgroundColor - } - window.hasShadow = ghostty.config.macosWindowShadow guard window.hasStyledTabs else { return } @@ -208,31 +195,20 @@ class TerminalController: BaseTerminalController { } override func windowDidLoad() { + super.windowDidLoad() + guard let window = window as? TerminalWindow else { return } - + // Setting all three of these is required for restoration to work. window.isRestorable = restorable if (restorable) { window.restorationClass = TerminalWindowRestoration.self window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } - + // If window decorations are disabled, remove our title if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) } - - // Terminals typically operate in sRGB color space and macOS defaults - // to "native" which is typically P3. There is a lot more resources - // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 - // Ghostty defaults to sRGB but this can be overridden. - switch (ghostty.config.windowColorspace) { - case "display-p3": - window.colorSpace = .displayP3 - case "srgb": - fallthrough - default: - window.colorSpace = .sRGB - } - + // If we have only a single surface (no splits) and that surface requested // an initial size then we set it here now. if case let .leaf(leaf) = surfaceTree { @@ -245,21 +221,21 @@ class TerminalController: BaseTerminalController { frame.size.height -= leaf.surface.frame.size.height frame.size.width += min(initialSize.width, screen.frame.width) frame.size.height += min(initialSize.height, screen.frame.height) - + // We have no tabs and we are not a split, so set the initial size of the window. window.setFrame(frame, display: true) } } - + // Center the window to start, we'll move the window frame automatically // when cascading. window.center() - + // Make sure our theme is set on the window so styling is correct. if let windowTheme = ghostty.config.windowTheme { window.windowTheme = .init(rawValue: windowTheme) } - + // Handle titlebar tabs config option. Something about what we do while setting up the // titlebar tabs interferes with the window restore process unless window.tabbingMode // is set to .preferred, so we set it, and switch back to automatic as soon as we can. @@ -272,50 +248,50 @@ class TerminalController: BaseTerminalController { } else if (ghostty.config.macosTitlebarStyle == "transparent") { window.transparentTabs = true } - + if window.hasStyledTabs { // Set the background color of the window let backgroundColor = NSColor(ghostty.config.backgroundColor) window.backgroundColor = backgroundColor - + // This makes sure our titlebar renders correctly when there is a transparent background window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) } - + // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self )) - + // If our titlebar style is "hidden" we adjust the style appropriately if (ghostty.config.macosTitlebarStyle == "hidden") { window.styleMask = [ // We need `titled` in the mask to get the normal window frame .titled, - + // Full size content view so we can extend // content in to the hidden titlebar's area - .fullSizeContentView, - - .resizable, + .fullSizeContentView, + + .resizable, .closable, .miniaturizable, ] - + // Hide the title window.titleVisibility = .hidden window.titlebarAppearsTransparent = true - + // Hide the traffic lights (window control buttons) window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true - + // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. window.tabbingMode = .disallowed - + // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are // some operations that appear to bring back the titlebar visibility so this ensures // it is gone forever. @@ -324,7 +300,7 @@ class TerminalController: BaseTerminalController { titleBarContainer.isHidden = true } } - + // In various situations, macOS automatically tabs new windows. Ghostty handles // its own tabbing so we DONT want this behavior. This detects this scenario and undoes // it. @@ -344,9 +320,9 @@ class TerminalController: BaseTerminalController { window.tabGroup?.removeWindow(window) } } - + window.focusFollowsMouse = ghostty.config.focusFollowsMouse - + // Apply any additional appearance-related properties to the new window. syncAppearance() } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index b8c7d2594..f8375adf1 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -128,13 +128,14 @@ extension Ghostty { return v } - var windowColorspace: String { - guard let config = self.config else { return "" } + var windowColorspace: WindowColorspace { + guard let config = self.config else { return .srgb } var v: UnsafePointer? = nil let key = "window-colorspace" - guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } - guard let ptr = v else { return "" } - return String(cString: ptr) + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .srgb } + guard let ptr = v else { return .srgb } + let str = String(cString: ptr) + return WindowColorspace(rawValue: str) ?? .srgb } var windowSaveState: String { @@ -474,4 +475,9 @@ extension Ghostty.Config { } } } + + enum WindowColorspace : String { + case srgb + case displayP3 = "display-p3" + } } From c97c0858bedf7f21af2175e9ddd1be38aa3bfe4b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 20:44:47 -0400 Subject: [PATCH 12/46] macos: rectangle select only requires option + drag Fixes #2537 This matches Terminal.app. iTerm2 requires cmd+option (our old behavior). Kitty doesn't seem to support rectangle select or I couldn't figure out how to make it work. WezTerm matches Terminal.app too. Outside of terminal emulators, this is also the rectangular select binding for neovim. --- .../Features/Terminal/BaseTerminalController.swift | 2 +- src/Surface.zig | 4 ++-- src/surface_mouse.zig | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 8acda4ed1..000d72418 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -146,7 +146,7 @@ class BaseTerminalController: NSWindowController, } // MARK: Notifications - + @objc private func didChangeScreenParametersNotification(_ notification: Notification) { // If we have a window that is visible and it is outside the bounds of the // screen then we clamp it back to within the screen. diff --git a/src/Surface.zig b/src/Surface.zig index 5c06ed985..9521e34dc 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3468,7 +3468,7 @@ fn dragLeftClickSingle( try self.setSelection(if (selected) terminal.Selection.init( drag_pin, drag_pin, - self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt, + SurfaceMouse.isRectangleSelectState(self.mouse.mods), ) else null); return; @@ -3503,7 +3503,7 @@ fn dragLeftClickSingle( try self.setSelection(terminal.Selection.init( start, drag_pin, - self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt, + SurfaceMouse.isRectangleSelectState(self.mouse.mods), )); return; } diff --git a/src/surface_mouse.zig b/src/surface_mouse.zig index 2ba88540c..cc1643a88 100644 --- a/src/surface_mouse.zig +++ b/src/surface_mouse.zig @@ -113,14 +113,19 @@ fn eligibleMouseShapeKeyEvent(physical_key: input.Key) bool { physical_key.leftOrRightAlt(); } -fn isRectangleSelectState(mods: input.Mods) bool { - return mods.ctrlOrSuper() and mods.alt; -} - fn isMouseModeOverrideState(mods: input.Mods) bool { return mods.shift; } +/// Returns true if our modifiers put us in a state where dragging +/// should cause a rectangle select. +pub fn isRectangleSelectState(mods: input.Mods) bool { + return if (comptime builtin.target.isDarwin()) + mods.alt + else + mods.ctrlOrSuper() and mods.alt; +} + test "keyToMouseShape" { const testing = std.testing; From b11b5871e94b732a7bf83000fb6898f07c09a31b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 20:56:38 -0400 Subject: [PATCH 13/46] cli: do not parse actions (+command) after -e Fixes #2506 --- src/cli/action.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/cli/action.zig b/src/cli/action.zig index 950577158..1da0c0609 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -71,6 +71,13 @@ pub const Action = enum { var pending_help: bool = false; var pending: ?Action = null; while (iter.next()) |arg| { + // If we see a "-e" and we haven't seen a command yet, then + // we are done looking for commands. This special case enables + // `ghostty -e ghostty +command`. If we've seen a command we + // still want to keep looking because + // `ghostty +command -e +command` is invalid. + if (std.mem.eql(u8, arg, "-e") and pending == null) return null; + // Special case, --version always outputs the version no // matter what, no matter what other args exist. if (std.mem.eql(u8, arg, "--version")) return .version; @@ -240,3 +247,30 @@ test "parse action plus" { try testing.expect(action.? == .version); } } + +test "parse action plus ignores -e" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 -e +version", + ); + defer iter.deinit(); + const action = try Action.detectIter(&iter); + try testing.expect(action == null); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+list-fonts --a=42 -e +version", + ); + defer iter.deinit(); + try testing.expectError( + Action.Error.MultipleActions, + Action.detectIter(&iter), + ); + } +} From 30e95e4b9a20880be0957395c803fdcfe18fb628 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 31 Oct 2024 09:28:08 -0700 Subject: [PATCH 14/46] Revert "macos: setup colorspace in base terminal controller" This reverts commit e64b231248f68b2fd1e19d538d243b886d5284ff. --- macos/Sources/App/macOS/AppDelegate.swift | 5 ++ .../QuickTerminalController.swift | 43 +++++++++- .../Terminal/BaseTerminalController.swift | 72 +---------------- .../Terminal/TerminalController.swift | 80 ++++++++++++------- macos/Sources/Ghostty/Ghostty.Config.swift | 16 ++-- 5 files changed, 105 insertions(+), 111 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 2d8314a7f..8179e1950 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -496,6 +496,11 @@ class AppDelegate: NSObject, // AppKit mutex on the appearance. DispatchQueue.main.async { self.syncAppearance() } + // Update all of our windows + terminalManager.windows.forEach { window in + window.controller.configDidReload() + } + // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance c.errors = state.config.errors diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 82052eabb..c382a62a0 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -33,6 +33,11 @@ class QuickTerminalController: BaseTerminalController { selector: #selector(onToggleFullscreen), name: Ghostty.Notification.ghosttyToggleFullscreen, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidReloadConfig), + name: Ghostty.Notification.ghosttyDidReloadConfig, + object: nil) } required init?(coder: NSCoder) { @@ -48,8 +53,6 @@ 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 @@ -60,6 +63,9 @@ class QuickTerminalController: BaseTerminalController { // make this restorable, but it isn't currently implemented. window.isRestorable = false + // Setup our configured appearance that we support. + syncAppearance() + // Setup our initial size based on our configured position position.setLoaded(window) @@ -291,6 +297,35 @@ class QuickTerminalController: BaseTerminalController { }) } + private func syncAppearance() { + guard let window else { return } + + // If our window is not visible, then delay this. This is possible specifically + // during state restoration but probably in other scenarios as well. To delay, + // we just loop directly on the dispatch queue. We have to delay because some + // APIs such as window blur have no effect unless the window is visible. + guard window.isVisible else { + // Weak window so that if the window changes or is destroyed we aren't holding a ref + DispatchQueue.main.async { [weak self] in self?.syncAppearance() } + return + } + + // If we have window transparency then set it transparent. Otherwise set it opaque. + if (ghostty.config.backgroundOpacity < 1) { + window.isOpaque = false + + // This is weird, but we don't use ".clear" because this creates a look that + // matches Terminal.app much more closer. This lets users transition from + // Terminal.app more easily. + window.backgroundColor = .white.withAlphaComponent(0.001) + + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + } else { + window.isOpaque = true + window.backgroundColor = .windowBackgroundColor + } + } + // MARK: First Responder @IBAction override func closeWindow(_ sender: Any) { @@ -322,6 +357,10 @@ class QuickTerminalController: BaseTerminalController { // We ignore the requested mode and always use non-native for the quick terminal toggleFullscreen(mode: .nonNative) } + + @objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) { + syncAppearance() + } } extension Notification.Name { diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 95c8d637d..000d72418 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -93,11 +93,6 @@ class BaseTerminalController: NSWindowController, selector: #selector(didChangeScreenParametersNotification), name: NSApplication.didChangeScreenParametersNotification, object: nil) - center.addObserver( - self, - selector: #selector(ghosttyDidReloadConfigNotification), - name: Ghostty.Notification.ghosttyDidReloadConfig, - object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -128,7 +123,7 @@ class BaseTerminalController: NSWindowController, /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// what surface is focused. This must be called whenever a surface OR window changes focus. - private func syncFocusToSurfaceTree() { + func syncFocusToSurfaceTree() { guard let tree = self.surfaceTree else { return } for leaf in tree { @@ -141,48 +136,6 @@ class BaseTerminalController: NSWindowController, } } - // Call this whenever we want to setup our appearance parameters based on - // configuration changes. - private func syncAppearance() { - guard let window else { return } - - // If our window is not visible, then delay this. This is possible specifically - // during state restoration but probably in other scenarios as well. To delay, - // we just loop directly on the dispatch queue. We have to delay because some - // APIs such as window blur have no effect unless the window is visible. - guard window.isVisible else { - // Weak window so that if the window changes or is destroyed we aren't holding a ref - DispatchQueue.main.async { [weak self] in self?.syncAppearance() } - return - } - - // If we have window transparency then set it transparent. Otherwise set it opaque. - if (ghostty.config.backgroundOpacity < 1) { - window.isOpaque = false - - // This is weird, but we don't use ".clear" because this creates a look that - // matches Terminal.app much more closer. This lets users transition from - // Terminal.app more easily. - window.backgroundColor = .white.withAlphaComponent(0.001) - - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) - } else { - window.isOpaque = true - window.backgroundColor = .windowBackgroundColor - } - - // Terminals typically operate in sRGB color space and macOS defaults - // to "native" which is typically P3. There is a lot more resources - // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 - // Ghostty defaults to sRGB but this can be overridden. - switch (ghostty.config.windowColorspace) { - case .displayP3: - window.colorSpace = .displayP3 - case .srgb: - window.colorSpace = .sRGB - } - } - // Call this whenever the frame changes private func windowFrameDidChange() { // We need to update our saved frame information in case of monitor @@ -192,14 +145,6 @@ class BaseTerminalController: NSWindowController, savedFrame = .init(window: window.frame, screen: screen.visibleFrame) } - // MARK: Overridable Callbacks - - /// Called whenever Ghostty reloads the configuration. Callers should call super. - open func ghosttyDidReloadConfig() { - // Whenever the config changes we setup our appearance. - syncAppearance() - } - // MARK: Notifications @objc private func didChangeScreenParametersNotification(_ notification: Notification) { @@ -246,10 +191,6 @@ class BaseTerminalController: NSWindowController, window.setFrame(newFrame, display: true) } - @objc private func ghosttyDidReloadConfigNotification(notification: SwiftUI.Notification) { - ghosttyDidReloadConfig() - } - // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -440,16 +381,7 @@ class BaseTerminalController: NSWindowController, } } - // MARK: NSWindowController - - override func windowDidLoad() { - super.windowDidLoad() - - // Setup our configured appearance that we support. - syncAppearance() - } - - // MARK: NSWindowDelegate + //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 f0216d520..f31740105 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -78,16 +78,14 @@ class TerminalController: BaseTerminalController { } } - override func ghosttyDidReloadConfig() { - super.ghosttyDidReloadConfig() + //MARK: - Methods + func configDidReload() { guard let window = window as? TerminalWindow else { return } window.focusFollowsMouse = ghostty.config.focusFollowsMouse syncAppearance() } - //MARK: - Methods - /// Update the accessory view of each tab according to the keyboard /// shortcut that activates it (if any). This is called when the key window /// changes, when a window is closed, and when tabs are reordered @@ -166,6 +164,21 @@ class TerminalController: BaseTerminalController { window.titlebarFont = nil } + // If we have window transparency then set it transparent. Otherwise set it opaque. + if (ghostty.config.backgroundOpacity < 1) { + window.isOpaque = false + + // This is weird, but we don't use ".clear" because this creates a look that + // matches Terminal.app much more closer. This lets users transition from + // Terminal.app more easily. + window.backgroundColor = .white.withAlphaComponent(0.001) + + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + } else { + window.isOpaque = true + window.backgroundColor = .windowBackgroundColor + } + window.hasShadow = ghostty.config.macosWindowShadow guard window.hasStyledTabs else { return } @@ -195,20 +208,31 @@ class TerminalController: BaseTerminalController { } override func windowDidLoad() { - super.windowDidLoad() - guard let window = window as? TerminalWindow else { return } - + // Setting all three of these is required for restoration to work. window.isRestorable = restorable if (restorable) { window.restorationClass = TerminalWindowRestoration.self window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } - + // If window decorations are disabled, remove our title if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) } - + + // Terminals typically operate in sRGB color space and macOS defaults + // to "native" which is typically P3. There is a lot more resources + // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 + // Ghostty defaults to sRGB but this can be overridden. + switch (ghostty.config.windowColorspace) { + case "display-p3": + window.colorSpace = .displayP3 + case "srgb": + fallthrough + default: + window.colorSpace = .sRGB + } + // If we have only a single surface (no splits) and that surface requested // an initial size then we set it here now. if case let .leaf(leaf) = surfaceTree { @@ -221,21 +245,21 @@ class TerminalController: BaseTerminalController { frame.size.height -= leaf.surface.frame.size.height frame.size.width += min(initialSize.width, screen.frame.width) frame.size.height += min(initialSize.height, screen.frame.height) - + // We have no tabs and we are not a split, so set the initial size of the window. window.setFrame(frame, display: true) } } - + // Center the window to start, we'll move the window frame automatically // when cascading. window.center() - + // Make sure our theme is set on the window so styling is correct. if let windowTheme = ghostty.config.windowTheme { window.windowTheme = .init(rawValue: windowTheme) } - + // Handle titlebar tabs config option. Something about what we do while setting up the // titlebar tabs interferes with the window restore process unless window.tabbingMode // is set to .preferred, so we set it, and switch back to automatic as soon as we can. @@ -248,50 +272,50 @@ class TerminalController: BaseTerminalController { } else if (ghostty.config.macosTitlebarStyle == "transparent") { window.transparentTabs = true } - + if window.hasStyledTabs { // Set the background color of the window let backgroundColor = NSColor(ghostty.config.backgroundColor) window.backgroundColor = backgroundColor - + // This makes sure our titlebar renders correctly when there is a transparent background window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) } - + // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self )) - + // If our titlebar style is "hidden" we adjust the style appropriately if (ghostty.config.macosTitlebarStyle == "hidden") { window.styleMask = [ // We need `titled` in the mask to get the normal window frame .titled, - + // Full size content view so we can extend // content in to the hidden titlebar's area - .fullSizeContentView, - - .resizable, + .fullSizeContentView, + + .resizable, .closable, .miniaturizable, ] - + // Hide the title window.titleVisibility = .hidden window.titlebarAppearsTransparent = true - + // Hide the traffic lights (window control buttons) window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true - + // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. window.tabbingMode = .disallowed - + // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are // some operations that appear to bring back the titlebar visibility so this ensures // it is gone forever. @@ -300,7 +324,7 @@ class TerminalController: BaseTerminalController { titleBarContainer.isHidden = true } } - + // In various situations, macOS automatically tabs new windows. Ghostty handles // its own tabbing so we DONT want this behavior. This detects this scenario and undoes // it. @@ -320,9 +344,9 @@ class TerminalController: BaseTerminalController { window.tabGroup?.removeWindow(window) } } - + window.focusFollowsMouse = ghostty.config.focusFollowsMouse - + // Apply any additional appearance-related properties to the new window. syncAppearance() } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index f8375adf1..b8c7d2594 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -128,14 +128,13 @@ extension Ghostty { return v } - var windowColorspace: WindowColorspace { - guard let config = self.config else { return .srgb } + var windowColorspace: String { + guard let config = self.config else { return "" } var v: UnsafePointer? = nil let key = "window-colorspace" - guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .srgb } - guard let ptr = v else { return .srgb } - let str = String(cString: ptr) - return WindowColorspace(rawValue: str) ?? .srgb + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } + guard let ptr = v else { return "" } + return String(cString: ptr) } var windowSaveState: String { @@ -475,9 +474,4 @@ extension Ghostty.Config { } } } - - enum WindowColorspace : String { - case srgb - case displayP3 = "display-p3" - } } From 63b11ceb5e0122e9feb3dac45fea0cf563ed6beb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 31 Oct 2024 09:29:09 -0700 Subject: [PATCH 15/46] macos: quick terminal set colorspace --- .../QuickTerminal/QuickTerminalController.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index c382a62a0..bdd427be0 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -310,6 +310,19 @@ class QuickTerminalController: BaseTerminalController { return } + // Terminals typically operate in sRGB color space and macOS defaults + // to "native" which is typically P3. There is a lot more resources + // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 + // Ghostty defaults to sRGB but this can be overridden. + switch (ghostty.config.windowColorspace) { + case "display-p3": + window.colorSpace = .displayP3 + case "srgb": + fallthrough + default: + window.colorSpace = .sRGB + } + // If we have window transparency then set it transparent. Otherwise set it opaque. if (ghostty.config.backgroundOpacity < 1) { window.isOpaque = false From b56cb7038a1ce15cd74f1bc6e6d05cc7104ce190 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 31 Oct 2024 09:34:37 -0700 Subject: [PATCH 16/46] core: only do cursor click to move without a mouse selection --- src/Surface.zig | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 9521e34dc..d19f9e812 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2744,12 +2744,20 @@ pub fn mouseButtonCallback( } // For left button click release we check if we are moving our cursor. - if (button == .left and action == .release and mods.alt) click_move: { + if (button == .left and + action == .release and + mods.alt) + click_move: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // If we have a selection then we do not do click to move because + // it means that we moved our cursor while pressing the mouse button. + if (self.io.terminal.screen.selection != null) break :click_move; + // Moving always resets the click count so that we don't highlight. self.mouse.left_click_count = 0; const pin = self.mouse.left_click_pin orelse break :click_move; - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); try self.clickMoveCursor(pin.*); return true; } From 24f505048454ca8ed861c419bdf7c52646a35bb4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 31 Oct 2024 17:50:52 +0100 Subject: [PATCH 17/46] apprt: also send color_change notifications when colors are reset --- src/termio/stream_handler.zig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 5fe2e95e3..e60f4f137 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1253,6 +1253,11 @@ pub const StreamHandler = struct { self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; mask.unset(i); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .{ .palette = @intCast(i) }, + .color = self.terminal.color_palette.colors[i], + } }); } } else { var it = std.mem.tokenizeScalar(u8, value, ';'); @@ -1263,6 +1268,11 @@ pub const StreamHandler = struct { self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; mask.unset(i); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .{ .palette = @intCast(i) }, + .color = self.terminal.color_palette.colors[i], + } }); } } } @@ -1272,18 +1282,35 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(.{ .foreground_color = self.foreground_color, }, .{ .forever = {} }); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .foreground, + .color = self.foreground_color, + } }); }, .background => { self.background_color = self.default_background_color; _ = self.renderer_mailbox.push(.{ .background_color = self.background_color, }, .{ .forever = {} }); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .background, + .color = self.background_color, + } }); }, .cursor => { self.cursor_color = self.default_cursor_color; _ = self.renderer_mailbox.push(.{ .cursor_color = self.cursor_color, }, .{ .forever = {} }); + + if (self.cursor_color) |color| { + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .cursor, + .color = color, + } }); + } }, } } From e7ccc60ed57f9897f390da42f0d7e06f094d46f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 31 Oct 2024 10:00:20 -0700 Subject: [PATCH 18/46] CONTRIBUTING.md --- CONTRIBUTING.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..af3c30be7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Ghostty Development Process + +This document describes the development process for Ghostty. It is intended for +anyone considering opening an **issue** or **pull request**. If in doubt, +please open a [discussion](https://github.com/ghostty-org/ghostty/discussions); +we can always convert that to an issue later. + +> [!NOTE] +> +> I'm sorry for the wall of text. I'm not trying to be difficult and I do +> appreciate your contributions. Ghostty is a personal project for me that +> I maintain in my free time. If you're expecting me to dedicate my personal +> time to fixing bugs, maintaining features, and reviewing code, I do kindly +> ask you spend a few minutes reading this document. Thank you. ❤️ + +## Quick Guide + +**I'd like to contribute!** + +All issues are actionable. Pick one and start working on it. Thank you. +If you need help or guidance, comment on the issue. Issues that are extra +friendly to new contributors are tagged with "contributor friendly". + +**I have a bug!** + +1. Search the issue tracker and discussions for similar issues. +2. If you don't have steps to reproduce, open a discussion. +3. If you have steps to reproduce, open an issue. + +**I have an idea for a feature!** + +1. Open a discussion. + +**I've implemented a feature!** + +1. If there is an issue for the feature, open a pull request. +2. If there is no issue, open a discussion and link to your branch. +3. If you want to live dangerously, open a pull request and hope for the best. + +**I have a question!** + +1. Open a discussion or use Discord. + +## General Patterns + +### Issues are Actionable + +The Ghostty [issue tracker](https://github.com/ghostty-org/ghostty/issues) +is for _actionable items_. + +Unlike some other projects, Ghostty **does not use the issue tracker for +discussion or feature requests**. Instead, we use GitHub +[discussions](https://github.com/ghostty-org/ghostty/discussions) for that. +Once a discussion reaches a point where a well-understood, actionable +item is identified, it is moved to the issue tracker. **This pattern +makes it easier for maintainers or contributors to find issues to work on +since _every issue_ is ready to be worked on.** + +If you are experiencing a bug and have clear steps to reproduce it, please +open an issue. If you are experiencing a bug but you are not sure how to +reproduce it or aren't sure if it's a bug, please open a discussion. +If you have an idea for a feature, please open a discussion. + +### Pull Requests Implement an Issue + +Pull requests should be associated with a previously accepted issue. +**If you open a pull request for something that wasn't previously discussed,** +it may be closed or remain stale for an indefinite period of time. I'm not +saying it will never be accepted, but the odds are stacked against you. + +Issues tagged with "feature" represent accepted, well-scoped feature requests. +If you implement an issue tagged with feature as described in the issue, your +pull request will be accepted with a high degree of certainty. + +> [!NOTE] +> +> **Pull requests are NOT a place to discuss feature design.** Please do +> not open a WIP pull request to discuss a feature. Instead, use a discussion +> and link to your branch. From 3a3da82aa98513de45fbc1abd081ea93d420c7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 31 Oct 2024 18:09:36 -0400 Subject: [PATCH 19/46] Update explanation for number of digits in port number The explanation now refers to RFC 793 instead of just claiming some arbitrary value as truth. The previous value was correct, but now there is a proper source for the correct value. --- src/termio/stream_handler.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 00762bc73..bd2017f89 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1071,7 +1071,9 @@ pub const StreamHandler = struct { // The URI will be parsed as if the last set o digit is a port, so we need to // make sure that part is included when it's set. if (uri.port) |port| { - // 65_535 is considered the highest port number on Linux. + // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent + // the maximum since 2^16 - 1 = 65_535. + // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. const PORT_NUMBER_MAX_DIGITS = 5; // Make sure there is space for a max length hostname + the max number of digits. From 3b0a34afbca40694b3b170b7f1cb681e9454435a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 31 Oct 2024 21:55:02 -0400 Subject: [PATCH 20/46] Extract OSC 7 hostname parsing into helper functions --- src/termio/stream_handler.zig | 111 ++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index bd2017f89..8db67f66c 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1030,6 +1030,48 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } + pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { + // Get the raw string of the URI. Its unclear to me if the various + // tags of this enum guarantee no percent-encoding so we just + // check all of it. This isn't a performance critical path. + const host_component = uri.host orelse return error.NoHostnameInUri; + const host = switch (host_component) { + .raw => |v| v, + .percent_encoded => |v| v, + }; + + // When the "Private Wi-Fi address" setting is toggled on macOS the hostname + // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. + // The URI will be parsed as if the last set o digit is a port, so we need to + // make sure that part is included when it's set. + if (uri.port) |port| { + var fbs = std.io.fixedBufferStream(buf); + std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { + error.NoSpaceLeft => return error.NoSpaceLeft, + else => unreachable, + }; + + return fbs.getWritten(); + } + + return host; + } + + pub fn isLocalHostname(hostname: []const u8) !bool { + // A 'localhost' hostname is always considered local. + if (std.mem.eql(u8, "localhost", hostname)) { + return true; + } + + // If hostname is not "localhost" it must match our hostname. + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const ourHostname = posix.gethostname(&buf) catch |err| { + return err; + }; + + return std.mem.eql(u8, hostname, ourHostname); + } + pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { if (builtin.os.tag == .windows) { log.warn("reportPwd unimplemented on windows", .{}); @@ -1048,57 +1090,34 @@ pub const StreamHandler = struct { return; } + // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent + // the maximum since 2^16 - 1 = 65_535. + // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. + const PORT_NUMBER_MAX_DIGITS = 5; + // Make sure there is space for a max length hostname + the max number of digits. + var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; + + const hostname = bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { + error.NoHostnameInUri => { + log.warn("OSC 7 uri must contain a hostname: {}", .{err}); + return; + }, + error.NoSpaceLeft => |e| { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{e}); + return; + }, + }; + // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = host_valid: { - const host_component = uri.host orelse break :host_valid false; - - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const host = host: { - const h = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - if (h.len == 0 or std.mem.eql(u8, "localhost", h)) { - break :host_valid true; - } - - // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. - // The URI will be parsed as if the last set o digit is a port, so we need to - // make sure that part is included when it's set. - if (uri.port) |port| { - // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent - // the maximum since 2^16 - 1 = 65_535. - // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. - const PORT_NUMBER_MAX_DIGITS = 5; - - // Make sure there is space for a max length hostname + the max number of digits. - var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - - const host_and_port = std.fmt.bufPrint(&host_and_port_buf, "{s}:{d}", .{ h, port }) catch |err| { - log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); - break :host_valid false; - }; - - break :host host_and_port; - } else { - break :host h; - } - }; - - // Otherwise, it must match our hostname. - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const hostname = posix.gethostname(&buf) catch |err| { + const host_valid = isLocalHostname(hostname) catch |err| switch (err) { + error.PermissionDenied, error.Unexpected => { log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); - break :host_valid false; - }; - - break :host_valid std.mem.eql(u8, host, hostname); + return; + }, }; + if (!host_valid) { log.warn("OSC 7 host must be local", .{}); return; From 6d8cf5504022fe377b9bc8771910521f16407edd Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 1 Nov 2024 10:29:00 -0500 Subject: [PATCH 21/46] gtk: use correct function to destroy window adw_application_window_destroy and gtk_application_window_destroy do not exist. I believe that this didn't trigger a compile error because the errdefer got compiled out because there are no potential error returns after this code in the function. --- src/apprt/gtk/Window.zig | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 7f3c50789..e220ac03b 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -92,13 +92,9 @@ pub fn init(self: *Window, app: *App) !void { break :window window; } }; + errdefer c.gtk_window_destroy(@ptrCast(window)); const gtk_window: *c.GtkWindow = @ptrCast(window); - errdefer if (self.isAdwWindow()) { - c.adw_application_window_destroy(window); - } else { - c.gtk_application_window_destroy(gtk_window); - }; self.window = gtk_window; c.gtk_window_set_title(gtk_window, "Ghostty"); c.gtk_window_set_default_size(gtk_window, 1000, 600); From 9262cc57049a8eb20fcbabad799242afd8f4a638 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 2 Nov 2024 10:14:08 -0700 Subject: [PATCH 22/46] macos: restore window frame on cascadeTopLeft since macOS 15 moves it Fixes #2565 This appears to be a bug in macOS 15. Specifically on macOS 15 when the new native window snapping feature is used, `cascadeTopLeft(from: zero)` will move the window frame back to its prior unsnapped position. The docs for `cascadeTopLeft(from:)` explicitly say: > When NSZeroPoint, the window is not moved, except as needed to constrain > to the visible screen This is not the behavior we are seeing on macOS 15. The window is on the visible screen, we're using NSZeroPoint, and yet the window is still being moved. This does not happen on macOS 14 (but its hard to say exactly because macOS 14 didn't have window snapping). This commit works around the issue by saving the window frame before calling `cascadeTopLeft(from: zero)` and then restoring it afterwards if it has changed. I've also filed a radar with Apple for this issue. --- .../Sources/Features/Terminal/TerminalManager.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index f71e198ee..0766f33b0 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -227,7 +227,19 @@ class TerminalManager { // are closing a tabbed window, we want to set the cascade point to be // the next cascade point from this window. if focusedWindow != controller.window { + // The cascadeTopLeft call below should NOT move the window. Starting with + // macOS 15, we found that specifically when used with the new window snapping + // features of macOS 15, this WOULD move the frame. So we keep track of the + // old frame and restore it if necessary. Issue: + // https://github.com/ghostty-org/ghostty/issues/2565 + let oldFrame = focusedWindow.frame + Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) + + if focusedWindow.frame != oldFrame { + focusedWindow.setFrame(oldFrame, display: true) + } + return } From f04b6c8768cd49c7ce2c9a9d94a696bf71a466a7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 3 Nov 2024 09:51:13 -0800 Subject: [PATCH 23/46] font/harfbuzz: force LTR font shaping Fixes #2570 Related to #1740 See #1740 for details. --- src/font/embedded.zig | 1 + src/font/res/KawkabMono-Regular.ttf | Bin 0 -> 265316 bytes src/font/shaper/harfbuzz.zig | 47 ++++++++++++++++++++++++++++ src/font/shaper/testdata/arabic.txt | 3 ++ 4 files changed, 51 insertions(+) create mode 100644 src/font/res/KawkabMono-Regular.ttf create mode 100644 src/font/shaper/testdata/arabic.txt diff --git a/src/font/embedded.zig b/src/font/embedded.zig index 2f496f86a..098aa3eb4 100644 --- a/src/font/embedded.zig +++ b/src/font/embedded.zig @@ -14,6 +14,7 @@ pub const emoji = @embedFile("res/NotoColorEmoji.ttf"); pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf"); /// Fonts with general properties +pub const arabic = @embedFile("res/KawkabMono-Regular.ttf"); pub const variable = @embedFile("res/Lilex-VF.ttf"); /// Font with nerd fonts embedded. diff --git a/src/font/res/KawkabMono-Regular.ttf b/src/font/res/KawkabMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4841678dec04e7e5d8e2f2b39e6a94dd1d4a7781 GIT binary patch literal 265316 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

YfYHlz}Xg8BiC4T(gwUd>Y8l!=R$ z-FWcNs%GKBr6+Iv)yunf>EOw+OQWXmvRPZU=^%JKGhxE0e+D;Rdm-1tt}blr8fEKQ zY3~SdyzR`lgkw)qu>ScNj5H=^G6R(7susRiM!Vjch|)PqKFKtu@u-x2!sRc2`&U`&_T=yLi04%qQ$F=ei)}`&rKV2A|maR--x7P+e78;<`@1 zzS>c)Zh`A4$5nhqh*bl-TGG^f|NUg^qpxg*@tlERY6@?PuiJ$={b8Y=6C$eHVtv}fOO z?R=@be2;CXW_FG~pabL}ilLbrJN%h0y{*+K$H@-=++2m+Xm0<0eIf=qD#zonw`1kl zfREOz#J!Jx`>y9y)4qLs7c9`hi;q3_eH|QK^yNc`u0P5n|Io~gu)mjwK{z2Z+9-HA z+KDg)-gWKIiT=(dA5GpJ8-QDMQr{3!B}myAbyNDpsUDxX^7899ZaJgW$$RO$=RCMvAI@7gsc~A# zl=4k;&bs{Q^siob=mz&FuVJ9vyG9*X-|e=(ikTAnBpskP>N0#^vUZf_?H;Cs65rMe z$8kGg*IRpsLT|gIP}r&<&i@}JdS!62Iw5#L?F&kE(Eg|P;BW0Z;P2Y+w|anQl&J^a zxAl>7+O1N4-f`vB4^qDWxN^=jDc{$l{6cI0v0O(6@d!45x69Qhbw5wLk>~Dd=YC-S z&XYZRzp3-I?2v|Dc_SVXT7XF zDdpMvd|poHW}oNpT6#hrwf!H%&P=~za11R2Dp-;R4I`wK36HAN6FB9k=+Vb!RUd z6PsB-{p2ybw^U6XcngvR$O^UQNi;IgD0y-tUAmi$6Z}v z-MH5Bjw8;o_j;6bj!F4>$CY!AN%{Wc$~niRd|$Ra_{6q7TYfUna&-SL*Eo5851$WZ z^?-8^hGS7?zEX&2d$FGK@_K7m4N}@MEiuY1hll^X2-fuwptlU3XYG@$0PS5-bt%fR zDw&&CT8mJI1p}>q{4dh9i+_>Tp8C>czAo-t79UvhMbce-)4JhRk(pzid-}Dh+(cfql|*B6V|_uqvA&=_5-*7B2j=lN=g!@zMsJw+ z?cgOfTAy(A1^e44G?sqA^~Oh*HjK$W!Y5r=AKb+V%YQ_M`W;^U5x0?CYAK z$LatM`qLKdZ+%F73yyYIIWlDIT~%!WeOM90w^l}JB;-dE(R}KU#%O(XV4^bp+YPV# z1=ZynzCI#Fx#-}tTYEQ;(npWt>3(*++BR4n!E@lp{6|tw9UFUCkF4xL5Tb}i+$n(@?@_g2>BIWd}uv~ZhRivEr zP@WgRio%xQQ~Uj8*p%s2q#`I*IT76=4wXvI#BjG}lv1(--QJ2wBxf0Na`xu5m1fEj zK~5xR)fYd@1uie)%LE&cn^#s_*wjp2p6&KT)}yHEn@`zc@K|kavNXn}t!eVs zS&8hJD4!a)D_e6zkN)D?ChZd6fWq>wBYL6^5QU$qj4H^c&RWLX>}U=*b%|)c-PASP zZCqnC-WZP#T>n@c6@K_10=kI_TDzf+r;zQtXJ2l<36{Y zpM$dN1%I;-hGeSqdiTopjO*ltJ^-!~TH9LzrOW!DPk|MRUl5N*(#_HOXmgP%&}Rn8 zQxjLN|0ogsZ+pAyJ9fe>b!%|>{t4Ic!Vcabq)*fViZCow8||$fO~uc5MQgRy@PJVF za%AsTD=I9E7SPzr&o8QP&gY`n(42@k2`8N>EQlteFKkVl)V8gQ^W2eJo3@T{`?TvI zcw*NsHT9@gcPzQ-_TU$)>h?R^f@QX^?C;yY;+=oTbefb?=SX>0=Pc-QZGE%lS^dr% zBbMs`g?3K$F^+<2u>*QrD@XTsd+$#n3I|1tIjmXzR60f7YpY&ZWNZJukJ}vO=C0oO z(JV)~DrdD0g34P44modxy5Oi*@7_{2wBN@1;7nV0`QD%M%*!1ayyLLM8lQqFZ)%CqaRlyf{td3GI^a<0Qto?VA+ zx!u>YTzAJ&nyntmKL4L_l+S-I8%L3H#!=+^vT+nCXB!qA%NoYGzU zvVC0FK3ZRX+sV?$yAH3c>^-77SrH#P;`HRvi(4;U)!pscK0jpJ$hW!l>~q()r7|q< zDU+Vh_ms&YD-(XtF&l`oI%LPYeE$KS6Ym~tj9rP$zZA|7ku7Z|DYrIL_Ic|&5SjBv zQ7iM{Q#)^B7>15aZ*F*1B zl%J@rqW`w=n8(&S#gDh2(4%KK|NgyT$+?-QEh<&nb1KfA6xn?L zma`78UVZqiYo8lhFvAaREPalhmuoI-X}OG-fuOGI9es%tM_%I`9Z5HbqpYx~xd=r{##S<|M1|;y zlUon8pVFEc)i$N|z~aW`$qA!_g2AeMMBC{0I(5$A;QbM^Mz`1L%TKy}_rt4atVnG+ z<@Q~NS0)q7(?Psd-8Nz7MXNT>3|34Eb&IrtbBXtT_8e!_d(?;0-(j4IH$3VGb$|gV z6<;(QS37GwYZ3Fd+}4P4UDE+$wwtzOGEQr^lHMGK3bK);`hqy8lL{ke^?vImYt+p5 z?z!iW!Ck8_Zc$T9SFc&Kx>Wu9-VSjLF8kKms^*46hj{4^Z|KvzbbwyS=X~~PQ)*YO zpMT3I@ove**nj?PX~c&ojW)D?J->w z^@UCzN>EMftfW9|2KXFTyN=$<0Kc{oT)QUia@+i5b#~!rnjvO> zLpV$Hg>_YH&aaqx>IpN?U%dGInPX0xK5)?qNk8uo;v69)^A+1>XMe0OL%z}S5SRHD5v_&@ZJg%{o-AtjCqAS56{t| zom7zDuMf8X@n|$+w*d4y)Z36R#(rMIdi<#VfOd!n5H9}WBQpMP)h>u)smq`)^k! zen+@N8|n}3xkGD6rPAr_vXu2}F$z~y)NO{OFB-Z%)3&&>zTwmvD<*EAIc4{d#;T>G zC(TJU4Bb3$$)t1U&%eCs?ja*5Hm2&z3Wt}iXdAP#v0+H2GC6i+eAwUtbrmaTk6S-x zEU(_~>I&;heZjk>Dcc|SD5q?td_T+Cw%`-nw$b!~*47jZbRE;*)k^CY7?U*s!yw)6 zo^cv~s9zW@rU{tzJ|Ck=rPARh_5T)u-rl@%T47DC1n&IS^G2TVe@f%p6({73@97&_ z=8hRT?f(`|Pr1-mlJ@V*$|Y=nw)|w?z0)m6jxBk9PxkpP*UBqfzKPF6VQJS9eS?z+ zAMKfbezIU-FT-G0u4H`B-Y$gJS$Q&K%gr8bnM_HKKRC{@SlDi~bv&BPukwBB2^(Uz zMs>Gj)9Gd|Kip+yqoVqR=}f$QTJyO11rr}yv3YY)u0C3{cgmV}UJ(wBDP2eOekTvbC`TqUu0UyLpt2)E z!8OVT_?>0crn_1rD$H5gjkZC9fI;PhVuMRjjH1ed6m=@PM>?}I;#Qe4o?AblsX1O) zSg#&GZ`#cB7cbg7O{-s+^~;CMiVaU~%FJD#8(f;RYL1h)>hP|!A6~Qe@EN@ZTzGY| zd`eZ_(edY>yZ1cSRq?iNeTmK4if3XjHI|KTWLFsQJa;4-joLk)_1-0-b$WT{dVN=W z`_U!(t{&gfpYbwR!BI*>t;vaKW4sQ~*C~sJJ!RfM8nufiz1Awqyo&_Aui_cT_LecLkq~3a(M7G`PxbZ|~}oy4pF&``5E^QiZzU6WfMDdV%}U2xHn==!V?Z z8u7H|1}u#8@3jt@LKH@=qm`*Y@z48L#bKrPmekZfmpg4#&4?ibj_T!|pL45P39w$_ z?^lNP%I-t!x?EeYY!Y2%@ot8x{a02>c&KVqCah^{Q4-< zlElr)0o}N4#R$35L9g0uwjdOKSNLI|;g}61<=kIM zd3Jv#<xg}iB7Np;r@OwpoHxuLD?j!; zZ=pX{p6q^}xBW|b);BG`o0YHBhdNx!vwH<8=l+T1;a(w>`v=bRD5VciDQ=zy6>Bwv z$%=O9X7^3`iTotH01@#UT2FJ>7uijd{$M{l!gfj3$h!Ws2kXBK4(75m)UUFgV`_$1 zBrAvVdi+oxAK3cxPX3<#3j zNs;Rh9TRSlJnN#0`UT;xCfiP%cPH(!d3V`zD;Fux`jdx;Iw-{BxWO2)L}w1P|fyUL{l(J)iVx|w!bH|2IsT^dKcI9f7DyoZHF zMfJ84`SiW9Mz%hwG&5?Qgmd~B?d3Yr!rVF2=S2Fr{c?=S8@1}RtqXJedQN`t`KRur zYc4)!&7}ENYEbaIisAj6QkiqqyGOM;AvvRB^^kx$nUtH2yV>zd8|c`0m2&C_DbMN$ zDW`sr@~nRN{P);#%vdR)_v!!xQIRPd*teH!WuUA=q?2~LZYvy66e>e$YjeGmpRi4h zB=TENKPkV)42o^q{Kl`hZkuFk3bvm7YaKinpVK`0x!}TJ*Q>i1reEcSIf^$S=__q6 zURkE3Z*O`yRrph~W(_Fr?{I5VU#{z$Cg^%b9bV;DMpLJr*o6%`_;AhqY4NrX zg8{+c?DhU#UGlwFMtmQ|F&Ww4voezMtc;|bGLrJFjHH})l$2-vwzXZZt#`IOs~hBb z>OOfss~e=8x{u|@bc2+0A1u#j=Rg~8fz0L$xC$y)IkaiJ1AR=EW2)tLr!6`lYCA*4 z8{-8k&gkAhf^$_6ELQg)eN*4nKJMtX?Y!E)t4qGu){n_gl*?;9%DLu9`Tpa|Ii{q1 z-*M%XiaGdaUs^%ir6@1QQMy98&@GIC@%1Y% z+*M)B#O~%Qi=%^O<7pLtPiJ@iF&{YNkLt>j!bC|V>-pXoy27uhu3F!#xw)tB``jov|RtY1q-c zV2iqO=S5?tHPo#b`NpF9-Vj;AsvapiY+m#2;^-_%{N`N!33Wq0|!9_1G~*LHtTargJIzS-v!@_di_ZtgmwcgW{> z;cBFWucff;A7_I9}Vm)d`->_0oQu@TqBGHUES%zx@J zG|QpXP7YUVQ|+-zmXBnu+>T7HpGrpadn5} zpR4_~BWG?IufCC-k*=FNZ1L>*Gu4g@XSR($`u&#li0KEHUsqY)w08W4(`~HW%KuE; zt~lBsEg57SiYniXj^WA?n7g-L&Q<%rosCrr@IGgBjO zKCtqVmg=OMa$0S5@cVdqm603Hfw#~XC1}6|T%IYI&^Wxd!tl(+2xZjFoQP)+ zUQo-*!3%q!!qZp>W4vX#%JcRxH9`V_+nu)3O!cvY7oUI5dL<$eE7ILAk6)CQ0^`P( zHk8)a4fPY{vEsr3{m~bFvd1r)zWDq_VNW%EoQ!)2boA8HL0_qDuN^*Z>_mU?;8bex zV85nszyw>tg5Q4ES)ZUzFVmulX&n!w}_9)m@zNW z-visNH;PMTn9p>{b#xtu+z)0A=~II>I`8t&Yh4vE$KX zvMSl14xRj-b4xtFMKZXOT-6Gini@}?pW}|)K6-&Px^>Bxbn|&rw(V3mPitJf;oeJi zvt6)k+h$GX8g}ZRhb}nOcQRkS2nPjSNAzQQD$*E+F<6{gI9RzRURvNp+y;fnh&~ZV zAxCLtnA*i#``7f!(cJbhPk2vj?_ABf#btT7ZmX-Ss;V1PH@a$A)zE@uGMz|NM)UFp z*CIkUPPoHK+u6||lZSpDo<0x_lJX+;sZ;_@&8fylnNIpK6TD_xGh-T)Q|6CZQr59* z(t?YxRSk9VspbCKx%JZv1}_@9aOwX{k9?>{j0}D~t!&__?ZZ+kS+R7JKe@iKZvDdP zOHLXvXKG2oloOiAju~~$)<@cxb}SCss{Aq2qWvc~RSoUx5;<@Ur$V=D_z!L0Tlr82 zh2`xmN2pt!@fe6oEPb@J#OtA3OQF;bF;|Z19J=zl_3Lqe{X+ff_t38ek^hf^UGF+o z>qnfvd-~2PHLFr;#;m#N1vSZe@e#9zPMF|~|L~Obu&joix}~PZYS;~YXP>m=45tl) zQG+d+{_JU5!HLSgj%S>36jx*eU-lrD9j7~)8jNK;!eCpK{K7}V6yjq_p_iZZ{X`-f z$t$&MM?+IRGh-8#k^f&CPdTw^R8cy$(-=3sVZ_$m(|6dmPMUYZ_u<9W+3npvGx<2P5=1b>g0H>{h)xxATqGtPKa+IOb$ypQBx@a+s&>1JsC zXc^^F*mcA{N4eY_+63uTv6Gv_BO2Bw$o^C1>a0zWHp=mojyRgfEOxXaCnvJ3mx@Go zw)W=fAZspIhi>hEf08zM__)3Pw5=mkTvi5@)s$5yD=`?QHv4R#pU0foY^p*!5wSkT z#`;KoK|;hiz=pQ7tFihl93UoV?xIEO)j$8KG{;TvSwph}Mi0C8OG^(so@yJz~<(sN&g047Ul4kS7cIc@n;r=~;fQ6Sh`?H-H!1cm(5dWUNC)rCeyrj!h|(V(s00Sw#SSLmBUo-0TTh`B+ z><^qfsJdayqRSTUUpRcqigD{kS5`Eg-Rw@TOvdL|4Xgk6gr>}bl5tbxGbW~Ywl2GH z`uqu#n%8GW%qtr=@v&rCVM+Z^IzqeKJGHyLvut}EYonj(w4o15k+%8$E{x+EHq9Dz z=g^?bX7ePIzV9D1uWfjNUgTrhJajH6x$;OBQsdOA=eD<>JN4T0&%ai^F=Am;)575! z7A@Ksd{!Ha)e>F%^xCygU-Z_SZ@#ttrk0kQwm*93osY(AYvUC)H9P{A-A6d%QGiOU z%H%PoU0K-2ZXra2bT16qjlq9x8|=c6{j1&9{>QeVphvrk>eEa;{cjtGGp3A~mvYUz z`MEl^dSuIK<37K0NG4Bw>$B%t(j%rFTygx~fqSCUorQ>DD9>dj`fDC%;xXDm#XKU` zOU(#RSgh-rbyHqmKD2yD+D|2I)=hUBRTz6prEGRYx|x{~5gyM<+pUg0b*N9Da_+*G zFSSiQyRB($MgM6{_m|x~vSaAR>9-8NIJ#V?mXBDvX5^v;TAF&6ItN!a_u4~hXkEOrynyEx=a8W_Df4{!D z5&BpL#drV>-futO*?Unx?;+d zp>0Fv&upFUJihvEHRk9O7are_6PwnKU%!o8K#s}a6U{MMi8|aOqcB>oJhv|4I1w*D zqBWeB($AMUPIg60@pP^gh+V|(h9KN5(x9~yZm()9&D8wIgLd)f_JPMudp_Ke82}6$ zlJ-%F%2Z)=P&Ak4%IrA)+(EwXoCnIv>A5Y|bLZ#k=BdRtdevM#A~j?4nCyU8$M(n0 z_|Kke8C^KFXFzzVGyVK!9U`@0dyg%1IEQa@#-j|?*pn%Ma@>3s(bX~6$%!~E96d8d zj7iRKIsRiM6p7d&vy+RyEcDhes`{^wS^4DTWOY_YCeN?rN&QsP=9Q~RkBJ@5pZYY< z4e2mdJ&UiFpvYICiuN+yc9j{5{I+N>*9GzRJ zdh1?Fx%Aq3ErV5WHM6w!fq`?E$dft$@ucbbqd`9tLgm27xsO9G+y8|HH*%a zpixt^-nwSw;-*ov6SXBxso~?sjf@sYlXw4eLNBLaT&iL2(E3I7m80^tnX5A2aC1H$ zSW^;jjEx;xHhjQ`yEe_^3#44eA*Da zqxBCf;!SJEuiIXi%FdCOo!KZyEw*I__ElQ-Q_!9=sI4e+JmY@hz9GA;d0r2X!twit z>?78h;2pPb@RRw8bS3u<;d%A?G!Mvz*4Hu5F1=^R)acadJ0}mHmvWrhZK<;S{saB; z1-aT^J!0mTaq9M|bB4_x+CFQZb)qGcCr+5xkvA)+sp06U;fk2mq;`IcI&OY!xAQ|G z)8*QILov^<=PHdHn*|Z!Z3nzxpSe;tKfvz)v+9xte10;~pE-4YB0QkcEKydT(&DL& zdVSS1r|z#=n5@WT=8k{wy`WQ__)KT;>9%tVqOBFJvsB-a-r;$?9)~tL>v6U-8wpI! zOe$9quRk#139-Fj(XtB20;E6N)NH&ws->hewL(N$%!>OuPGw5h=xqk^AqK0QBw zM&Eu@r>S8hJvC(W>Cya_exd(=YFC4P(HV~l48f$#gnr7XA#v@QQsp{M3zVm3X5DgH z@1UE~%5RTsJ!Nm?CzFYSRD$Ee7uSAw?xkH_ZRk3TtoGEI>TjlU<%pIo}oC+x!{R{JLoeSAf?MQ*+vu=B)td| zV)S-5m=ZZ-`O^8x6SptAbl$x4+ve^ZA3SzaAGO|FJX6I=R<3WDG->(;|Bt&jjgRZ7 z@`kJG-d@yNyIZ}h)#|>zXlbpj?$++sF11?wVzng8mL=Qq632xE+mUxU&{E?&0LS4#26^GKllc?Uv!`HyIors*?ImuDAJ~L0e_WW% zLSFm7DeX*G3*#h=OvE>OK_j!QG`6h zEhrfi;yEzJ2Gp)_Ofz98?CeW_0fyMJnwpx%ng(kyC{_C~Bq_sc`Pta5u{fMXSLD4N zlm4E8%IXTq=??ct`@^;Fx+`*GU01XcEY`Jct#zYbeLi0jO7_G3f(;Ts8a*Dc21y)d(F>NBg0C&$9kQ`4vJrv+ZIysu_#d?ewnSg5HVT5XOD^o4`VQ=A`X zwl1=DHb@wI23+}gFM_>ZPHMrMT0nUSFJT&it>?IW6j^nerUHI%pf2ammIGn~v=)Ps z6jkD5IsOWK7@WOpcKvLmv3=;aW6O6<(hF1DYCN6&eeK<=vBP_Y$LWc;4^1TN21~`_ z;r6NJzFT(=whT3eYU^SfgZmFfAg-B_5%9Aqaw(DMHc-ky^Fq1AsK_AdS;K?$xVb}- zsWuxSAex*GVkH)nlpmwV&#AbVvx%0AC;G^Z$>}@iuiG!pEUlc-$phlRnW^bj9o;C} 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
KPpnw+z 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 z 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�~Z@d;qS{Km2$ z-jUxpaXuHTGau*k0rE1Pf%n)>QbpSMY(o6#4kO-Sk~Ltc%Ay5VCy;SMiB6!pb(lPF ziU>p)@_MsNtnN5MWO~DzDLWMMqHB=K))EJm$l1`7T>axW)M3z>Q%ubipVUmj=6h~RQ z(WENJf1^KwM;XTL%#E*)1IB|ox zE5yY}%>mvI`BhEjp5{oPxI}L(brh7i^5~CBz16*`5U8l_aAum)EO~yTp`yT9f@^R& z{g}3}D@kr#C$gFF@TD)8CrUlRRG=nFEh}-xnWKeE1R6-WU$rJ1NSB-29jWGwEKgBh zZ?|NyWNj|1oXBx`SVmK8N}9!LFEJUdSyo?Dc_!wpR1|fc1qX48Kj-xCi1IoFRQd-2 zjs9JYxFF;w%KR)|hR&6_bY7X~7AW&IIs<>zU6N?$r|~i{*Uou#20jt06J?%_mx=d9 zfc%XffgiD}h>cWn+;nD|(Y~MTJj8GM;r_Cb>~eO(>e*BVh&##~=1vb__2|VEp{Us1 zVouG=?exz6Vp1L>X=H?t#i$1$ z27Vr>(q!%-OE%88{G8e%R8o==HvtH92Zo}{8I&(vLM`d0w3IZPc~y_4JTsFu3Bu_( zE@-3s;eXg6LM15;*Yyc{0}b)->3R6PfZh(jWET=D88a68o=R_rC-`$iD%O9NUQHSM z3ZYV60e(M1uZDl|@4npla|{1o%b#O;J^xz23hviUUTB-{ zYG>|L`THi6dkbhj+{#ju%Sc|hz`3}IrM{c`E{AQr9V+6%DupTKFVZ>8%d=1F4|hZs zVNNp;MfEO$$16IV9vtK&L zns(&{yKXVt=lKVnQ@!5xE21~89jBf> zxm$)h%Em_>%X&kQxo0%?9c#zR_4cim&RiY?k#ZqrhW?P0lWI~Msm=fj<$B8aT}Z&I zMR+Gnbsk|%w?fVEZq5&sR^myQ#mQ6Os#5$(LTIuG&m30At;j_ztqtwLf&yj`bRk>5 z)!i^)O4EsuJ+`Q_->w2C!RmJgp$0ENYapGAvp0-`6S% z&kM7i{gyZh<~L;GKKehL<_#(z)3bFA#7Vx5>nT;?DFe@`LFiYh%wwtB@ct)W@FSwM z&LHR+rI?YO5LlrDo|jz|39(wzQ^gc9#c0q=$oR}=qbVE1^jI-C4!-lqBARE__rCry z-1Uur94Rf}ZiA&%j!ledZ}QWnrNtP=K~cyh$|CwG`-C{jvV1~Z;vR~ZB$&8|UB<`1 zOrE7TvR>lkxhK%_@*MwtMERI*WRDUjV1hd;Rbt9R!jw;xkLjzdm1Gh(sU@wErhFr% zhJqAz8TG_K^@epioZF-T0O$0JfS%40Fm@3e@Ku#~oo0*2WzH~~3MwqFp#T44@BHJt zuBylXJm>Y=_TKj1@39|jz?(6)0o&MMz=VMV1`HU#Q6@~?V3SP-n`0>IG*VJbv{htS zRH&qsSfE&_Z*3+T78WVxGAb%6s+DAzsAKy+?(6+}kAYzI{eC~+zrNe!zVCV6ALpKX z&bjBF*XwofeZ>V%L`e?JJLAMj8td#@bd$1-wB2sE+}4?s=}k^dO-&q>Fn578w7ogc zo18c>){{IazGA_v*@FgWXAd5ft$$l|_SQ+4g@w-z_k=anY2Ko|sU40?OnRlY!%f$)bn{8b zzVg56OV8{}w{@YBt(pyAMz)8X^6NoA=ioQ%ehvOk=b2K3rpR|Kvzs?Guy5aaX&+cg z8I~=xh82#ESg`cYNL@5+P`XbhS`)_9rw6`2D&hU=I^TH0*>^f1(Y$6EC*eyML>3ed z8ytD3{&<2lQGDrxhI#YS>z(Ho2Q^*Av763!T|9m3jzb-{&T;j#9>MO{g=N= zN}P@f$;wJhPl!s4j1622NKc53jhsAul&8G>@`4EcMQoyHWKLvcxF!;c{$byj)!qQZ!RDHkK5ZkRRN_9c0`YH+ue14n;G5fO%dni@U2$GRi)v>vohlnGD5ur1IG&_k2O0#vjfMZWQ`l{JOgnb zg3Oq8(~PSZSP=^rhwJc_V`i_LB9CR3XOCW(zij^E+0wdYcEyylug=LXoOM^#0~tfc zubfhM!{UD9cRm@Rz0P>N&y07Jn%;NxYHJBujKxB6=dQ(ER;x7imxdPdHK;*_YcNbmCG z2v1n}fTZ|AscES_x%JB~ugp)nVbPcc&hwdootZ&~YiH)^v3~YvOkaA1V}I=44~f+m zEt_n{F`YSP|K-fGDU%BFGR*$VtntpRzI$Gb>6l{}0_LG#=L0u1<{Yhz;BhkU)qrI8 z1#?Kzgi&Rayxz1CnOS4vAN58}Doae7P&9k{9d~S>eRRNp0p=@(S;MokWawe%DnraB z{Gq?|TsddCml@xiIFX#Ci)i*(vOQ71XHUpakicGe@;U;ju}R%Ys; z)W`3<^YPU5)SjQ+Uf%QPo|EObFJ7}@&4vw)H?B#PoUE)I8Ls9NZ2DL2e|1CL!1#oe zxPc-u*IswswVs6)xw&CYH{8%1mOHBFaM``9C9LQD)%TXkyH(dr9y@mOHC0CqGc zMwa$LUk{6nj){qm3^Vs)@93xHE^G4z{_|CNGv`~IyQ2T(%u!?YKKZHp{m^rM`1Px= znqhrCzUPBTef>T~#o?+Rd(8AGEy%T3!Q!dkK*1*)P*wGQ= z%r-?sqwWxY5a-A2XE<*!gRHy6UIr1>ugZ_DTA~b%=6%z@q0w3WFiv!3b;PXV5hIf- zrdT7#g%sKYQ<9SH{O}Puy}jITP1HQ=XO6dPPV6^+?RWJFwXL_@y={}8wp1*$yDe*B zWoeWp#x$9YQ@5Cf0||lUq37+Sa#qz0Hw(dyztsPj4oACPH&3=sx?$voA<4-@O!(c9 znT@U))pYqq zWOmo!wrBod$Sl1n?~}O~k=fQEoAzB)W{>G-HPPDQ>dfV(;Q^tkk=ls=V|_kPYS)Y& zTd_KQKvH^o(tvb{NKZ;iOG`>hx3+Xe{^{1Vw4~&;v}BWHVn=3=>k}GheT!7*KC7kF z8ylnItp9aR=`h1>FnGjlR9R&n3frQq!XzZbUJ)v`eT&)poB5Ffq2ipPWJ-jr2$K*y zWWiOXi7+~EME0Z4lGk~T1;a8hh+l-B?yv2jt}n?I@~L3{JCE2G5$Lz5KhER+>$%)5GL!EE4+lJvd| zTbzxw<|(YR&o;1=7E+TF;-VtMLYx)6Vdl1Nz|iwPncBZc@3m4VxfSvxiS;xr`W_`!qQW@Z1yqRsygKBj) zxXLZY8%}W&%>!$dWyX>_d=*`A)ps*9Glvaup24`AFfXM7T}>fP!`CfZ8~0JB{rrk6 zGS;kl?$Ug#b!i=0n%%emQ_P-=j}Y`9lk=z6kk*l{mFuHF%DCFf%Aw88`!B*f25wiBpe_Qtmc);TJ=-Z_hCnQ^$>|!1=A3jYL6l z%RkbxawG3s>)q}4+xlu^TvDQr30*KRCTB>n|1ao+(rbN_6nv~JO^Xq64ph=PDk`(1 zg>v7JLFs8B_N=*klNMH%CPT>i1|HjPwJXv}2V<+f9TwF*|$jI!}5y^?kQGR1o=qx4nO<7jf3_Ox8$2jI#Gdu>G(*}zclzlg_~R+I z67BJtm!CEwIX*QyG0}1^hNs1+MyK_uUs_t|m`U-mUT@mK(87taabEB8THm~RX5c7~ z>4Q4JGT-8jV`%B%xX1|e#P7U!KhDzq;aSGJcgFX5yJ)2S@*79b@mxM({NU#5ZRg$qr+#<-w?6%{BAB8nw+eYv@maB{K%+t z-Bnk1%l-=*vf~mR8v0%eeuRek##-FJ&O7%!7Fb7(&9DkX7S4^!2|vdQvs2)jTlLgVb>kWnN0 zOhd6fYGi75YGO*%$dJp%j=3zPICSyCg-b%M#^ltH!U@sQ5fKI`Hm0J&m!IzoFPbx_ z$gvT{8fMRP`Z2R~`Z*gB?8yIMCo~~oEOJsVG!`SmipPwZ5H=xfX~n#yVRme4TF97* zaRHMt#xWU-Gjek?!pF~=H9jz&f1qEKe_4;?CDG`mH#EeeJTW0IHYVf}tI-lNGt7+T zoI@1c@E^3iXFP>@OA_)t<Wi^L14YwXo&zKUKS5lI9&&t8GXPb>d$_rM$ z^xE^B`7^h4MBtPQXBNF=yTi}58$1WHui?{SMWaU-g-r-uJbU({(DjvTve$;J&9dhW z8a8ZDc)^q@1)r-u=VN?d{qnwL)_H`P-4*8UTFjC@xk^Q(g$moY!|bpD&dE&1voN2$ z_*VYcvrhKBYW-qT{peb=uTgo?s+4ZqoS_?LR`dq&d~`+c#jrg&`oZD9=y}<`zIbi^ zl?I!^w_CreiOzWp33#54yU3mr*JYKo;Q0#k?+^LRaC3l2@MML!yY3qH&_e-yd#qxO zb@r(IJA6ZeyHgf!4Y_aF?G3l}?MoTDW=-bpw+HZjQ}30xoo8Cn&Ql4Y+#9KKk=<=^ z1Dsjryw?NcK@j?7)OAY>K^fRi~w@D?F8%h(y1uJQQS-&&KBUX9F zPz{+icW>&#N^|RFp<=07x+AtZ%E;V|8jr=?iw^y0_5}v&DznvP@St?(CNy(+remXm zD^uJFy=B|Bi1l2h%%3EyyEFXW_5 zMtM~4m47*Wj4^T&k@A>mOYMuCGm;$i2D18Iw1j2LSFV~r`GygrLS`*$@mibXPDahh zN=enpT9kjqK==K(Y2%OQ+OV89%<)yTF7$EFwGrCCjc4*o=GOJK(G_*_fseFtfxP4Jp3L?p`Rviwu#73mIYuvU=y_IuWFN&M9naHgG0vHH31j2J;>_Xo zQ*>P1unEb-%<1Ol-ACN88&<9?sY4cAkIud>FaEhx^Lu;y&e_vYvyMQZ=UMgJ z<_p20PpR`wB8hWWCOp^O8yYwlrT4v_5A{|1R_A;~+ng~kGC?F;uRi;w5`Ez8^(FG0 z-hJ8G(zB(``4tY%AG$c*?+l9m@a%Z|`S;Jd-{S85XU~WFauBC;Hhi*3nCLwvXTPM^ zm7KlazV))RH=n)vGUr{CIdI&(K>KiG-L)|#?;_SoW8PkW;m8ev_^qSetGa&~UL2B- z5N4JrnsoCUl6dw;h&a`~>g-G5#cq1vU+7=d&n)wFS^W(ApAEe|+81ciKp3Ka=3XMy zsh?=|JF$V2T{zv1H>>}WVf)AA^{@9Sx7?T1&*c9gtWZCZ>UZKiH#TV|tW!UqbYlbm zr=d#yPH0yDdEv|I=bP$xVgm=mJQp@}EV&?5tN)T=Wxu=$>i-`Nqt)*O)6Q`9o3(&! zP(KsZ&-#B8O#ePEELT5eF8*LU7sf%E{7=ON>UV;X!G*!-(Ww6*RH*-w!N~FI|1zxk znDR5!f64r{>SsW|VDJRP7t~LZtDiwVy)ZVogJDWP*rZwgu1nSL1e3ntV}j}X$L06W zYfwLn-8RZy{3hS%$4&jlhS9qV$BMb0>DIr_jjwgf867h`Fnu?1iTZobs-O6Nyb1E? zR`qk0YtIZlLECnz*whQ=)vCX*&K#G16JM!*Zn;SCT|&l2|AYEsXfd#{nZdNj`^h0F zD-(}#gWH$hQy1x9Fz@|-c}*_dAJ@g;r59<}*n^9e{kSwEr~dg~w|zsuq0i*)c0&+0 zcHX3Y%MAvet$wa>VvtuGW8R2iD39)@;@$2b@PkW@5s~0^y5N*IsH3$*#&a_cjYgYZ|2>9 zH?Nrd{^iVAHNnUwSa+xTo$#v5Ka+P@{p?XckEx#*)Xxvp&+Bfvpw0#3Kd7H4)X$IH zVCp`ietxNbey@HOx#?eb@$6K;3{*dU^*d<>{{i*$P{8K!UG?*@>j$iI{j;F$2>Jsi zPQAe2G5(zK>-x`st?GAzx;D<_nfckoW_~t)n6V>9Z%r`1nVHK>x+#(1Hz$EwHWQ;?@7zTFK&-Tnmai-}EtOdo^eCC9ZT zX8vgGr^|+)?doSlppOoZjP4rSVQ4e`4eFn*}I`jT}sQL#yFYkWNEemTU72 zEx~ba#<8Kn&}i_R@+J=U)$}Vkc3j$xKkm%?#_u=zMvsg=HGDOF4D!|ZvBozwK989* z&HQZo(O-AWd}U}hG7IXB>5tJF)4#$Cf}zEv8Qi9hY1fpU=JvthHacKE@dw*Q{k_Ki^o#q-1k3i1&G_kmeJS_`^;2{~S>qG-Yq#(Ejlh_f zF!hVk*C0-lZsx%YuZ>S-LjUv`>eTP6bA4bO_L_OAKWuDPrmNfi;UKR6G|kAy+}{Ud zQ~zFf{F%AajKi&Nob8q~cBW4b5vQEK2JL{cQ^sBxJ7~sszWN=1#`sAFpGi0KmMPai z{HJv;R?hIcKW;Ps8-L&%?i_D?2{Zp2`(XIE*Ud9>G{M-hAZ%h|bNXNZC$PDXc~1Sj zqkhMiIpq2qCN@6FQI{s;=lrMTj9+8?p6|G{KkdfPxHyd8@@@5tNBuHL{bH+MV${zT z*LHu+ooCE*2&3=jzSHbS43;xC*w}az-{Y1ww!_SGro5RG+&Yf!H*?lg>KAkGYT7mN ziGUs&oi{q*#O5B$%+1F38vngq{bFqWC)AIj?>;yFg8Ieip21;o8=F2;{k>)`xjB$0 zM#p{UU{l^nGdkL;eld2Ve;n%iHUCgQ_p4vb{o{KV#P7OeE;uh4o$vqbXovdy!ZmK5 z@iX65znF1lo~syrH?fiTztm56AQ;-5zM8(6c}dkTLtLJl`TR|nzAviZ_(ziH`aZ@N zOH{u^x^2Ae#=-G?VLV;^&OM=#mzfjHSTk}qJU0G@@nOt-ZRS#QZ*Asf<0l56qnP+M z_x{EB+$Naa=zrTR@U4`&^`W(*koCYW*C7Jz#{49vsqQ@8c2 z!E45w(XC+GBv<~1R+DCEH)GKFT}DO*HomyIHuIvfJx%KGHM}?JhSw%9Sf|f6^|P)2 z8?aoXel@y`I`GACm@*ejckX!&UB;I-JT>hCr8H3pxlV`6g;WyYGRZ{oYv-}}=*;K(`Hq?zmfX_tacSuy=K^Qns~ z7|ZC7rkgb5|6f?=!urPlb;_9XSr;$=?{Ea^xG*+2O{7{R-0M z!u{zFrdWC)5p#;Oq-<_7C9X*FeuI=$Q>Yx4ogzGu92o&~p>?B%Ev0 z;0?BU_9f`44CFgyei5kGy$ZV9;p_*{vjTeJpl4~Iy|bqYg!iDQ0M5P+Jt2W`_IWt_ zH*oR{4VS^$zrfj3fqCAUYs`Bj6O0Ws_u0Qvzd4J{;peYhpJ1r^onW4Y8QWu?b(m+W zCVo}@V*EK%&)D!EtDj!=^Y?%~5wpteNf2{L>Og0gav#k7z!SiC0P~f_&jXKuIVRqm z8{nKBV9p{m;d$V0V2iVY6wGd1lm0C58Q^O!4s#-aS!KE(xDEIz@HSvh-!$;Oz#YI= z;Ko3n!++x^nCE5BxjvBbgQDE~K;!F~`#}?4c7xG9<9``HCfu!So(Uzp^^-kC0pFd; z%wqwYSi<7ocd2KF`@dcyB%Fi1#6e!+4c>6lYgo_v-Vb|!%R4ewq6CQ&b1K+(*~d=j z_c0!KVw3VBuP}-0ocINv<}se?3s3VjKi~zv&x`D458vdgJj0_r$}`+RHAT?-9`u?s z5(9sY;DlqqX%MqH-yD=t22usm3bGYs7l_kg!sLXkPZn&kWc{$ zwUE#T3ELoH45x$b85NMx2pOAUSOE+xhhf!_ zSq_;d-UylPkhvR%w?dW=vT7mgFl3#8>{`ffgY0dPy$5n^$VrEsBFL$LoLb0fgPd)U zWAYC|&N0Y24I{iTA`?dJf)RcgaTG>$!AKiMro+f07+C?MY#5aeql#eEb{Mr6a+gAG zBjj#|+#Qg+AM$*Vw-NH%A#XS2bwFMxtz#&3iPB~V-j#Z^$;3YV3@#9WwI1{0UUq-L0O8cMuSk_jcp zpyV`6J`9sjz?3a;`BErNhpFi>tpcW(!SpJa(F`+dVP+f5vSHQ%DBlFL(_wZI%&vgh zwJ^I4W^aSp`(gHBn0*3fcf*`Sn3D^0%3w|v%xQ%=TVc*Fn7bb;s-U6~=B2VR1SvE`r4su%rl*xM3^YunStMp=Beqv_s2oXz75KPG~s=>pigE2d!RcZHEoD zu%Qh$Y=axy;HHIeQ#Wkf4L5IrTXNx+GPu1*K-+O>I|H}1 z!EN1edmDUW3v9}PO^0C9aoBVQK6w=G*bR4dz#W}%$0@ke19x`Arz+r62jQ+pxH}f^ z?u5Hf!IopN zaQ|uej2AwW37;u}&n$({G(vj{v==~oIkZ{qY-@*YyJ1@gZ0m$CG{YB;!vhQ9fqHmg6FjgT9@q;H9EAtE;EOhV zu^PU(5x#f|wwJ^9YS_LJwztEB54FKV+u)%+@K85AoCpu+!oy|oa1}h<3J-6Chxfq42jSsk@bGDP#0!t?#yNy$ z*Bd+0aM9z3U{4u5;e{u5!M7H|x4Plm z?XWi%_Eo{YBk*JaJh=m&%7mvj!BZz-{{eWq0-iny-`NS@ZG!_oIM4ylRKqi!@N6|a z+X2rN!E?L7UkUy_@VpP6Z-*CR;RPSOPz^6^gclCM_qM|Kx}d{`j&yjj0A4%}-!F&n zSHt%=!b_#_QY9R;;b0;h%!Grx;ot!{cnA)5!oid9auvMX3a^yIE0yqyAAV2@KiCF` za^TPocr_MYZHFJG!;doIa4a15!QuVz+Cq4(8eZE7uWbRdF!0BR;U^VvBnOW8;iuK` zx({C81aF*%qsQRqz|RlEo3+px3!OgbY=>Wzz%Ne1TT9`sT6n7s-r5Fl?SZ%a@YYdy zs|()hhF>PaFLU9Sr{UNU_|-=E)dBd`5%{$ae!UxhQwG1Og5R{l@lEjC9QbWJ{B}3| zwgY~*34XU7eqRK?KLRH*;lyq@aR5#nfrOFUGPCS zd{_n_E`<*dz=wz6!%pbl58VghY%!cY>fYNPf!t|#_9D6i zv8oa45MpmeLQ0U3gGgvK655S~)gxiY5zkU2JQE3TLn0O;5r>e-jfgiE@pd9nWk}Q^ zB)S!eK8(cFBeAhaYz`7zio{kTvCT;A79@5j5_MMpgpBM$M#UndHY2${BzHfO+ll0MBY8d~ zuN29vM)EczdApJPBS=9NQrL`)*^P|dh!nLWMf;JWPNb+C8RtXBl_BHmk@027giK_@ zF{F40a#=MpF%g-Vi%cv-Ce|Vo+mMOdkcoScNwr8xI#O~NncRj<-hoWsk4!#_Og@85 zIgDK1ft2n9UMn&mWd1f}K^d|z1zDJb zEG$MAd5}eCkj00QB^!|?XON{kkt-6BD|RAR)+1MTBFh?)Wk--@$C1i1noAg6r{BTX{||sMK<+$-YzEn!iEJ)I?kYpJq$688kbAZv_tqo# zZbPyhmp$b(yuFO?%-I);3?82R!kWXDeAD?a2a9mrRXAYaWzzFLBO z4diQEk*}RXcD5o9l_C%ALLRO{9;rtj=|FbvM819oc{CS!v=VuAGxF$O+mY{|MqWCJ96XA=ya##t6!J9wb2l7TYa@30)Ek}-4B1h|yqq~r!`;nsuk)y|vqut2Qa*&@bMSivw`I#U2Sts(d z)5y=4B0oQjytxp0vl)4FC-PsgyEk?Bw)orMD zq2`oVdpByIL_?OMq1|ZMb~Nk^>M2J(hfvQcG`s{2uR_B&q2c?{@J=+sgGLmf5mji! z7Bpf%8qtMDW}=Z*Xyi6DvIC9mLcNKow*>XpqTa2jw*&Q_Mx%1js46sSHyU*UjrO6@ z3(@EuXmlqUlZeKYp)q^Vm@{Z>85-M$#_mO9kE3y3G_DAZt3~6sqH!H)+!-`J6OFGx z9pgK$8!lDK?r?gr?M@DLc@V!)Qu3np%XW zHlwL~(9{#?Kp#4=3LUrw9q32XJZM@GnpTabZAR1fqG_FII%v8NO)o{$8`1Rr=pfKR zK6FqeI%o$vr~@6;g$_(Rkm(ZL7N!Kct6>FAJ3bjTKT$N_Z7Db(jfeG5_F z7SwkT^_@Y7=Ac8X(4pJVp&jVZQ)osCno)sfw4xb%(2P!Wm#+t?0-$ zbW|ofYB!o&gy!x*^9s~{2 zE!vKb+lG!WLMK$B6Z~j#DO!9Ky=);mF%zA*5uLabop=bHcnY1Af=+5iC!In|JZMP; zT5=SfoQO_7h)(H3FRwr^-;S16pru>U(hhWL89Ma=ryWA4d(i2% z==5V~*><$76D>Q5&L~A^w4yV*(V11~%p>TmQgl{3I_m^lz7Q?njFulqXIG-L52JH( z&^h(!oc-vWqv)K|=-j2~+zzxN2d!vCD~@Bnw5{{1(0Og>ydCJgW9a;5bpBCvK{>i$ zC%Ui#UAP5ZcnDpTi7skK7j>eGy3xfxbn#MjaXY&B5V|BCU9uZpT7)ibM3=UqOZTEz z#G+T!qgNb4uPj5aJb*4sM3?2D%ZkxuZRoP&=(1C2Wi?vaidJqxE4QPSr_rk_(W`c# zR~<*MI*l&(pv%+ISGIV(*y1Wrxz6o8v4PCw)UG7JhA3>L&Kv(3TD>~5?-Ds5$ zttvySn$fBqXw?z)Y8$<}483{_di7!SnppIj?dUZ}&}&Yi*X}~AE79s>m~R1SO(t4X zj@C4yHSK84ezc|&T^Wn6YC~7;M^|;CwVTn}-DurL^g0iET|0W+e)PId^tx_zwGUlg zhOX{FS06#^E7AITw0kpvyhtT>?wEiSoe+F&vpbaT#Lk`+dj5d^` z4V7p^Bihi8Hta_m4x-oZMb|WqJ<8@=d_)##0_=uKYqrmg749CTv`dUGjy^D*?66X>n&Xj?kkb_l(#620vJ zdbp0OJFYEw$)973e*C(N8C$pYBHQtwrzMkKTI_z4s)#H3i*Tf^NOu z?4}HxL_4uuPbf+*CER4*4oChD+u3V*Yse3SdoIPCq8i+n87V_uSP7F{7q%EAV_n!L zQWm;!2vN>wOwC^?@lx-mdx(--TsWL4x!Z*!iI;6I>}8NV>cY_sl$TsMf$8$A04(59 zp9>2f&2eERTnk*-!lReDu#H!zx^M_lI?sheiPYsT97crJxv+<5ZF1poV)Zr`jvz_z zbKyu*^br^KlC00Ua1`nKx(i2>qRnh#w_07;!fUm=u#MN+;ld$!t=%phir3oj!ePW)9WLx4%X-~~!^yJFxNrm` z?ZGY_$#}cmg}s#7x4Uo@6YcN2a5U+5w+qKGCM3m$V@VI0*HezO=t_ZOaSGuqg9}(zd zxRFc5MmOCiKEj+)Dt5Sp5Fhb?3x^UP5s*>1kxRt0Zn}reh{G-%jxWNcUn0njjCIo^ zDTpj_VK4cSE>9$i;>a(#>Ct3G2ILzqF_c6GT1**_BgYs=Cq3atjxnEe(=EI)yIj~NK4z~AhY%m*=)Wi2 z$T7y@gU69$%pct{9x`Jbo_oTL9Ah0mcpN##I~Z86>x>Ie zxRGP5`6jc0V{I~X=uz})wFg*zR!2A>X?GUf?V@E#`!JsO|5NO zS?61F^ZM4hwd;Me*R5)5X>Mw%X|1dE<7wAT9; z*R8K>xv{R+H?wJ7t8aeI+B#qMlBNwUtLn1zeN!77eTxmP>-);(`vSSXhV{N0Uu#QE zZQa_MmNmYn>pqT#hIPKy`a0hg>lzG2OImAM>(={f*42(~YVkF-*4MT8RyA!{*V@uh zw?2Pv%}r}+R{G{Ot!r9bw|YZkO-o^ZK|#T!sb$kSjYH4V0U7x?cp)tRyW%cL< zGb^sW_S$QG<|pv$I~NC&&dcvB&@X#^!`kM?I$w<&uU=o*(r{f}t*>E8&DsXvjCHG5 zZD?8FaATcsWnF7c5N@sSchOwmu%xbKWkbz6-^#k_H@7r2Ha4vCt!$~gv2J~1&HDPf zTHh^oEls`^1y{G;+}yOfrKY*QVU@4`=H~jkb-opO=cU%HZ>?)-Sii=%V&v7q+OI?H4XSIzx6aeYoGP3^`iB(<+l!7&s$Gh-?tvI_E>)FyZB^| z^`iBJ^{VweKI_ST@PU5tE7l{{6V{8?lh!lVv)1?U*@f1#*2~t5)-%>~)^kqkVYmK1 z>j&0r_*kKxTy5I0_FGRl*aNgYYwfWPSWjCA4IWldz}414>t(CM`kwWa^`f=Udd0!_ zEqqpowaEAEUZUka_^hX`=dFDX7YvsVI<)U`>CJUo@5f(q9ZJv1 z>s;P}3-VSw*B{~g$4d1awpZNR=cN39fd@>diDqu5g$7nrPb)rhSj7l@jG=%+#+i1R z%1Ub4KqHORQ17IeI@dYnYc9ymr-o8ivz8hfXe6IiG_jTuuIHqwXDa-Y}Pq#w9rfwEz~%9wGMTrSFJQNk(h(2~s(n%F=KtEeN}>El#~Q$7~= z@zqdyAwI*6{&jscIOS^a(dzV~*6CplEv&&ulf&<+eU$$QGdzL~!N`!;-itz7BQx}FAy5+4QRGtSZE{#Y*6 z(xvtM(mChrm(lq(tYHId`f1jGQpWHw$jLm`x|}?}=6Y5+yldXXm;p?(1oF8&Q*En^uGEtLoW-R6FS4e5Wqeobbe@F=zORCh|u}?LdS$= zhi1984gG6rrqR(#vqroB^=fzY89Niu)CQI~lLQE7SLF?23Fx?B`wqium^E@o42{$M&pt z#%(k8E~a`X-HaeJ{#KYd?~-Lr|BX#Eqiel0;+@&lrJR}XLO$Dt|3;b@!}Q93*LVAJyVRayPqnAn zQ|!y|*##G-u-d*7pFI_yJ;5%pr`qG}S@uM`z%H?i?WuNw15LFj+Qp{6%yHl;4%W+^ z5|iz@CdYd6k~s%1ne&R%Ua4Jpt_8c$X{*#OwkO$>>|%SCJ>_(gfKyJLksm|H1bebwU{A#7)G@s(FcdiTGP!tL#r8N?A|+1$ z4eitHafTx_R*N-|Uij4*YjIF$|dFZ zZ}@h>$ov1c^8edP<8z*y{r*c^8TkGs^gfOG>R#YEe80c2NzQjaGI>?Z*}ArbNT`H~ zhaX6|L`bA~C5n|2%@YzMu@c8BiRW9iOM)aaTn0!IS)8SpT1l1^viY;5%0NkzbQvUr z$)Qe$h>t@uR5E0kWXf=UC|Q!tk0eJ%$VeF_xr|_>p~GK0}FQ)WrI%$7MaS1R}&1u{?O z%K{2zp)8WcvP7236>_C4<2uH0L@MPfek#jlg;dGaat*8HTHcUqsgaekN@}G}u9MYL zFAZ`%pOrOyo4t(XZxk_38fC4llO}1F8>EHtvYr!sD6O(VZj_s3quk5{xkYZ3Hn~l1 zmrqd4QQ0J)%UAiMd`))BL-MdZLKzLROTI3T%44!yz9HXajXW-U zq3uInMj?Tlt;*UQWm#*G+L+O^Y(j&bJ^UT@6YB67J4be~yQx6L?Tq876y&9#_ z8l$lqr}3J=?=_KyIzW>&SyMDs2eL@hG+hVjU>%}99jX~ROfz-3X0egQ+^pG}qa(Og zN9rie)jZ9oO-E}1w`ie`(Xm>j<8-`E&|)rfGx9C0kX}wps>V0~@en#8% zv-&yxyl&Gk=mYvi-L4Pnm-NfJL%*V5)vxJJeMleHM|78dT_4rQbhmy(zp0Pw9(_W; zrQg=Qx=)|fr*ywQt>4k_>H&R5pVjBoug~iX`aL>$k+1Mo?a&wX`}&d|)R*-Y{ed3R zSM`VbBR#CI>5ug%dPIM!uj?CnRDY&F*Eh9Of1z*bFZG!IN`I}t(c}7C{hj_^Pv{@? zkNPJ)sejhL=wG!<|E7P}xAm0%L*LPV>S=vf|E2Hg8GT)?=v=w8;T5(pqm0%@W1FR$~*-Eiet$|jWm2M5P23tccpEcCV zu!dQg)^ID!%C>T>5!Og+l$C4cS^3sztH3I>##m#mB5RyA-kM+)TbEfAtw~miHQAbC zU2c_HQ>|&%bgRsoVa>E=S>@JjYmPP7s<7r+^Z!>T-vQmmaWs73mSa`1{t_IuhkyE8kpyVH&!`^f=v1vy9#k;CK&xevLL%#t~B6}d0DAGtqy0C^yp zClOg76;dTNa+EBRB~m9>lVhYomPwPe$O^fJv`L3_$tpQcPLONKb>w>TAo5`H5b{v+ zF!FHn2=YksDDr6X81h*1IC2AdJUL08K%PjRM4n8ZLY_*VMxIWdL7qvTMV?KbL!L{X zN1jh!Kwd~*L|#l@LS9NWR096 zHChR zocx0PlKhJNn*4_Rmi&(Vp4>|QK>kSnME*?vLjFqrM*dFzLHW!j`IxoxX$KMBho@Mc+-|L*Gl^M{lOL(D&01&=1lN(GSy) z(2vrO(T~$l&`;7&S?{s_NIy+KLqBW%$@(+>9Q{1~0{tTW68$p$3jHem8vQ!`2K^@e z7X3E;4*f3u9{oQ30sSHU5&bd!3H>Sk8T~o^1^p%c75z2+4gD?s9sNDMmHvVLk^YJP znf`_TmHv(Xo&JOVlm3hTo8HDOmSO{pFv=L?%w~gZhz+w5Hp<4>CbpSvVO!a0>~w3) zx)TrySzE31**VtB*|^25Ve35WG|Ok_vOBZ$ z*j-qfO|bJ>hF!q6TMw}2txK$3>_T=|b`iUqHDZmj9c+?i*%ZsMyIVI}rnZG*^;~Pv`iQm3dWQ9U*05g1maXTpCTp=3b`5K@4(qa2cATAH*Rt!__3T0H!R#UI zq3mJo;p`FYk?c|I(d;qovFvf|2KIP%l0AVvkv)k$nLUL)l|79;ojrp+lRb+)n>~j; zmpzX?pS^&+kiCe#n7xF(l)a3-oV|j*lD*1$Cwnz}4SOwn9eX`{1A8NT6MHjz3yawr zJH>8fZ)I;|Z)fjdH?eoJcd>V~_ptY}_pzJVE$sd51MGwBL+r!sBkZH>W9;MX6YP`h zQ|#01GwieMbL{i%3+#*ROYF<+E9|T6YwYXn8|<6xTkPBHJM6pcd+ht{2keLJN9@P! zC+w%}XYA+f7wnhpSM1m9H|)3UckK7NA@T7XZ9ENSN1pdclHnVPxdeNZ+08E zc#02j!YOB*bDIzHAwJAU_$VLaoA_qFg>U7j@zeP>eg;32pT*DScjD*paegkpGe3{t zg{S!hKc8p#1$;Zdkl&SG#P7y;@JXKKQ#{A-&INb4@_X`2 z_`UcpUgW-s3I;snyZNPj55J7><@@-(tp{2^voh9@bpyZLdV+P5@3$UnJ<)m;Kftfx z2dyGM#1Hc${65wrtq<@k`7EE~SMmGu`|fvUg1?<<45@-U*dIsH9y81 ze3>_Si?8r&c$;^4m#^~U`~<(2U&pWK58@Bz58)5x591H#kKm8wkK&K!kKvEykK;G+ z$Mci?3H*utN&LzDDg3GYY5eK@8T^_2S^U}jIsCc&dHnhO1^k8lMf}D5CH$rQW&Gv* z75tU_Rs7ZbHT<>wb^P`G4g8J#P5jOLEj;FH{FL=1>zmg1t?yW0vz}~y%PLvlwZ36} z&-%9Yb$%m%D}Nh*JAVhiiNBM-i@%${hrgGm;;Xma+<3H!W;J@U*;=ks<;lJg-tJ!B8tBlf60W^b}L+gt3d_G$L%_BQ(r`%L>R`)vD8_Br;r zeXf0H`#k$DcG{k>&$l!71@?CPLi?`vMfTn79rmQ1wWsWyeRo^fjxFuH?b^!r>}k7T z&)65+_po=`_p~pu?`7|@i?(l<>@qK!9Iw6FtUEz5fYSG{6j(|uU0n*NREBoZw4I<> z!Zyw;7O-8!64xn8ySN-x+O4KtY%R8$wPVA@cD=b6RaUz-yBI)ePy~olm?edD^CP87 zy2Q4Yj%0L66poa1{}f5m3AxYLQ@#Jgi}B|M8E^HT^{sJ@^RNC zq`rjImXL&!I~?@T!NNXn&@b8nZV2#ub^segI3yr+`EG-8WcT_eMt4_Q%gYgLV|e!l z6vCOw!xB=^OBbRxHgF@dFmmboLO6!xOLt(;WqeOhtimaU08zr(rGmW&son$B@;%*p zqgoreY<{RD7S=)abrGP`a<$O5l;AJaQ|K`18nc7a8qJemSgn zIjnbi-+F-B#gpd3ad5%{37U&IR^a>lc3XC!yNs)s5qlY_FXK|>MBI3IO99)z)N12N zDkH7QIV>Yo8EFs1*#4!}=3>-dU2a6HU3-83IZy_daDw8xIDz=1^h@q zH_b5=PI(%pm1nT@u~fQrf&jT1caocQig?9*!zL&2@eulWi+#f;;gpbyBC@TRyrej< zXxQrnMZ;bvDB?Or5Q9{vEu@lmgsh72C5_f!QY~PqgdNp-TM|x?EP*5mkf@wNi5A32 zWh2vsRBE`gfek{=6nM3FjGVwh0)^R#kf0KTRJb8w7$c;@vS7}MkjmpV!#`f@Tg^{i zDx6)y)k{coNe$OF(BlL?a?JPbTK^SvrtRALQ!J!{11sT938~<2NVs!C77&Gx8v$_( zCj`k!FV7bZr6Y^$TpC^MyMx1v8%PrpuA`7MxaS#M3WO?T5l@svp?Z=otv~g60_4od z(mFyoof6q26WUOe9vt2G-@vs$lUCz3rA@W%Q9m0pQA42MS> zee2N1x<0{e=i$Uk4=+<5Me8MHB0@Y(c`RK#O)lPqY2^E~d7XsxLf(X@r5UZXI^A|_ zWvRxSKvT1iCfJ5+w&4UWaNh}MNDXw7kkcqy7fGE?P~uUTR=m|O5d`n)EJ< zo%NGA+PMRM4~6cb1PYQhTFu1{Y(&k_uGWlQL%!*A5=aLro55q39INE5_6$3PEEpvx zBv`*f7VtI}X83Bqj6w7c*dxLL3qv?ySqLYwM*=)n!8BW4muY}(3~@Uk9VZA-wm~?y zxKRxCSTIIXiTsr(sX@)TfYz^2l}TKyWV# zc+5h@uj_$2L71>58OEzo2>A6qfjYqq%6rBczJ9}d>I8{NoV=qmf<(D!Ffy}s#c(?p z4S_QgCN+P$S95wb1vRabbJ19|zKH~)h!9RtEcg0)D;51*G!i8|>5V&@472H6G=v&j z*FXrl$TnI!7mcoKYdynABNvgygs4#EqQOPn&mbtCTr}3FdUDaGe&v&kM%UF(LXLED z5vk{*;cBbfsC7E^Tr^luxCI&^L0A^KXsq7Pg^~L0cUSWD{A72iFLKckY7AUTdIb%J zTj~u~wN%U?)@I7N>Toi(8V&b`BOc{Ct1A&-graVWu~xf!w6q4;##c?Q3KyfV;*FkZ;Dd2emw^X7p^_&ft18NCyN(gbP-na3NSC zTnNGl7lJRsh2V>DAt)nUALm1uLbzZf3m2?9;es9!F6tQUT!;z?7Y%b4 ztvNT3^30=rAZj37h#CkNtU2MjNIzP2E*kbOgbaiWW}0x(Omi`W;JPReG|^l%KwKB` zy2xi2sgTd$0SOl!3>O^`*G0a&c#beu;G)^#qN(UkbtASrT3(LK1D2n0?O>%-Z?u~9s#-T9 zK{sND_1~huoOA4R41jSIyum(1DfK@u)*JLHCvrg#Aq6WK^J<5G(s51^`U`ln#S<{<=P?w zY4AiTZbqP_ZJzf0jGJ4Ix=YX~&X@*^3>);{u&_nV23D;#x{+N&CS0$Xpa;$0v(UO` z!ZE{yhG7D1$1e>uR|l@G+b!S(X)m={$FQLrt=g-|hLxo{sVvpWN~g|Rn(`sN9&Ah3 z5^}fpH((f-FpbUFNWizQ{A;|NB+|hb7>NS>Y z)q1Z{Z`KoZ<{b(H(9n_P|jJeRxkoAKszR$9Kg8eHWtB9|oXjLUdb%2YN6J zK<|VB1O>tX-w$uv=*Q59AVTPSNgLir5A|H9_ZCD0QV`x z82BzA^4(mgR%_1H%oz*=Kac(S{(j+s;tVm%5(a(&2`S_{OB)uL#sPg;*9U|LDmT>W z2iQVs6PCG7R9z>)$8Nu`TX>+DL(F@GK}i5uN#v^ivqPL6_H_#n)Om>6kuWG3^%r7@ zPI#c^L(HaxI@w@ogOHx^K>df9j0l5L3Aj`$<@%%$VsJ`$U><}4L{7t?EVi2U$|AS8 zv~dO~A`c9M5Ci@p2K++|rU(xYpbz1q&WaD)fON{DHOFeoDz%B9?~=xA~l!=UT~cglW$zwjVV9Qxo) zhkkNq(b)Gu`G-Cj51|h(bLe9z(gWit#B@d&1b7(&kqe`JHT`$RApJn(_C?E=)|O5) zT!-eqXn9Yw-d%?<6F|=85FpE;p11*0cl98J2ZmXQDV7jZ2w@Q9HA8io>vYgN@xW>e z1F&L4%q)ZfS|c7_1wVO}z}gFgpoml!aUbi=9w}h}`G^psD#8PsI`rWU3jKuLkk}Mn zNrKEvvQOraM+|*1z(OBl`=JlnQQ?8Z5(YCa&@kg7NpNIJX4}Fm<@?qIBQ*>He9VF% zcOp8GoDboFvlIpe56(%!+c*x!Z5V_C#zV33EdnDq3_^+HvVS}bLcHB*+f8a!G*4lteiwFahB zJ6>y=TCKjg)HOgR+MyRPE4P}f%Y%ApQ_*Yd$}lXht3zgWsOjYmoL=9+0RmG63451Y9dN;G%DU`Uda~0skljiLWw+Al$!?|7lif-uk=+Vd z?H(psyhK}KlFNfQyYMg^;-Ne1dEg#<9=Nj}9;Awe2Z3(k;pOv^ z7btY$=?&f7YP-4$Zzm?{ywJlzuYYhFJE!|Q(Yl3)LJG$ls$E-YM9bBMhB+hxo=zzs z{vZR0he-i(2q{3rNZ}U7R-4sYyHjbkYrWa|a5(uDgj_UivJ&PnjI7k!^;Q*5i%F~G zj<%x;LTFJY5JuK8QQw@TyLXEMc>5)00c8Mwi40_Z(@NB?H5;{~a1=M2ruqF4gST&3 zBk3R3?Y?1+tbbUyhmY15SKGB})G>-n{}>4{34GSGf2jILK zyJof3t?3__Xm@JW;YzE~f;eh#(<_~k+Oo!DqMswR=6W3w=y>i1a#wCg?n)ou-~`KQ zFd^i0Vme^J!oz^3hXG9w1D77!hh-3aSk8w-QM1*pHEMPA&8LI0UXOmQVT<6nNPz)G zQ53_Sm8eoP0yjre6oqMM_%H}V8mzWkFxgjQh9k?ZX002wucOr#ze5V&rHy(U z`3d%q6h)7()H=0p(rtWcDIlvY1!T*mfDf3YfMBB(#hF3)few_9gs88SjvA?5vsynM zHEPXDZKQT>z0<8VyY;9sTyJ)DSiP~(?Nz%gqq?`7+9=13mhj zBa8<<3$5-F)K*uj%~4#s+Fa<=VBP9MV|8T=)>~*8EC5wqXlMkzMsIBec}S_i>zAsK zx2w&CMyp~z^^&Rpw?KhAELCBe;pj>q6xiQVf!!_@e3c?qp=>vf_Q8SuBo%zQCRJg^ zZg=~}!Jd~2g3D4ta8N4nRi%O;p;X}XO9jrpRN(weHDi2Rso)DCsb)gE(d{fn)fzMZ z+SOxFYJIIrA=jCqhROa)1@@6trJ3Q@O0BuD+Gy0eL-pqI_1Xq-Z}@1v(Wq71wMusg zstYZXER+hOmr{XuFICaEyX|_kh$lh$9<-|cC>0)j1;)Bm5cQM_KG2a0FNlIKQKW(|Pox61B^8<^3QZOTQE#b0eMtpTZK=>i zQD|x?P?u7HT9gXZrc|I7r9zWOp-H45${`hGb)kqRCssleHl3LhH<*&wOF z*^mmdHc~+#Oe)k`3e>VxXmTltLQ4fvY^fkREfqv{q=KM_RFGYg3bHOzfqy2|w9%(h zL9|LL$ht@c*%PTCASV@MU!;OZQ7VW&Nd-}Vso?RI3X;-NK{iS%$Yx0e(RisKswEW! z>!bo_QYwfxNd+MisZe_>i26tc;RLC`@sSD~ajD>25~<*^mkN^mQo*+&QbATgDu~`o z1yOpbAnPL)1OcU*HqmjZAZjKRC`GA2xkv?BH>n^?Bo#!-q=IadRFI{T3cl=+3bIsE zL3TSH>n^fBo$gW3ZiII zLAXXLw1yM}4x|FJO)ALRNQI`k0tZbh2m?t44x?0%HIxdnf>J@YQ7Uk7rGl`MR1nRS z3iXddqeh{Tp}=gE3bH9up=+SPER_n(PpKfABNYT*LDo_# z_~1z@FlVKLY?Aa)jvn4G&rw62<$9xbG^*4_>&@f62I;Ie`O(^P)TmMY59_Q%)f%ld zRu|Y(Eoyh|YCT$RHLD}bs~uE>wM~8X#Dtv2t2T{Sa~iL#gICCLriW^sZhcw*U>oQ^ zoZ%pI^T_NxJ{Ec0WZo$aRu>v8?fPYR(-u2;(mFwKl7Ln&tpP7j|uTSCbaXI(9UBbJCF8W9uwHeC4t-% z$UTvlVx-#YF4P*W6FuCRtj=SyI*&=}JSMI4n5@oYvO15+={zQ@^O#i5!}Bfjn1sz^ z5;l)X);uOn^O!8nW0EwFNzyzfN%NS*%zKWBbL2%q+oM>Y*7Y%vMr(P&v_K@-LO8!~ zj_j+K`s!X^y{PMBAghflxqVQouO6<~9nn{J*6Y$x3lz!zU3j_5b!{C%(v<(PSet`PKci*hagBNgh-_m z;@`l8_;F>3Uv`K12Ld7f(M5WgsNGsp%j@$D zGb+mqTN8Bf$!BGG;fy=Ia z_8qrdFWFk(dP$dt95Yf+$lJQ1wLYFu0OLtZkB^ucK+5L6`uf}i8_Z2wdc2Q6W@cr% zpUR|5Lv8|jBxIhpv8f@pCbtj%2uw@v4n2k@b1ajEpq1vyWMjIAEz`i(*Z zCJ>O#4+Qw(fq*Q1AmHn>KtKQ~z@Sqg{Ji<~n$&H#S)2QQzUKe;KmD7KhpYu_fh_4M zY}>z%uh*W3kh3{2m4>un-|BKr)S zX59EEKXlb>?2gA5&CMT8pFA=f4{VR#IcN=9l}ct|d}1QD=3=XqDW9@ZR%za! zic{G*J%4m69>}Jv>G+ldF}?7rQx~O%OF<=wc`!Q>lkIZ{?>jq@nHWDgn~o10n4O4s z&5ftyg023I0S?EH8-PV2E@UGsF! zn;)N_o0}VtQ3B5b&&6Z9b#OKvk7fM1sd#fX9d9u#N#8`R z%E?R=Zz|2FPtK>~O&LEk6>rJzJv_TctL3>1;;~xh+NpSJcJCFldk;b9_{7`=@#&`j zwCtL-xpZW9ZS&?*oQnK-^Q1nr@xXR}ZB+j=Hn81~Q)gz!5DoX0(Fs`QzY>(niJ0|Ah(bDdCth9SR9@r?V|02LO%Q`(%j#H)By5v+Ul`{9{ z^sKdJ4Frd0?fN*O)HP$*yr-j%U|wU7nj=8`R~Uvui`T zyjwPt@IB`9dk@cM(z!Twe|a-ZGyqW#ZJlJ`0h)dRcu|bJ=)mGM?Ho72iFZPVY|t7b#|WpAk%ozbX{hYbuu*{~Fp*Nqk;#6?Q*C0+!{>N8B_giPa)u^)a zC7HsSOr5b~Dt5E!o$1~GWk<19iV9P)%I416ITd^V8)%#=RSHw_barjPI&*tEm)@-} zemt;!&&iX!GrKcUdUj!4Kdc%5l$T1Kv2DjxT*$`OnK9iS)9nT%9x6@NPUbS{^v;uo zsd#36IGuwvVw&+aR63s5Z^f=FW^Wuwll1tF0~eBW=6wBr4wljn&d0QL`)fF# zj#HDME(S{T)l5uEQT4!VJWz_pV^W&e@7L|;M45CtrWaVbz#k7-66 z`3y=XqmS`Dk%Cepnt841Z_l_yl$T^P>73?TcyKnob1t`*OKqE+itp9yKG5I2tJiIy zlc^hEirKg@nT%{$)X&BfwPHM8>|NSWL{!a(v|8n{?-}L*kGgBC!=#%%v9MTwQW+Y4v_5LR`=QWAA zkKUGdXJhLOxSXdfeGgC1#qN%&_|pH)wAtdy?o&z?$|o&(uaDbsU>h6yI8;)5DSR}N_y zT{)~_bmfSKDNats_t7xAa;1jRm01m=D{~sAG&vPtrD1gCz8XeX?x$gN<^CEboScdu zpkZ|7ff`0v<~5A2L>k7QoQfATjILBPjILBQjIPwO@x?thAJvWclF6xf(bVoWITbIN z^K|iKylZkQu4m(W^q{ZS4Fi45)HHO%)HLXFHs09-ZR&;rZJC+|T`@HcdQCRIXAiWk z8wRvvY8td_Y8rGkd*cutNDME3G9IkO=5}pr4=(V7{F1?}eIDbZJ8nwd_L!JHX>GtN-^i-mDwnllGrv3fKQVH9pa1{> literal 0 HcmV?d00001 diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index b3c8400b3..ccb422f20 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -190,6 +190,11 @@ pub const Shaper = struct { // Reset the buffer for our current run self.shaper.hb_buf.reset(); self.shaper.hb_buf.setContentType(.unicode); + + // We don't support RTL text because RTL in terminals is messy. + // Its something we want to improve. For now, we force LTR because + // our renderers assume a strictly increasing X value. + self.shaper.hb_buf.setDirection(.ltr); } pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { @@ -453,6 +458,46 @@ test "shape monaspace ligs" { } } +// Ghostty doesn't currently support RTL and our renderers assume +// that cells are in strict LTR order. This means that we need to +// force RTL text to be LTR for rendering. This test ensures that +// we are correctly forcing RTL text to be LTR. +test "shape arabic forced LTR" { + const testing = std.testing; + const alloc = testing.allocator; + + var testdata = try testShaperWithFont(alloc, .arabic); + defer testdata.deinit(); + + var screen = try terminal.Screen.init(alloc, 120, 30, 0); + defer screen.deinit(); + try screen.testWriteString(@embedFile("testdata/arabic.txt")); + + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + null, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + try testing.expectEqual(@as(usize, 25), run.cells); + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 25), cells.len); + + var x: u16 = cells[0].x; + for (cells[1..]) |cell| { + try testing.expectEqual(x + 1, cell.x); + x = cell.x; + } + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape emoji width" { const testing = std.testing; const alloc = testing.allocator; @@ -1146,6 +1191,7 @@ const TestShaper = struct { const TestFont = enum { inconsolata, monaspace_neon, + arabic, }; /// Helper to return a fully initialized shaper. @@ -1159,6 +1205,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { const testFont = switch (font_req) { .inconsolata => font.embedded.inconsolata, .monaspace_neon => font.embedded.monaspace_neon, + .arabic => font.embedded.arabic, }; var lib = try Library.init(); diff --git a/src/font/shaper/testdata/arabic.txt b/src/font/shaper/testdata/arabic.txt new file mode 100644 index 000000000..d450c7623 --- /dev/null +++ b/src/font/shaper/testdata/arabic.txt @@ -0,0 +1,3 @@ +غريبه لاني عربي أبا عن جد +واتكلم الانجليزية بطلاقة اكثر من ٢٥ سنه +ومع هذا اجد العربيه افضل لان فيها الكثير من المفردات الاكثر دقه بالوصف From 89f7f882869661ddccd0bf92892ce185bfafaa76 Mon Sep 17 00:00:00 2001 From: FineFindus Date: Sun, 3 Nov 2024 13:23:08 +0100 Subject: [PATCH 24/46] apprt/adw: reapply headerbar colors Fixes a regression in ca42b4ca1c7a9319b7fbc17cabab0cbb4120f0fb that caused the headerbar to no longer use the same color as the ghostty-theme. --- src/apprt/gtk/App.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 017fc0ed4..e6bf24bd1 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -929,6 +929,13 @@ fn loadRuntimeCss( \\ --headerbar-bg-color: rgb({d},{d},{d}); \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha); \\}} + \\windowhandle {{ + \\ background-color: var(--headerbar-bg-color); + \\ color: var(--headerbar-fg-color); + \\}} + \\windowhandle:backdrop {{ + \\ background-color: var(--headerbar-backdrop-color); + \\}} , .{ headerbar_foreground.r, headerbar_foreground.g, From 038b3dec797dc591b8ba9b73295fee77b2ee6405 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 3 Nov 2024 15:05:57 -0800 Subject: [PATCH 25/46] update zig-objc This fixes a hack we had around apple paths since we do this now upstream in zig-objc. This also adds in support for NSFastEnumeration needed for #2586 --- build.zig | 9 +-------- build.zig.zon | 4 ++-- nix/zigCacheHash.nix | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/build.zig b/build.zig index 4e7a072e6..cabccbb05 100644 --- a/build.zig +++ b/build.zig @@ -1227,14 +1227,7 @@ fn addDeps( .optimize = optimize, }); - // This is a bit of a hack that should probably be fixed upstream - // in zig-objc, but we need to add the apple SDK paths to the - // zig-objc module so that it can find the objc runtime headers. - const module = objc_dep.module("objc"); - module.resolved_target = step.root_module.resolved_target; - try @import("apple_sdk").addPaths(b, module); - step.root_module.addImport("objc", module); - + step.root_module.addImport("objc", objc_dep.module("objc")); step.root_module.addImport("macos", macos_dep.module("macos")); step.linkLibrary(macos_dep.artifact("macos")); try static_libs.append(macos_dep.artifact("macos").getEmittedBin()); diff --git a/build.zig.zon b/build.zig.zon index b0c409778..c4e02afd9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,8 +14,8 @@ .lazy = true, }, .zig_objc = .{ - .url = "https://github.com/mitchellh/zig-objc/archive/fe5ac419530cf800294369d996133fe9cd067aec.tar.gz", - .hash = "122034b3e15d582d8d101a7713e5f13c872b8b8eb6d9cb47515b8e34ee75e122630d", + .url = "https://github.com/mitchellh/zig-objc/archive/7e1acc8a45227fcdf0163cc1d9360a4f8b70e33e.tar.gz", + .hash = "122060402a6931a32ee40538291fd87d3eee7b43739c7ac63573f4f5b8deea7ce214", }, .zig_js = .{ .url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz", diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index fc57ed034..c3ac19cf1 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-5LBZAExb4PJefW+M0Eo+TcoszhBdIFTGBOv6lte5L0Q=" +"sha256-SmBLDC1DqAcccoWoHyBsHKVqNr3sBYW1b+YwAXhFZyY=" From e5f9f222b20a2cb4b21ce4c7967f638193f903f2 Mon Sep 17 00:00:00 2001 From: Emily Date: Sun, 3 Nov 2024 21:54:38 +0000 Subject: [PATCH 26/46] renderer/metal: use `release()` consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’m not sure why this variation was here previously – maybe it predated the introduction of `release()`? --- src/renderer/Metal.zig | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f586d22b4..990425517 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -177,7 +177,7 @@ pub const GPUState = struct { pub fn init() !GPUState { const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); - errdefer queue.msgSend(void, objc.sel("release"), .{}); + errdefer queue.release(); var instance = try InstanceBuffer.initFill(device, &.{ 0, 1, 3, // Top-left triangle @@ -206,7 +206,7 @@ pub const GPUState = struct { for (0..BufferCount) |_| self.frame_sema.wait(); for (&self.frames) |*frame| frame.deinit(); self.instance.deinit(); - self.queue.msgSend(void, objc.sel("release"), .{}); + self.queue.release(); } /// Get the next frame state to draw to. This will wait on the @@ -269,13 +269,13 @@ pub const FrameState = struct { .size = 8, .format = .grayscale, }); - errdefer deinitMTLResource(grayscale); + errdefer grayscale.release(); const color = try initAtlasTexture(device, &.{ .data = undefined, .size = 8, .format = .rgba, }); - errdefer deinitMTLResource(color); + errdefer color.release(); return .{ .uniforms = uniforms, @@ -290,8 +290,8 @@ pub const FrameState = struct { self.uniforms.deinit(); self.cells.deinit(); self.cells_bg.deinit(); - deinitMTLResource(self.grayscale); - deinitMTLResource(self.color); + self.grayscale.release(); + self.color.release(); } }; @@ -319,8 +319,8 @@ pub const CustomShaderState = struct { } pub fn deinit(self: *CustomShaderState) void { - deinitMTLResource(self.front_texture); - deinitMTLResource(self.back_texture); + self.front_texture.release(); + self.back_texture.release(); self.sampler.deinit(); } }; @@ -2057,8 +2057,8 @@ pub fn setScreenSize( // Only free our previous texture if this isn't our first // time setting the custom shader state. if (state.uniforms.resolution[0] > 0) { - deinitMTLResource(state.front_texture); - deinitMTLResource(state.back_texture); + state.front_texture.release(); + state.back_texture.release(); } state.uniforms.resolution = .{ @@ -2982,7 +2982,7 @@ fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *obj const width = texture.getProperty(c_ulong, "width"); if (atlas.size > width) { // Free our old texture - deinitMTLResource(texture.*); + texture.*.release(); // Reallocate texture.* = try initAtlasTexture(device, atlas); @@ -3049,12 +3049,6 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object return objc.Object.fromId(id); } -/// Deinitialize a metal resource (buffer, texture, etc.) and free the -/// memory associated with it. -fn deinitMTLResource(obj: objc.Object) void { - obj.msgSend(void, objc.sel("release"), .{}); -} - test { _ = mtl_cell; } From 9c8b00f87d65c075d7e23c9b90fb6f209affdfe2 Mon Sep 17 00:00:00 2001 From: Emily Date: Sat, 2 Nov 2024 17:48:56 +0000 Subject: [PATCH 27/46] renderer/metal: release device on `deinit()` --- src/renderer/Metal.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 990425517..6aac83419 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -207,6 +207,7 @@ pub const GPUState = struct { for (&self.frames) |*frame| frame.deinit(); self.instance.deinit(); self.queue.release(); + self.device.release(); } /// Get the next frame state to draw to. This will wait on the From 20a77123d43841fc873dc3b57d337ae6cad51868 Mon Sep 17 00:00:00 2001 From: Emily Date: Sat, 2 Nov 2024 17:48:56 +0000 Subject: [PATCH 28/46] =?UTF-8?q?renderer/metal:=20prefer=20low=E2=80=90po?= =?UTF-8?q?wer=20GPUs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Intel MacBook Pro laptops have both an integrated and discrete GPU and support automatically switching between them. The system uses the integrated GPU by default, but the default Metal device on those systems is the discrete GPU. This means that Metal‐using applications activate it by default, presumably as the intended audience is high‐performance graphics applications. This is unfortunate for productivity applications like terminals, however, as the discrete GPU decreases battery life and worsens the thermal throttling problems these machines have always had. Prefer to use an integrated GPU when present and not using an external GPU. The behaviour should be unchanged on Apple Silicon, as the platform only supports one GPU. I have confirmed that the resulting app runs, works, and doesn’t activate the AMD GPU on my MacBook Pro, but have not done any measurements of the resulting performance impact. If it is considered sufficiently noticeable, a GPU preference setting could be added. See , , , and for discussion, measurements, and changes relating to this issue in the Zed project. The logic implemented here reflects what Zed ended up settling on. The [Metal documentation] recommends using `MTLCopyAllDevicesWithObserver` to receive notifications of when the list of available GPUs changes, such as when [external GPUs are connected or disconnected]. I didn’t bother implementing that because it seemed like a lot of fussy work to deal with migrating everything to a new GPU on the fly just for a niche use case on a legacy platform. Zed doesn’t implement it and I haven’t heard about anyone complaining that their computer caught fire when they unplugged an external GPU, so hopefully it’s fine. [Metal documentation]: https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/multi-gpu_systems/finding_multiple_gpus_on_an_intel-based_mac [external GPUs are connected or disconnected]: https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/multi-gpu_systems/handling_external_gpu_additions_and_removals Closes: #2572 --- src/renderer/Metal.zig | 21 ++++++++++++++++++++- src/renderer/metal/api.zig | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6aac83419..cb0f5a3de 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -175,7 +175,7 @@ pub const GPUState = struct { instance: InstanceBuffer, // MTLBuffer pub fn init() !GPUState { - const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + const device = try chooseDevice(); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); errdefer queue.release(); @@ -200,6 +200,25 @@ pub const GPUState = struct { return result; } + fn chooseDevice() error{NoMetalDevice}!objc.Object { + const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); + defer devices.release(); + var chosen_device: ?objc.Object = null; + var iter = devices.iterate(); + while (iter.next()) |device| { + // We want a GPU that’s connected to a display. + if (device.getProperty(bool, "isHeadless")) continue; + chosen_device = device; + // If the user has an eGPU plugged in, they probably want + // to use it. Otherwise, integrated GPUs are better for + // battery life and thermals. + if (device.getProperty(bool, "isRemovable") or + device.getProperty(bool, "isLowPower")) break; + } + const device = chosen_device orelse return error.NoMetalDevice; + return device.retain(); + } + pub fn deinit(self: *GPUState) void { // Wait for all of our inflight draws to complete so that // we can cleanly deinit our GPU state. diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 0781812ac..bd4f407cd 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -175,4 +175,4 @@ pub const MTLSize = extern struct { depth: c_ulong, }; -pub extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; +pub extern "c" fn MTLCopyAllDevices() ?*anyopaque; From c7413bcf571b27c5da561d101c43e2ca375ab875 Mon Sep 17 00:00:00 2001 From: Sam Atman Date: Sun, 3 Nov 2024 17:33:35 -1000 Subject: [PATCH 29/46] ECMA 48 is de jure: as stated by law I am actually not sure if this was meant as a sly pun. It definitely works that way, terminals are _de jour_ meaning _of the day_, but the contrastive with _de facto_ makes me lean toward typo / malaprop. The thing is it's a good pun, and I almost let it be for that reason. But I lean towards unintended, so here's a patch, feel free to close it if I read it wrong, er, right? Y'know. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b1077795..47035ead6 100644 --- a/README.md +++ b/README.md @@ -375,9 +375,9 @@ test cases. We believe Ghostty is one of the most compliant terminal emulators available. -Terminal behavior is partially a dejour standard +Terminal behavior is partially a de jure standard (i.e. [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/)) -but mostly a defacto standard as defined by popular terminal emulators +but mostly a de facto standard as defined by popular terminal emulators worldwide. Ghostty takes the approach that our behavior is defined by (1) standards, if available, (2) xterm, if the feature exists, (3) other popular terminals, in that order. This defines what the Ghostty project From 21f5a2aa51db283052b47e8f5112375079823164 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 4 Nov 2024 09:37:12 -0800 Subject: [PATCH 30/46] README: syntax highlight the config --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b1077795..20b1d4cb3 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to The file format is documented below as an example: -``` +```ini # The syntax is "key = value". The whitespace around the equals doesn't matter. background = 282c34 foreground= ffffff From f8d2ac6c8cd2d75e8c3a4dbeb077d086f6655773 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 4 Nov 2024 09:51:45 -0800 Subject: [PATCH 31/46] update zig-objc removes usingnamespace --- 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 c4e02afd9..b475b25ca 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,8 +14,8 @@ .lazy = true, }, .zig_objc = .{ - .url = "https://github.com/mitchellh/zig-objc/archive/7e1acc8a45227fcdf0163cc1d9360a4f8b70e33e.tar.gz", - .hash = "122060402a6931a32ee40538291fd87d3eee7b43739c7ac63573f4f5b8deea7ce214", + .url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz", + .hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634", }, .zig_js = .{ .url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz", diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index c3ac19cf1..65d26e146 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-SmBLDC1DqAcccoWoHyBsHKVqNr3sBYW1b+YwAXhFZyY=" +"sha256-dNGDbVaPxDbIrDUkDwjzeRHHVcX4KnWKciXiTp1c7lE=" From 03bb16fcec53ba884c04e725a48fe828b907308d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 16:54:38 -0500 Subject: [PATCH 32/46] Move hostname helpers to src/os/hostname.zig --- src/os/hostname.zig | 44 ++++++++++++++++++++++++++++++++ src/termio/stream_handler.zig | 47 +++-------------------------------- 2 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 src/os/hostname.zig diff --git a/src/os/hostname.zig b/src/os/hostname.zig new file mode 100644 index 000000000..0bed2d547 --- /dev/null +++ b/src/os/hostname.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const posix = std.posix; + +pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { + // Get the raw string of the URI. Its unclear to me if the various + // tags of this enum guarantee no percent-encoding so we just + // check all of it. This isn't a performance critical path. + const host_component = uri.host orelse return error.NoHostnameInUri; + const host = switch (host_component) { + .raw => |v| v, + .percent_encoded => |v| v, + }; + + // When the "Private Wi-Fi address" setting is toggled on macOS the hostname + // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. + // The URI will be parsed as if the last set o digit is a port, so we need to + // make sure that part is included when it's set. + if (uri.port) |port| { + var fbs = std.io.fixedBufferStream(buf); + std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { + error.NoSpaceLeft => return error.NoSpaceLeft, + else => unreachable, + }; + + return fbs.getWritten(); + } + + return host; +} + +pub fn isLocalHostname(hostname: []const u8) !bool { + // A 'localhost' hostname is always considered local. + if (std.mem.eql(u8, "localhost", hostname)) { + return true; + } + + // If hostname is not "localhost" it must match our hostname. + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const ourHostname = posix.gethostname(&buf) catch |err| { + return err; + }; + + return std.mem.eql(u8, hostname, ourHostname); +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 8db67f66c..c97c533ea 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -5,6 +5,7 @@ const xev = @import("xev"); const apprt = @import("../apprt.zig"); const build_config = @import("../build_config.zig"); const configpkg = @import("../config.zig"); +const hostname = @import("../os/hostname.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); const terminal = @import("../terminal/main.zig"); @@ -1030,48 +1031,6 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } - pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return error.NoHostnameInUri; - const host = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. - // The URI will be parsed as if the last set o digit is a port, so we need to - // make sure that part is included when it's set. - if (uri.port) |port| { - var fbs = std.io.fixedBufferStream(buf); - std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { - error.NoSpaceLeft => return error.NoSpaceLeft, - else => unreachable, - }; - - return fbs.getWritten(); - } - - return host; - } - - pub fn isLocalHostname(hostname: []const u8) !bool { - // A 'localhost' hostname is always considered local. - if (std.mem.eql(u8, "localhost", hostname)) { - return true; - } - - // If hostname is not "localhost" it must match our hostname. - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = posix.gethostname(&buf) catch |err| { - return err; - }; - - return std.mem.eql(u8, hostname, ourHostname); - } - pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { if (builtin.os.tag == .windows) { log.warn("reportPwd unimplemented on windows", .{}); @@ -1097,7 +1056,7 @@ pub const StreamHandler = struct { // Make sure there is space for a max length hostname + the max number of digits. var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - const hostname = bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { + const hostname_from_uri = hostname.bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { error.NoHostnameInUri => { log.warn("OSC 7 uri must contain a hostname: {}", .{err}); return; @@ -1111,7 +1070,7 @@ pub const StreamHandler = struct { // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = isLocalHostname(hostname) catch |err| switch (err) { + const host_valid = hostname.isLocalHostname(hostname_from_uri) catch |err| switch (err) { error.PermissionDenied, error.Unexpected => { log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); return; From 78abd051a21cf493fb353dd72c651795dbf22401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 17:13:12 -0500 Subject: [PATCH 33/46] os/hostname: test isLocalHostname --- src/os/hostname.zig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 0bed2d547..dcc802b65 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -42,3 +42,18 @@ pub fn isLocalHostname(hostname: []const u8) !bool { return std.mem.eql(u8, hostname, ourHostname); } + +test "isLocalHostname returns true when provided hostname is localhost" { + try std.testing.expect(try isLocalHostname("localhost")); +} + +test "isLocalHostname returns true when hostname is local" { + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const localHostname = try posix.gethostname(&buf); + + try std.testing.expect(try isLocalHostname(localHostname)); +} + +test "isLocalHostname returns false when hostname is not local" { + try std.testing.expectEqual(false, try isLocalHostname("not-the-local-hostname")); +} From 9ae6806e30e1d83dfa697ed26bac39db2dbad912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 17:25:00 -0500 Subject: [PATCH 34/46] os/hostname: test bufPrintHostnameFromFileUri Note that this includes some failing tests because I want to make the uri handling better and more specific. It's a little bit too general right now so I want to lock it down to: 1. macOS only; and 2. valid mac address values because that's how the macOS private Wi-Fi address thing works; randomizes your mac address and sets that as your hostname. --- src/os/hostname.zig | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index dcc802b65..31738a90f 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -43,6 +43,50 @@ pub fn isLocalHostname(hostname: []const u8) !bool { return std.mem.eql(u8, hostname, ourHostname); } +test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { + const uri = try std.Uri.parse("file://localhost/"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectEqualStrings("localhost", actual); +} + +test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { + const uri = try std.Uri.parse("file://12:34:56:78:90:12"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); +} + +test "bufPrintHostnameFromFileUri returns only hostname when there is a port component in the URI" { + // First: try with a non-2-digit port, to test general port handling. + const four_port_uri = try std.Uri.parse("file://has-a-port:1234"); + + var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; + const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri); + + try std.testing.expectEqualStrings("has-a-port", four_port_actual); + + // Second: try with a 2-digit port to test mac-address handling. + const two_port_uri = try std.Uri.parse("file://has-a-port:12"); + + var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; + const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri); + + try std.testing.expectEqualStrings("has-a-port", two_port_actual); + + // Third: try with a mac-address that has a port-component added to it to test mac-address handling. + const mac_with_port_uri = try std.Uri.parse("file://12:34:56:78:90:12:1234"); + + var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; + const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri); + + try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); +} + test "isLocalHostname returns true when provided hostname is localhost" { try std.testing.expect(try isLocalHostname("localhost")); } From e85b11403145ea01aff8efcbbf5e1480334f5ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 18:58:50 -0500 Subject: [PATCH 35/46] os/hostname: add better validation for mac-address hostnames --- src/os/hostname.zig | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 31738a90f..81ef0e6e3 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -12,10 +12,26 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { }; // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. - // The URI will be parsed as if the last set o digit is a port, so we need to - // make sure that part is included when it's set. + // is set to a random mac address, e.g. '12:34:56:78:90:ab'. + // The URI will be parsed as if the last set of digits is a port number, so + // we need to make sure that part is included when it's set. + + // We're only interested in special port handling when the current hostname is a + // partial MAC address that's potentially missing the last component. + // If that's not the case we just return the plain URI hostname directly. + // NOTE: This implementation is not sufficient to verify a valid mac address, but + // it's probably sufficient for this specific purpose. + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) { + return host; + } + if (uri.port) |port| { + // If the port is not a 2-digit number we're not looking at a partial MAC-address, + // and instead just a regular port so we return the plain URI hostname. + if (port < 10 or port > 99) { + return host; + } + var fbs = std.io.fixedBufferStream(buf); std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { error.NoSpaceLeft => return error.NoSpaceLeft, From 9c2f260351f9445133e2f7dde5cef90975cb7268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 19:20:09 -0500 Subject: [PATCH 36/46] os/hostname: add and use explicit error structs --- src/os/hostname.zig | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 81ef0e6e3..e05586167 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,11 +1,21 @@ const std = @import("std"); const posix = std.posix; -pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { +const HostnameParsingError = error{ + NoHostnameInUri, + NoSpaceLeft, +}; + +const LocalHostnameValidationError = error{ + PermissionDenied, + Unexpected, +}; + +pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) HostnameParsingError![]const u8 { // Get the raw string of the URI. Its unclear to me if the various // tags of this enum guarantee no percent-encoding so we just // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return error.NoHostnameInUri; + const host_component = uri.host orelse return HostnameParsingError.NoHostnameInUri; const host = switch (host_component) { .raw => |v| v, .percent_encoded => |v| v, @@ -34,7 +44,7 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { var fbs = std.io.fixedBufferStream(buf); std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { - error.NoSpaceLeft => return error.NoSpaceLeft, + error.NoSpaceLeft => return HostnameParsingError.NoSpaceLeft, else => unreachable, }; @@ -44,7 +54,7 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { return host; } -pub fn isLocalHostname(hostname: []const u8) !bool { +pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. if (std.mem.eql(u8, "localhost", hostname)) { return true; @@ -52,8 +62,9 @@ pub fn isLocalHostname(hostname: []const u8) !bool { // If hostname is not "localhost" it must match our hostname. var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = posix.gethostname(&buf) catch |err| { - return err; + const ourHostname = posix.gethostname(&buf) catch |err| switch (err) { + error.PermissionDenied => return LocalHostnameValidationError.PermissionDenied, + error.Unexpected => return LocalHostnameValidationError.Unexpected, }; return std.mem.eql(u8, hostname, ourHostname); @@ -103,6 +114,24 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); } +test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is missing from uri" { + const uri = try std.Uri.parse("file:///"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual); +} + +test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer has insufficient size" { + const uri = try std.Uri.parse("file://12:34:56:78:90:12/"); + + var buf: [5]u8 = undefined; + const actual = bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual); +} + test "isLocalHostname returns true when provided hostname is localhost" { try std.testing.expect(try isLocalHostname("localhost")); } From 16e8095d52ee3579488fa04097b3c12ffa46baec Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Mon, 4 Nov 2024 19:58:50 -0600 Subject: [PATCH 37/46] feat: install neovim plugin $prefix/share/vim/vimfiles is not always read by Neovim. It is distribution dependent. $prefix/share/nvim/site is a default path for Neovim however. Signed-off-by: Tristan Partin --- build.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/build.zig b/build.zig index cabccbb05..fb74de340 100644 --- a/build.zig +++ b/build.zig @@ -499,6 +499,22 @@ pub fn build(b: *std.Build) !void { }); } + // Neovim plugin + // This is just a copy-paste of the Vim plugin, but using a Neovim subdir. + // By default, Neovim doesn't look inside share/vim/vimfiles. Some distros + // configure it to do that however. Fedora, does not as a counterexample. + { + const wf = b.addWriteFiles(); + _ = wf.add("syntax/ghostty.vim", config_vim.syntax); + _ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect); + _ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin); + b.installDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/nvim/site", + }); + } + // Documentation if (emit_docs) { try buildDocumentation(b, config); From 4a263f43afa946047a4f967799af5c12cc0a6501 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 10:30:48 -0800 Subject: [PATCH 38/46] stylistic changes --- src/os/hostname.zig | 80 ++++++++++++++++------------------- src/os/main.zig | 5 +++ src/termio/stream_handler.zig | 17 +++++--- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index e05586167..6956ed71f 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,22 +1,21 @@ const std = @import("std"); const posix = std.posix; -const HostnameParsingError = error{ +pub const HostnameParsingError = error{ NoHostnameInUri, NoSpaceLeft, }; -const LocalHostnameValidationError = error{ - PermissionDenied, - Unexpected, -}; - -pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) HostnameParsingError![]const u8 { +/// Print the hostname from a file URI into a buffer. +pub fn bufPrintHostnameFromFileUri( + buf: []u8, + uri: std.Uri, +) HostnameParsingError![]const u8 { // Get the raw string of the URI. Its unclear to me if the various // tags of this enum guarantee no percent-encoding so we just // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return HostnameParsingError.NoHostnameInUri; - const host = switch (host_component) { + const host_component = uri.host orelse return error.NoHostnameInUri; + const host: []const u8 = switch (host_component) { .raw => |v| v, .percent_encoded => |v| v, }; @@ -31,42 +30,42 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) HostnameParsingError // If that's not the case we just return the plain URI hostname directly. // NOTE: This implementation is not sufficient to verify a valid mac address, but // it's probably sufficient for this specific purpose. - if (host.len != 14 or std.mem.count(u8, host, ":") != 4) { - return host; - } + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) return host; - if (uri.port) |port| { - // If the port is not a 2-digit number we're not looking at a partial MAC-address, - // and instead just a regular port so we return the plain URI hostname. - if (port < 10 or port > 99) { - return host; - } + // If we don't have a port then we can return the hostname as-is because + // it's not a partial MAC-address. + const port = uri.port orelse return host; - var fbs = std.io.fixedBufferStream(buf); - std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { - error.NoSpaceLeft => return HostnameParsingError.NoSpaceLeft, - else => unreachable, - }; + // If the port is not a 2-digit number we're not looking at a partial + // MAC-address, and instead just a regular port so we return the plain + // URI hostname. + if (port < 10 or port > 99) return host; - return fbs.getWritten(); - } + var fbs = std.io.fixedBufferStream(buf); + try std.fmt.format( + fbs.writer(), + "{s}:{d}", + .{ host, port }, + ); - return host; + return fbs.getWritten(); } +pub const LocalHostnameValidationError = error{ + PermissionDenied, + Unexpected, +}; + +/// Checks if a hostname is local to the current machine. This matches +/// both "localhost" and the current hostname of the machine (as returned +/// by `gethostname`). pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. - if (std.mem.eql(u8, "localhost", hostname)) { - return true; - } + if (std.mem.eql(u8, "localhost", hostname)) return true; // If hostname is not "localhost" it must match our hostname. var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = posix.gethostname(&buf) catch |err| switch (err) { - error.PermissionDenied => return LocalHostnameValidationError.PermissionDenied, - error.Unexpected => return LocalHostnameValidationError.Unexpected, - }; - + const ourHostname = try posix.gethostname(&buf); return std.mem.eql(u8, hostname, ourHostname); } @@ -75,7 +74,6 @@ test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("localhost", actual); } @@ -84,7 +82,6 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); } @@ -94,7 +91,6 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri); - try std.testing.expectEqualStrings("has-a-port", four_port_actual); // Second: try with a 2-digit port to test mac-address handling. @@ -102,7 +98,6 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri); - try std.testing.expectEqualStrings("has-a-port", two_port_actual); // Third: try with a mac-address that has a port-component added to it to test mac-address handling. @@ -110,7 +105,6 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); } @@ -119,7 +113,6 @@ test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is var buf: [posix.HOST_NAME_MAX]u8 = undefined; const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual); } @@ -128,7 +121,6 @@ test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer var buf: [5]u8 = undefined; const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual); } @@ -139,10 +131,12 @@ test "isLocalHostname returns true when provided hostname is localhost" { test "isLocalHostname returns true when hostname is local" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const localHostname = try posix.gethostname(&buf); - try std.testing.expect(try isLocalHostname(localHostname)); } test "isLocalHostname returns false when hostname is not local" { - try std.testing.expectEqual(false, try isLocalHostname("not-the-local-hostname")); + try std.testing.expectEqual( + false, + try isLocalHostname("not-the-local-hostname"), + ); } diff --git a/src/os/main.zig b/src/os/main.zig index 7eed97445..fbe0ac411 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -17,6 +17,7 @@ const resourcesdir = @import("resourcesdir.zig"); // Namespaces pub const args = @import("args.zig"); pub const cgroup = @import("cgroup.zig"); +pub const hostname = @import("hostname.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); @@ -42,3 +43,7 @@ pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index c97c533ea..2d399e8c1 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -5,7 +5,7 @@ const xev = @import("xev"); const apprt = @import("../apprt.zig"); const build_config = @import("../build_config.zig"); const configpkg = @import("../config.zig"); -const hostname = @import("../os/hostname.zig"); +const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); const terminal = @import("../terminal/main.zig"); @@ -1055,8 +1055,10 @@ pub const StreamHandler = struct { const PORT_NUMBER_MAX_DIGITS = 5; // Make sure there is space for a max length hostname + the max number of digits. var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - - const hostname_from_uri = hostname.bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { + const hostname_from_uri = internal_os.hostname.bufPrintHostnameFromFileUri( + &host_and_port_buf, + uri, + ) catch |err| switch (err) { error.NoHostnameInUri => { log.warn("OSC 7 uri must contain a hostname: {}", .{err}); return; @@ -1070,13 +1072,16 @@ pub const StreamHandler = struct { // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = hostname.isLocalHostname(hostname_from_uri) catch |err| switch (err) { - error.PermissionDenied, error.Unexpected => { + const host_valid = internal_os.hostname.isLocalHostname( + hostname_from_uri, + ) catch |err| switch (err) { + error.PermissionDenied, + error.Unexpected, + => { log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); return; }, }; - if (!host_valid) { log.warn("OSC 7 host must be local", .{}); return; From c8b99f78914ec017be3fb425711032c78e8e02a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 10:36:11 -0800 Subject: [PATCH 39/46] remove refalldecls test --- src/os/main.zig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/os/main.zig b/src/os/main.zig index fbe0ac411..48a712d40 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -43,7 +43,3 @@ pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; - -test { - @import("std").testing.refAllDecls(@This()); -} From 34c3da43023b0c058c0647b604b8bc4b2f87ff7b Mon Sep 17 00:00:00 2001 From: Nico Elbers Date: Tue, 5 Nov 2024 18:29:40 +0100 Subject: [PATCH 40/46] docs: fix the nixos install instructions Not updating inputs resulted in a crash for me, this fixed it. Relevant link from discord: https://discord.com/channels/1005603569187160125/1301217629268213770 --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 796590401..3c3a2460d 100644 --- a/README.md +++ b/README.md @@ -789,7 +789,14 @@ Below is an example: # # Instead, either run `nix flake update` or `nixos-rebuild build` # as the current user, and then run `sudo nixos-rebuild switch`. - ghostty.url = "git+ssh://git@github.com/ghostty-org/ghostty"; + ghostty = { + url = "git+ssh://git@github.com/ghostty-org/ghostty"; + + # NOTE: The below 2 lines are only required on nixos-unstable, + # if you're on stable, they may break your build + inputs.nixpkgs-stable.follows = "nixpkgs"; + inputs.nixpkgs-unstable.follows = "nixpkgs"; + }; }; outputs = { nixpkgs, ghostty, ... }: { From e08eeb2b2ad810c4db22530a181858caee834b22 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 16:13:53 -0800 Subject: [PATCH 41/46] coretext: set variations on deferred face load This commit makes CoreText behave a lot like FreeType where we set the variation axes on the deferred face load. This fixes a bug where the `slnt` variation axis could not be set with CoreText with the Monaspace Argon Variable font. This was a bug found in Discord. Specifically, with the Monaspace Argon Variable font, the `slnt` variation axis could not be set with CoreText. I'm not sure _exactly_ what causes this but I suspect it has to do with the `slnt` axis being a negative value. I'm not sure if this is a bug with CoreText or not. What was happening was that with CoreText, we set the variation axes during discovery and expect them to be preserved in the resulting discovered faces. That seems to be true with the `wght` axis but not the `slnt` axis for whatever reason. --- src/font/DeferredFace.zig | 48 ++++++++------------------------------ src/font/discovery.zig | 11 +++++++-- src/font/face/coretext.zig | 3 +++ 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index db9e23623..3ee104386 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -57,6 +57,11 @@ pub const CoreText = struct { /// The initialized font font: *macos.text.Font, + /// Variations to apply to this font. We apply the variations to the + /// search descriptor but sometimes when the font collection is + /// made the variation axes are reset so we have to reapply them. + variations: []const font.face.Variation, + pub fn deinit(self: *CoreText) void { self.font.release(); self.* = undefined; @@ -194,7 +199,10 @@ fn loadCoreText( ) !Face { _ = lib; const ct = self.ct.?; - return try Face.initFontCopy(ct.font, opts); + var face = try Face.initFontCopy(ct.font, opts); + errdefer face.deinit(); + try face.setVariations(ct.variations, opts); + return face; } fn loadCoreTextFreetype( @@ -236,43 +244,7 @@ fn loadCoreTextFreetype( //std.log.warn("path={s}", .{path_slice}); var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts); errdefer face.deinit(); - - // If our ct font has variations, apply them to the face. - if (ct.font.copyAttribute(.variation)) |variations| vars: { - defer variations.release(); - if (variations.getCount() == 0) break :vars; - - // This configuration is just used for testing so we don't want to - // have to pass a full allocator through so use the stack. We - // shouldn't have a lot of variations and if we do we should use - // another mechanism. - // - // On macOS the default stack size for a thread is 512KB and the main - // thread gets megabytes so 16KB is a safe stack allocation. - var data: [1024 * 16]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&data); - const alloc = fba.allocator(); - - var face_vars = std.ArrayList(font.face.Variation).init(alloc); - const kav = try variations.getKeysAndValues(alloc); - for (kav.keys, kav.values) |key, value| { - const num: *const macos.foundation.Number = @ptrCast(key.?); - const val: *const macos.foundation.Number = @ptrCast(value.?); - - var num_i32: i32 = undefined; - if (!num.getValue(.sint32, &num_i32)) continue; - - var val_f64: f64 = undefined; - if (!val.getValue(.float64, &val_f64)) continue; - - try face_vars.append(.{ - .id = @bitCast(num_i32), - .value = val_f64, - }); - } - - try face.setVariations(face_vars.items, opts); - } + try face.setVariations(ct.variations, opts); return face; } diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 3aa16eebf..aeaeb955e 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -79,7 +79,7 @@ pub const Descriptor = struct { // This is not correct, but we don't currently depend on the // hash value being different based on decimal values of variations. - autoHash(hasher, @as(u64, @intFromFloat(variation.value))); + autoHash(hasher, @as(i64, @intFromFloat(variation.value))); } } @@ -384,6 +384,7 @@ pub const CoreText = struct { return DiscoverIterator{ .alloc = alloc, .list = zig_list, + .variations = desc.variations, .i = 0, }; } @@ -420,6 +421,7 @@ pub const CoreText = struct { return DiscoverIterator{ .alloc = alloc, .list = list, + .variations = desc.variations, .i = 0, }; } @@ -443,6 +445,7 @@ pub const CoreText = struct { return DiscoverIterator{ .alloc = alloc, .list = list, + .variations = desc.variations, .i = 0, }; } @@ -721,6 +724,7 @@ pub const CoreText = struct { pub const DiscoverIterator = struct { alloc: Allocator, list: []const *macos.text.FontDescriptor, + variations: []const Variation, i: usize, pub fn deinit(self: *DiscoverIterator) void { @@ -756,7 +760,10 @@ pub const CoreText = struct { defer self.i += 1; return DeferredFace{ - .ct = .{ .font = font }, + .ct = .{ + .font = font, + .variations = self.variations, + }, }; } }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 2403b3902..363dbacd8 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -229,6 +229,9 @@ pub const Face = struct { vs: []const font.face.Variation, opts: font.face.Options, ) !void { + // If we have no variations, we don't need to do anything. + if (vs.len == 0) return; + // Create a new font descriptor with all the variations set. var desc = self.font.copyDescriptor(); defer desc.release(); From 98c4c453ee70256f205d2615e1595c51236084c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 16:32:23 -0800 Subject: [PATCH 42/46] update iterm2 themes --- 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 b475b25ca..ad9a92d11 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -49,8 +49,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b4a9c4d.tar.gz", - .hash = "122056fbb29863ec1678b7954fb76b1533ad8c581a34577c1b2efe419e29e05596df", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/da56d590c4237c96d81cc5ed987ea098eebefdf6.tar.gz", + .hash = "1220fac17a112b0dd11ec85e5b31a30f05bfaed897c03f31285276544db30d010c41", }, .vaxis = .{ .url = "git+https://github.com/rockorager/libvaxis?ref=main#a1b43d24653670d612b91f0855b165e6c987b809", diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index 65d26e146..ebc46799f 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-dNGDbVaPxDbIrDUkDwjzeRHHVcX4KnWKciXiTp1c7lE=" +"sha256-fTNqNTfElvZPxJiNQJ/RxrSMCiKZPU3705CY7fznKhY=" From 65f1cefb4e8fb2da369d8afdcf2ea6e15ffde164 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 16:51:13 -0800 Subject: [PATCH 43/46] config: add "initial-command" config, "-e" sets that Fixes #2601 It is more expected behavior that `-e` affects only the first window. By introducing a dedicated configuration we avoid making `-e` too magical: its simply syntax sugar for setting the "initial-command" configuration. --- src/App.zig | 5 +++++ src/Surface.zig | 17 +++++++++++++---- src/config/Config.zig | 31 +++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/App.zig b/src/App.zig index c54c67167..cc8277c52 100644 --- a/src/App.zig +++ b/src/App.zig @@ -66,6 +66,11 @@ font_grid_set: font.SharedGridSet, last_notification_time: ?std.time.Instant = null, last_notification_digest: u64 = 0, +/// Set to false once we've created at least one surface. This +/// never goes true again. This can be used by surfaces to determine +/// if they are the first surface. +first: bool = true, + pub const CreateError = Allocator.Error || font.SharedGridSet.InitError; /// Initialize the main app instance. This creates the main window, sets diff --git a/src/Surface.zig b/src/Surface.zig index d19f9e812..d0c199010 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -474,13 +474,19 @@ pub fn init( .config = derived_config, }; + // The command we're going to execute + const command: ?[]const u8 = if (app.first) + config.@"initial-command" orelse config.command + else + config.command; + // Start our IO implementation // This separate block ({}) is important because our errdefers must // be scoped here to be valid. { // Initialize our IO backend var io_exec = try termio.Exec.init(alloc, .{ - .command = config.command, + .command = command, .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", .working_directory = config.@"working-directory", @@ -618,9 +624,9 @@ pub fn init( // For xdg-terminal-exec execution we special-case and set the window // title to the command being executed. This allows window managers // to set custom styling based on the command being executed. - const command = config.command orelse break :xdg; - if (command.len > 0) { - const title = alloc.dupeZ(u8, command) catch |err| { + const v = command orelse break :xdg; + if (v.len > 0) { + const title = alloc.dupeZ(u8, v) catch |err| { log.warn( "error copying command for title, title will not be set err={}", .{err}, @@ -635,6 +641,9 @@ pub fn init( ); } } + + // We are no longer the first surface + app.first = false; } pub fn deinit(self: *Surface) void { diff --git a/src/config/Config.zig b/src/config/Config.zig index a674046e1..e6b9d35ab 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -513,7 +513,26 @@ palette: Palette = .{}, /// arguments are provided, the command will be executed using `/bin/sh -c`. /// Ghostty does not do any shell command parsing. /// -/// If you're using the `ghostty` CLI there is also a shortcut to run a command +/// This command will be used for all new terminal surfaces, i.e. new windows, +/// tabs, etc. If you want to run a command only for the first terminal surface +/// created when Ghostty starts, use the `initial-command` configuration. +/// +/// Ghostty supports the common `-e` flag for executing a command with +/// arguments. For example, `ghostty -e fish --with --custom --args`. +/// This flag sets the `initial-command` configuration, see that for more +/// information. +command: ?[]const u8 = null, + +/// This is the same as "command", but only applies to the first terminal +/// surface created when Ghostty starts. Subsequent terminal surfaces will use +/// the `command` configuration. +/// +/// After the first terminal surface is created (or closed), there is no +/// way to run this initial command again automatically. As such, setting +/// this at runtime works but will only affect the next terminal surface +/// if it is the first one ever created. +/// +/// If you're using the `ghostty` CLI there is also a shortcut to set this /// with arguments directly: you can use the `-e` flag. For example: `ghostty -e /// fish --with --custom --args`. The `-e` flag automatically forces some /// other behaviors as well: @@ -525,7 +544,7 @@ palette: Palette = .{}, /// process will exit when the command exits. Additionally, the /// `quit-after-last-window-closed-delay` is unset. /// -command: ?[]const u8 = null, +@"initial-command": ?[]const u8 = null, /// If true, keep the terminal open after the command exits. Normally, the /// terminal window closes when the running command (such as a shell) exits. @@ -2356,7 +2375,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { } self.@"_xdg-terminal-exec" = true; - self.command = command.items[0 .. command.items.len - 1]; + self.@"initial-command" = command.items[0 .. command.items.len - 1]; return; } } @@ -2755,7 +2774,7 @@ pub fn parseManuallyHook( return false; } - self.command = command.items[0 .. command.items.len - 1]; + self.@"initial-command" = command.items[0 .. command.items.len - 1]; // See "command" docs for the implied configurations and why. self.@"gtk-single-instance" = .false; @@ -2945,7 +2964,7 @@ test "parse e: command only" { var it: TestIterator = .{ .data = &.{"foo"} }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("foo", cfg.command.?); + try testing.expectEqualStrings("foo", cfg.@"initial-command".?); } test "parse e: command and args" { @@ -2956,7 +2975,7 @@ test "parse e: command and args" { var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); + try testing.expectEqualStrings("echo foo bar baz", cfg.@"initial-command".?); } test "clone default" { From 964f2ce96a01b5d05719c08fb48101d7c61ccdf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Nov 2024 12:53:34 -0800 Subject: [PATCH 44/46] font/coretext: always score based on style string length This fixes an issue where for the regular style we were picking a suboptimal style because for some font faces we were choosing more bold faces (just as chance). This modifies our scoring to take the style length into account even for regular style. We already had this logic for explicit styles. --- src/font/discovery.zig | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index aeaeb955e..67236d5c9 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -685,30 +685,29 @@ pub const CoreText = struct { break :style .unmatched; defer style.release(); + // Get our style string + var buf: [128]u8 = undefined; + const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; + // If we have a specific desired style, attempt to search for that. if (desc.style) |desired_style| { - var buf: [128]u8 = undefined; - const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; - // Matching style string gets highest score if (std.mem.eql(u8, desired_style, style_str)) break :style .match; - - // Otherwise the score is based on the length of the style string. - // Shorter styles are scored higher. - break :style @enumFromInt(100 -| style_str.len); + } else if (!desc.bold and !desc.italic) { + // If we do not, and we have no symbolic traits, then we try + // to find "regular" (or no style). If we have symbolic traits + // we do nothing but we can improve scoring by taking that into + // account, too. + if (std.mem.eql(u8, "Regular", style_str)) { + break :style .match; + } } - // If we do not, and we have no symbolic traits, then we try - // to find "regular" (or no style). If we have symbolic traits - // we do nothing but we can improve scoring by taking that into - // account, too. - if (!desc.bold and !desc.italic) { - var buf: [128]u8 = undefined; - const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; - if (std.mem.eql(u8, "Regular", style_str)) break :style .match; - } - - break :style .unmatched; + // Otherwise the score is based on the length of the style string. + // Shorter styles are scored higher. This is a heuristic that + // if we don't have a desired style then shorter tends to be + // more often the "regular" style. + break :style @enumFromInt(100 -| style_str.len); }; score_acc.traits = traits: { From 94542b04f2f9e958f922be7e4400e1742e617a2d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Nov 2024 12:56:12 -0800 Subject: [PATCH 45/46] font/coretext: do not set variation axes in discovery This was causing discovery to find some odd fonts under certain scenarios (namely: Recursive Mono). Due to our prior fix in e08eeb2b2ad810c4db22530a181858caee834b22 we no longer need to set variations here for them to stick. --- src/font/discovery.zig | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 67236d5c9..e73ea626f 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -235,21 +235,7 @@ pub const Descriptor = struct { ); } - // Build our descriptor from attrs - var desc = try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs)); - errdefer desc.release(); - - // Variations are built by copying the descriptor. I don't know a way - // to set it on attrs directly. - for (self.variations) |v| { - const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id)); - defer id.release(); - const next = try desc.createCopyWithVariation(id, v.value); - desc.release(); - desc = next; - } - - return desc; + return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs)); } }; From b5ed4cb6802e9c947729940825bdc95188668f12 Mon Sep 17 00:00:00 2001 From: phillip-hirsch Date: Wed, 6 Nov 2024 17:06:24 -0500 Subject: [PATCH 46/46] feat: Add syntax highlighting for bat --- build.zig | 17 ++++++++++++ src/config/sublime_syntax.zig | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/config/sublime_syntax.zig diff --git a/build.zig b/build.zig index fb74de340..2cce41a31 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,7 @@ const font = @import("src/font/main.zig"); const renderer = @import("src/renderer.zig"); const terminfo = @import("src/terminfo/main.zig"); const config_vim = @import("src/config/vim.zig"); +const config_sublime_syntax = @import("src/config/sublime_syntax.zig"); const fish_completions = @import("src/build/fish_completions.zig"); const build_config = @import("src/build_config.zig"); const BuildConfig = build_config.BuildConfig; @@ -515,6 +516,22 @@ pub fn build(b: *std.Build) !void { }); } + // Sublime syntax highlighting for bat cli tool + // NOTE: The current implementation requires symlinking the generated + // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' + // directory. The syntax then needs to be mapped to the correct language in + // the config file within the '~.config/bat' directory + // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). + { + const wf = b.addWriteFiles(); + _ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax); + b.installDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/bat/syntaxes", + }); + } + // Documentation if (emit_docs) { try buildDocumentation(b, config); diff --git a/src/config/sublime_syntax.zig b/src/config/sublime_syntax.zig new file mode 100644 index 000000000..1b7c4900a --- /dev/null +++ b/src/config/sublime_syntax.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const Config = @import("Config.zig"); + +const Template = struct { + const header = + \\%YAML 1.2 + \\--- + \\# See http://www.sublimetext.com/docs/syntax.html + \\name: Ghostty Config + \\file_extensions: + \\ - ghostty + \\scope: source.ghostty + \\ + \\contexts: + \\ main: + \\ # Comments + \\ - match: '#.*$' + \\ scope: comment.line.number-sign.ghostty + \\ + \\ # Keywords + \\ - match: '\b( + ; + const footer = + \\)\b' + \\ scope: keyword.other.ghostty + \\ + ; +}; + +/// Check if a field is internal (starts with underscore) +fn isInternal(name: []const u8) bool { + return name.len > 0 and name[0] == '_'; +} + +/// Generate keywords from Config fields +fn generateKeywords() []const u8 { + @setEvalBranchQuota(5000); + var keywords: []const u8 = ""; + const config_fields = @typeInfo(Config).Struct.fields; + + for (config_fields) |field| { + if (isInternal(field.name)) continue; + if (keywords.len > 0) keywords = keywords ++ "|"; + keywords = keywords ++ field.name; + } + + return keywords; +} + +/// Complete Sublime syntax file content +pub const syntax = Template.header ++ generateKeywords() ++ Template.footer;