diff --git a/src/Window.zig b/src/Window.zig index f95965f89..cf18862f0 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -10,6 +10,7 @@ const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const renderer = @import("renderer.zig"); +const termio = @import("termio.zig"); const objc = @import("objc"); const glfw = @import("glfw"); const imgui = @import("imgui"); @@ -76,6 +77,11 @@ command: Command, /// Mouse state. 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" /// that manages input, grid updating, etc. and is renderer-agnostic. It /// 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); 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. var mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; @@ -484,6 +502,9 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .pty = pty, .command = cmd, .mouse = .{}, + .io = io, + .io_thread = io_thread, + .io_thr = undefined, .terminal = term, .terminal_stream = .{ .handler = self }, .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 // all our GLFW setup is complete. if (DevMode.enabled) { - const io = try imgui.IO.get(); - io.cval().IniFilename = "ghostty_dev_mode.ini"; + const dev_io = try imgui.IO.get(); + dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; // 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( face_ttf, @intToFloat(f32, font_size.pixels()), @@ -553,6 +574,13 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .{&self.renderer_thread}, ); + // Start our IO thread + self.io_thr = try std.Thread.spawn( + .{}, + termio.Thread.threadMain, + .{&self.io_thread}, + ); + return self; } @@ -579,6 +607,17 @@ pub fn destroy(self: *Window) void { 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 // cause a close in the our subprocess so just wait for that. self.pty.deinit(); diff --git a/src/termio.zig b/src/termio.zig new file mode 100644 index 000000000..9d0de3ffb --- /dev/null +++ b/src/termio.zig @@ -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()); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig new file mode 100644 index 000000000..b5c7c4817 --- /dev/null +++ b/src/termio/Exec.zig @@ -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; +} diff --git a/src/termio/Options.zig b/src/termio/Options.zig new file mode 100644 index 000000000..750a07771 --- /dev/null +++ b/src/termio/Options.zig @@ -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, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig new file mode 100644 index 000000000..ac30fb070 --- /dev/null +++ b/src/termio/Thread.zig @@ -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(); +}