mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #117 from mitchellh/config-stuff
Reloadable Configuration
This commit is contained in:
@ -216,6 +216,7 @@ typedef struct {
|
||||
} ghostty_surface_config_s;
|
||||
|
||||
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 const char* (*ghostty_runtime_read_clipboard_cb)(void *);
|
||||
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 {
|
||||
void *userdata;
|
||||
ghostty_runtime_wakeup_cb wakeup_cb;
|
||||
ghostty_runtime_reload_config_cb reload_config_cb;
|
||||
ghostty_runtime_set_title_cb set_title_cb;
|
||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
||||
@ -241,6 +243,7 @@ int ghostty_init(void);
|
||||
|
||||
ghostty_config_t ghostty_config_new();
|
||||
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_default_files(ghostty_config_t);
|
||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||
|
@ -12,12 +12,25 @@ extension Ghostty {
|
||||
/// The readiness value of the state.
|
||||
@Published var readiness: AppReadiness = .loading
|
||||
|
||||
/// The ghostty global configuration.
|
||||
var config: ghostty_config_t? = nil
|
||||
/// The ghostty global configuration. This should only be changed when it is definitely
|
||||
/// 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
|
||||
/// 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.
|
||||
private var cached_clipboard_string: String? = nil
|
||||
@ -31,29 +44,18 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
// Initialize the global configuration.
|
||||
guard let cfg = ghostty_config_new() else {
|
||||
GhosttyApp.logger.critical("ghostty_config_new failed")
|
||||
guard let cfg = Self.reloadConfig() else {
|
||||
readiness = .error
|
||||
return
|
||||
}
|
||||
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
|
||||
// uses to interface with the application runtime environment.
|
||||
var runtime_cfg = ghostty_runtime_config_s(
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
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) },
|
||||
read_clipboard_cb: { userdata in AppState.readClipboard(userdata) },
|
||||
write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) },
|
||||
@ -74,8 +76,32 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
deinit {
|
||||
ghostty_app_free(app)
|
||||
ghostty_config_free(config)
|
||||
// This will force the didSet callbacks to run which free.
|
||||
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() {
|
||||
@ -140,6 +166,20 @@ extension Ghostty {
|
||||
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?) {
|
||||
let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
|
||||
|
32
src/App.zig
32
src/App.zig
@ -18,7 +18,6 @@ const renderer = @import("renderer.zig");
|
||||
const font = @import("font/main.zig");
|
||||
const macos = @import("macos");
|
||||
const objc = @import("objc");
|
||||
const DevMode = @import("DevMode.zig");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
@ -30,9 +29,6 @@ alloc: Allocator,
|
||||
/// The list of surfaces that are currently active.
|
||||
surfaces: SurfaceList,
|
||||
|
||||
// The configuration for the app.
|
||||
config: *const Config,
|
||||
|
||||
/// 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).
|
||||
mailbox: Mailbox.Queue,
|
||||
@ -45,17 +41,12 @@ quit: bool,
|
||||
/// "startup" logic.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
config: *const Config,
|
||||
) !*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);
|
||||
errdefer alloc.destroy(app);
|
||||
app.* = .{
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.config = config,
|
||||
.mailbox = .{},
|
||||
.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;
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// implementations to call and should NOT be called by general app users.
|
||||
/// 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| {
|
||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||
switch (message) {
|
||||
.reload_config => try self.reloadConfig(rt_app),
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| try self.closeSurface(rt_app, surface),
|
||||
.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 {
|
||||
if (!self.hasSurface(surface)) return;
|
||||
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.
|
||||
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.
|
||||
new_window: NewWindow,
|
||||
|
||||
|
@ -27,7 +27,7 @@ pub var instance: DevMode = .{};
|
||||
visible: bool = false,
|
||||
|
||||
/// Our app config
|
||||
config: ?*const Config = null,
|
||||
config: ?Config = null,
|
||||
|
||||
/// The surface we're tracking.
|
||||
surface: ?*Surface = null,
|
||||
|
124
src/Surface.zig
124
src/Surface.zig
@ -19,6 +19,7 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const renderer = @import("renderer.zig");
|
||||
const termio = @import("termio.zig");
|
||||
const objc = @import("objc");
|
||||
@ -28,7 +29,7 @@ const font = @import("font/main.zig");
|
||||
const Command = @import("Command.zig");
|
||||
const trace = @import("tracy").trace;
|
||||
const terminal = @import("terminal/main.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
const configpkg = @import("config.zig");
|
||||
const input = @import("input.zig");
|
||||
const DevMode = @import("DevMode.zig");
|
||||
const App = @import("App.zig");
|
||||
@ -70,7 +71,6 @@ renderer_thr: std.Thread,
|
||||
|
||||
/// Mouse state.
|
||||
mouse: Mouse,
|
||||
mouse_interval: u64,
|
||||
|
||||
/// The terminal IO handler.
|
||||
io: termio.Impl,
|
||||
@ -85,8 +85,10 @@ cell_size: renderer.CellSize,
|
||||
/// Explicit padding due to configuration
|
||||
padding: renderer.Padding,
|
||||
|
||||
/// The app configuration
|
||||
config: *const Config,
|
||||
/// The configuration derived from the main config. We "derive" it so that
|
||||
/// 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
|
||||
/// 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 = .{},
|
||||
};
|
||||
|
||||
/// 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
|
||||
/// pointer to the memory for the surface must be provided and must be
|
||||
/// stable due to interfacing with various callbacks.
|
||||
pub fn init(
|
||||
self: *Surface,
|
||||
alloc: Allocator,
|
||||
config: *const Config,
|
||||
config: *const configpkg.Config,
|
||||
app_mailbox: App.Mailbox,
|
||||
rt_surface: *apprt.runtime.Surface,
|
||||
) !void {
|
||||
@ -284,7 +323,7 @@ pub fn init(
|
||||
|
||||
// Create our terminal grid with the initial size
|
||||
var renderer_impl = try Renderer.init(alloc, .{
|
||||
.config = config,
|
||||
.config = try Renderer.DerivedConfig.init(alloc, config),
|
||||
.font_group = font_group,
|
||||
.padding = .{
|
||||
.explicit = padding,
|
||||
@ -324,7 +363,8 @@ pub fn init(
|
||||
var io = try termio.Impl.init(alloc, .{
|
||||
.grid_size = grid_size,
|
||||
.screen_size = screen_size,
|
||||
.config = config,
|
||||
.full_config = config,
|
||||
.config = try termio.Impl.DerivedConfig.init(alloc, config),
|
||||
.renderer_state = &self.renderer_state,
|
||||
.renderer_wakeup = render_thread.wakeup,
|
||||
.renderer_mailbox = render_thread.mailbox,
|
||||
@ -361,7 +401,6 @@ pub fn init(
|
||||
},
|
||||
.renderer_thr = undefined,
|
||||
.mouse = .{},
|
||||
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
||||
.io = io,
|
||||
.io_thread = io_thread,
|
||||
.io_thr = undefined,
|
||||
@ -369,7 +408,7 @@ pub fn init(
|
||||
.grid_size = grid_size,
|
||||
.cell_size = cell_size,
|
||||
.padding = padding,
|
||||
.config = config,
|
||||
.config = try DerivedConfig.init(alloc, config),
|
||||
|
||||
.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.renderer_state.mutex);
|
||||
self.config.deinit();
|
||||
log.info("surface closed addr={x}", .{@ptrToInt(self)});
|
||||
}
|
||||
|
||||
@ -489,6 +529,8 @@ pub fn close(self: *Surface) void {
|
||||
/// surface.
|
||||
pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
switch (msg) {
|
||||
.change_config => |config| try self.changeConfig(config),
|
||||
|
||||
.set_title => |*v| {
|
||||
// The ptrCast just gets sliceTo to return the proper type.
|
||||
// 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)
|
||||
/// keyboard should be rendered.
|
||||
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 {
|
||||
if (!self.config.@"clipboard-read") {
|
||||
if (!self.config.clipboard_read) {
|
||||
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
||||
return;
|
||||
}
|
||||
@ -593,7 +683,7 @@ fn clipboardRead(self: *const Surface, kind: 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", .{});
|
||||
return;
|
||||
}
|
||||
@ -793,6 +883,12 @@ pub fn keyCallback(
|
||||
.unbind => unreachable,
|
||||
.ignore => {},
|
||||
|
||||
.reload_config => {
|
||||
_ = self.app_mailbox.push(.{
|
||||
.reload_config = {},
|
||||
}, .{ .instant = {} });
|
||||
},
|
||||
|
||||
.csi => |data| {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[",
|
||||
@ -833,7 +929,7 @@ pub fn keyCallback(
|
||||
var buf = self.io.terminal.screen.selectionString(
|
||||
self.alloc,
|
||||
sel,
|
||||
self.config.@"clipboard-trim-trailing-spaces",
|
||||
self.config.clipboard_trim_trailing_spaces,
|
||||
) catch |err| {
|
||||
log.err("error reading selection string err={}", .{err});
|
||||
return;
|
||||
@ -901,7 +997,7 @@ pub fn keyCallback(
|
||||
log.debug("reset font size", .{});
|
||||
|
||||
var size = self.font_size;
|
||||
size.points = self.config.@"font-size";
|
||||
size.points = self.config.original_font_size;
|
||||
self.setFontSize(size);
|
||||
},
|
||||
|
||||
@ -1423,7 +1519,7 @@ pub fn mouseButtonCallback(
|
||||
// is less than and our interval and if so, increase the count.
|
||||
if (self.mouse.left_click_count > 0) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ const apprt = @import("../apprt.zig");
|
||||
const input = @import("../input.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
|
||||
const log = std.log.scoped(.embedded_window);
|
||||
|
||||
@ -35,6 +36,11 @@ pub const App = struct {
|
||||
/// a full tick of the app loop.
|
||||
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.
|
||||
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
|
||||
|
||||
@ -57,16 +63,31 @@ pub const App = struct {
|
||||
};
|
||||
|
||||
core_app: *CoreApp,
|
||||
config: *const Config,
|
||||
opts: Options,
|
||||
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
return .{ .core_app = core_app, .opts = opts };
|
||||
pub fn init(core_app: *CoreApp, config: *const Config, opts: Options) !App {
|
||||
return .{
|
||||
.core_app = core_app,
|
||||
.config = config,
|
||||
.opts = opts,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn terminate(self: App) void {
|
||||
_ = 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 {
|
||||
self.opts.wakeup(self.opts.userdata);
|
||||
}
|
||||
@ -143,7 +164,7 @@ pub const Surface = struct {
|
||||
// ready to use.
|
||||
try self.core_surface.init(
|
||||
app.core_app.alloc,
|
||||
app.core_app.config,
|
||||
app.config,
|
||||
.{ .rt_app = app, .mailbox = &app.core_app.mailbox },
|
||||
self,
|
||||
);
|
||||
@ -338,7 +359,6 @@ pub const Surface = struct {
|
||||
// C API
|
||||
pub const CAPI = struct {
|
||||
const global = &@import("../main.zig").state;
|
||||
const Config = @import("../config.zig").Config;
|
||||
|
||||
/// Create a new app.
|
||||
export fn ghostty_app_new(
|
||||
@ -355,13 +375,13 @@ pub const CAPI = struct {
|
||||
opts: *const apprt.runtime.App.Options,
|
||||
config: *const Config,
|
||||
) !*App {
|
||||
var core_app = try CoreApp.create(global.alloc, config);
|
||||
var core_app = try CoreApp.create(global.alloc);
|
||||
errdefer core_app.destroy();
|
||||
|
||||
// Create our runtime app
|
||||
var app = try global.alloc.create(App);
|
||||
errdefer global.alloc.destroy(app);
|
||||
app.* = try App.init(core_app, opts.*);
|
||||
app.* = try App.init(core_app, config, opts.*);
|
||||
errdefer app.terminate();
|
||||
|
||||
return app;
|
||||
|
@ -19,6 +19,8 @@ const Renderer = renderer.Renderer;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const CoreApp = @import("../App.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.
|
||||
const glfwNative = glfw.Native(.{
|
||||
@ -29,6 +31,7 @@ const log = std.log.scoped(.glfw);
|
||||
|
||||
pub const App = struct {
|
||||
app: *CoreApp,
|
||||
config: Config,
|
||||
|
||||
/// Mac-specific state.
|
||||
darwin: if (Darwin.enabled) Darwin else void,
|
||||
@ -53,14 +56,24 @@ pub const App = struct {
|
||||
var darwin = if (Darwin.enabled) try Darwin.init() else {};
|
||||
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 .{
|
||||
.app = core_app,
|
||||
.config = config,
|
||||
.darwin = darwin,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn terminate(self: App) void {
|
||||
_ = self;
|
||||
pub fn terminate(self: *App) void {
|
||||
self.config.deinit();
|
||||
glfw.terminate();
|
||||
}
|
||||
|
||||
@ -90,6 +103,23 @@ pub const App = struct {
|
||||
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.
|
||||
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
_ = try self.newSurface(parent_);
|
||||
@ -139,7 +169,7 @@ pub const App = struct {
|
||||
errdefer surface.deinit();
|
||||
|
||||
// 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| {
|
||||
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.
|
||||
try self.core_surface.init(
|
||||
app.app.alloc,
|
||||
app.app.config,
|
||||
&app.config,
|
||||
.{ .rt_app = app, .mailbox = &app.app.mailbox },
|
||||
self,
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ const apprt = @import("../apprt.zig");
|
||||
const input = @import("../input.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
@ -37,6 +38,7 @@ pub const App = struct {
|
||||
};
|
||||
|
||||
core_app: *CoreApp,
|
||||
config: Config,
|
||||
|
||||
app: *c.GtkApplication,
|
||||
ctx: *c.GMainContext,
|
||||
@ -53,6 +55,10 @@ pub const App = struct {
|
||||
// rid of this dep.
|
||||
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.
|
||||
const app = @ptrCast(?*c.GtkApplication, c.gtk_application_new(
|
||||
null,
|
||||
@ -108,6 +114,7 @@ pub const App = struct {
|
||||
return .{
|
||||
.core_app = core_app,
|
||||
.app = app,
|
||||
.config = config,
|
||||
.ctx = ctx,
|
||||
.cursor_default = cursor_default,
|
||||
.cursor_ibeam = cursor_ibeam,
|
||||
@ -116,7 +123,7 @@ pub const App = struct {
|
||||
|
||||
// Terminate the application. The application will not be restarted after
|
||||
// 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();
|
||||
while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
|
||||
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_default);
|
||||
|
||||
self.config.deinit();
|
||||
|
||||
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 {
|
||||
_ = self;
|
||||
c.g_main_context_wakeup(null);
|
||||
@ -575,7 +601,7 @@ pub const Surface = struct {
|
||||
// Initialize our surface now that we have the stable pointer.
|
||||
try self.core_surface.init(
|
||||
self.app.core_app.alloc,
|
||||
self.app.core_app.config,
|
||||
&self.app.config,
|
||||
.{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox },
|
||||
self,
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ const App = @import("../App.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const termio = @import("../termio.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
|
||||
/// The message types that can be sent to a single surface.
|
||||
pub const Message = union(enum) {
|
||||
@ -24,6 +25,11 @@ pub const Message = union(enum) {
|
||||
/// Write the clipboard contents.
|
||||
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
|
||||
/// receives this, not the full application.
|
||||
close: void,
|
||||
|
353
src/config.zig
353
src/config.zig
@ -1,3 +1,4 @@
|
||||
const config = @This();
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
@ -166,11 +167,65 @@ pub const Config = struct {
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_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 {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
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 {
|
||||
// Build up our basic config
|
||||
var result: Config = .{
|
||||
@ -180,6 +235,12 @@ pub const Config = struct {
|
||||
const alloc = result._arena.?.allocator();
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub const Color = struct {
|
||||
r: u8,
|
||||
@ -626,6 +906,16 @@ pub const Color = struct {
|
||||
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 "#"
|
||||
/// is optional.
|
||||
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 };
|
||||
}
|
||||
|
||||
/// 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" {
|
||||
const testing = std.testing;
|
||||
|
||||
@ -722,6 +1022,23 @@ pub const RepeatableString = struct {
|
||||
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" {
|
||||
const testing = std.testing;
|
||||
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" {
|
||||
const testing = std.testing;
|
||||
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
|
||||
/// the file-based syntax for the desktop version of the terminal.
|
||||
export fn ghostty_config_load_string(
|
||||
|
@ -187,6 +187,12 @@ pub const Action = union(enum) {
|
||||
/// Focus on a split in a given direction.
|
||||
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,
|
||||
/// etc. This only closes ONE surface.
|
||||
close_surface: void,
|
||||
|
23
src/main.zig
23
src/main.zig
@ -14,8 +14,6 @@ const xdg = @import("xdg.zig");
|
||||
const apprt = @import("apprt.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;
|
||||
|
||||
/// Global process state. This is initialized in main() for exe artifacts
|
||||
@ -29,27 +27,8 @@ pub fn main() !void {
|
||||
defer state.deinit();
|
||||
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
|
||||
var app = try App.create(alloc, &config);
|
||||
var app = try App.create(alloc);
|
||||
defer app.destroy();
|
||||
|
||||
// Create our runtime app
|
||||
|
@ -11,6 +11,7 @@ const objc = @import("objc");
|
||||
const macos = @import("macos");
|
||||
const imgui = @import("imgui");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
@ -31,6 +32,9 @@ const log = std.log.scoped(.metal);
|
||||
/// Allocator that can be used
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
/// The configuration we need derived from the main config.
|
||||
config: DerivedConfig,
|
||||
|
||||
/// The mailbox for communicating with the window.
|
||||
surface_mailbox: apprt.surface.Mailbox,
|
||||
|
||||
@ -51,17 +55,6 @@ focused: bool,
|
||||
/// blinking.
|
||||
cursor_visible: bool,
|
||||
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
|
||||
/// but we keep this around so that we don't reallocate. Each set of
|
||||
@ -117,6 +110,48 @@ const GPUCellMode = enum(u8) {
|
||||
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
|
||||
pub fn glfwWindowHints() glfw.Window.Hints {
|
||||
return .{
|
||||
@ -233,6 +268,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
|
||||
return Metal{
|
||||
.alloc = alloc,
|
||||
.config = options.config,
|
||||
.surface_mailbox = options.surface_mailbox,
|
||||
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
|
||||
.screen_size = null,
|
||||
@ -240,17 +276,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.focused = true,
|
||||
.cursor_visible = true,
|
||||
.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
|
||||
.cells_bg = .{},
|
||||
@ -286,6 +311,8 @@ pub fn deinit(self: *Metal) void {
|
||||
self.font_shaper.deinit();
|
||||
self.alloc.free(self.font_shaper.cell_buf);
|
||||
|
||||
self.config.deinit();
|
||||
|
||||
deinitMTLResource(self.buf_cells_bg);
|
||||
deinitMTLResource(self.buf_cells);
|
||||
deinitMTLResource(self.buf_instance);
|
||||
@ -482,15 +509,15 @@ pub fn render(
|
||||
}
|
||||
|
||||
// Swap bg/fg if the terminal is reversed
|
||||
const bg = self.background;
|
||||
const fg = self.foreground;
|
||||
const bg = self.config.background;
|
||||
const fg = self.config.foreground;
|
||||
defer {
|
||||
self.background = bg;
|
||||
self.foreground = fg;
|
||||
self.config.background = bg;
|
||||
self.config.foreground = fg;
|
||||
}
|
||||
if (state.terminal.modes.reverse_colors) {
|
||||
self.background = fg;
|
||||
self.foreground = bg;
|
||||
self.config.background = fg;
|
||||
self.config.foreground = bg;
|
||||
}
|
||||
|
||||
// We used to share terminal state, but we've since learned through
|
||||
@ -516,7 +543,7 @@ pub fn render(
|
||||
null;
|
||||
|
||||
break :critical .{
|
||||
.bg = self.background,
|
||||
.bg = self.config.background,
|
||||
.devmode = if (state.devmode) |dm| dm.visible else false,
|
||||
.selection = selection,
|
||||
.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.
|
||||
pub fn setScreenSize(self: *Metal, dim: renderer.ScreenSize) !void {
|
||||
// Store our screen size
|
||||
@ -878,8 +910,8 @@ pub fn updateCell(
|
||||
// If we are selected, we our colors are just inverted fg/bg
|
||||
if (sel.contains(screen_point)) {
|
||||
break :colors BgFg{
|
||||
.bg = self.selection_background orelse self.foreground,
|
||||
.fg = self.selection_foreground orelse self.background,
|
||||
.bg = self.config.selection_background orelse self.config.foreground,
|
||||
.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
|
||||
// un-optionalize the fg by defaulting to our fg color.
|
||||
.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 .{
|
||||
// In inverted mode, the background MUST be set to something
|
||||
// (is never null) so it is either the fg or default fg. The
|
||||
// fg is either the bg or default background.
|
||||
.bg = if (cell.attrs.has_fg) cell.fg else self.foreground,
|
||||
.fg = if (cell.attrs.has_bg) cell.bg else self.background,
|
||||
.bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground,
|
||||
.fg = if (cell.attrs.has_bg) cell.bg else self.config.background,
|
||||
};
|
||||
break :colors res;
|
||||
};
|
||||
@ -988,7 +1020,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void {
|
||||
screen.cursor.x,
|
||||
);
|
||||
|
||||
const color = self.cursor_color orelse terminal.color.RGB{
|
||||
const color = self.config.cursor_color orelse terminal.color.RGB{
|
||||
.r = 0xFF,
|
||||
.g = 0xFF,
|
||||
.b = 0xFF,
|
||||
|
@ -8,6 +8,7 @@ const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const imgui = @import("imgui");
|
||||
const renderer = @import("../renderer.zig");
|
||||
@ -43,6 +44,9 @@ const drawMutexZero = if (DrawMutex == void) void{} else .{};
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
/// The configuration we need derived from the main config.
|
||||
config: DerivedConfig,
|
||||
|
||||
/// Current cell dimensions for this grid.
|
||||
cell_size: renderer.CellSize,
|
||||
|
||||
@ -84,17 +88,6 @@ font_shaper: font.Shaper,
|
||||
/// blinking.
|
||||
cursor_visible: bool,
|
||||
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
|
||||
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 {
|
||||
// Create the initial font shaper
|
||||
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{
|
||||
.alloc = alloc,
|
||||
.config = options.config,
|
||||
.cells_bg = .{},
|
||||
.cells = .{},
|
||||
.cells_lru = CellsLRU.init(0),
|
||||
@ -369,18 +405,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
||||
.font_shaper = shaper,
|
||||
.cursor_visible = true,
|
||||
.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(),
|
||||
.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,
|
||||
.draw_background = options.config.background,
|
||||
.focused = true,
|
||||
.padding = options.padding,
|
||||
.surface_mailbox = options.surface_mailbox,
|
||||
@ -404,6 +429,9 @@ pub fn deinit(self: *OpenGL) void {
|
||||
|
||||
self.cells.deinit(self.alloc);
|
||||
self.cells_bg.deinit(self.alloc);
|
||||
|
||||
self.config.deinit();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -673,15 +701,15 @@ pub fn render(
|
||||
}
|
||||
|
||||
// Swap bg/fg if the terminal is reversed
|
||||
const bg = self.background;
|
||||
const fg = self.foreground;
|
||||
const bg = self.config.background;
|
||||
const fg = self.config.foreground;
|
||||
defer {
|
||||
self.background = bg;
|
||||
self.foreground = fg;
|
||||
self.config.background = bg;
|
||||
self.config.foreground = fg;
|
||||
}
|
||||
if (state.terminal.modes.reverse_colors) {
|
||||
self.background = fg;
|
||||
self.foreground = bg;
|
||||
self.config.background = fg;
|
||||
self.config.foreground = bg;
|
||||
}
|
||||
|
||||
// Build our devmode draw data
|
||||
@ -723,7 +751,7 @@ pub fn render(
|
||||
null;
|
||||
|
||||
break :critical .{
|
||||
.gl_bg = self.background,
|
||||
.gl_bg = self.config.background,
|
||||
.devmode_data = devmode_data,
|
||||
.active_screen = state.terminal.active_screen,
|
||||
.selection = selection,
|
||||
@ -944,7 +972,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
|
||||
screen.cursor.x,
|
||||
);
|
||||
|
||||
const color = self.cursor_color orelse terminal.color.RGB{
|
||||
const color = self.config.cursor_color orelse terminal.color.RGB{
|
||||
.r = 0xFF,
|
||||
.g = 0xFF,
|
||||
.b = 0xFF,
|
||||
@ -1031,8 +1059,8 @@ pub fn updateCell(
|
||||
// If we are selected, we use the selection colors
|
||||
if (sel.contains(screen_point)) {
|
||||
break :colors BgFg{
|
||||
.bg = self.selection_background orelse self.foreground,
|
||||
.fg = self.selection_foreground orelse self.background,
|
||||
.bg = self.config.selection_background orelse self.config.foreground,
|
||||
.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
|
||||
// un-optionalize the fg by defaulting to our fg color.
|
||||
.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 .{
|
||||
// In inverted mode, the background MUST be set to something
|
||||
// (is never null) so it is either the fg or default fg. The
|
||||
// fg is either the bg or default background.
|
||||
.bg = if (cell.attrs.has_fg) cell.fg else self.foreground,
|
||||
.fg = if (cell.attrs.has_bg) cell.bg else self.background,
|
||||
.bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground,
|
||||
.fg = if (cell.attrs.has_bg) cell.bg else self.config.background,
|
||||
};
|
||||
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
|
||||
/// used for the shader so that the scaling of the grid is correct.
|
||||
pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void {
|
||||
|
@ -5,8 +5,8 @@ const font = @import("../font/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
|
||||
/// The app configuration.
|
||||
config: *const Config,
|
||||
/// The derived configuration for this renderer implementation.
|
||||
config: renderer.Renderer.DerivedConfig,
|
||||
|
||||
/// The font group that should be used.
|
||||
font_group: *font.GroupCache,
|
||||
|
@ -255,6 +255,11 @@ fn drainMailbox(self: *Thread) !void {
|
||||
.screen_size => |size| {
|
||||
try self.renderer.setScreenSize(size);
|
||||
},
|
||||
|
||||
.change_config => |config| {
|
||||
defer config.alloc.destroy(config.ptr);
|
||||
try self.renderer.changeConfig(config.ptr);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,4 +22,10 @@ pub const Message = union(enum) {
|
||||
|
||||
/// Change the screen size.
|
||||
screen_size: renderer.ScreenSize,
|
||||
|
||||
/// The derived configuration to update the renderer with.
|
||||
change_config: struct {
|
||||
alloc: Allocator,
|
||||
ptr: *renderer.Renderer.DerivedConfig,
|
||||
},
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ const builtin = @import("builtin");
|
||||
const build_config = @import("../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const EnvMap = std.process.EnvMap;
|
||||
const termio = @import("../termio.zig");
|
||||
const Command = @import("../Command.zig");
|
||||
@ -19,6 +20,7 @@ const trace = tracy.trace;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
|
||||
@ -62,9 +64,35 @@ grid_size: renderer.GridSize,
|
||||
/// The data associated with the currently running thread.
|
||||
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
|
||||
/// process.
|
||||
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
|
||||
var term = try terminal.Terminal.init(
|
||||
alloc,
|
||||
@ -72,7 +100,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
||||
opts.grid_size.rows,
|
||||
);
|
||||
errdefer term.deinit(alloc);
|
||||
term.color_palette = opts.config.palette.value;
|
||||
term.color_palette = opts.config.palette;
|
||||
|
||||
var subprocess = try Subprocess.init(alloc, opts);
|
||||
errdefer subprocess.deinit();
|
||||
@ -189,6 +217,21 @@ pub fn threadExit(self: *Exec, data: ThreadData) void {
|
||||
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.
|
||||
pub fn resize(
|
||||
self: *Exec,
|
||||
@ -413,7 +456,7 @@ const Subprocess = struct {
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// 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;
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
// 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 .{
|
||||
.arena = arena,
|
||||
.env = env,
|
||||
.cwd = opts.config.@"working-directory",
|
||||
.cwd = cwd,
|
||||
.path = if (internal_os.isFlatpak()) args[0] else path,
|
||||
.args = args,
|
||||
.grid_size = opts.grid_size,
|
||||
|
@ -4,6 +4,7 @@ const xev = @import("xev");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
const termio = @import("../termio.zig");
|
||||
|
||||
/// The size of the terminal grid.
|
||||
grid_size: renderer.GridSize,
|
||||
@ -11,8 +12,13 @@ grid_size: renderer.GridSize,
|
||||
/// The size of the viewport in pixels.
|
||||
screen_size: renderer.ScreenSize,
|
||||
|
||||
/// The app configuration.
|
||||
config: *const Config,
|
||||
/// The full app configuration. This is only available during initialization.
|
||||
/// 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
|
||||
/// surface thread will setup the initial "terminal" pointer but the IO impl
|
||||
|
@ -150,6 +150,10 @@ fn drainMailbox(self: *Thread) !void {
|
||||
|
||||
log.debug("mailbox message={}", .{message});
|
||||
switch (message) {
|
||||
.change_config => |config| {
|
||||
defer config.alloc.destroy(config.ptr);
|
||||
try self.impl.changeConfig(config.ptr);
|
||||
},
|
||||
.resize => |v| self.handleResize(v),
|
||||
.clear_screen => |v| try self.impl.clearScreen(v.history),
|
||||
.write_small => |v| try self.impl.queueWrite(v.data[0..v.len]),
|
||||
|
@ -3,6 +3,7 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const termio = @import("../termio.zig");
|
||||
|
||||
/// The messages that can be sent to an IO thread.
|
||||
///
|
||||
@ -28,6 +29,13 @@ pub const Message = union(enum) {
|
||||
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: Resize,
|
||||
|
||||
|
Reference in New Issue
Block a user