diff --git a/include/ghostty.h b/include/ghostty.h index 2feb35ad9..b56b8827e 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -404,6 +404,15 @@ typedef struct { const char* command; } ghostty_surface_config_s; +typedef struct { + uint16_t columns; + uint16_t rows; + uint32_t width_px; + uint32_t height_px; + uint32_t cell_width_px; + uint32_t cell_height_px; +} ghostty_surface_size_s; + typedef void (*ghostty_runtime_wakeup_cb)(void*); typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*); typedef void (*ghostty_runtime_open_config_cb)(void*); @@ -530,6 +539,7 @@ void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_occlusion(ghostty_surface_t, bool); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); +ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_color_scheme_e); ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index ab583e956..63997e621 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -331,5 +331,82 @@ extension Ghostty { let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4) return Color(newColor) } + + var resizeOverlay: ResizeOverlay { + guard let config = self.config else { return .after_first } + var v: UnsafePointer? = nil + let key = "resize-overlay" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .after_first } + guard let ptr = v else { return .after_first } + let str = String(cString: ptr) + return ResizeOverlay(rawValue: str) ?? .after_first + } + + var resizeOverlayPosition: ResizeOverlayPosition { + let defaultValue = ResizeOverlayPosition.center + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "resize-overlay-position" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return ResizeOverlayPosition(rawValue: str) ?? defaultValue + } + + var resizeOverlayDuration: UInt { + guard let config = self.config else { return 1000 } + var v: UInt = 0 + let key = "resize-overlay-duration" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v; + } + } +} + +// MARK: Configuration Enums + +extension Ghostty.Config { + enum ResizeOverlay : String { + case always + case never + case after_first = "after-first" + } + + enum ResizeOverlayPosition : String { + case center + case top_left = "top-left" + case top_center = "top-center" + case top_right = "top-right" + case bottom_left = "bottom-left" + case bottom_center = "bottom-center" + case bottom_right = "bottom-right" + + func top() -> Bool { + switch (self) { + case .top_left, .top_center, .top_right: return true; + default: return false; + } + } + + func bottom() -> Bool { + switch (self) { + case .bottom_left, .bottom_center, .bottom_right: return true; + default: return false; + } + } + + func left() -> Bool { + switch (self) { + case .top_left, .bottom_left: return true; + default: return false; + } + } + + func right() -> Bool { + switch (self) { + case .top_right, .bottom_right: return true; + default: return false; + } + } } } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 92d899300..e1c0a684b 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -52,6 +52,9 @@ extension Ghostty { // True if we're hovering over the left URL view, so we can show it on the right. @State private var isHoveringURLLeft: Bool = false + // The last size so we can detect resizes and show our resize overlay + @State private var lastSize: CGSize? = nil + @EnvironmentObject private var ghostty: Ghostty.App var body: some View { @@ -145,6 +148,54 @@ extension Ghostty { // I don't know how older macOS versions behave but Ghostty only // supports back to macOS 12 so its moot. } + + // If our geo size changed then we show the resize overlay as configured. + if (lastSize != geo.size) { + let resizeOverlay = ghostty.config.resizeOverlay + if (resizeOverlay != .never) { + if let surfaceSize = surfaceView.surfaceSize { + let padding: CGFloat = 5 + let resizeDuration = ghostty.config.resizeOverlayDuration + let resizePosition = ghostty.config.resizeOverlayPosition + + VStack { + if (!resizePosition.top()) { + Spacer() + } + + HStack { + if (!resizePosition.left()) { + Spacer() + } + + Text(verbatim: "\(surfaceSize.columns)c ⨯ \(surfaceSize.rows)r") + .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) + .background( + RoundedRectangle(cornerRadius: 4) + .fill(.background) + .shadow(radius: 3) + ).lineLimit(1) + .truncationMode(.middle) + + if (!resizePosition.right()) { + Spacer() + } + } + + if (!resizePosition.bottom()) { + Spacer() + } + } + .allowsHitTesting(false) + .opacity(resizeOverlay == .after_first && lastSize == nil ? 0 : 1) + .onAppear() { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(resizeDuration))) { + self.lastSize = geo.size + } + } + } + } + } } .ghosttySurfaceView(surfaceView) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 26e5c7d7a..3d6c17750 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -52,6 +52,13 @@ extension Ghostty { return v } + // Returns sizing information for the surface. This is the raw C + // structure because I'm lazy. + var surfaceSize: ghostty_surface_size_s? { + guard let surface = self.surface else { return nil } + return ghostty_surface_size(surface) + } + // Returns the inspector instance for this surface, or nil if the // surface has been closed. var inspector: ghostty_inspector_t? { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index b7c8b2bed..9b7fcccb7 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1418,6 +1418,15 @@ pub const CAPI = struct { offset_len: u32, }; + const SurfaceSize = extern struct { + columns: u16, + rows: u16, + width_px: u32, + height_px: u32, + cell_width_px: u32, + cell_height_px: u32, + }; + /// Create a new app. export fn ghostty_app_new( opts: *const apprt.runtime.App.Options, @@ -1593,6 +1602,18 @@ pub const CAPI = struct { surface.updateSize(w, h); } + /// Return the size information a surface has. + export fn ghostty_surface_size(surface: *Surface) SurfaceSize { + return .{ + .columns = surface.core_surface.grid_size.columns, + .rows = surface.core_surface.grid_size.rows, + .width_px = surface.core_surface.screen_size.width, + .height_px = surface.core_surface.screen_size.height, + .cell_width_px = surface.core_surface.cell_size.width, + .cell_height_px = surface.core_surface.cell_size.height, + }; + } + /// Update the color scheme of the surface. export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void { const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch { diff --git a/src/config/Config.zig b/src/config/Config.zig index a346ad983..78081dc86 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -871,10 +871,9 @@ keybind: Keybinds = .{}, /// /// The default is `after-first`. /// -/// Changing this value at runtime and reloading the configuration will only -/// affect new windows, tabs, and splits. -/// -/// Linux/GTK only. +/// Changing this value at runtime and reloading the configuration will take +/// effect immediately on macOS, but will only affect new terminals on +/// Linux. @"resize-overlay": ResizeOverlay = .@"after-first", /// If resize overlays are enabled, this controls the position of the overlay. @@ -889,8 +888,6 @@ keybind: Keybinds = .{}, /// * `bottom-right` /// /// The default is `center`. -/// -/// Linux/GTK only. @"resize-overlay-position": ResizeOverlayPosition = .center, /// If resize overlays are enabled, this controls how long the overlay is @@ -923,8 +920,6 @@ keybind: Keybinds = .{}, /// /// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any /// value larger than this will be clamped to the maximum value. -/// -/// Linux/GTK only. @"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms }, // If true, when there are multiple split panes, the mouse selects the pane @@ -4027,11 +4022,11 @@ pub const Duration = struct { .{ .name = "ns", .factor = 1 }, }; - pub fn clone(self: *const @This(), _: Allocator) !@This() { + pub fn clone(self: *const Duration, _: Allocator) !Duration { return .{ .duration = self.duration }; } - pub fn equal(self: @This(), other: @This()) bool { + pub fn equal(self: Duration, other: Duration) bool { return self.duration == other.duration; } @@ -4099,7 +4094,7 @@ pub const Duration = struct { return if (value) |v| .{ .duration = v } else error.ValueRequired; } - pub fn formatEntry(self: @This(), formatter: anytype) !void { + pub fn formatEntry(self: Duration, formatter: anytype) !void { var buf: [64]u8 = undefined; var fbs = std.io.fixedBufferStream(&buf); const writer = fbs.writer(); @@ -4107,7 +4102,7 @@ pub const Duration = struct { try formatter.formatEntry([]const u8, fbs.getWritten()); } - pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(self: Duration, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { var value = self.duration; var i: usize = 0; for (units) |unit| { @@ -4122,9 +4117,14 @@ pub const Duration = struct { } } + pub fn c_get(self: Duration, ptr_raw: *anyopaque) void { + const ptr: *usize = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @intCast(self.asMilliseconds()); + } + /// Convenience function to convert to milliseconds since many OS and /// library timing functions operate on that timescale. - pub fn asMilliseconds(self: @This()) c_uint { + pub fn asMilliseconds(self: Duration) c_uint { const ms: u64 = std.math.divTrunc( u64, self.duration, diff --git a/src/config/c_get.zig b/src/config/c_get.zig index ff3523c29..32a19df1c 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -60,6 +60,12 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool { }, .Struct => |info| { + // If the struct implements c_get then we call that + if (@hasDecl(@TypeOf(value), "c_get")) { + value.c_get(ptr_raw); + return true; + } + // Packed structs that are less than or equal to the // size of a C int can be passed directly as their // bit representation.