Merge pull request #117 from mitchellh/config-stuff

Reloadable Configuration
This commit is contained in:
Mitchell Hashimoto
2023-03-19 12:32:23 -07:00
committed by GitHub
21 changed files with 871 additions and 154 deletions

View File

@ -216,6 +216,7 @@ typedef struct {
} ghostty_surface_config_s; } ghostty_surface_config_s;
typedef void (*ghostty_runtime_wakeup_cb)(void *); typedef void (*ghostty_runtime_wakeup_cb)(void *);
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *); typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *);
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *); typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *);
@ -226,6 +227,7 @@ typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direc
typedef struct { typedef struct {
void *userdata; void *userdata;
ghostty_runtime_wakeup_cb wakeup_cb; ghostty_runtime_wakeup_cb wakeup_cb;
ghostty_runtime_reload_config_cb reload_config_cb;
ghostty_runtime_set_title_cb set_title_cb; ghostty_runtime_set_title_cb set_title_cb;
ghostty_runtime_read_clipboard_cb read_clipboard_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb;
ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb;
@ -241,6 +243,7 @@ int ghostty_init(void);
ghostty_config_t ghostty_config_new(); ghostty_config_t ghostty_config_new();
void ghostty_config_free(ghostty_config_t); void ghostty_config_free(ghostty_config_t);
void ghostty_config_load_cli_args(ghostty_config_t);
void ghostty_config_load_string(ghostty_config_t, const char *, uintptr_t); void ghostty_config_load_string(ghostty_config_t, const char *, uintptr_t);
void ghostty_config_load_default_files(ghostty_config_t); void ghostty_config_load_default_files(ghostty_config_t);
void ghostty_config_load_recursive_files(ghostty_config_t); void ghostty_config_load_recursive_files(ghostty_config_t);

View File

@ -12,12 +12,25 @@ extension Ghostty {
/// The readiness value of the state. /// The readiness value of the state.
@Published var readiness: AppReadiness = .loading @Published var readiness: AppReadiness = .loading
/// The ghostty global configuration. /// The ghostty global configuration. This should only be changed when it is definitely
var config: ghostty_config_t? = nil /// safe to change. It is definite safe to change only when the embedded app runtime
/// in Ghostty says so (usually, only in a reload configuration callback).
var config: ghostty_config_t? = nil {
didSet {
// Free the old value whenever we change
guard let old = oldValue else { return }
ghostty_config_free(old)
}
}
/// The ghostty app instance. We only have one of these for the entire app, although I guess /// The ghostty app instance. We only have one of these for the entire app, although I guess
/// in theory you can have multiple... I don't know why you would... /// in theory you can have multiple... I don't know why you would...
var app: ghostty_app_t? = nil var app: ghostty_app_t? = nil {
didSet {
guard let old = oldValue else { return }
ghostty_app_free(old)
}
}
/// Cached clipboard string for `read_clipboard` callback. /// Cached clipboard string for `read_clipboard` callback.
private var cached_clipboard_string: String? = nil private var cached_clipboard_string: String? = nil
@ -31,29 +44,18 @@ extension Ghostty {
} }
// Initialize the global configuration. // Initialize the global configuration.
guard let cfg = ghostty_config_new() else { guard let cfg = Self.reloadConfig() else {
GhosttyApp.logger.critical("ghostty_config_new failed")
readiness = .error readiness = .error
return return
} }
self.config = cfg; self.config = cfg;
// Load our configuration files from the home directory.
ghostty_config_load_default_files(cfg);
ghostty_config_load_recursive_files(cfg);
// TODO: we'd probably do some config loading here... for now we'd
// have to do this synchronously. When we support config updating we can do
// this async and update later.
// Finalize will make our defaults available.
ghostty_config_finalize(cfg)
// Create our "runtime" config. The "runtime" is the configuration that ghostty // Create our "runtime" config. The "runtime" is the configuration that ghostty
// uses to interface with the application runtime environment. // uses to interface with the application runtime environment.
var runtime_cfg = ghostty_runtime_config_s( var runtime_cfg = ghostty_runtime_config_s(
userdata: Unmanaged.passUnretained(self).toOpaque(), userdata: Unmanaged.passUnretained(self).toOpaque(),
wakeup_cb: { userdata in AppState.wakeup(userdata) }, wakeup_cb: { userdata in AppState.wakeup(userdata) },
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
read_clipboard_cb: { userdata in AppState.readClipboard(userdata) }, read_clipboard_cb: { userdata in AppState.readClipboard(userdata) },
write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) }, write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) },
@ -74,8 +76,32 @@ extension Ghostty {
} }
deinit { deinit {
ghostty_app_free(app) // This will force the didSet callbacks to run which free.
ghostty_config_free(config) self.app = nil
self.config = nil
}
/// Initializes a new configuration and loads all the values.
static func reloadConfig() -> ghostty_config_t? {
// Initialize the global configuration.
guard let cfg = ghostty_config_new() else {
GhosttyApp.logger.critical("ghostty_config_new failed")
return nil
}
// Load our configuration files from the home directory.
ghostty_config_load_default_files(cfg);
ghostty_config_load_cli_args(cfg);
ghostty_config_load_recursive_files(cfg);
// TODO: we'd probably do some config loading here... for now we'd
// have to do this synchronously. When we support config updating we can do
// this async and update later.
// Finalize will make our defaults available.
ghostty_config_finalize(cfg)
return cfg
} }
func appTick() { func appTick() {
@ -140,6 +166,20 @@ extension Ghostty {
pb.setString(valueStr, forType: .string) pb.setString(valueStr, forType: .string)
} }
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
guard let newConfig = AppState.reloadConfig() else {
GhosttyApp.logger.warning("failed to reload configuration")
return nil
}
// Assign the new config. This will automatically free the old config.
// It is safe to free the old config from within this function call.
let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue()
state.config = newConfig
return newConfig
}
static func wakeup(_ userdata: UnsafeMutableRawPointer?) { static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue() let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue()

View File

@ -18,7 +18,6 @@ const renderer = @import("renderer.zig");
const font = @import("font/main.zig"); const font = @import("font/main.zig");
const macos = @import("macos"); const macos = @import("macos");
const objc = @import("objc"); const objc = @import("objc");
const DevMode = @import("DevMode.zig");
const log = std.log.scoped(.app); const log = std.log.scoped(.app);
@ -30,9 +29,6 @@ alloc: Allocator,
/// The list of surfaces that are currently active. /// The list of surfaces that are currently active.
surfaces: SurfaceList, surfaces: SurfaceList,
// The configuration for the app.
config: *const Config,
/// The mailbox that can be used to send this thread messages. Note /// The mailbox that can be used to send this thread messages. Note
/// this is a blocking queue so if it is full you will get errors (or block). /// this is a blocking queue so if it is full you will get errors (or block).
mailbox: Mailbox.Queue, mailbox: Mailbox.Queue,
@ -45,17 +41,12 @@ quit: bool,
/// "startup" logic. /// "startup" logic.
pub fn create( pub fn create(
alloc: Allocator, alloc: Allocator,
config: *const Config,
) !*App { ) !*App {
// If we have DevMode on, store the config so we can show it
if (DevMode.enabled) DevMode.instance.config = config;
var app = try alloc.create(App); var app = try alloc.create(App);
errdefer alloc.destroy(app); errdefer alloc.destroy(app);
app.* = .{ app.* = .{
.alloc = alloc, .alloc = alloc,
.surfaces = .{}, .surfaces = .{},
.config = config,
.mailbox = .{}, .mailbox = .{},
.quit = false, .quit = false,
}; };
@ -97,6 +88,16 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
return self.quit or self.surfaces.items.len == 0; return self.quit or self.surfaces.items.len == 0;
} }
/// Update the configuration associated with the app. This can only be
/// called from the main thread. The caller owns the config memory. The
/// memory can be freed immediately when this returns.
pub fn updateConfig(self: *App, config: *const Config) !void {
// Go through and update all of the surface configurations.
for (self.surfaces.items) |surface| {
try surface.core_surface.handleMessage(.{ .change_config = config });
}
}
/// Add an initialized surface. This is really only for the runtime /// Add an initialized surface. This is really only for the runtime
/// implementations to call and should NOT be called by general app users. /// implementations to call and should NOT be called by general app users.
/// The surface must be from the pool. /// The surface must be from the pool.
@ -123,6 +124,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
while (self.mailbox.pop()) |message| { while (self.mailbox.pop()) |message| {
log.debug("mailbox message={s}", .{@tagName(message)}); log.debug("mailbox message={s}", .{@tagName(message)});
switch (message) { switch (message) {
.reload_config => try self.reloadConfig(rt_app),
.new_window => |msg| try self.newWindow(rt_app, msg), .new_window => |msg| try self.newWindow(rt_app, msg),
.close => |surface| try self.closeSurface(rt_app, surface), .close => |surface| try self.closeSurface(rt_app, surface),
.quit => try self.setQuit(), .quit => try self.setQuit(),
@ -132,6 +134,14 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
} }
} }
fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
log.debug("reloading configuration", .{});
if (try rt_app.reloadConfig()) |new| {
log.debug("new configuration received, applying", .{});
try self.updateConfig(new);
}
}
fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void {
if (!self.hasSurface(surface)) return; if (!self.hasSurface(surface)) return;
rt_app.closeSurface(surface.rt_surface); rt_app.closeSurface(surface.rt_surface);
@ -193,6 +203,10 @@ fn hasSurface(self: *App, surface: *Surface) bool {
/// The message types that can be sent to the app thread. /// The message types that can be sent to the app thread.
pub const Message = union(enum) { pub const Message = union(enum) {
/// Reload the configuration for the entire app and propagate it to
/// all the active surfaces.
reload_config: void,
/// Create a new terminal window. /// Create a new terminal window.
new_window: NewWindow, new_window: NewWindow,

View File

@ -27,7 +27,7 @@ pub var instance: DevMode = .{};
visible: bool = false, visible: bool = false,
/// Our app config /// Our app config
config: ?*const Config = null, config: ?Config = null,
/// The surface we're tracking. /// The surface we're tracking.
surface: ?*Surface = null, surface: ?*Surface = null,

View File

@ -19,6 +19,7 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const renderer = @import("renderer.zig"); const renderer = @import("renderer.zig");
const termio = @import("termio.zig"); const termio = @import("termio.zig");
const objc = @import("objc"); const objc = @import("objc");
@ -28,7 +29,7 @@ const font = @import("font/main.zig");
const Command = @import("Command.zig"); const Command = @import("Command.zig");
const trace = @import("tracy").trace; const trace = @import("tracy").trace;
const terminal = @import("terminal/main.zig"); const terminal = @import("terminal/main.zig");
const Config = @import("config.zig").Config; const configpkg = @import("config.zig");
const input = @import("input.zig"); const input = @import("input.zig");
const DevMode = @import("DevMode.zig"); const DevMode = @import("DevMode.zig");
const App = @import("App.zig"); const App = @import("App.zig");
@ -70,7 +71,6 @@ renderer_thr: std.Thread,
/// Mouse state. /// Mouse state.
mouse: Mouse, mouse: Mouse,
mouse_interval: u64,
/// The terminal IO handler. /// The terminal IO handler.
io: termio.Impl, io: termio.Impl,
@ -85,8 +85,10 @@ cell_size: renderer.CellSize,
/// Explicit padding due to configuration /// Explicit padding due to configuration
padding: renderer.Padding, padding: renderer.Padding,
/// The app configuration /// The configuration derived from the main config. We "derive" it so that
config: *const Config, /// we don't have a shared pointer hanging around that we need to worry about
/// the lifetime of. This makes updating config at runtime easier.
config: DerivedConfig,
/// Set to true for a single GLFW key/char callback cycle to cause the /// Set to true for a single GLFW key/char callback cycle to cause the
/// char callback to ignore. GLFW seems to always do key followed by char /// char callback to ignore. GLFW seems to always do key followed by char
@ -123,13 +125,50 @@ const Mouse = struct {
event_point: terminal.point.Viewport = .{}, event_point: terminal.point.Viewport = .{},
}; };
/// The configuration that a surface has, this is copied from the main
/// Config struct usually to prevent sharing a single value.
const DerivedConfig = struct {
arena: ArenaAllocator,
/// For docs for these, see the associated config they are derived from.
original_font_size: u8,
keybind: configpkg.Keybinds,
clipboard_read: bool,
clipboard_write: bool,
clipboard_trim_trailing_spaces: bool,
mouse_interval: u64,
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
var arena = ArenaAllocator.init(alloc_gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
return .{
.original_font_size = config.@"font-size",
.keybind = try config.keybind.clone(alloc),
.clipboard_read = config.@"clipboard-read",
.clipboard_write = config.@"clipboard-write",
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
// Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above.
.arena = arena,
};
}
pub fn deinit(self: *DerivedConfig) void {
self.arena.deinit();
}
};
/// Create a new surface. This must be called from the main thread. The /// Create a new surface. This must be called from the main thread. The
/// pointer to the memory for the surface must be provided and must be /// pointer to the memory for the surface must be provided and must be
/// stable due to interfacing with various callbacks. /// stable due to interfacing with various callbacks.
pub fn init( pub fn init(
self: *Surface, self: *Surface,
alloc: Allocator, alloc: Allocator,
config: *const Config, config: *const configpkg.Config,
app_mailbox: App.Mailbox, app_mailbox: App.Mailbox,
rt_surface: *apprt.runtime.Surface, rt_surface: *apprt.runtime.Surface,
) !void { ) !void {
@ -284,7 +323,7 @@ pub fn init(
// Create our terminal grid with the initial size // Create our terminal grid with the initial size
var renderer_impl = try Renderer.init(alloc, .{ var renderer_impl = try Renderer.init(alloc, .{
.config = config, .config = try Renderer.DerivedConfig.init(alloc, config),
.font_group = font_group, .font_group = font_group,
.padding = .{ .padding = .{
.explicit = padding, .explicit = padding,
@ -324,7 +363,8 @@ pub fn init(
var io = try termio.Impl.init(alloc, .{ var io = try termio.Impl.init(alloc, .{
.grid_size = grid_size, .grid_size = grid_size,
.screen_size = screen_size, .screen_size = screen_size,
.config = config, .full_config = config,
.config = try termio.Impl.DerivedConfig.init(alloc, config),
.renderer_state = &self.renderer_state, .renderer_state = &self.renderer_state,
.renderer_wakeup = render_thread.wakeup, .renderer_wakeup = render_thread.wakeup,
.renderer_mailbox = render_thread.mailbox, .renderer_mailbox = render_thread.mailbox,
@ -361,7 +401,6 @@ pub fn init(
}, },
.renderer_thr = undefined, .renderer_thr = undefined,
.mouse = .{}, .mouse = .{},
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
.io = io, .io = io,
.io_thread = io_thread, .io_thread = io_thread,
.io_thr = undefined, .io_thr = undefined,
@ -369,7 +408,7 @@ pub fn init(
.grid_size = grid_size, .grid_size = grid_size,
.cell_size = cell_size, .cell_size = cell_size,
.padding = padding, .padding = padding,
.config = config, .config = try DerivedConfig.init(alloc, config),
.imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(), .imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(),
}; };
@ -476,6 +515,7 @@ pub fn deinit(self: *Surface) void {
self.alloc.destroy(self.font_group); self.alloc.destroy(self.font_group);
self.alloc.destroy(self.renderer_state.mutex); self.alloc.destroy(self.renderer_state.mutex);
self.config.deinit();
log.info("surface closed addr={x}", .{@ptrToInt(self)}); log.info("surface closed addr={x}", .{@ptrToInt(self)});
} }
@ -489,6 +529,8 @@ pub fn close(self: *Surface) void {
/// surface. /// surface.
pub fn handleMessage(self: *Surface, msg: Message) !void { pub fn handleMessage(self: *Surface, msg: Message) !void {
switch (msg) { switch (msg) {
.change_config => |config| try self.changeConfig(config),
.set_title => |*v| { .set_title => |*v| {
// The ptrCast just gets sliceTo to return the proper type. // The ptrCast just gets sliceTo to return the proper type.
// We know that our title should end in 0. // We know that our title should end in 0.
@ -514,6 +556,54 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
} }
} }
/// Update our configuration at runtime.
fn changeConfig(self: *Surface, config: *const configpkg.Config) !void {
// Update our new derived config immediately
const derived = DerivedConfig.init(self.alloc, config) catch |err| {
// If the derivation fails then we just log and return. We don't
// hard fail in this case because we don't want to error the surface
// when config fails we just want to keep using the old config.
log.err("error updating configuration err={}", .{err});
return;
};
self.config.deinit();
self.config = derived;
// We need to store our configs in a heap-allocated pointer so that
// our messages aren't huge.
var renderer_config_ptr = try self.alloc.create(Renderer.DerivedConfig);
errdefer self.alloc.destroy(renderer_config_ptr);
var termio_config_ptr = try self.alloc.create(termio.Impl.DerivedConfig);
errdefer self.alloc.destroy(termio_config_ptr);
// Update our derived configurations for the renderer and termio,
// then send them a message to update.
renderer_config_ptr.* = try Renderer.DerivedConfig.init(self.alloc, config);
errdefer renderer_config_ptr.deinit();
termio_config_ptr.* = try termio.Impl.DerivedConfig.init(self.alloc, config);
errdefer termio_config_ptr.deinit();
_ = self.renderer_thread.mailbox.push(.{
.change_config = .{
.alloc = self.alloc,
.ptr = renderer_config_ptr,
},
}, .{ .forever = {} });
_ = self.io_thread.mailbox.push(.{
.change_config = .{
.alloc = self.alloc,
.ptr = termio_config_ptr,
},
}, .{ .forever = {} });
// With mailbox messages sent, we have to wake them up so they process it.
self.queueRender() catch |err| {
log.warn("failed to notify renderer of config change err={}", .{err});
};
self.io_thread.wakeup.notify() catch |err| {
log.warn("failed to notify io thread of config change err={}", .{err});
};
}
/// Returns the x/y coordinate of where the IME (Input Method Editor) /// Returns the x/y coordinate of where the IME (Input Method Editor)
/// keyboard should be rendered. /// keyboard should be rendered.
pub fn imePoint(self: *const Surface) apprt.IMEPos { pub fn imePoint(self: *const Surface) apprt.IMEPos {
@ -557,7 +647,7 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
} }
fn clipboardRead(self: *const Surface, kind: u8) !void { fn clipboardRead(self: *const Surface, kind: u8) !void {
if (!self.config.@"clipboard-read") { if (!self.config.clipboard_read) {
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
return; return;
} }
@ -593,7 +683,7 @@ fn clipboardRead(self: *const Surface, kind: u8) !void {
} }
fn clipboardWrite(self: *const Surface, data: []const u8) !void { fn clipboardWrite(self: *const Surface, data: []const u8) !void {
if (!self.config.@"clipboard-write") { if (!self.config.clipboard_write) {
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{}); log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
return; return;
} }
@ -793,6 +883,12 @@ pub fn keyCallback(
.unbind => unreachable, .unbind => unreachable,
.ignore => {}, .ignore => {},
.reload_config => {
_ = self.app_mailbox.push(.{
.reload_config = {},
}, .{ .instant = {} });
},
.csi => |data| { .csi => |data| {
_ = self.io_thread.mailbox.push(.{ _ = self.io_thread.mailbox.push(.{
.write_stable = "\x1B[", .write_stable = "\x1B[",
@ -833,7 +929,7 @@ pub fn keyCallback(
var buf = self.io.terminal.screen.selectionString( var buf = self.io.terminal.screen.selectionString(
self.alloc, self.alloc,
sel, sel,
self.config.@"clipboard-trim-trailing-spaces", self.config.clipboard_trim_trailing_spaces,
) catch |err| { ) catch |err| {
log.err("error reading selection string err={}", .{err}); log.err("error reading selection string err={}", .{err});
return; return;
@ -901,7 +997,7 @@ pub fn keyCallback(
log.debug("reset font size", .{}); log.debug("reset font size", .{});
var size = self.font_size; var size = self.font_size;
size.points = self.config.@"font-size"; size.points = self.config.original_font_size;
self.setFontSize(size); self.setFontSize(size);
}, },
@ -1423,7 +1519,7 @@ pub fn mouseButtonCallback(
// is less than and our interval and if so, increase the count. // is less than and our interval and if so, increase the count.
if (self.mouse.left_click_count > 0) { if (self.mouse.left_click_count > 0) {
const since = now.since(self.mouse.left_click_time); const since = now.since(self.mouse.left_click_time);
if (since > self.mouse_interval) { if (since > self.config.mouse_interval) {
self.mouse.left_click_count = 0; self.mouse.left_click_count = 0;
} }
} }

View File

@ -13,6 +13,7 @@ const apprt = @import("../apprt.zig");
const input = @import("../input.zig"); const input = @import("../input.zig");
const CoreApp = @import("../App.zig"); const CoreApp = @import("../App.zig");
const CoreSurface = @import("../Surface.zig"); const CoreSurface = @import("../Surface.zig");
const Config = @import("../config.zig").Config;
const log = std.log.scoped(.embedded_window); const log = std.log.scoped(.embedded_window);
@ -35,6 +36,11 @@ pub const App = struct {
/// a full tick of the app loop. /// a full tick of the app loop.
wakeup: *const fn (AppUD) callconv(.C) void, wakeup: *const fn (AppUD) callconv(.C) void,
/// Reload the configuration and return the new configuration.
/// The old configuration can be freed immediately when this is
/// called.
reload_config: *const fn (AppUD) callconv(.C) ?*const Config,
/// Called to set the title of the window. /// Called to set the title of the window.
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void, set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
@ -57,16 +63,31 @@ pub const App = struct {
}; };
core_app: *CoreApp, core_app: *CoreApp,
config: *const Config,
opts: Options, opts: Options,
pub fn init(core_app: *CoreApp, opts: Options) !App { pub fn init(core_app: *CoreApp, config: *const Config, opts: Options) !App {
return .{ .core_app = core_app, .opts = opts }; return .{
.core_app = core_app,
.config = config,
.opts = opts,
};
} }
pub fn terminate(self: App) void { pub fn terminate(self: App) void {
_ = self; _ = self;
} }
pub fn reloadConfig(self: *App) !?*const Config {
// Reload
if (self.opts.reload_config(self.opts.userdata)) |new| {
self.config = new;
return self.config;
}
return null;
}
pub fn wakeup(self: App) void { pub fn wakeup(self: App) void {
self.opts.wakeup(self.opts.userdata); self.opts.wakeup(self.opts.userdata);
} }
@ -143,7 +164,7 @@ pub const Surface = struct {
// ready to use. // ready to use.
try self.core_surface.init( try self.core_surface.init(
app.core_app.alloc, app.core_app.alloc,
app.core_app.config, app.config,
.{ .rt_app = app, .mailbox = &app.core_app.mailbox }, .{ .rt_app = app, .mailbox = &app.core_app.mailbox },
self, self,
); );
@ -338,7 +359,6 @@ pub const Surface = struct {
// C API // C API
pub const CAPI = struct { pub const CAPI = struct {
const global = &@import("../main.zig").state; const global = &@import("../main.zig").state;
const Config = @import("../config.zig").Config;
/// Create a new app. /// Create a new app.
export fn ghostty_app_new( export fn ghostty_app_new(
@ -355,13 +375,13 @@ pub const CAPI = struct {
opts: *const apprt.runtime.App.Options, opts: *const apprt.runtime.App.Options,
config: *const Config, config: *const Config,
) !*App { ) !*App {
var core_app = try CoreApp.create(global.alloc, config); var core_app = try CoreApp.create(global.alloc);
errdefer core_app.destroy(); errdefer core_app.destroy();
// Create our runtime app // Create our runtime app
var app = try global.alloc.create(App); var app = try global.alloc.create(App);
errdefer global.alloc.destroy(app); errdefer global.alloc.destroy(app);
app.* = try App.init(core_app, opts.*); app.* = try App.init(core_app, config, opts.*);
errdefer app.terminate(); errdefer app.terminate();
return app; return app;

View File

@ -19,6 +19,8 @@ const Renderer = renderer.Renderer;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const CoreApp = @import("../App.zig"); const CoreApp = @import("../App.zig");
const CoreSurface = @import("../Surface.zig"); const CoreSurface = @import("../Surface.zig");
const Config = @import("../config.zig").Config;
const DevMode = @import("../DevMode.zig");
// Get native API access on certain platforms so we can do more customization. // Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{ const glfwNative = glfw.Native(.{
@ -29,6 +31,7 @@ const log = std.log.scoped(.glfw);
pub const App = struct { pub const App = struct {
app: *CoreApp, app: *CoreApp,
config: Config,
/// Mac-specific state. /// Mac-specific state.
darwin: if (Darwin.enabled) Darwin else void, darwin: if (Darwin.enabled) Darwin else void,
@ -53,14 +56,24 @@ pub const App = struct {
var darwin = if (Darwin.enabled) try Darwin.init() else {}; var darwin = if (Darwin.enabled) try Darwin.init() else {};
errdefer if (Darwin.enabled) darwin.deinit(); errdefer if (Darwin.enabled) darwin.deinit();
// Load our configuration
var config = try Config.load(core_app.alloc);
errdefer config.deinit();
// If we have DevMode on, store the config so we can show it. This
// is messy because we're copying a thing here. We should clean this
// up when we take a pass at cleaning up the dev mode.
if (DevMode.enabled) DevMode.instance.config = config;
return .{ return .{
.app = core_app, .app = core_app,
.config = config,
.darwin = darwin, .darwin = darwin,
}; };
} }
pub fn terminate(self: App) void { pub fn terminate(self: *App) void {
_ = self; self.config.deinit();
glfw.terminate(); glfw.terminate();
} }
@ -90,6 +103,23 @@ pub const App = struct {
glfw.postEmptyEvent(); glfw.postEmptyEvent();
} }
/// Reload the configuration. This should return the new configuration.
/// The old value can be freed immediately at this point assuming a
/// successful return.
///
/// The returned pointer value is only valid for a stable self pointer.
pub fn reloadConfig(self: *App) !?*const Config {
// Load our configuration
var config = try Config.load(self.app.alloc);
errdefer config.deinit();
// Update the existing config, be sure to clean up the old one.
self.config.deinit();
self.config = config;
return &self.config;
}
/// Create a new window for the app. /// Create a new window for the app.
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
_ = try self.newSurface(parent_); _ = try self.newSurface(parent_);
@ -139,7 +169,7 @@ pub const App = struct {
errdefer surface.deinit(); errdefer surface.deinit();
// If we have a parent, inherit some properties // If we have a parent, inherit some properties
if (self.app.config.@"window-inherit-font-size") { if (self.config.@"window-inherit-font-size") {
if (parent_) |parent| { if (parent_) |parent| {
surface.core_surface.setFontSize(parent.font_size); surface.core_surface.setFontSize(parent.font_size);
} }
@ -313,7 +343,7 @@ pub const Surface = struct {
// Initialize our surface now that we have the stable pointer. // Initialize our surface now that we have the stable pointer.
try self.core_surface.init( try self.core_surface.init(
app.app.alloc, app.app.alloc,
app.app.config, &app.config,
.{ .rt_app = app, .mailbox = &app.app.mailbox }, .{ .rt_app = app, .mailbox = &app.app.mailbox },
self, self,
); );

View File

@ -9,6 +9,7 @@ const apprt = @import("../apprt.zig");
const input = @import("../input.zig"); const input = @import("../input.zig");
const CoreApp = @import("../App.zig"); const CoreApp = @import("../App.zig");
const CoreSurface = @import("../Surface.zig"); const CoreSurface = @import("../Surface.zig");
const Config = @import("../config.zig").Config;
pub const c = @cImport({ pub const c = @cImport({
@cInclude("gtk/gtk.h"); @cInclude("gtk/gtk.h");
@ -37,6 +38,7 @@ pub const App = struct {
}; };
core_app: *CoreApp, core_app: *CoreApp,
config: Config,
app: *c.GtkApplication, app: *c.GtkApplication,
ctx: *c.GMainContext, ctx: *c.GMainContext,
@ -53,6 +55,10 @@ pub const App = struct {
// rid of this dep. // rid of this dep.
if (!glfw.init(.{})) return error.GlfwInitFailed; if (!glfw.init(.{})) return error.GlfwInitFailed;
// Load our configuration
var config = try Config.load(core_app.alloc);
errdefer config.deinit();
// Create our GTK Application which encapsulates our process. // Create our GTK Application which encapsulates our process.
const app = @ptrCast(?*c.GtkApplication, c.gtk_application_new( const app = @ptrCast(?*c.GtkApplication, c.gtk_application_new(
null, null,
@ -108,6 +114,7 @@ pub const App = struct {
return .{ return .{
.core_app = core_app, .core_app = core_app,
.app = app, .app = app,
.config = config,
.ctx = ctx, .ctx = ctx,
.cursor_default = cursor_default, .cursor_default = cursor_default,
.cursor_ibeam = cursor_ibeam, .cursor_ibeam = cursor_ibeam,
@ -116,7 +123,7 @@ pub const App = struct {
// Terminate the application. The application will not be restarted after // Terminate the application. The application will not be restarted after
// this so all global state can be cleaned up. // this so all global state can be cleaned up.
pub fn terminate(self: App) void { pub fn terminate(self: *App) void {
c.g_settings_sync(); c.g_settings_sync();
while (c.g_main_context_iteration(self.ctx, 0) != 0) {} while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
c.g_main_context_release(self.ctx); c.g_main_context_release(self.ctx);
@ -125,9 +132,28 @@ pub const App = struct {
c.g_object_unref(self.cursor_ibeam); c.g_object_unref(self.cursor_ibeam);
c.g_object_unref(self.cursor_default); c.g_object_unref(self.cursor_default);
self.config.deinit();
glfw.terminate(); glfw.terminate();
} }
/// Reload the configuration. This should return the new configuration.
/// The old value can be freed immediately at this point assuming a
/// successful return.
///
/// The returned pointer value is only valid for a stable self pointer.
pub fn reloadConfig(self: *App) !?*const Config {
// Load our configuration
var config = try Config.load(self.core_app.alloc);
errdefer config.deinit();
// Update the existing config, be sure to clean up the old one.
self.config.deinit();
self.config = config;
return &self.config;
}
pub fn wakeup(self: App) void { pub fn wakeup(self: App) void {
_ = self; _ = self;
c.g_main_context_wakeup(null); c.g_main_context_wakeup(null);
@ -575,7 +601,7 @@ pub const Surface = struct {
// Initialize our surface now that we have the stable pointer. // Initialize our surface now that we have the stable pointer.
try self.core_surface.init( try self.core_surface.init(
self.app.core_app.alloc, self.app.core_app.alloc,
self.app.core_app.config, &self.app.config,
.{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox }, .{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox },
self, self,
); );

View File

@ -2,6 +2,7 @@ const App = @import("../App.zig");
const Surface = @import("../Surface.zig"); const Surface = @import("../Surface.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const termio = @import("../termio.zig"); const termio = @import("../termio.zig");
const Config = @import("../config.zig").Config;
/// The message types that can be sent to a single surface. /// The message types that can be sent to a single surface.
pub const Message = union(enum) { pub const Message = union(enum) {
@ -24,6 +25,11 @@ pub const Message = union(enum) {
/// Write the clipboard contents. /// Write the clipboard contents.
clipboard_write: WriteReq, clipboard_write: WriteReq,
/// Change the configuration to the given configuration. The pointer is
/// not valid after receiving this message so any config must be used
/// and derived immediately.
change_config: *const Config,
/// Close the surface. This will only close the current surface that /// Close the surface. This will only close the current surface that
/// receives this, not the full application. /// receives this, not the full application.
close: void, close: void,

View File

@ -1,3 +1,4 @@
const config = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@ -166,11 +167,65 @@ pub const Config = struct {
/// This is set by the CLI parser for deinit. /// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null, _arena: ?ArenaAllocator = null,
/// Key is an enum of all the available configuration keys. This is used
/// when paired with diff to determine what fields have changed in a config,
/// amongst other things.
pub const Key = key: {
const field_infos = std.meta.fields(Config);
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
var i: usize = 0;
inline for (field_infos) |field| {
// Ignore fields starting with "_" since they're internal and
// not copied ever.
if (field.name[0] == '_') continue;
enumFields[i] = .{
.name = field.name,
.value = i,
};
i += 1;
}
var decls = [_]std.builtin.Type.Declaration{};
break :key @Type(.{
.Enum = .{
.tag_type = std.math.IntFittingRange(0, field_infos.len - 1),
.fields = enumFields[0..i],
.decls = &decls,
.is_exhaustive = true,
},
});
};
pub fn deinit(self: *Config) void { pub fn deinit(self: *Config) void {
if (self._arena) |arena| arena.deinit(); if (self._arena) |arena| arena.deinit();
self.* = undefined; self.* = undefined;
} }
/// Load the configuration according to the default rules:
///
/// 1. Defaults
/// 2. XDG Config File
/// 3. CLI flags
/// 4. Recursively defined configuration files
///
pub fn load(alloc_gpa: Allocator) !Config {
var result = try default(alloc_gpa);
errdefer result.deinit();
// If we have a configuration file in our home directory, parse that first.
try result.loadDefaultFiles(alloc_gpa);
// Parse the config from the CLI args
try result.loadCliArgs(alloc_gpa);
// Parse the config files that were added from our file and CLI args.
try result.loadRecursiveFiles(alloc_gpa);
try result.finalize();
return result;
}
pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Build up our basic config // Build up our basic config
var result: Config = .{ var result: Config = .{
@ -180,6 +235,12 @@ pub const Config = struct {
const alloc = result._arena.?.allocator(); const alloc = result._arena.?.allocator();
// Add our default keybindings // Add our default keybindings
try result.keybind.set.put(
alloc,
.{ .key = .space, .mods = .{ .super = true, .alt = true, .ctrl = true } },
.{ .reload_config = {} },
);
{ {
// On macOS we default to super but Linux ctrl+shift since // On macOS we default to super but Linux ctrl+shift since
// ctrl+c is to kill the process. // ctrl+c is to kill the process.
@ -499,6 +560,14 @@ pub const Config = struct {
} }
} }
/// Load and parse the CLI args.
pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// Parse the config from the CLI args
var iter = try std.process.argsWithAllocator(alloc_gpa);
defer iter.deinit();
try cli_args.parse(Config, alloc_gpa, self, &iter);
}
/// Load and parse the config files that were added in the "config-file" key. /// Load and parse the config files that were added in the "config-file" key.
pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void { pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void {
// TODO(mitchellh): we should parse the files form the homedir first // TODO(mitchellh): we should parse the files form the homedir first
@ -605,8 +674,219 @@ pub const Config = struct {
self.@"click-repeat-interval" = internal_os.clickInterval() orelse 500; self.@"click-repeat-interval" = internal_os.clickInterval() orelse 500;
} }
} }
/// Create a copy of this configuration. This is useful as a starting
/// point for modifying a configuration since a config can NOT be
/// modified once it is in use by an app or surface.
pub fn clone(self: *const Config, alloc_gpa: Allocator) !Config {
// Start with an empty config with a new arena we're going
// to use for all our copies.
var result: Config = .{
._arena = ArenaAllocator.init(alloc_gpa),
};
errdefer result.deinit();
const alloc = result._arena.?.allocator();
inline for (@typeInfo(Config).Struct.fields) |field| {
if (!@hasField(Key, field.name)) continue;
@field(result, field.name) = try cloneValue(
alloc,
field.type,
@field(self, field.name),
);
}
return result;
}
fn cloneValue(alloc: Allocator, comptime T: type, src: T) !T {
// Do known named types first
switch (T) {
[]const u8 => return try alloc.dupe(u8, src),
[:0]const u8 => return try alloc.dupeZ(u8, src),
else => {},
}
// Back into types of types
switch (@typeInfo(T)) {
inline .Bool,
.Int,
=> return src,
.Optional => |info| return try cloneValue(
alloc,
info.child,
src orelse return null,
),
.Struct => return try src.clone(alloc),
else => {
@compileLog(T);
@compileError("unsupported field type");
},
}
}
/// Returns an iterator that goes through each changed field from
/// old to new. The order of old or new do not matter.
pub fn changeIterator(old: *const Config, new: *const Config) ChangeIterator {
return .{
.old = old,
.new = new,
};
}
/// Returns true if the given key has changed from old to new. This
/// requires the key to be comptime known to make this more efficient.
pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool {
// Get the field at comptime
const field = comptime field: {
const fields = std.meta.fields(Config);
for (fields) |field| {
if (@field(Key, field.name) == key) {
break :field field;
}
}
unreachable;
};
const old_value = @field(self, field.name);
const new_value = @field(new, field.name);
return !equal(field.type, old_value, new_value);
}
/// This yields a key for every changed field between old and new.
pub const ChangeIterator = struct {
old: *const Config,
new: *const Config,
i: usize = 0,
pub fn next(self: *ChangeIterator) ?Key {
const fields = comptime std.meta.fields(Key);
while (self.i < fields.len) {
switch (self.i) {
inline 0...(fields.len - 1) => |i| {
const field = fields[i];
const key = @field(Key, field.name);
self.i += 1;
if (self.old.changed(self.new, key)) return key;
},
else => unreachable,
}
}
return null;
}
};
test "clone default" {
const testing = std.testing;
const alloc = testing.allocator;
var source = try Config.default(alloc);
defer source.deinit();
var dest = try source.clone(alloc);
defer dest.deinit();
// Should have no changes
var it = source.changeIterator(&dest);
try testing.expectEqual(@as(?Key, null), it.next());
// I want to do this but this doesn't work (the API doesn't work)
// try testing.expectEqualDeep(dest, source);
}
test "changed" {
const testing = std.testing;
const alloc = testing.allocator;
var source = try Config.default(alloc);
defer source.deinit();
var dest = try source.clone(alloc);
defer dest.deinit();
dest.@"font-family" = "something else";
try testing.expect(source.changed(&dest, .@"font-family"));
try testing.expect(!source.changed(&dest, .@"font-size"));
}
}; };
/// A config-specific helper to determine if two values of the same
/// type are equal. This isn't the same as std.mem.eql or std.testing.equals
/// because we expect structs to implement their own equality.
///
/// This also doesn't support ALL Zig types, because we only add to it
/// as we need types for the config.
fn equal(comptime T: type, old: T, new: T) bool {
// Do known named types first
switch (T) {
inline []const u8,
[:0]const u8,
=> return std.mem.eql(u8, old, new),
else => {},
}
// Back into types of types
switch (@typeInfo(T)) {
.Void => return true,
inline .Bool,
.Int,
.Enum,
=> return old == new,
.Optional => |info| {
if (old == null and new == null) return true;
if (old == null or new == null) return false;
return equal(info.child, old.?, new.?);
},
.Struct => |info| {
if (@hasDecl(T, "equal")) return old.equal(new);
// If a struct doesn't declare an "equal" function, we fall back
// to a recursive field-by-field compare.
inline for (info.fields) |field_info| {
if (!equal(
field_info.type,
@field(old, field_info.name),
@field(new, field_info.name),
)) return false;
}
return true;
},
.Union => |info| {
const tag_type = info.tag_type.?;
const old_tag = std.meta.activeTag(old);
const new_tag = std.meta.activeTag(new);
if (old_tag != new_tag) return false;
inline for (info.fields) |field_info| {
if (@field(tag_type, field_info.name) == old_tag) {
return equal(
field_info.type,
@field(old, field_info.name),
@field(new, field_info.name),
);
}
}
unreachable;
},
else => {
@compileLog(T);
@compileError("unsupported field type");
},
}
}
/// Color represents a color using RGB. /// Color represents a color using RGB.
pub const Color = struct { pub const Color = struct {
r: u8, r: u8,
@ -626,6 +906,16 @@ pub const Color = struct {
return fromHex(input orelse return error.ValueRequired); return fromHex(input orelse return error.ValueRequired);
} }
/// Deep copy of the struct. Required by Config.
pub fn clone(self: Color, _: Allocator) !Color {
return self;
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Color, other: Color) bool {
return std.meta.eql(self, other);
}
/// fromHex parses a color from a hex value such as #RRGGBB. The "#" /// fromHex parses a color from a hex value such as #RRGGBB. The "#"
/// is optional. /// is optional.
pub fn fromHex(input: []const u8) !Color { pub fn fromHex(input: []const u8) !Color {
@ -689,6 +979,16 @@ pub const Palette = struct {
self.value[key] = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b }; self.value[key] = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
} }
/// Deep copy of the struct. Required by Config.
pub fn clone(self: Self, _: Allocator) !Self {
return self;
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
return std.meta.eql(self, other);
}
test "parseCLI" { test "parseCLI" {
const testing = std.testing; const testing = std.testing;
@ -722,6 +1022,23 @@ pub const RepeatableString = struct {
try self.list.append(alloc, value); try self.list.append(alloc, value);
} }
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.list = try self.list.clone(alloc),
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
const itemsA = self.list.items;
const itemsB = other.list.items;
if (itemsA.len != itemsB.len) return false;
for (itemsA, itemsB) |a, b| {
if (!std.mem.eql(u8, a, b)) return false;
} else return true;
}
test "parseCLI" { test "parseCLI" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); var arena = ArenaAllocator.init(testing.allocator);
@ -766,6 +1083,35 @@ pub const Keybinds = struct {
} }
} }
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Keybinds, alloc: Allocator) !Keybinds {
return .{
.set = .{
.bindings = try self.set.bindings.clone(alloc),
},
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Keybinds, other: Keybinds) bool {
const self_map = self.set.bindings;
const other_map = other.set.bindings;
if (self_map.count() != other_map.count()) return false;
var it = self_map.iterator();
while (it.next()) |self_entry| {
const other_entry = other_map.getEntry(self_entry.key_ptr.*) orelse
return false;
if (!config.equal(
inputpkg.Binding.Action,
self_entry.value_ptr.*,
other_entry.value_ptr.*,
)) return false;
}
return true;
}
test "parseCLI" { test "parseCLI" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); var arena = ArenaAllocator.init(testing.allocator);
@ -856,6 +1202,13 @@ pub const CAPI = struct {
} }
} }
/// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void {
self.loadCliArgs(global.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
/// Load the configuration from a string in the same format as /// Load the configuration from a string in the same format as
/// the file-based syntax for the desktop version of the terminal. /// the file-based syntax for the desktop version of the terminal.
export fn ghostty_config_load_string( export fn ghostty_config_load_string(

View File

@ -187,6 +187,12 @@ pub const Action = union(enum) {
/// Focus on a split in a given direction. /// Focus on a split in a given direction.
goto_split: SplitFocusDirection, goto_split: SplitFocusDirection,
/// Reload the configuration. The exact meaning depends on the app runtime
/// in use but this usually involves re-reading the configuration file
/// and applying any changes. Note that not all changes can be applied at
/// runtime.
reload_config: void,
/// Close the current "surface", whether that is a window, tab, split, /// Close the current "surface", whether that is a window, tab, split,
/// etc. This only closes ONE surface. /// etc. This only closes ONE surface.
close_surface: void, close_surface: void,

View File

@ -14,8 +14,6 @@ const xdg = @import("xdg.zig");
const apprt = @import("apprt.zig"); const apprt = @import("apprt.zig");
const App = @import("App.zig"); const App = @import("App.zig");
const cli_args = @import("cli_args.zig");
const Config = @import("config.zig").Config;
const Ghostty = @import("main_c.zig").Ghostty; const Ghostty = @import("main_c.zig").Ghostty;
/// Global process state. This is initialized in main() for exe artifacts /// Global process state. This is initialized in main() for exe artifacts
@ -29,27 +27,8 @@ pub fn main() !void {
defer state.deinit(); defer state.deinit();
const alloc = state.alloc; const alloc = state.alloc;
// Try reading our config
var config = try Config.default(alloc);
defer config.deinit();
// If we have a configuration file in our home directory, parse that first.
try config.loadDefaultFiles(alloc);
// Parse the config from the CLI args
{
var iter = try std.process.argsWithAllocator(alloc);
defer iter.deinit();
try cli_args.parse(Config, alloc, &config, &iter);
}
// Parse the config files that were added from our file and CLI args.
try config.loadRecursiveFiles(alloc);
try config.finalize();
//std.log.debug("config={}", .{config});
// Create our app state // Create our app state
var app = try App.create(alloc, &config); var app = try App.create(alloc);
defer app.destroy(); defer app.destroy();
// Create our runtime app // Create our runtime app

View File

@ -11,6 +11,7 @@ const objc = @import("objc");
const macos = @import("macos"); const macos = @import("macos");
const imgui = @import("imgui"); const imgui = @import("imgui");
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig"); const font = @import("../font/main.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
@ -31,6 +32,9 @@ const log = std.log.scoped(.metal);
/// Allocator that can be used /// Allocator that can be used
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
/// The configuration we need derived from the main config.
config: DerivedConfig,
/// The mailbox for communicating with the window. /// The mailbox for communicating with the window.
surface_mailbox: apprt.surface.Mailbox, surface_mailbox: apprt.surface.Mailbox,
@ -51,17 +55,6 @@ focused: bool,
/// blinking. /// blinking.
cursor_visible: bool, cursor_visible: bool,
cursor_style: renderer.CursorStyle, cursor_style: renderer.CursorStyle,
cursor_color: ?terminal.color.RGB,
/// Default foreground color
foreground: terminal.color.RGB,
/// Default background color
background: terminal.color.RGB,
/// Default selection color
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
/// The current set of cells to render. This is rebuilt on every frame /// The current set of cells to render. This is rebuilt on every frame
/// but we keep this around so that we don't reallocate. Each set of /// but we keep this around so that we don't reallocate. Each set of
@ -117,6 +110,48 @@ const GPUCellMode = enum(u8) {
strikethrough = 8, strikethrough = 8,
}; };
/// The configuration for this renderer that is derived from the main
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct {
cursor_color: ?terminal.color.RGB,
background: terminal.color.RGB,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
pub fn init(
alloc_gpa: Allocator,
config: *const configpkg.Config,
) !DerivedConfig {
_ = alloc_gpa;
return .{
.cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB()
else
null,
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
};
}
pub fn deinit(self: *DerivedConfig) void {
_ = self;
}
};
/// Returns the hints that we want for this /// Returns the hints that we want for this
pub fn glfwWindowHints() glfw.Window.Hints { pub fn glfwWindowHints() glfw.Window.Hints {
return .{ return .{
@ -233,6 +268,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
return Metal{ return Metal{
.alloc = alloc, .alloc = alloc,
.config = options.config,
.surface_mailbox = options.surface_mailbox, .surface_mailbox = options.surface_mailbox,
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
.screen_size = null, .screen_size = null,
@ -240,17 +276,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.focused = true, .focused = true,
.cursor_visible = true, .cursor_visible = true,
.cursor_style = .box, .cursor_style = .box,
.cursor_color = if (options.config.@"cursor-color") |col| col.toTerminalRGB() else null,
.background = options.config.background.toTerminalRGB(),
.foreground = options.config.foreground.toTerminalRGB(),
.selection_background = if (options.config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (options.config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
// Render state // Render state
.cells_bg = .{}, .cells_bg = .{},
@ -286,6 +311,8 @@ pub fn deinit(self: *Metal) void {
self.font_shaper.deinit(); self.font_shaper.deinit();
self.alloc.free(self.font_shaper.cell_buf); self.alloc.free(self.font_shaper.cell_buf);
self.config.deinit();
deinitMTLResource(self.buf_cells_bg); deinitMTLResource(self.buf_cells_bg);
deinitMTLResource(self.buf_cells); deinitMTLResource(self.buf_cells);
deinitMTLResource(self.buf_instance); deinitMTLResource(self.buf_instance);
@ -482,15 +509,15 @@ pub fn render(
} }
// Swap bg/fg if the terminal is reversed // Swap bg/fg if the terminal is reversed
const bg = self.background; const bg = self.config.background;
const fg = self.foreground; const fg = self.config.foreground;
defer { defer {
self.background = bg; self.config.background = bg;
self.foreground = fg; self.config.foreground = fg;
} }
if (state.terminal.modes.reverse_colors) { if (state.terminal.modes.reverse_colors) {
self.background = fg; self.config.background = fg;
self.foreground = bg; self.config.foreground = bg;
} }
// We used to share terminal state, but we've since learned through // We used to share terminal state, but we've since learned through
@ -516,7 +543,7 @@ pub fn render(
null; null;
break :critical .{ break :critical .{
.bg = self.background, .bg = self.config.background,
.devmode = if (state.devmode) |dm| dm.visible else false, .devmode = if (state.devmode) |dm| dm.visible else false,
.selection = selection, .selection = selection,
.screen = screen_copy, .screen = screen_copy,
@ -697,6 +724,11 @@ fn drawCells(
} }
} }
/// Update the configuration.
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
self.config = config.*;
}
/// Resize the screen. /// Resize the screen.
pub fn setScreenSize(self: *Metal, dim: renderer.ScreenSize) !void { pub fn setScreenSize(self: *Metal, dim: renderer.ScreenSize) !void {
// Store our screen size // Store our screen size
@ -878,8 +910,8 @@ pub fn updateCell(
// If we are selected, we our colors are just inverted fg/bg // If we are selected, we our colors are just inverted fg/bg
if (sel.contains(screen_point)) { if (sel.contains(screen_point)) {
break :colors BgFg{ break :colors BgFg{
.bg = self.selection_background orelse self.foreground, .bg = self.config.selection_background orelse self.config.foreground,
.fg = self.selection_foreground orelse self.background, .fg = self.config.selection_foreground orelse self.config.background,
}; };
} }
} }
@ -888,13 +920,13 @@ pub fn updateCell(
// In normal mode, background and fg match the cell. We // In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color. // un-optionalize the fg by defaulting to our fg color.
.bg = if (cell.attrs.has_bg) cell.bg else null, .bg = if (cell.attrs.has_bg) cell.bg else null,
.fg = if (cell.attrs.has_fg) cell.fg else self.foreground, .fg = if (cell.attrs.has_fg) cell.fg else self.config.foreground,
} else .{ } else .{
// In inverted mode, the background MUST be set to something // In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The // (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background. // fg is either the bg or default background.
.bg = if (cell.attrs.has_fg) cell.fg else self.foreground, .bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground,
.fg = if (cell.attrs.has_bg) cell.bg else self.background, .fg = if (cell.attrs.has_bg) cell.bg else self.config.background,
}; };
break :colors res; break :colors res;
}; };
@ -988,7 +1020,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void {
screen.cursor.x, screen.cursor.x,
); );
const color = self.cursor_color orelse terminal.color.RGB{ const color = self.config.cursor_color orelse terminal.color.RGB{
.r = 0xFF, .r = 0xFF,
.g = 0xFF, .g = 0xFF,
.b = 0xFF, .b = 0xFF,

View File

@ -8,6 +8,7 @@ const assert = std.debug.assert;
const testing = std.testing; const testing = std.testing;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig"); const font = @import("../font/main.zig");
const imgui = @import("imgui"); const imgui = @import("imgui");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
@ -43,6 +44,9 @@ const drawMutexZero = if (DrawMutex == void) void{} else .{};
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
/// The configuration we need derived from the main config.
config: DerivedConfig,
/// Current cell dimensions for this grid. /// Current cell dimensions for this grid.
cell_size: renderer.CellSize, cell_size: renderer.CellSize,
@ -84,17 +88,6 @@ font_shaper: font.Shaper,
/// blinking. /// blinking.
cursor_visible: bool, cursor_visible: bool,
cursor_style: renderer.CursorStyle, cursor_style: renderer.CursorStyle,
cursor_color: ?terminal.color.RGB,
/// Default foreground color
foreground: terminal.color.RGB,
/// Default background color
background: terminal.color.RGB,
/// Default selection color
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
/// True if the window is focused /// True if the window is focused
focused: bool, focused: bool,
@ -232,6 +225,48 @@ const GPUCellMode = enum(u8) {
} }
}; };
/// The configuration for this renderer that is derived from the main
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct {
cursor_color: ?terminal.color.RGB,
background: terminal.color.RGB,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
pub fn init(
alloc_gpa: Allocator,
config: *const configpkg.Config,
) !DerivedConfig {
_ = alloc_gpa;
return .{
.cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB()
else
null,
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
};
}
pub fn deinit(self: *DerivedConfig) void {
_ = self;
}
};
pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
// Create the initial font shaper // Create the initial font shaper
var shape_buf = try alloc.alloc(font.shape.Cell, 1); var shape_buf = try alloc.alloc(font.shape.Cell, 1);
@ -354,6 +389,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
return OpenGL{ return OpenGL{
.alloc = alloc, .alloc = alloc,
.config = options.config,
.cells_bg = .{}, .cells_bg = .{},
.cells = .{}, .cells = .{},
.cells_lru = CellsLRU.init(0), .cells_lru = CellsLRU.init(0),
@ -369,18 +405,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.font_shaper = shaper, .font_shaper = shaper,
.cursor_visible = true, .cursor_visible = true,
.cursor_style = .box, .cursor_style = .box,
.cursor_color = if (options.config.@"cursor-color") |col| col.toTerminalRGB() else null, .draw_background = options.config.background,
.background = options.config.background.toTerminalRGB(),
.foreground = options.config.foreground.toTerminalRGB(),
.draw_background = options.config.background.toTerminalRGB(),
.selection_background = if (options.config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (options.config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
.focused = true, .focused = true,
.padding = options.padding, .padding = options.padding,
.surface_mailbox = options.surface_mailbox, .surface_mailbox = options.surface_mailbox,
@ -404,6 +429,9 @@ pub fn deinit(self: *OpenGL) void {
self.cells.deinit(self.alloc); self.cells.deinit(self.alloc);
self.cells_bg.deinit(self.alloc); self.cells_bg.deinit(self.alloc);
self.config.deinit();
self.* = undefined; self.* = undefined;
} }
@ -673,15 +701,15 @@ pub fn render(
} }
// Swap bg/fg if the terminal is reversed // Swap bg/fg if the terminal is reversed
const bg = self.background; const bg = self.config.background;
const fg = self.foreground; const fg = self.config.foreground;
defer { defer {
self.background = bg; self.config.background = bg;
self.foreground = fg; self.config.foreground = fg;
} }
if (state.terminal.modes.reverse_colors) { if (state.terminal.modes.reverse_colors) {
self.background = fg; self.config.background = fg;
self.foreground = bg; self.config.foreground = bg;
} }
// Build our devmode draw data // Build our devmode draw data
@ -723,7 +751,7 @@ pub fn render(
null; null;
break :critical .{ break :critical .{
.gl_bg = self.background, .gl_bg = self.config.background,
.devmode_data = devmode_data, .devmode_data = devmode_data,
.active_screen = state.terminal.active_screen, .active_screen = state.terminal.active_screen,
.selection = selection, .selection = selection,
@ -944,7 +972,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
screen.cursor.x, screen.cursor.x,
); );
const color = self.cursor_color orelse terminal.color.RGB{ const color = self.config.cursor_color orelse terminal.color.RGB{
.r = 0xFF, .r = 0xFF,
.g = 0xFF, .g = 0xFF,
.b = 0xFF, .b = 0xFF,
@ -1031,8 +1059,8 @@ pub fn updateCell(
// If we are selected, we use the selection colors // If we are selected, we use the selection colors
if (sel.contains(screen_point)) { if (sel.contains(screen_point)) {
break :colors BgFg{ break :colors BgFg{
.bg = self.selection_background orelse self.foreground, .bg = self.config.selection_background orelse self.config.foreground,
.fg = self.selection_foreground orelse self.background, .fg = self.config.selection_foreground orelse self.config.background,
}; };
} }
} }
@ -1041,13 +1069,13 @@ pub fn updateCell(
// In normal mode, background and fg match the cell. We // In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color. // un-optionalize the fg by defaulting to our fg color.
.bg = if (cell.attrs.has_bg) cell.bg else null, .bg = if (cell.attrs.has_bg) cell.bg else null,
.fg = if (cell.attrs.has_fg) cell.fg else self.foreground, .fg = if (cell.attrs.has_fg) cell.fg else self.config.foreground,
} else .{ } else .{
// In inverted mode, the background MUST be set to something // In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The // (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background. // fg is either the bg or default background.
.bg = if (cell.attrs.has_fg) cell.fg else self.foreground, .bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground,
.fg = if (cell.attrs.has_bg) cell.bg else self.background, .fg = if (cell.attrs.has_bg) cell.bg else self.config.background,
}; };
break :colors res; break :colors res;
}; };
@ -1204,6 +1232,11 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid
); );
} }
/// Update the configuration.
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
self.config = config.*;
}
/// Set the screen size for rendering. This will update the projection /// Set the screen size for rendering. This will update the projection
/// used for the shader so that the scaling of the grid is correct. /// used for the shader so that the scaling of the grid is correct.
pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void { pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void {

View File

@ -5,8 +5,8 @@ const font = @import("../font/main.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const Config = @import("../config.zig").Config; const Config = @import("../config.zig").Config;
/// The app configuration. /// The derived configuration for this renderer implementation.
config: *const Config, config: renderer.Renderer.DerivedConfig,
/// The font group that should be used. /// The font group that should be used.
font_group: *font.GroupCache, font_group: *font.GroupCache,

View File

@ -255,6 +255,11 @@ fn drainMailbox(self: *Thread) !void {
.screen_size => |size| { .screen_size => |size| {
try self.renderer.setScreenSize(size); try self.renderer.setScreenSize(size);
}, },
.change_config => |config| {
defer config.alloc.destroy(config.ptr);
try self.renderer.changeConfig(config.ptr);
},
} }
} }
} }

View File

@ -22,4 +22,10 @@ pub const Message = union(enum) {
/// Change the screen size. /// Change the screen size.
screen_size: renderer.ScreenSize, screen_size: renderer.ScreenSize,
/// The derived configuration to update the renderer with.
change_config: struct {
alloc: Allocator,
ptr: *renderer.Renderer.DerivedConfig,
},
}; };

View File

@ -6,6 +6,7 @@ const builtin = @import("builtin");
const build_config = @import("../build_config.zig"); const build_config = @import("../build_config.zig");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const EnvMap = std.process.EnvMap; const EnvMap = std.process.EnvMap;
const termio = @import("../termio.zig"); const termio = @import("../termio.zig");
const Command = @import("../Command.zig"); const Command = @import("../Command.zig");
@ -19,6 +20,7 @@ const trace = tracy.trace;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const fastmem = @import("../fastmem.zig"); const fastmem = @import("../fastmem.zig");
const internal_os = @import("../os/main.zig"); const internal_os = @import("../os/main.zig");
const configpkg = @import("../config.zig");
const log = std.log.scoped(.io_exec); const log = std.log.scoped(.io_exec);
@ -62,9 +64,35 @@ grid_size: renderer.GridSize,
/// The data associated with the currently running thread. /// The data associated with the currently running thread.
data: ?*EventData, data: ?*EventData,
/// The configuration for this IO that is derived from the main
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct {
palette: terminal.color.Palette,
pub fn init(
alloc_gpa: Allocator,
config: *const configpkg.Config,
) !DerivedConfig {
_ = alloc_gpa;
return .{
.palette = config.palette.value,
};
}
pub fn deinit(self: *DerivedConfig) void {
_ = self;
}
};
/// Initialize the exec implementation. This will also start the child /// Initialize the exec implementation. This will also start the child
/// process. /// process.
pub fn init(alloc: Allocator, opts: termio.Options) !Exec { pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
// Clean up our derived config because we don't need it after this.
var config = opts.config;
defer config.deinit();
// Create our terminal // Create our terminal
var term = try terminal.Terminal.init( var term = try terminal.Terminal.init(
alloc, alloc,
@ -72,7 +100,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
opts.grid_size.rows, opts.grid_size.rows,
); );
errdefer term.deinit(alloc); errdefer term.deinit(alloc);
term.color_palette = opts.config.palette.value; term.color_palette = opts.config.palette;
var subprocess = try Subprocess.init(alloc, opts); var subprocess = try Subprocess.init(alloc, opts);
errdefer subprocess.deinit(); errdefer subprocess.deinit();
@ -189,6 +217,21 @@ pub fn threadExit(self: *Exec, data: ThreadData) void {
data.read_thread.join(); data.read_thread.join();
} }
/// Update the configuration.
pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void {
defer config.deinit();
// Update the configuration that we know about.
//
// Specific things we don't update:
// - command, working-directory: we never restart the underlying
// process so we don't care or need to know about these.
// Update the palette. Note this will only apply to new colors drawn
// since we decode all palette colors to RGB on usage.
self.terminal.color_palette = config.palette;
}
/// Resize the terminal. /// Resize the terminal.
pub fn resize( pub fn resize(
self: *Exec, self: *Exec,
@ -413,7 +456,7 @@ const Subprocess = struct {
const alloc = arena.allocator(); const alloc = arena.allocator();
// Determine the path to the binary we're executing // Determine the path to the binary we're executing
const path = (try Command.expandPath(alloc, opts.config.command orelse "sh")) orelse const path = (try Command.expandPath(alloc, opts.full_config.command orelse "sh")) orelse
return error.CommandNotFound; return error.CommandNotFound;
// On macOS, we launch the program as a login shell. This is a Mac-specific // On macOS, we launch the program as a login shell. This is a Mac-specific
@ -487,10 +530,17 @@ const Subprocess = struct {
break :args try args.toOwnedSlice(); break :args try args.toOwnedSlice();
}; };
// We have to copy the cwd because there is no guarantee that
// pointers in full_config remain valid.
var cwd: ?[]u8 = if (opts.full_config.@"working-directory") |cwd|
try alloc.dupe(u8, cwd)
else
null;
return .{ return .{
.arena = arena, .arena = arena,
.env = env, .env = env,
.cwd = opts.config.@"working-directory", .cwd = cwd,
.path = if (internal_os.isFlatpak()) args[0] else path, .path = if (internal_os.isFlatpak()) args[0] else path,
.args = args, .args = args,
.grid_size = opts.grid_size, .grid_size = opts.grid_size,

View File

@ -4,6 +4,7 @@ const xev = @import("xev");
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const Config = @import("../config.zig").Config; const Config = @import("../config.zig").Config;
const termio = @import("../termio.zig");
/// The size of the terminal grid. /// The size of the terminal grid.
grid_size: renderer.GridSize, grid_size: renderer.GridSize,
@ -11,8 +12,13 @@ grid_size: renderer.GridSize,
/// The size of the viewport in pixels. /// The size of the viewport in pixels.
screen_size: renderer.ScreenSize, screen_size: renderer.ScreenSize,
/// The app configuration. /// The full app configuration. This is only available during initialization.
config: *const Config, /// The memory it points to is NOT stable after the init call so any values
/// in here must be copied.
full_config: *const Config,
/// The derived configuration for this termio implementation.
config: termio.Impl.DerivedConfig,
/// The render state. The IO implementation can modify anything here. The /// The render state. The IO implementation can modify anything here. The
/// surface thread will setup the initial "terminal" pointer but the IO impl /// surface thread will setup the initial "terminal" pointer but the IO impl

View File

@ -150,6 +150,10 @@ fn drainMailbox(self: *Thread) !void {
log.debug("mailbox message={}", .{message}); log.debug("mailbox message={}", .{message});
switch (message) { switch (message) {
.change_config => |config| {
defer config.alloc.destroy(config.ptr);
try self.impl.changeConfig(config.ptr);
},
.resize => |v| self.handleResize(v), .resize => |v| self.handleResize(v),
.clear_screen => |v| try self.impl.clearScreen(v.history), .clear_screen => |v| try self.impl.clearScreen(v.history),
.write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]),

View File

@ -3,6 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
const termio = @import("../termio.zig");
/// The messages that can be sent to an IO thread. /// The messages that can be sent to an IO thread.
/// ///
@ -28,6 +29,13 @@ pub const Message = union(enum) {
padding: renderer.Padding, padding: renderer.Padding,
}; };
/// The derived configuration to update the implementation with. This
/// is allocated via the allocator and is expected to be freed when done.
change_config: struct {
alloc: Allocator,
ptr: *termio.Impl.DerivedConfig,
},
/// Resize the window. /// Resize the window.
resize: Resize, resize: Resize,