From a2edbb4698e9ec1d31e0ccd2a2b6bdb9deeecaf9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 10:05:08 -0800 Subject: [PATCH 01/13] App prepare for multi-window --- src/App.zig | 85 ++++++++++++++++++++++++++++++++++++++++++++------ src/Window.zig | 4 +++ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/App.zig b/src/App.zig index 89e96192e..c80665577 100644 --- a/src/App.zig +++ b/src/App.zig @@ -9,46 +9,111 @@ 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 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, + /// 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 + // The mailbox for messaging this thread + var mailbox = try Mailbox.create(alloc); + errdefer mailbox.destroy(alloc); + + // Create the first window var window = try Window.create(alloc, config); errdefer window.destroy(); + // Create our windows list and add our initial window. + var windows: WindowList = .{}; + errdefer windows.deinit(alloc); + try windows.append(alloc, window); + return App{ .alloc = alloc, - .window = window, + .windows = windows, .config = config, + .mailbox = mailbox, }; } pub fn deinit(self: *App) void { - self.window.destroy(); + // Clean up all our windows + for (self.windows.items) |window| window.destroy(); + self.windows.deinit(self.alloc); + self.mailbox.destroy(self.alloc); self.* = undefined; } -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.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 + 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 => unreachable, + } + } +} + +/// The message types that can be sent to the app thread. +pub const Message = union(enum) { + /// Create a new terminal window. + new_window: void, +}; diff --git a/src/Window.zig b/src/Window.zig index cd899c086..83237d391 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -136,6 +136,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); From ecbd119654e7846fb9931ad3445c9625a0f3414e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 10:34:43 -0800 Subject: [PATCH 02/13] Hook up new window, modify renderers --- src/App.zig | 48 ++++++++++++++++++++++++++++------------- src/Window.zig | 16 ++++++++++++-- src/config.zig | 6 ++++++ src/input/Binding.zig | 3 +++ src/main.zig | 4 ++-- src/renderer/Metal.zig | 20 +++++++++++------ src/renderer/OpenGL.zig | 26 +++++++++++++++------- 7 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/App.zig b/src/App.zig index c80665577..744615f6c 100644 --- a/src/App.zig +++ b/src/App.zig @@ -10,6 +10,7 @@ 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); @@ -34,34 +35,33 @@ mailbox: *Mailbox, /// 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 { +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); - // Create the first window - var window = try Window.create(alloc, config); - errdefer window.destroy(); - - // Create our windows list and add our initial window. - var windows: WindowList = .{}; - errdefer windows.deinit(alloc); - try windows.append(alloc, window); - - return App{ + var app = try alloc.create(App); + errdefer alloc.destroy(app); + app.* = .{ .alloc = alloc, - .windows = windows, + .windows = .{}, .config = config, .mailbox = mailbox, }; + errdefer app.windows.deinit(alloc); + + // Create the first window + try app.newWindow(); + + return app; } -pub fn deinit(self: *App) void { +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.* = undefined; + self.alloc.destroy(self); } /// Wake up the app event loop. This should be called after any messages @@ -86,6 +86,11 @@ pub fn run(self: *App) !void { while (i < self.windows.items.len) { const window = self.windows.items[i]; if (window.shouldClose()) { + // If this was our final window, deinitialize the renderer + if (self.windows.items.len == 1) { + renderer.Renderer.lastWindowDeinit(); + } + window.destroy(); _ = self.windows.swapRemove(i); continue; @@ -107,11 +112,24 @@ fn drainMailbox(self: *App) !void { while (drain.next()) |message| { log.debug("mailbox message={}", .{message}); switch (message) { - .new_window => unreachable, + .new_window => try self.newWindow(), } } } +/// 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(); + + // This was the first window, so we need to initialize our renderer. + if (self.windows.items.len == 1) { + try window.renderer.firstWindowInit(window.window); + } +} + /// The message types that can be sent to the app thread. pub const Message = union(enum) { /// Create a new terminal window. diff --git a/src/Window.zig b/src/Window.zig index 83237d391..89721364d 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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); @@ -313,6 +317,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window { self.* = .{ .alloc = alloc, + .app = app, .font_lib = font_lib, .font_group = font_group, .window = window, @@ -386,7 +391,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*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( @@ -752,6 +757,13 @@ 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 = {}, + }, .{ .forever = {} }); + win.app.wakeup(); + }, } // Bindings always result in us ignoring the char if printable diff --git a/src/config.zig b/src/config.zig index 9cd4ebdc3..4874f99ec 100644 --- a/src/config.zig +++ b/src/config.zig @@ -157,6 +157,12 @@ pub const Config = struct { .{ .toggle_dev_mode = {} }, ); + try result.keybind.set.put( + alloc, + .{ .key = .up, .mods = .{ .super = true } }, + .{ .new_window = {} }, + ); + return result; } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index f6e602fc5..dd015b0c1 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -133,6 +133,9 @@ pub const Action = union(enum) { /// Dev mode toggle_dev_mode: void, + + /// Open a new terminal window. + new_window: void, }; /// Trigger is the associated key state that can trigger an action. diff --git a/src/main.zig b/src/main.zig index a985416cb..41d03e5a9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -128,8 +128,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(); } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1b4a9112a..489826ebd 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.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,12 @@ 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 only after the first window is opened. This may be +/// called multiple times if all windows are closed and a new one is +/// reopened. +pub fn firstWindowInit(self: *const Metal, window: glfw.Window) !void { if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOther( @@ -279,6 +279,14 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void { } } +/// This is called only when the last window is destroyed. +pub fn lastWindowDeinit() void { + 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; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index b5479ce69..efa1f3a1e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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,17 @@ 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 only after the first window is opened. This may be +/// called multiple times if all windows are closed and a new one is +/// reopened. +pub fn firstWindowInit(self: *const OpenGL, window: glfw.Window) !void { + _ = self; + if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOpenGL( @@ -396,9 +401,14 @@ 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 only when the last window is destroyed. +pub fn lastWindowDeinit() void { + if (DevMode.enabled) { + imgui.ImplOpenGL3.shutdown(); + imgui.ImplGlfw.shutdown(); + } } /// Callback called by renderer.Thread when it begins. From 410b2b44866aab4ccc023bebf518c211c5f9fff2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 10:41:57 -0800 Subject: [PATCH 03/13] Do not block on app mailbox --- src/Window.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Window.zig b/src/Window.zig index 89721364d..b9c57cfb9 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -761,7 +761,7 @@ fn keyCallback( .new_window => { _ = win.app.mailbox.push(.{ .new_window = {}, - }, .{ .forever = {} }); + }, .{ .instant = {} }); win.app.wakeup(); }, } From be76bc6c1a9bad8a7ccd7972adbcc6c07a35db1f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 10:44:23 -0800 Subject: [PATCH 04/13] close window action --- src/Window.zig | 2 ++ src/config.zig | 7 ++++++- src/input/Binding.zig | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index b9c57cfb9..1f5e48c27 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -764,6 +764,8 @@ fn keyCallback( }, .{ .instant = {} }); win.app.wakeup(); }, + + .close_window => win.window.setShouldClose(true), } // Bindings always result in us ignoring the char if printable diff --git a/src/config.zig b/src/config.zig index 4874f99ec..179a5b95d 100644 --- a/src/config.zig +++ b/src/config.zig @@ -159,9 +159,14 @@ pub const Config = struct { try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true } }, + .{ .key = .n, .mods = .{ .super = true } }, .{ .new_window = {} }, ); + try result.keybind.set.put( + alloc, + .{ .key = .w, .mods = .{ .super = true } }, + .{ .close_window = {} }, + ); return result; } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index dd015b0c1..63a32678e 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -134,8 +134,11 @@ pub const Action = union(enum) { /// Dev mode toggle_dev_mode: void, - /// Open a new terminal window. + /// Open a new window new_window: void, + + /// Close the current window + close_window: void, }; /// Trigger is the associated key state that can trigger an action. From ac6f960b923de513a511594afc6d5e047e3895b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 14:08:42 -0800 Subject: [PATCH 05/13] termio: on deinit, send SIGHUP to child process to exit it --- src/termio/Exec.zig | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 88e791b9a..ad03d9e74 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -15,6 +15,11 @@ const renderer = @import("../renderer.zig"); const log = std.log.scoped(.io_exec); +const c = @cImport({ + @cInclude("signal.h"); + @cInclude("unistd.h"); +}); + /// Allocator alloc: Allocator, @@ -78,8 +83,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 +118,8 @@ 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(); _ = self.command.wait() catch |err| log.err("error waiting for command to exit: {}", .{err}); @@ -123,6 +127,22 @@ 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.getpgid(pid); + if (pgid > 0) { + _ = c.killpg(pgid, c.SIGHUP); + } + } +} + pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { assert(self.data == null); @@ -367,15 +387,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 +433,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 { From c9b01fdc6c0a3daf5db6eb44434ba4756446928b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 14:10:28 -0800 Subject: [PATCH 06/13] support app quitting to close all windows --- src/App.zig | 11 ++++++++--- src/Window.zig | 9 ++++++++- src/config.zig | 8 ++++++++ src/input/Binding.zig | 3 +++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/App.zig b/src/App.zig index 744615f6c..00187bdcd 100644 --- a/src/App.zig +++ b/src/App.zig @@ -74,7 +74,8 @@ pub fn wakeup(self: App) void { /// 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.windows.items.len > 0) { + var quit: bool = false; + while (!quit and self.windows.items.len > 0) { // Block for any glfw events. try glfw.waitEvents(); @@ -100,12 +101,12 @@ pub fn run(self: *App) !void { } // Drain our mailbox - try self.drainMailbox(); + try self.drainMailbox(&quit); } } /// Drain the mailbox. -fn drainMailbox(self: *App) !void { +fn drainMailbox(self: *App, quit: *bool) !void { var drain = self.mailbox.drain(); defer drain.deinit(); @@ -113,6 +114,7 @@ fn drainMailbox(self: *App) !void { log.debug("mailbox message={}", .{message}); switch (message) { .new_window => try self.newWindow(), + .quit => quit.* = true, } } } @@ -134,4 +136,7 @@ fn newWindow(self: *App) !void { pub const Message = union(enum) { /// Create a new terminal window. new_window: void, + + /// Quit + quit: void, }; diff --git a/src/Window.zig b/src/Window.zig index 1f5e48c27..2dad88b67 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -342,7 +342,7 @@ pub fn create(alloc: Allocator, app: *App, 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(); @@ -766,6 +766,13 @@ fn keyCallback( }, .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 diff --git a/src/config.zig b/src/config.zig index 179a5b95d..9249254df 100644 --- a/src/config.zig +++ b/src/config.zig @@ -168,6 +168,14 @@ pub const Config = struct { .{ .close_window = {} }, ); + if (builtin.os.tag == .macos) { + try result.keybind.set.put( + alloc, + .{ .key = .q, .mods = .{ .super = true } }, + .{ .close_window = {} }, + ); + } + return result; } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 63a32678e..81cc18fdc 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -139,6 +139,9 @@ pub const Action = union(enum) { /// Close the current window close_window: void, + + /// Quit ghostty + quit: void, }; /// Trigger is the associated key state that can trigger an action. From 705772ed282ab91be414ab32d1c414a89565f9c6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 15:26:18 -0800 Subject: [PATCH 07/13] termio: clean up error handling for kill --- src/termio/Exec.zig | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index ad03d9e74..c1785492a 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -16,6 +16,7 @@ const renderer = @import("../renderer.zig"); const log = std.log.scoped(.io_exec); const c = @cImport({ + @cInclude("errno.h"); @cInclude("signal.h"); @cInclude("unistd.h"); }); @@ -119,7 +120,8 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { pub fn deinit(self: *Exec) void { // Kill our command - self.killCommand(); + 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}); @@ -130,15 +132,35 @@ pub fn deinit(self: *Exec) void { /// 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 { +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.getpgid(pid); - if (pgid > 0) { - _ = c.killpg(pgid, c.SIGHUP); + 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; + } } } } From afd4800da4a03f7f626cc39e2e41216d196d391d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 16:03:03 -0800 Subject: [PATCH 08/13] pkg/imgui: support context current --- pkg/imgui/context.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/imgui/context.zig b/pkg/imgui/context.zig index 2b68df9ec..c2830f65c 100644 --- a/pkg/imgui/context.zig +++ b/pkg/imgui/context.zig @@ -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(); } From 05cd77e7cf78f179b991a14b7b975d5f2b88e86e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 16:06:34 -0800 Subject: [PATCH 09/13] DevMode only renders on first window --- src/Window.zig | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 2dad88b67..f9ac53d54 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -315,6 +315,11 @@ pub fn create(alloc: Allocator, app: *App, 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, @@ -332,7 +337,7 @@ pub fn create(alloc: Allocator, app: *App, 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 = .{}, @@ -370,7 +375,7 @@ pub fn create(alloc: Allocator, app: *App, 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"; @@ -385,7 +390,7 @@ pub fn create(alloc: Allocator, app: *App, 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; } @@ -425,7 +430,7 @@ pub fn destroy(self: *Window) void { self.renderer.deinit(); } - if (DevMode.enabled) { + if (DevMode.enabled and DevMode.instance.window == self) { // Clear the window DevMode.instance.window = null; From e0db46ac979b384a8e1e16dd586aeb08e8b633de Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 16:23:36 -0800 Subject: [PATCH 10/13] clean up some resources better on error --- src/Window.zig | 1 + src/renderer/Thread.zig | 6 +++++- src/termio/Thread.zig | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index f9ac53d54..20546b5e9 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -255,6 +255,7 @@ pub fn create(alloc: Allocator, app: *App, 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, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index ebc06fd8d..68808de92 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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. diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 2dcfe9476..af175aeab 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -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. From 135b859b8f2033172455c860dc5683747b220543 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 16:38:33 -0800 Subject: [PATCH 11/13] raise max file descriptors on launch --- src/main.zig | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/main.zig b/src/main.zig index 41d03e5a9..2346e24fa 100644 --- a/src/main.zig +++ b/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 @@ -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"); From fd304c93386d5cfb732983ecd2511d72d251f45f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 17:26:01 -0800 Subject: [PATCH 12/13] Deinit devmode more cleanly --- src/App.zig | 32 ++++++++++++++++++-------------- src/Window.zig | 23 +++++++++++++++-------- src/renderer/Metal.zig | 12 ++++++------ src/renderer/OpenGL.zig | 12 ++++++------ 4 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/App.zig b/src/App.zig index 00187bdcd..b0dc4fdfb 100644 --- a/src/App.zig +++ b/src/App.zig @@ -32,6 +32,9 @@ config: *const Config, /// 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. @@ -47,6 +50,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*App { .windows = .{}, .config = config, .mailbox = mailbox, + .quit = false, }; errdefer app.windows.deinit(alloc); @@ -74,8 +78,7 @@ pub fn wakeup(self: App) void { /// 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 { - var quit: bool = false; - while (!quit and self.windows.items.len > 0) { + while (!self.quit and self.windows.items.len > 0) { // Block for any glfw events. try glfw.waitEvents(); @@ -87,11 +90,6 @@ pub fn run(self: *App) !void { while (i < self.windows.items.len) { const window = self.windows.items[i]; if (window.shouldClose()) { - // If this was our final window, deinitialize the renderer - if (self.windows.items.len == 1) { - renderer.Renderer.lastWindowDeinit(); - } - window.destroy(); _ = self.windows.swapRemove(i); continue; @@ -100,13 +98,13 @@ pub fn run(self: *App) !void { i += 1; } - // Drain our mailbox - try self.drainMailbox(&quit); + // Drain our mailbox only if we're not quitting. + if (!self.quit) try self.drainMailbox(); } } /// Drain the mailbox. -fn drainMailbox(self: *App, quit: *bool) !void { +fn drainMailbox(self: *App) !void { var drain = self.mailbox.drain(); defer drain.deinit(); @@ -114,7 +112,7 @@ fn drainMailbox(self: *App, quit: *bool) !void { log.debug("mailbox message={}", .{message}); switch (message) { .new_window => try self.newWindow(), - .quit => quit.* = true, + .quit => try self.setQuit(), } } } @@ -125,10 +123,16 @@ fn newWindow(self: *App) !void { errdefer window.destroy(); try self.windows.append(self.alloc, window); errdefer _ = self.windows.pop(); +} - // This was the first window, so we need to initialize our renderer. - if (self.windows.items.len == 1) { - try window.renderer.firstWindowInit(window.window); +/// 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); } } diff --git a/src/Window.zig b/src/Window.zig index 20546b5e9..62960f9b0 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -393,6 +393,9 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // 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 @@ -427,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 and DevMode.instance.window == self) { - // Clear the window - DevMode.instance.window = null; - - // Uninitialize imgui - self.imgui_ctx.destroy(); - } - { // Stop our IO thread self.io_thread.stop.send() catch |err| diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 489826ebd..65e8b0d9d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -265,10 +265,8 @@ pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void { layer.setProperty("contentsScale", scaleFactor); } -/// This is called only after the first window is opened. This may be -/// called multiple times if all windows are closed and a new one is -/// reopened. -pub fn firstWindowInit(self: *const Metal, window: glfw.Window) !void { +/// 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,8 +277,10 @@ pub fn firstWindowInit(self: *const Metal, window: glfw.Window) !void { } } -/// This is called only when the last window is destroyed. -pub fn lastWindowDeinit() 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(); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index efa1f3a1e..b574a8a42 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -387,10 +387,8 @@ pub fn finalizeWindowInit(self: *const OpenGL, window: glfw.Window) !void { _ = window; } -/// This is called only after the first window is opened. This may be -/// called multiple times if all windows are closed and a new one is -/// reopened. -pub fn firstWindowInit(self: *const OpenGL, window: glfw.Window) !void { +/// This is called if this renderer runs DevMode. +pub fn initDevMode(self: *const OpenGL, window: glfw.Window) !void { _ = self; if (DevMode.enabled) { @@ -403,8 +401,10 @@ pub fn firstWindowInit(self: *const OpenGL, window: glfw.Window) !void { } } -/// This is called only when the last window is destroyed. -pub fn lastWindowDeinit() void { +/// 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(); From c602820dc9098e94be1c42771349d18198ff7763 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2022 17:27:17 -0800 Subject: [PATCH 13/13] Set proper keybinds --- src/config.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.zig b/src/config.zig index 9249254df..341b41bb8 100644 --- a/src/config.zig +++ b/src/config.zig @@ -157,6 +157,7 @@ pub const Config = struct { .{ .toggle_dev_mode = {} }, ); + // Windowing try result.keybind.set.put( alloc, .{ .key = .n, .mods = .{ .super = true } }, @@ -167,12 +168,11 @@ pub const Config = struct { .{ .key = .w, .mods = .{ .super = true } }, .{ .close_window = {} }, ); - if (builtin.os.tag == .macos) { try result.keybind.set.put( alloc, .{ .key = .q, .mods = .{ .super = true } }, - .{ .close_window = {} }, + .{ .quit = {} }, ); }