mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
Start pulling out IO thread and IO implementation
This commit is contained in:
@ -10,6 +10,7 @@ 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 renderer = @import("renderer.zig");
|
const renderer = @import("renderer.zig");
|
||||||
|
const termio = @import("termio.zig");
|
||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
const imgui = @import("imgui");
|
const imgui = @import("imgui");
|
||||||
@ -76,6 +77,11 @@ command: Command,
|
|||||||
/// Mouse state.
|
/// Mouse state.
|
||||||
mouse: Mouse,
|
mouse: Mouse,
|
||||||
|
|
||||||
|
/// The terminal IO handler.
|
||||||
|
io: termio.Impl,
|
||||||
|
io_thread: termio.Thread,
|
||||||
|
io_thr: std.Thread,
|
||||||
|
|
||||||
/// The terminal emulator internal state. This is the abstract "terminal"
|
/// The terminal emulator internal state. This is the abstract "terminal"
|
||||||
/// that manages input, grid updating, etc. and is renderer-agnostic. It
|
/// that manages input, grid updating, etc. and is renderer-agnostic. It
|
||||||
/// just stores internal state about a grid. This is connected back to
|
/// just stores internal state about a grid. This is connected back to
|
||||||
@ -445,6 +451,18 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
|||||||
var io_arena = std.heap.ArenaAllocator.init(alloc);
|
var io_arena = std.heap.ArenaAllocator.init(alloc);
|
||||||
errdefer io_arena.deinit();
|
errdefer io_arena.deinit();
|
||||||
|
|
||||||
|
// Start our IO implementation
|
||||||
|
var io = try termio.Impl.init(alloc, .{
|
||||||
|
.grid_size = grid_size,
|
||||||
|
.screen_size = screen_size,
|
||||||
|
.config = config,
|
||||||
|
});
|
||||||
|
errdefer io.deinit(alloc);
|
||||||
|
|
||||||
|
// Create the IO thread
|
||||||
|
var io_thread = try termio.Thread.init(alloc, &self.io);
|
||||||
|
errdefer io_thread.deinit();
|
||||||
|
|
||||||
// The mutex used to protect our renderer state.
|
// The mutex used to protect our renderer state.
|
||||||
var mutex = try alloc.create(std.Thread.Mutex);
|
var mutex = try alloc.create(std.Thread.Mutex);
|
||||||
mutex.* = .{};
|
mutex.* = .{};
|
||||||
@ -484,6 +502,9 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
|||||||
.pty = pty,
|
.pty = pty,
|
||||||
.command = cmd,
|
.command = cmd,
|
||||||
.mouse = .{},
|
.mouse = .{},
|
||||||
|
.io = io,
|
||||||
|
.io_thread = io_thread,
|
||||||
|
.io_thr = undefined,
|
||||||
.terminal = term,
|
.terminal = term,
|
||||||
.terminal_stream = .{ .handler = self },
|
.terminal_stream = .{ .handler = self },
|
||||||
.terminal_cursor = .{ .timer = timer },
|
.terminal_cursor = .{ .timer = timer },
|
||||||
@ -524,11 +545,11 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
|||||||
// Load imgui. This must be done LAST because it has to be done after
|
// Load imgui. This must be done LAST because it has to be done after
|
||||||
// all our GLFW setup is complete.
|
// all our GLFW setup is complete.
|
||||||
if (DevMode.enabled) {
|
if (DevMode.enabled) {
|
||||||
const io = try imgui.IO.get();
|
const dev_io = try imgui.IO.get();
|
||||||
io.cval().IniFilename = "ghostty_dev_mode.ini";
|
dev_io.cval().IniFilename = "ghostty_dev_mode.ini";
|
||||||
|
|
||||||
// Add our built-in fonts so it looks slightly better
|
// Add our built-in fonts so it looks slightly better
|
||||||
const dev_atlas = @ptrCast(*imgui.FontAtlas, io.cval().Fonts);
|
const dev_atlas = @ptrCast(*imgui.FontAtlas, dev_io.cval().Fonts);
|
||||||
dev_atlas.addFontFromMemoryTTF(
|
dev_atlas.addFontFromMemoryTTF(
|
||||||
face_ttf,
|
face_ttf,
|
||||||
@intToFloat(f32, font_size.pixels()),
|
@intToFloat(f32, font_size.pixels()),
|
||||||
@ -553,6 +574,13 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
|||||||
.{&self.renderer_thread},
|
.{&self.renderer_thread},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start our IO thread
|
||||||
|
self.io_thr = try std.Thread.spawn(
|
||||||
|
.{},
|
||||||
|
termio.Thread.threadMain,
|
||||||
|
.{&self.io_thread},
|
||||||
|
);
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,6 +607,17 @@ pub fn destroy(self: *Window) void {
|
|||||||
self.imgui_ctx.destroy();
|
self.imgui_ctx.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Stop our IO thread
|
||||||
|
self.io_thread.stop.send() catch |err|
|
||||||
|
log.err("error notifying io thread to stop, may stall err={}", .{err});
|
||||||
|
self.io_thr.join();
|
||||||
|
self.io_thread.deinit();
|
||||||
|
|
||||||
|
// Deinitialize our terminal IO
|
||||||
|
self.io.deinit(self.alloc);
|
||||||
|
}
|
||||||
|
|
||||||
// Deinitialize the pty. This closes the pty handles. This should
|
// Deinitialize the pty. This closes the pty handles. This should
|
||||||
// cause a close in the our subprocess so just wait for that.
|
// cause a close in the our subprocess so just wait for that.
|
||||||
self.pty.deinit();
|
self.pty.deinit();
|
||||||
|
17
src/termio.zig
Normal file
17
src/termio.zig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//! IO implementation and utilities. The IO implementation is responsible
|
||||||
|
//! for taking the config, spinning up a child process, and handling IO
|
||||||
|
//! with the termianl.
|
||||||
|
|
||||||
|
pub const Exec = @import("termio/Exec.zig");
|
||||||
|
pub const Options = @import("termio/Options.zig");
|
||||||
|
pub const Thread = @import("termio/Thread.zig");
|
||||||
|
|
||||||
|
/// The implementation to use for the IO. This is just "exec" for now but
|
||||||
|
/// this is somewhat pluggable so that in the future we can introduce other
|
||||||
|
/// options for other platforms (i.e. wasm) or even potentially a vtable
|
||||||
|
/// implementation for runtime polymorphism.
|
||||||
|
pub const Impl = Exec;
|
||||||
|
|
||||||
|
test {
|
||||||
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
}
|
100
src/termio/Exec.zig
Normal file
100
src/termio/Exec.zig
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
//! Implementation of IO that uses child exec to talk to the child process.
|
||||||
|
pub const Exec = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const termio = @import("../termio.zig");
|
||||||
|
const Command = @import("../Command.zig");
|
||||||
|
const Pty = @import("../Pty.zig");
|
||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
|
const libuv = @import("libuv");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.io_exec);
|
||||||
|
|
||||||
|
/// This is the pty fd created for the subcommand.
|
||||||
|
pty: Pty,
|
||||||
|
|
||||||
|
/// This is the container for the subcommand.
|
||||||
|
command: Command,
|
||||||
|
|
||||||
|
/// The terminal emulator internal state. This is the abstract "terminal"
|
||||||
|
/// that manages input, grid updating, etc. and is renderer-agnostic. It
|
||||||
|
/// just stores internal state about a grid.
|
||||||
|
terminal: terminal.Terminal,
|
||||||
|
|
||||||
|
/// Initialize the exec implementation. This will also start the child
|
||||||
|
/// process.
|
||||||
|
pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
||||||
|
// Create our pty
|
||||||
|
var pty = try Pty.open(.{
|
||||||
|
.ws_row = @intCast(u16, opts.grid_size.rows),
|
||||||
|
.ws_col = @intCast(u16, opts.grid_size.columns),
|
||||||
|
.ws_xpixel = @intCast(u16, opts.screen_size.width),
|
||||||
|
.ws_ypixel = @intCast(u16, opts.screen_size.height),
|
||||||
|
});
|
||||||
|
errdefer pty.deinit();
|
||||||
|
|
||||||
|
// Determine the path to the binary we're executing
|
||||||
|
const path = (try Command.expandPath(alloc, opts.config.command orelse "sh")) orelse
|
||||||
|
return error.CommandNotFound;
|
||||||
|
defer alloc.free(path);
|
||||||
|
|
||||||
|
// Set our env vars
|
||||||
|
var env = try std.process.getEnvMap(alloc);
|
||||||
|
defer env.deinit();
|
||||||
|
try env.put("TERM", "xterm-256color");
|
||||||
|
|
||||||
|
// Build our subcommand
|
||||||
|
var cmd: Command = .{
|
||||||
|
.path = path,
|
||||||
|
.args = &[_][]const u8{path},
|
||||||
|
.env = &env,
|
||||||
|
.cwd = opts.config.@"working-directory",
|
||||||
|
.pre_exec = (struct {
|
||||||
|
fn callback(c: *Command) void {
|
||||||
|
const p = c.getData(Pty) orelse unreachable;
|
||||||
|
p.childPreExec() catch |err|
|
||||||
|
log.err("error initializing child: {}", .{err});
|
||||||
|
}
|
||||||
|
}).callback,
|
||||||
|
.data = &pty,
|
||||||
|
};
|
||||||
|
// note: can't set these in the struct initializer because it
|
||||||
|
// sets the handle to "0". Probably a stage1 zig bug.
|
||||||
|
cmd.stdin = std.fs.File{ .handle = pty.slave };
|
||||||
|
cmd.stdout = cmd.stdin;
|
||||||
|
cmd.stderr = cmd.stdin;
|
||||||
|
try cmd.start(alloc);
|
||||||
|
log.info("started subcommand path={s} pid={?}", .{ path, cmd.pid });
|
||||||
|
|
||||||
|
// Create our terminal
|
||||||
|
var term = try terminal.Terminal.init(alloc, opts.grid_size.columns, opts.grid_size.rows);
|
||||||
|
errdefer term.deinit(alloc);
|
||||||
|
|
||||||
|
return Exec{
|
||||||
|
.pty = pty,
|
||||||
|
.command = cmd,
|
||||||
|
.terminal = term,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Exec, alloc: Allocator) void {
|
||||||
|
// Deinitialize the pty. This closes the pty handles. This should
|
||||||
|
// cause a close in the our subprocess so just wait for that.
|
||||||
|
self.pty.deinit();
|
||||||
|
_ = self.command.wait() catch |err|
|
||||||
|
log.err("error waiting for command to exit: {}", .{err});
|
||||||
|
|
||||||
|
// Clean up the terminal state
|
||||||
|
self.terminal.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn threadEnter(self: *Exec, loop: libuv.Loop) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn threadExit(self: *Exec) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
13
src/termio/Options.zig
Normal file
13
src/termio/Options.zig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//! The options that are used to configure a terminal IO implementation.
|
||||||
|
|
||||||
|
const renderer = @import("../renderer.zig");
|
||||||
|
const Config = @import("../config.zig").Config;
|
||||||
|
|
||||||
|
/// The size of the terminal grid.
|
||||||
|
grid_size: renderer.GridSize,
|
||||||
|
|
||||||
|
/// The size of the viewport in pixels.
|
||||||
|
screen_size: renderer.ScreenSize,
|
||||||
|
|
||||||
|
/// The app configuration.
|
||||||
|
config: *const Config,
|
140
src/termio/Thread.zig
Normal file
140
src/termio/Thread.zig
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
//! Represents the IO thread logic. The IO thread is responsible for
|
||||||
|
//! the child process and pty management.
|
||||||
|
pub const Thread = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const libuv = @import("libuv");
|
||||||
|
const termio = @import("../termio.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.io_thread);
|
||||||
|
|
||||||
|
/// The main event loop for the thread. The user data of this loop
|
||||||
|
/// is always the allocator used to create the loop. This is a convenience
|
||||||
|
/// so that users of the loop always have an allocator.
|
||||||
|
loop: libuv.Loop,
|
||||||
|
|
||||||
|
/// This can be used to wake up the thread.
|
||||||
|
wakeup: libuv.Async,
|
||||||
|
|
||||||
|
/// This can be used to stop the thread on the next loop iteration.
|
||||||
|
stop: libuv.Async,
|
||||||
|
|
||||||
|
/// The underlying IO implementation.
|
||||||
|
impl: *termio.Impl,
|
||||||
|
|
||||||
|
/// Initialize the thread. This does not START the thread. This only sets
|
||||||
|
/// up all the internal state necessary prior to starting the thread. It
|
||||||
|
/// is up to the caller to start the thread with the threadMain entrypoint.
|
||||||
|
pub fn init(
|
||||||
|
alloc: Allocator,
|
||||||
|
impl: *termio.Impl,
|
||||||
|
) !Thread {
|
||||||
|
// We always store allocator pointer on the loop data so that
|
||||||
|
// handles can use our global allocator.
|
||||||
|
const allocPtr = try alloc.create(Allocator);
|
||||||
|
errdefer alloc.destroy(allocPtr);
|
||||||
|
allocPtr.* = alloc;
|
||||||
|
|
||||||
|
// Create our event loop.
|
||||||
|
var loop = try libuv.Loop.init(alloc);
|
||||||
|
errdefer loop.deinit(alloc);
|
||||||
|
loop.setData(allocPtr);
|
||||||
|
|
||||||
|
// This async handle is used to "wake up" the renderer and force a render.
|
||||||
|
var wakeup_h = try libuv.Async.init(alloc, loop, wakeupCallback);
|
||||||
|
errdefer wakeup_h.close((struct {
|
||||||
|
fn callback(h: *libuv.Async) void {
|
||||||
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
||||||
|
h.deinit(loop_alloc);
|
||||||
|
}
|
||||||
|
}).callback);
|
||||||
|
|
||||||
|
// This async handle is used to stop the loop and force the thread to end.
|
||||||
|
var stop_h = try libuv.Async.init(alloc, loop, stopCallback);
|
||||||
|
errdefer stop_h.close((struct {
|
||||||
|
fn callback(h: *libuv.Async) void {
|
||||||
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
||||||
|
h.deinit(loop_alloc);
|
||||||
|
}
|
||||||
|
}).callback);
|
||||||
|
|
||||||
|
return Thread{
|
||||||
|
.loop = loop,
|
||||||
|
.wakeup = wakeup_h,
|
||||||
|
.stop = stop_h,
|
||||||
|
.impl = impl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up the thread. This is only safe to call once the thread
|
||||||
|
/// completes executing; the caller must join prior to this.
|
||||||
|
pub fn deinit(self: *Thread) void {
|
||||||
|
// Get a copy to our allocator
|
||||||
|
const alloc_ptr = self.loop.getData(Allocator).?;
|
||||||
|
const alloc = alloc_ptr.*;
|
||||||
|
|
||||||
|
// Schedule our handles to close
|
||||||
|
self.stop.close((struct {
|
||||||
|
fn callback(h: *libuv.Async) void {
|
||||||
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
||||||
|
h.deinit(handle_alloc);
|
||||||
|
}
|
||||||
|
}).callback);
|
||||||
|
self.wakeup.close((struct {
|
||||||
|
fn callback(h: *libuv.Async) void {
|
||||||
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
||||||
|
h.deinit(handle_alloc);
|
||||||
|
}
|
||||||
|
}).callback);
|
||||||
|
|
||||||
|
// Run the loop one more time, because destroying our other things
|
||||||
|
// like windows usually cancel all our event loop stuff and we need
|
||||||
|
// one more run through to finalize all the closes.
|
||||||
|
_ = self.loop.run(.default) catch |err|
|
||||||
|
log.err("error finalizing event loop: {}", .{err});
|
||||||
|
|
||||||
|
// Dealloc our allocator copy
|
||||||
|
alloc.destroy(alloc_ptr);
|
||||||
|
|
||||||
|
self.loop.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main entrypoint for the thread.
|
||||||
|
pub fn threadMain(self: *Thread) void {
|
||||||
|
// Call child function so we can use errors...
|
||||||
|
self.threadMain_() catch |err| {
|
||||||
|
// In the future, we should expose this on the thread struct.
|
||||||
|
log.warn("error in io thread err={}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn threadMain_(self: *Thread) !void {
|
||||||
|
// Run our thread start/end callbacks. This allows the implementation
|
||||||
|
// to hook into the event loop as needed.
|
||||||
|
try self.impl.threadEnter(self.loop);
|
||||||
|
defer self.impl.threadExit();
|
||||||
|
|
||||||
|
// Set up our async handler to support rendering
|
||||||
|
self.wakeup.setData(self);
|
||||||
|
defer self.wakeup.setData(null);
|
||||||
|
|
||||||
|
// Run
|
||||||
|
log.debug("starting IO thread", .{});
|
||||||
|
defer log.debug("exiting IO thread", .{});
|
||||||
|
_ = try self.loop.run(.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wakeupCallback(h: *libuv.Async) void {
|
||||||
|
_ = h;
|
||||||
|
// const t = h.getData(Thread) orelse {
|
||||||
|
// // This shouldn't happen so we log it.
|
||||||
|
// log.warn("render callback fired without data set", .{});
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stopCallback(h: *libuv.Async) void {
|
||||||
|
h.loop().stop();
|
||||||
|
}
|
Reference in New Issue
Block a user