diff --git a/include/ghostty.h b/include/ghostty.h index d65eb72034..78fac089ac 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -30,6 +30,12 @@ typedef void* ghostty_surface_t; typedef void* ghostty_inspector_t; // Enums are up top so we can reference them later. +typedef enum { + GHOSTTY_SURFACE_KIND_SPLIT, + GHOSTTY_SURFACE_KIND_TAB, + GHOSTTY_SURFACE_KIND_WINDOW, +} ghostty_surface_kind_e; + typedef enum { GHOSTTY_PLATFORM_INVALID, GHOSTTY_PLATFORM_MACOS, @@ -375,6 +381,7 @@ typedef union { } ghostty_platform_u; typedef struct { + ghostty_surface_kind_e kind; ghostty_platform_e platform_tag; ghostty_platform_u platform; void* userdata; diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index d007a898c4..d29cf7b8b8 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -251,6 +251,8 @@ extension Ghostty { /// The configuration for a surface. For any configuration not set, defaults will be chosen from /// libghostty, usually from the Ghostty configuration. struct SurfaceConfiguration { + var kind: ghostty_surface_kind_e? = nil; + /// Explicit font size to use in points var fontSize: UInt8? = nil @@ -263,6 +265,7 @@ extension Ghostty { init() {} init(from config: ghostty_surface_config_s) { + self.kind = config.kind; self.fontSize = config.font_size self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8) self.command = String.init(cString: config.command, encoding: .utf8) @@ -294,6 +297,7 @@ extension Ghostty { #error("unsupported target") #endif + if let kind = kind { config.kind = kind } if let fontSize = fontSize { config.font_size = fontSize } if let workingDirectory = workingDirectory { config.working_directory = (workingDirectory as NSString).utf8String diff --git a/src/Surface.zig b/src/Surface.zig index f142515c5f..853cb38e9d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -125,6 +125,13 @@ config: DerivedConfig, /// This is used to determine if we need to confirm, hold open, etc. child_exited: bool = false, +/// The kind of surface. +pub const Kind = enum(c_int) { + split, + tab, + window, +}; + /// 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 diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index d9f9d35f20..f33ef8c4d9 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -301,6 +301,9 @@ pub const Surface = struct { inspector: ?*Inspector = null, pub const Options = extern struct { + /// The initial kind of surface. + kind: CoreSurface.Kind = .window, + /// The platform that this surface is being initialized for and /// the associated platform-specific configuration. platform_tag: c_int = 0, @@ -358,7 +361,7 @@ pub const Surface = struct { errdefer app.core_app.deleteSurface(self); // Shallow copy the config so that we can modify it. - var config = try apprt.surface.newConfig(app.core_app, app.config); + var config = try apprt.surface.newConfig(app.core_app, app.config, opts.kind); defer config.deinit(); // If we have a working directory from the options then we set it. @@ -466,7 +469,7 @@ pub const Surface = struct { return; }; - const options = self.newSurfaceOptions(); + const options = self.newSurfaceOptions(.split); func(self.opts.userdata, direction, options); } @@ -999,7 +1002,7 @@ pub const Surface = struct { return; }; - const options = self.newSurfaceOptions(); + const options = self.newSurfaceOptions(.tab); func(self.opts.userdata, options); } @@ -1009,7 +1012,7 @@ pub const Surface = struct { return; }; - const options = self.newSurfaceOptions(); + const options = self.newSurfaceOptions(.window); func(self.opts.userdata, options); } @@ -1040,13 +1043,14 @@ pub const Surface = struct { func(self.opts.userdata, width, height); } - fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { + fn newSurfaceOptions(self: *const Surface, kind: CoreSurface.Kind) apprt.Surface.Options { const font_size: u8 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; break :font_size self.core_surface.font_size.points; }; return .{ + .kind = kind, .font_size = font_size, }; } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 932e27de52..3b90b5d52c 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -448,7 +448,8 @@ pub const Surface = struct { errdefer app.app.deleteSurface(self); // Get our new surface config - var config = try apprt.surface.newConfig(app.app, &app.config); + const kind: CoreSurface.Kind = .window; // TODO + var config = try apprt.surface.newConfig(app.app, &app.config, kind); defer config.deinit(); // Initialize our surface now that we have the stable pointer. diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index c7aaf93439..7f53660fc0 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -397,7 +397,8 @@ fn realize(self: *Surface) !void { errdefer self.app.core_app.deleteSurface(self); // Get our new surface config - var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config); + const kind: CoreSurface.Kind = .window; // TODO + var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config, kind); defer config.deinit(); if (!self.parent_surface) { // A hack, see the "parent_surface" field for more information. diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index ae3ba050ab..92ed85b68e 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const apprt = @import("../apprt.zig"); const App = @import("../App.zig"); const Surface = @import("../Surface.zig"); @@ -85,7 +86,7 @@ pub const Mailbox = struct { /// Returns a new config for a surface for the given app that should be /// used for any new surfaces. The resulting config should be deinitialized /// after the surface is initialized. -pub fn newConfig(app: *const App, config: *const Config) !Config { +pub fn newConfig(app: *const App, config: *const Config, kind: Surface.Kind) !Config { // Create a shallow clone var copy = config.shallowClone(app.alloc); @@ -95,7 +96,12 @@ pub fn newConfig(app: *const App, config: *const Config) !Config { // Get our previously focused surface for some inherited values. const prev = app.focusedSurface(); if (prev) |p| { - if (config.@"window-inherit-working-directory") { + const inherit_pwd: bool = switch (kind) { + .split => config.@"window-inherit-working-directory".split, + .tab => config.@"window-inherit-working-directory".tab, + .window => config.@"window-inherit-working-directory".window, + }; + if (inherit_pwd) { if (try p.pwd(alloc)) |pwd| { copy.@"working-directory" = pwd; } diff --git a/src/config.zig b/src/config.zig index ba87fb6db6..b2c55ed7ea 100644 --- a/src/config.zig +++ b/src/config.zig @@ -20,6 +20,7 @@ pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; +pub const WindowInheritWorkingDirectory = Config.WindowInheritWorkingDirectory; // Alternate APIs pub const CAPI = @import("config/CAPI.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 64c9460fc4..b0f0a5ed04 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -33,6 +33,9 @@ const help_strings = @import("help_strings"); const log = std.log.scoped(.config); +// For trimming +const whitespace = " \t"; + /// Used on Unixes for some defaults. const c = @cImport({ @cInclude("unistd.h"); @@ -613,10 +616,15 @@ keybind: Keybinds = .{}, /// given a certain viewport size and grid cell size. @"window-padding-balance": bool = false, -/// If true, new windows and tabs will inherit the working directory of the -/// previously focused window. If no window was previously focused, the default -/// working directory will be used (the `working-directory` option). -@"window-inherit-working-directory": bool = true, +/// Controls whether new splits, tabs, and windows will inherit the working +/// directory of the previously focused window. If no window was previously +/// focused, the default working directory will be used (the `working-directory` +/// option). +/// +/// Value value are `split`, `tab`, and `window`. You can specify multiple +/// values using a comma-delimited string (`tab` or `split,window`). You +/// can also set this to `true` (always inherit) or `false` (never inherit). +@"window-inherit-working-directory": WindowInheritWorkingDirectory = .{}, /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default font size @@ -2687,7 +2695,6 @@ pub const RepeatableFontVariation = struct { pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue; - const whitespace = " \t"; const key = std.mem.trim(u8, input[0..eql_idx], whitespace); const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace); if (key.len != 4) return error.InvalidValue; @@ -2950,7 +2957,6 @@ pub const RepeatableCodepointMap = struct { pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue; - const whitespace = " \t"; const key = std.mem.trim(u8, input[0..eql_idx], whitespace); const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace); const valueZ = try alloc.dupeZ(u8, value); @@ -3462,6 +3468,70 @@ pub const WindowNewTabPosition = enum { end, }; +/// Options for inheriting the working directory of the previous window. +pub const WindowInheritWorkingDirectory = packed struct { + const Self = @This(); + + split: bool = false, + tab: bool = false, + window: bool = false, + + pub fn parseCLI(self: *Self, _: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + const fields = @typeInfo(Self).Struct.fields; + + if (std.mem.eql(u8, value, "true")) { + self.* = .{ .split = true, .tab = true, .window = true }; + return; + } + + if (std.mem.eql(u8, value, "false")) { + self.* = .{ .split = false, .tab = false, .window = false }; + return; + } + + // Enable all of the fields named in the comma-separated value. + self.* = .{}; + var iter = std.mem.splitSequence(u8, value, ","); + loop: while (iter.next()) |part_raw| { + const part = std.mem.trim(u8, part_raw, whitespace); + + inline for (fields) |field| { + assert(field.type == bool); + if (std.mem.eql(u8, field.name, part)) { + @field(self, field.name) = true; + continue :loop; + } + } + + // No field matched + return error.InvalidValue; + } + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var p: Self = .{}; + try p.parseCLI(alloc, "true"); + try testing.expectEqual(Self{ .split = true, .tab = true, .window = true }, p); + + try p.parseCLI(alloc, "false"); + try testing.expectEqual(Self{ .split = false, .tab = false, .window = false }, p); + + try p.parseCLI(alloc, "tab"); + try testing.expectEqual(Self{ .split = false, .tab = true, .window = false }, p); + + try p.parseCLI(alloc, "split,window"); + try testing.expectEqual(Self{ .split = true, .tab = false, .window = true }, p); + + try testing.expectError(error.InvalidValue, p.parseCLI(alloc, "unknown")); + } +}; + /// See grapheme-width-method pub const GraphemeWidthMethod = enum { wcswidth,