mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: support env vars for surface config, clean up surface config
This commit is contained in:
@ -385,6 +385,11 @@ typedef struct {
|
||||
bool rectangle;
|
||||
} ghostty_selection_s;
|
||||
|
||||
typedef struct {
|
||||
const char* key;
|
||||
const char* value;
|
||||
} ghostty_env_var_s;
|
||||
|
||||
typedef struct {
|
||||
void* nsview;
|
||||
} ghostty_platform_macos_s;
|
||||
@ -406,6 +411,8 @@ typedef struct {
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
const char* command;
|
||||
ghostty_env_var_s* env_vars;
|
||||
size_t env_var_count;
|
||||
} ghostty_surface_config_s;
|
||||
|
||||
typedef struct {
|
||||
@ -807,7 +814,8 @@ void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
|
||||
|
||||
ghostty_surface_config_s ghostty_surface_config_new();
|
||||
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t,
|
||||
const ghostty_surface_config_s*);
|
||||
void ghostty_surface_free(ghostty_surface_t);
|
||||
void* ghostty_surface_userdata(ghostty_surface_t);
|
||||
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
|
||||
|
@ -15,6 +15,7 @@
|
||||
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
|
||||
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
|
||||
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; };
|
||||
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
@ -153,6 +154,7 @@
|
||||
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; };
|
||||
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; };
|
||||
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = "<group>"; };
|
||||
@ -506,6 +508,7 @@
|
||||
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
|
||||
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
|
||||
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
|
||||
A51194122E05D003007258CC /* Optional+Extension.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
|
||||
@ -786,6 +789,7 @@
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */,
|
||||
|
@ -418,18 +418,36 @@ extension Ghostty {
|
||||
|
||||
/// Explicit command to set
|
||||
var command: String? = nil
|
||||
|
||||
/// Environment variables to set for the terminal
|
||||
var environmentVariables: [String: String] = [:]
|
||||
|
||||
init() {}
|
||||
|
||||
init(from config: ghostty_surface_config_s) {
|
||||
self.fontSize = config.font_size
|
||||
self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8)
|
||||
self.command = String.init(cString: config.command, encoding: .utf8)
|
||||
if let workingDirectory = config.working_directory {
|
||||
self.workingDirectory = String.init(cString: workingDirectory, encoding: .utf8)
|
||||
}
|
||||
if let command = config.command {
|
||||
self.command = String.init(cString: command, encoding: .utf8)
|
||||
}
|
||||
|
||||
// Convert the C env vars to Swift dictionary
|
||||
if config.env_var_count > 0, let envVars = config.env_vars {
|
||||
for i in 0..<config.env_var_count {
|
||||
let envVar = envVars[i]
|
||||
if let key = String(cString: envVar.key, encoding: .utf8),
|
||||
let value = String(cString: envVar.value, encoding: .utf8) {
|
||||
self.environmentVariables[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ghostty configuration for this surface configuration struct. The memory
|
||||
/// in the returned struct is only valid as long as this struct is retained.
|
||||
func ghosttyConfig(view: SurfaceView) -> ghostty_surface_config_s {
|
||||
/// Provides a C-compatible ghostty configuration within a closure. The configuration
|
||||
/// and all its string pointers are only valid within the closure.
|
||||
func withCValue<T>(view: SurfaceView, _ body: (inout ghostty_surface_config_s) throws -> T) rethrows -> T {
|
||||
var config = ghostty_surface_config_new()
|
||||
config.userdata = Unmanaged.passUnretained(view).toOpaque()
|
||||
#if os(macOS)
|
||||
@ -438,7 +456,6 @@ extension Ghostty {
|
||||
nsview: Unmanaged.passUnretained(view).toOpaque()
|
||||
))
|
||||
config.scale_factor = NSScreen.main!.backingScaleFactor
|
||||
|
||||
#elseif os(iOS)
|
||||
config.platform_tag = GHOSTTY_PLATFORM_IOS
|
||||
config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s(
|
||||
@ -453,15 +470,42 @@ extension Ghostty {
|
||||
#error("unsupported target")
|
||||
#endif
|
||||
|
||||
if let fontSize = fontSize { config.font_size = fontSize }
|
||||
if let workingDirectory = workingDirectory {
|
||||
config.working_directory = (workingDirectory as NSString).utf8String
|
||||
}
|
||||
if let command = command {
|
||||
config.command = (command as NSString).utf8String
|
||||
}
|
||||
// Zero is our default value that means to inherit the font size.
|
||||
config.font_size = fontSize ?? 0
|
||||
|
||||
return config
|
||||
// Use withCString to ensure strings remain valid for the duration of the closure
|
||||
return try workingDirectory.withCString { cWorkingDir in
|
||||
config.working_directory = cWorkingDir
|
||||
|
||||
return try command.withCString { cCommand in
|
||||
config.command = cCommand
|
||||
|
||||
// Convert dictionary to arrays for easier processing
|
||||
let keys = Array(environmentVariables.keys)
|
||||
let values = Array(environmentVariables.values)
|
||||
|
||||
// Create C strings for all keys and values
|
||||
return try keys.withCStrings { keyCStrings in
|
||||
return try values.withCStrings { valueCStrings in
|
||||
// Create array of ghostty_env_var_s
|
||||
var envVars = Array<ghostty_env_var_s>()
|
||||
envVars.reserveCapacity(environmentVariables.count)
|
||||
for i in 0..<environmentVariables.count {
|
||||
envVars.append(ghostty_env_var_s(
|
||||
key: keyCStrings[i],
|
||||
value: valueCStrings[i]
|
||||
))
|
||||
}
|
||||
|
||||
return try envVars.withUnsafeMutableBufferPointer { buffer in
|
||||
config.env_vars = buffer.baseAddress
|
||||
config.env_var_count = environmentVariables.count
|
||||
return try body(&config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,8 +290,10 @@ extension Ghostty {
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
|
||||
let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
|
||||
ghostty_surface_new(app, &surface_cfg_c)
|
||||
}
|
||||
guard let surface = surface else {
|
||||
self.error = Ghostty.Error.apiFailed
|
||||
return
|
||||
}
|
||||
|
@ -57,8 +57,10 @@ extension Ghostty {
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
|
||||
let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
|
||||
ghostty_surface_new(app, &surface_cfg_c)
|
||||
}
|
||||
guard let surface = surface else {
|
||||
// TODO
|
||||
return
|
||||
}
|
||||
|
@ -21,3 +21,28 @@ extension Array {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == String {
|
||||
/// Executes a closure with an array of C string pointers.
|
||||
func withCStrings<T>(_ body: ([UnsafePointer<Int8>?]) throws -> T) rethrows -> T {
|
||||
// Handle empty array
|
||||
if isEmpty {
|
||||
return try body([])
|
||||
}
|
||||
|
||||
// Recursive helper to process strings
|
||||
func helper(index: Int, accumulated: [UnsafePointer<Int8>?], body: ([UnsafePointer<Int8>?]) throws -> T) rethrows -> T {
|
||||
if index == count {
|
||||
return try body(accumulated)
|
||||
} else {
|
||||
return try self[index].withCString { cStr in
|
||||
var newAccumulated = accumulated
|
||||
newAccumulated.append(cStr)
|
||||
return try helper(index: index + 1, accumulated: newAccumulated, body: body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return try helper(index: 0, accumulated: [], body: body)
|
||||
}
|
||||
}
|
||||
|
10
macos/Sources/Helpers/Extensions/Optional+Extension.swift
Normal file
10
macos/Sources/Helpers/Extensions/Optional+Extension.swift
Normal file
@ -0,0 +1,10 @@
|
||||
extension Optional where Wrapped == String {
|
||||
/// Executes a closure with a C string pointer, handling nil gracefully.
|
||||
func withCString<T>(_ body: (UnsafePointer<Int8>?) throws -> T) rethrows -> T {
|
||||
if let string = self {
|
||||
return try string.withCString(body)
|
||||
} else {
|
||||
return try body(nil)
|
||||
}
|
||||
}
|
||||
}
|
@ -376,6 +376,14 @@ pub const PlatformTag = enum(c_int) {
|
||||
ios = 2,
|
||||
};
|
||||
|
||||
pub const EnvVar = extern struct {
|
||||
/// The name of the environment variable.
|
||||
key: [*:0]const u8,
|
||||
|
||||
/// The value of the environment variable.
|
||||
value: [*:0]const u8,
|
||||
};
|
||||
|
||||
pub const Surface = struct {
|
||||
app: *App,
|
||||
platform: Platform,
|
||||
@ -407,7 +415,7 @@ pub const Surface = struct {
|
||||
font_size: f32 = 0,
|
||||
|
||||
/// The working directory to load into.
|
||||
working_directory: [*:0]const u8 = "",
|
||||
working_directory: ?[*:0]const u8 = null,
|
||||
|
||||
/// The command to run in the new surface. If this is set then
|
||||
/// the "wait-after-command" option is also automatically set to true,
|
||||
@ -417,7 +425,11 @@ pub const Surface = struct {
|
||||
/// despite Ghostty allowing directly executed commands via config.
|
||||
/// This is a legacy thing and we should probably change it in the
|
||||
/// future once we have a concrete use case.
|
||||
command: [*:0]const u8 = "",
|
||||
command: ?[*:0]const u8 = null,
|
||||
|
||||
/// Extra environment variables to set for the surface.
|
||||
env_vars: ?[*]EnvVar = null,
|
||||
env_var_count: usize = 0,
|
||||
};
|
||||
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
@ -443,41 +455,59 @@ pub const Surface = struct {
|
||||
defer config.deinit();
|
||||
|
||||
// If we have a working directory from the options then we set it.
|
||||
const wd = std.mem.sliceTo(opts.working_directory, 0);
|
||||
if (wd.len > 0) wd: {
|
||||
var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
|
||||
log.warn(
|
||||
"error opening requested working directory dir={s} err={}",
|
||||
.{ wd, err },
|
||||
);
|
||||
break :wd;
|
||||
};
|
||||
defer dir.close();
|
||||
if (opts.working_directory) |c_wd| {
|
||||
const wd = std.mem.sliceTo(c_wd, 0);
|
||||
if (wd.len > 0) wd: {
|
||||
var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
|
||||
log.warn(
|
||||
"error opening requested working directory dir={s} err={}",
|
||||
.{ wd, err },
|
||||
);
|
||||
break :wd;
|
||||
};
|
||||
defer dir.close();
|
||||
|
||||
const stat = dir.stat() catch |err| {
|
||||
log.warn(
|
||||
"failed to stat requested working directory dir={s} err={}",
|
||||
.{ wd, err },
|
||||
);
|
||||
break :wd;
|
||||
};
|
||||
const stat = dir.stat() catch |err| {
|
||||
log.warn(
|
||||
"failed to stat requested working directory dir={s} err={}",
|
||||
.{ wd, err },
|
||||
);
|
||||
break :wd;
|
||||
};
|
||||
|
||||
if (stat.kind != .directory) {
|
||||
log.warn(
|
||||
"requested working directory is not a directory dir={s}",
|
||||
.{wd},
|
||||
);
|
||||
break :wd;
|
||||
if (stat.kind != .directory) {
|
||||
log.warn(
|
||||
"requested working directory is not a directory dir={s}",
|
||||
.{wd},
|
||||
);
|
||||
break :wd;
|
||||
}
|
||||
|
||||
config.@"working-directory" = wd;
|
||||
}
|
||||
|
||||
config.@"working-directory" = wd;
|
||||
}
|
||||
|
||||
// If we have a command from the options then we set it.
|
||||
const cmd = std.mem.sliceTo(opts.command, 0);
|
||||
if (cmd.len > 0) {
|
||||
config.command = .{ .shell = cmd };
|
||||
config.@"wait-after-command" = true;
|
||||
if (opts.command) |c_command| {
|
||||
const cmd = std.mem.sliceTo(c_command, 0);
|
||||
if (cmd.len > 0) {
|
||||
config.command = .{ .shell = cmd };
|
||||
config.@"wait-after-command" = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply any environment variables that were requested.
|
||||
if (opts.env_var_count > 0) {
|
||||
const alloc = config.arenaAlloc();
|
||||
for (opts.env_vars.?[0..opts.env_var_count]) |env_var| {
|
||||
const key = std.mem.sliceTo(env_var.key, 0);
|
||||
const value = std.mem.sliceTo(env_var.value, 0);
|
||||
try config.env.map.put(
|
||||
alloc,
|
||||
try alloc.dupeZ(u8, key),
|
||||
try alloc.dupeZ(u8, value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize our surface right away. We're given a view that is
|
||||
|
@ -3004,6 +3004,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the arena allocator associated with the configuration.
|
||||
pub fn arenaAlloc(self: *Config) Allocator {
|
||||
return self._arena.?.allocator();
|
||||
}
|
||||
|
||||
/// Change the state of conditionals and reload the configuration
|
||||
/// based on the new state. This returns a new configuration based
|
||||
/// on the new state. The caller must free the old configuration if they
|
||||
|
Reference in New Issue
Block a user