mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Multi-Window
This adds the ability to have multiple windows by introducing new keybind actions `new_window`, `close_window`, and `quit`. These are bound on macOS by default to the standard `cmd+n`, `cmd+w`, and `cmd+q`, respectively. The multi-window logic is absolutely minimal: we don't do any GPU data sharing between windows, so each window recreates a full font texture for example. We can continue to further optimize this in the future. DevMode is also pretty limited (arguably broken) with multi-window: DevMode only works on the _first_ window opened. If you close the first window, DevMode is no longer available. This is due to complexities around multi-threading and imgui. It is possible to fix this but I deferred it since it was a bit of a mess!
This commit is contained in:
@ -14,6 +14,10 @@ pub const Context = opaque {
|
||||
c.igDestroyContext(self.cval());
|
||||
}
|
||||
|
||||
pub fn setCurrent(self: *Context) void {
|
||||
c.igSetCurrentContext(self.cval());
|
||||
}
|
||||
|
||||
pub inline fn cval(self: *Context) *c.ImGuiContext {
|
||||
return @ptrCast(
|
||||
*c.ImGuiContext,
|
||||
@ -25,4 +29,6 @@ pub const Context = opaque {
|
||||
test {
|
||||
var ctx = try Context.create();
|
||||
defer ctx.destroy();
|
||||
|
||||
ctx.setCurrent();
|
||||
}
|
||||
|
124
src/App.zig
124
src/App.zig
@ -9,46 +9,138 @@ const glfw = @import("glfw");
|
||||
const Window = @import("Window.zig");
|
||||
const tracy = @import("tracy");
|
||||
const Config = @import("config.zig").Config;
|
||||
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||
const renderer = @import("renderer.zig");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
const WindowList = std.ArrayListUnmanaged(*Window);
|
||||
|
||||
/// The type used for sending messages to the app thread.
|
||||
pub const Mailbox = BlockingQueue(Message, 64);
|
||||
|
||||
/// General purpose allocator
|
||||
alloc: Allocator,
|
||||
|
||||
/// The primary window for the application. We currently support only
|
||||
/// single window operations.
|
||||
window: *Window,
|
||||
/// The list of windows that are currently open
|
||||
windows: WindowList,
|
||||
|
||||
// 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,
|
||||
|
||||
/// Set to true once we're quitting. This never goes false again.
|
||||
quit: bool,
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
pub fn init(alloc: Allocator, config: *const Config) !App {
|
||||
// Create the window
|
||||
var window = try Window.create(alloc, config);
|
||||
errdefer window.destroy();
|
||||
pub fn create(alloc: Allocator, config: *const Config) !*App {
|
||||
// The mailbox for messaging this thread
|
||||
var mailbox = try Mailbox.create(alloc);
|
||||
errdefer mailbox.destroy(alloc);
|
||||
|
||||
return App{
|
||||
var app = try alloc.create(App);
|
||||
errdefer alloc.destroy(app);
|
||||
app.* = .{
|
||||
.alloc = alloc,
|
||||
.window = window,
|
||||
.windows = .{},
|
||||
.config = config,
|
||||
.mailbox = mailbox,
|
||||
.quit = false,
|
||||
};
|
||||
errdefer app.windows.deinit(alloc);
|
||||
|
||||
// Create the first window
|
||||
try app.newWindow();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App) void {
|
||||
self.window.destroy();
|
||||
self.* = undefined;
|
||||
pub fn destroy(self: *App) void {
|
||||
// Clean up all our windows
|
||||
for (self.windows.items) |window| window.destroy();
|
||||
self.windows.deinit(self.alloc);
|
||||
self.mailbox.destroy(self.alloc);
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn run(self: App) !void {
|
||||
while (!self.window.shouldClose()) {
|
||||
// Block for any glfw events. This may also be an "empty" event
|
||||
// posted by the libuv watcher so that we trigger a libuv loop tick.
|
||||
/// Wake up the app event loop. This should be called after any messages
|
||||
/// are sent to the mailbox.
|
||||
pub fn wakeup(self: App) void {
|
||||
_ = self;
|
||||
glfw.postEmptyEvent() catch {};
|
||||
}
|
||||
|
||||
/// Run the main event loop for the application. This blocks until the
|
||||
/// application quits or every window is closed.
|
||||
pub fn run(self: *App) !void {
|
||||
while (!self.quit and self.windows.items.len > 0) {
|
||||
// Block for any glfw events.
|
||||
try glfw.waitEvents();
|
||||
|
||||
// Mark this so we're in a totally different "frame"
|
||||
tracy.frameMark();
|
||||
|
||||
// If any windows are closing, destroy them
|
||||
var i: usize = 0;
|
||||
while (i < self.windows.items.len) {
|
||||
const window = self.windows.items[i];
|
||||
if (window.shouldClose()) {
|
||||
window.destroy();
|
||||
_ = self.windows.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Drain our mailbox only if we're not quitting.
|
||||
if (!self.quit) try self.drainMailbox();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the mailbox.
|
||||
fn drainMailbox(self: *App) !void {
|
||||
var drain = self.mailbox.drain();
|
||||
defer drain.deinit();
|
||||
|
||||
while (drain.next()) |message| {
|
||||
log.debug("mailbox message={}", .{message});
|
||||
switch (message) {
|
||||
.new_window => try self.newWindow(),
|
||||
.quit => try self.setQuit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new window
|
||||
fn newWindow(self: *App) !void {
|
||||
var window = try Window.create(self.alloc, self, self.config);
|
||||
errdefer window.destroy();
|
||||
try self.windows.append(self.alloc, window);
|
||||
errdefer _ = self.windows.pop();
|
||||
}
|
||||
|
||||
/// Start quitting
|
||||
fn setQuit(self: *App) !void {
|
||||
if (self.quit) return;
|
||||
self.quit = true;
|
||||
|
||||
// Mark that all our windows should close
|
||||
for (self.windows.items) |window| {
|
||||
window.window.setShouldClose(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// The message types that can be sent to the app thread.
|
||||
pub const Message = union(enum) {
|
||||
/// Create a new terminal window.
|
||||
new_window: void,
|
||||
|
||||
/// Quit
|
||||
quit: void,
|
||||
};
|
||||
|
@ -22,6 +22,7 @@ const terminal = @import("terminal/main.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
const input = @import("input.zig");
|
||||
const DevMode = @import("DevMode.zig");
|
||||
const App = @import("App.zig");
|
||||
|
||||
// Get native API access on certain platforms so we can do more customization.
|
||||
const glfwNative = glfw.Native(.{
|
||||
@ -36,6 +37,9 @@ const Renderer = renderer.Renderer;
|
||||
/// Allocator
|
||||
alloc: Allocator,
|
||||
|
||||
/// The app that this window is a part of.
|
||||
app: *App,
|
||||
|
||||
/// The font structures
|
||||
font_lib: font.Library,
|
||||
font_group: *font.GroupCache,
|
||||
@ -107,7 +111,7 @@ const Mouse = struct {
|
||||
/// Create a new window. This allocates and returns a pointer because we
|
||||
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
||||
/// initialization is not currently possible.
|
||||
pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
|
||||
var self = try alloc.create(Window);
|
||||
errdefer alloc.destroy(self);
|
||||
|
||||
@ -136,6 +140,10 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
};
|
||||
|
||||
// Find all the fonts for this window
|
||||
//
|
||||
// Future: we can share the font group amongst all windows to save
|
||||
// some new window init time and some memory. This will require making
|
||||
// thread-safe changes to font structs.
|
||||
var font_lib = try font.Library.init();
|
||||
errdefer font_lib.deinit();
|
||||
var font_group = try alloc.create(font.GroupCache);
|
||||
@ -247,6 +255,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
|
||||
// Create our terminal grid with the initial window size
|
||||
var renderer_impl = try Renderer.init(alloc, font_group);
|
||||
errdefer renderer_impl.deinit();
|
||||
renderer_impl.background = .{
|
||||
.r = config.background.r,
|
||||
.g = config.background.g,
|
||||
@ -307,8 +316,14 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
var io_thread = try termio.Thread.init(alloc, &self.io);
|
||||
errdefer io_thread.deinit();
|
||||
|
||||
// True if this window is hosting devmode. We only host devmode on
|
||||
// the first window since imgui is not threadsafe. We need to do some
|
||||
// work to make DevMode work with multiple threads.
|
||||
const host_devmode = DevMode.enabled and DevMode.instance.window == null;
|
||||
|
||||
self.* = .{
|
||||
.alloc = alloc,
|
||||
.app = app,
|
||||
.font_lib = font_lib,
|
||||
.font_group = font_group,
|
||||
.window = window,
|
||||
@ -323,7 +338,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
.visible = true,
|
||||
},
|
||||
.terminal = &self.io.terminal,
|
||||
.devmode = if (!DevMode.enabled) null else &DevMode.instance,
|
||||
.devmode = if (!host_devmode) null else &DevMode.instance,
|
||||
},
|
||||
.renderer_thr = undefined,
|
||||
.mouse = .{},
|
||||
@ -333,7 +348,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
.grid_size = grid_size,
|
||||
.config = config,
|
||||
|
||||
.imgui_ctx = if (!DevMode.enabled) void else try imgui.Context.create(),
|
||||
.imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(),
|
||||
};
|
||||
errdefer if (DevMode.enabled) self.imgui_ctx.destroy();
|
||||
|
||||
@ -361,7 +376,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
|
||||
// Load imgui. This must be done LAST because it has to be done after
|
||||
// all our GLFW setup is complete.
|
||||
if (DevMode.enabled) {
|
||||
if (DevMode.enabled and DevMode.instance.window == null) {
|
||||
const dev_io = try imgui.IO.get();
|
||||
dev_io.cval().IniFilename = "ghostty_dev_mode.ini";
|
||||
|
||||
@ -376,13 +391,16 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
||||
const style = try imgui.Style.get();
|
||||
style.colorsDark();
|
||||
|
||||
// Add our window to the instance
|
||||
// Add our window to the instance if it isn't set.
|
||||
DevMode.instance.window = self;
|
||||
|
||||
// Let our renderer setup
|
||||
try renderer_impl.initDevMode(window);
|
||||
}
|
||||
|
||||
// Give the renderer one more opportunity to finalize any window
|
||||
// setup on the main thread prior to spinning up the rendering thread.
|
||||
try renderer_impl.finalizeInit(window);
|
||||
try renderer_impl.finalizeWindowInit(window);
|
||||
|
||||
// Start our renderer thread
|
||||
self.renderer_thr = try std.Thread.spawn(
|
||||
@ -412,18 +430,22 @@ pub fn destroy(self: *Window) void {
|
||||
self.renderer.threadEnter(self.window) catch unreachable;
|
||||
self.renderer_thread.deinit();
|
||||
|
||||
// If we are devmode-owning, clean that up.
|
||||
if (DevMode.enabled and DevMode.instance.window == self) {
|
||||
// Let our renderer clean up
|
||||
self.renderer.deinitDevMode();
|
||||
|
||||
// Clear the window
|
||||
DevMode.instance.window = null;
|
||||
|
||||
// Uninitialize imgui
|
||||
self.imgui_ctx.destroy();
|
||||
}
|
||||
|
||||
// Deinit our renderer
|
||||
self.renderer.deinit();
|
||||
}
|
||||
|
||||
if (DevMode.enabled) {
|
||||
// Clear the window
|
||||
DevMode.instance.window = null;
|
||||
|
||||
// Uninitialize imgui
|
||||
self.imgui_ctx.destroy();
|
||||
}
|
||||
|
||||
{
|
||||
// Stop our IO thread
|
||||
self.io_thread.stop.send() catch |err|
|
||||
@ -748,6 +770,22 @@ fn keyCallback(
|
||||
DevMode.instance.visible = !DevMode.instance.visible;
|
||||
win.queueRender() catch unreachable;
|
||||
} else log.warn("dev mode was not compiled into this binary", .{}),
|
||||
|
||||
.new_window => {
|
||||
_ = win.app.mailbox.push(.{
|
||||
.new_window = {},
|
||||
}, .{ .instant = {} });
|
||||
win.app.wakeup();
|
||||
},
|
||||
|
||||
.close_window => win.window.setShouldClose(true),
|
||||
|
||||
.quit => {
|
||||
_ = win.app.mailbox.push(.{
|
||||
.quit = {},
|
||||
}, .{ .instant = {} });
|
||||
win.app.wakeup();
|
||||
},
|
||||
}
|
||||
|
||||
// Bindings always result in us ignoring the char if printable
|
||||
|
@ -157,6 +157,25 @@ pub const Config = struct {
|
||||
.{ .toggle_dev_mode = {} },
|
||||
);
|
||||
|
||||
// Windowing
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .n, .mods = .{ .super = true } },
|
||||
.{ .new_window = {} },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .w, .mods = .{ .super = true } },
|
||||
.{ .close_window = {} },
|
||||
);
|
||||
if (builtin.os.tag == .macos) {
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .q, .mods = .{ .super = true } },
|
||||
.{ .quit = {} },
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -133,6 +133,15 @@ pub const Action = union(enum) {
|
||||
|
||||
/// Dev mode
|
||||
toggle_dev_mode: void,
|
||||
|
||||
/// Open a new window
|
||||
new_window: void,
|
||||
|
||||
/// Close the current window
|
||||
close_window: void,
|
||||
|
||||
/// Quit ghostty
|
||||
quit: void,
|
||||
};
|
||||
|
||||
/// Trigger is the associated key state that can trigger an action.
|
||||
|
55
src/main.zig
55
src/main.zig
@ -22,6 +22,9 @@ pub fn main() !void {
|
||||
}
|
||||
std.log.info("renderer={}", .{renderer.Renderer});
|
||||
|
||||
// First things first, we fix our file descriptors
|
||||
fixMaxFiles();
|
||||
|
||||
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
||||
var gpa: ?GPA = gpa: {
|
||||
// Use the libc allocator if it is available beacuse it is WAY
|
||||
@ -128,8 +131,8 @@ pub fn main() !void {
|
||||
defer glfw.terminate();
|
||||
|
||||
// Run our app
|
||||
var app = try App.init(alloc, &config);
|
||||
defer app.deinit();
|
||||
var app = try App.create(alloc, &config);
|
||||
defer app.destroy();
|
||||
try app.run();
|
||||
}
|
||||
|
||||
@ -188,6 +191,54 @@ fn glfwErrorCallback(code: glfw.Error, desc: [:0]const u8) void {
|
||||
std.log.warn("glfw error={} message={s}", .{ code, desc });
|
||||
}
|
||||
|
||||
/// This maximizes the number of file descriptors we can have open. We
|
||||
/// need to do this because each window consumes at least a handful of fds.
|
||||
/// This is extracted from the Zig compiler source code.
|
||||
fn fixMaxFiles() void {
|
||||
if (!@hasDecl(std.os.system, "rlimit")) return;
|
||||
const posix = std.os;
|
||||
|
||||
var lim = posix.getrlimit(.NOFILE) catch {
|
||||
std.log.warn("failed to query file handle limit, may limit max windows", .{});
|
||||
return; // Oh well; we tried.
|
||||
};
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
// On Darwin, `NOFILE` is bounded by a hardcoded value `OPEN_MAX`.
|
||||
// According to the man pages for setrlimit():
|
||||
// setrlimit() now returns with errno set to EINVAL in places that historically succeeded.
|
||||
// It no longer accepts "rlim_cur = RLIM.INFINITY" for RLIM.NOFILE.
|
||||
// Use "rlim_cur = min(OPEN_MAX, rlim_max)".
|
||||
lim.max = std.math.min(std.os.darwin.OPEN_MAX, lim.max);
|
||||
}
|
||||
|
||||
// If we're already at the max, we're done.
|
||||
if (lim.cur >= lim.max) {
|
||||
std.log.debug("file handle limit already maximized value={}", .{lim.cur});
|
||||
return;
|
||||
}
|
||||
|
||||
// Do a binary search for the limit.
|
||||
var min: posix.rlim_t = lim.cur;
|
||||
var max: posix.rlim_t = 1 << 20;
|
||||
// But if there's a defined upper bound, don't search, just set it.
|
||||
if (lim.max != posix.RLIM.INFINITY) {
|
||||
min = lim.max;
|
||||
max = lim.max;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
lim.cur = min + @divTrunc(max - min, 2); // on freebsd rlim_t is signed
|
||||
if (posix.setrlimit(.NOFILE, lim)) |_| {
|
||||
min = lim.cur;
|
||||
} else |_| {
|
||||
max = lim.cur;
|
||||
}
|
||||
if (min + 1 >= max) break;
|
||||
}
|
||||
|
||||
std.log.debug("file handle limit raised value={}", .{lim.cur});
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("Atlas.zig");
|
||||
_ = @import("Pty.zig");
|
||||
|
@ -240,11 +240,6 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Metal) void {
|
||||
if (DevMode.enabled) {
|
||||
imgui.ImplMetal.shutdown();
|
||||
imgui.ImplGlfw.shutdown();
|
||||
}
|
||||
|
||||
self.cells.deinit(self.alloc);
|
||||
|
||||
self.font_shaper.deinit();
|
||||
@ -255,7 +250,7 @@ pub fn deinit(self: *Metal) void {
|
||||
|
||||
/// This is called just prior to spinning up the renderer thread for
|
||||
/// final main thread setup requirements.
|
||||
pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void {
|
||||
pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void {
|
||||
// Set our window backing layer to be our swapchain
|
||||
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?);
|
||||
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
||||
@ -268,7 +263,10 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void {
|
||||
const layer = contentView.getProperty(objc.Object, "layer");
|
||||
const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor");
|
||||
layer.setProperty("contentsScale", scaleFactor);
|
||||
}
|
||||
|
||||
/// This is called if this renderer runs DevMode.
|
||||
pub fn initDevMode(self: *const Metal, window: glfw.Window) !void {
|
||||
if (DevMode.enabled) {
|
||||
// Initialize for our window
|
||||
assert(imgui.ImplGlfw.initForOther(
|
||||
@ -279,6 +277,16 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is called if this renderer runs DevMode.
|
||||
pub fn deinitDevMode(self: *const Metal) void {
|
||||
_ = self;
|
||||
|
||||
if (DevMode.enabled) {
|
||||
imgui.ImplMetal.shutdown();
|
||||
imgui.ImplGlfw.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback called by renderer.Thread when it begins.
|
||||
pub fn threadEnter(self: *const Metal, window: glfw.Window) !void {
|
||||
_ = self;
|
||||
|
@ -300,11 +300,6 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *OpenGL) void {
|
||||
if (DevMode.enabled) {
|
||||
imgui.ImplOpenGL3.shutdown();
|
||||
imgui.ImplGlfw.shutdown();
|
||||
}
|
||||
|
||||
self.font_shaper.deinit();
|
||||
self.alloc.free(self.font_shaper.cell_buf);
|
||||
|
||||
@ -387,7 +382,15 @@ pub fn windowInit(window: glfw.Window) !void {
|
||||
|
||||
/// This is called just prior to spinning up the renderer thread for
|
||||
/// final main thread setup requirements.
|
||||
pub fn finalizeInit(self: *const OpenGL, window: glfw.Window) !void {
|
||||
pub fn finalizeWindowInit(self: *const OpenGL, window: glfw.Window) !void {
|
||||
_ = self;
|
||||
_ = window;
|
||||
}
|
||||
|
||||
/// This is called if this renderer runs DevMode.
|
||||
pub fn initDevMode(self: *const OpenGL, window: glfw.Window) !void {
|
||||
_ = self;
|
||||
|
||||
if (DevMode.enabled) {
|
||||
// Initialize for our window
|
||||
assert(imgui.ImplGlfw.initForOpenGL(
|
||||
@ -396,9 +399,16 @@ pub fn finalizeInit(self: *const OpenGL, window: glfw.Window) !void {
|
||||
));
|
||||
assert(imgui.ImplOpenGL3.init("#version 330 core"));
|
||||
}
|
||||
}
|
||||
|
||||
// Call thread exit to clean up our context
|
||||
self.threadExit();
|
||||
/// This is called if this renderer runs DevMode.
|
||||
pub fn deinitDevMode(self: *const OpenGL) void {
|
||||
_ = self;
|
||||
|
||||
if (DevMode.enabled) {
|
||||
imgui.ImplOpenGL3.shutdown();
|
||||
imgui.ImplGlfw.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback called by renderer.Thread when it begins.
|
||||
|
@ -65,7 +65,11 @@ pub fn init(
|
||||
|
||||
// Create our event loop.
|
||||
var loop = try libuv.Loop.init(alloc);
|
||||
errdefer loop.deinit(alloc);
|
||||
errdefer {
|
||||
// Run the loop once to close any of our handles
|
||||
_ = loop.run(.nowait) catch 0;
|
||||
loop.deinit(alloc);
|
||||
}
|
||||
loop.setData(allocPtr);
|
||||
|
||||
// This async handle is used to "wake up" the renderer and force a render.
|
||||
|
@ -15,6 +15,12 @@ const renderer = @import("../renderer.zig");
|
||||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("errno.h");
|
||||
@cInclude("signal.h");
|
||||
@cInclude("unistd.h");
|
||||
});
|
||||
|
||||
/// Allocator
|
||||
alloc: Allocator,
|
||||
|
||||
@ -78,8 +84,8 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
||||
.env = &env,
|
||||
.cwd = opts.config.@"working-directory",
|
||||
.pre_exec = (struct {
|
||||
fn callback(c: *Command) void {
|
||||
const p = c.getData(Pty) orelse unreachable;
|
||||
fn callback(cmd: *Command) void {
|
||||
const p = cmd.getData(Pty) orelse unreachable;
|
||||
p.childPreExec() catch |err|
|
||||
log.err("error initializing child: {}", .{err});
|
||||
}
|
||||
@ -113,9 +119,9 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Exec) 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();
|
||||
// Kill our command
|
||||
self.killCommand() catch |err|
|
||||
log.err("error sending SIGHUP to command, may hang: {}", .{err});
|
||||
_ = self.command.wait() catch |err|
|
||||
log.err("error waiting for command to exit: {}", .{err});
|
||||
|
||||
@ -123,6 +129,42 @@ pub fn deinit(self: *Exec) void {
|
||||
self.terminal.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// Kill the underlying subprocess. This closes the pty file handle and
|
||||
/// sends a SIGHUP to the child process. This doesn't wait for the child
|
||||
/// process to be exited.
|
||||
fn killCommand(self: *Exec) !void {
|
||||
// Close our PTY
|
||||
self.pty.deinit();
|
||||
|
||||
// We need to get our process group ID and send a SIGHUP to it.
|
||||
if (self.command.pid) |pid| {
|
||||
const pgid_: ?c.pid_t = pgid: {
|
||||
const pgid = c.getpgid(pid);
|
||||
|
||||
// Don't know why it would be zero but its not a valid pid
|
||||
if (pgid == 0) break :pgid null;
|
||||
|
||||
// If the pid doesn't exist then... okay.
|
||||
if (pgid == c.ESRCH) break :pgid null;
|
||||
|
||||
// If we have an error...
|
||||
if (pgid < 0) {
|
||||
log.warn("error getting pgid for kill", .{});
|
||||
break :pgid null;
|
||||
}
|
||||
|
||||
break :pgid pgid;
|
||||
};
|
||||
|
||||
if (pgid_) |pgid| {
|
||||
if (c.killpg(pgid, c.SIGHUP) < 0) {
|
||||
log.warn("error killing process group pgid={}", .{pgid});
|
||||
return error.KillFailed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData {
|
||||
assert(self.data == null);
|
||||
|
||||
@ -367,15 +409,15 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
|
||||
var i: usize = 0;
|
||||
const end = @intCast(usize, n);
|
||||
if (ev.terminal_stream.parser.state == .ground) {
|
||||
for (buf[i..end]) |c| {
|
||||
switch (terminal.parse_table.table[c][@enumToInt(terminal.Parser.State.ground)].action) {
|
||||
for (buf[i..end]) |ch| {
|
||||
switch (terminal.parse_table.table[ch][@enumToInt(terminal.Parser.State.ground)].action) {
|
||||
// Print, call directly.
|
||||
.print => ev.terminal_stream.handler.print(@intCast(u21, c)) catch |err|
|
||||
.print => ev.terminal_stream.handler.print(@intCast(u21, ch)) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err}),
|
||||
|
||||
// C0 execute, let our stream handle this one but otherwise
|
||||
// continue since we're guaranteed to be back in ground.
|
||||
.execute => ev.terminal_stream.execute(c) catch |err|
|
||||
.execute => ev.terminal_stream.execute(ch) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err}),
|
||||
|
||||
// Otherwise, break out and go the slow path until we're
|
||||
@ -413,8 +455,8 @@ const StreamHandler = struct {
|
||||
try self.ev.queueWrite(data);
|
||||
}
|
||||
|
||||
pub fn print(self: *StreamHandler, c: u21) !void {
|
||||
try self.terminal.print(c);
|
||||
pub fn print(self: *StreamHandler, ch: u21) !void {
|
||||
try self.terminal.print(ch);
|
||||
}
|
||||
|
||||
pub fn bell(self: StreamHandler) !void {
|
||||
|
@ -49,7 +49,11 @@ pub fn init(
|
||||
|
||||
// Create our event loop.
|
||||
var loop = try libuv.Loop.init(alloc);
|
||||
errdefer loop.deinit(alloc);
|
||||
errdefer {
|
||||
// Run the loop once to close any of our handles
|
||||
_ = loop.run(.nowait) catch 0;
|
||||
loop.deinit(alloc);
|
||||
}
|
||||
loop.setData(allocPtr);
|
||||
|
||||
// This async handle is used to "wake up" the renderer and force a render.
|
||||
|
Reference in New Issue
Block a user