From 2052b8df6cdaa721d008e0658dc4c42f071456e0 Mon Sep 17 00:00:00 2001 From: pnodet Date: Sun, 29 Dec 2024 01:35:23 +0100 Subject: [PATCH] feat: add handling logic to store and retrieve closed tabs --- macos/Sources/App/macOS/AppDelegate.swift | 1 + macos/Sources/Ghostty/Ghostty.App.swift | 12 ++- src/App.zig | 9 +- src/Surface.zig | 25 +++++ src/apprt/glfw.zig | 112 +++++++++++++--------- src/apprt/gtk/App.zig | 20 ++++ src/apprt/gtk/Window.zig | 17 ++++ src/input/Binding.zig | 3 +- 8 files changed, 150 insertions(+), 49 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7b0ff6fc2..4e5c6c570 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -345,6 +345,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow) syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab) + syncMenuShortcut(config, action: "reopen_last_tab", menuItem: self.menuReopenLastTab) syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose) syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow) syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2d9822d6e..95bd6ef23 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -107,7 +107,7 @@ extension Ghostty { deinit { // This will force the didSet callbacks to run which free. self.app = nil - + #if os(macOS) NotificationCenter.default.removeObserver(self) #endif @@ -197,6 +197,13 @@ extension Ghostty { } } + func reopenLastTab(surface: ghostty_surface_t) { + let action = "reopen_last_tab" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + func newWindow(surface: ghostty_surface_t) { let action = "new_window" if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { @@ -460,6 +467,9 @@ extension Ghostty { case GHOSTTY_ACTION_NEW_TAB: newTab(app, target: target) + case GHOSTTY_ACTION_REOPEN_LAST_TAB: + reopenLastTab(app, target: target) + case GHOSTTY_ACTION_NEW_SPLIT: newSplit(app, target: target, direction: action.action.new_split) diff --git a/src/App.zig b/src/App.zig index 32d921c3d..9dfda80e9 100644 --- a/src/App.zig +++ b/src/App.zig @@ -24,6 +24,7 @@ const objc = @import("objc"); const log = std.log.scoped(.app); const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface); +const LastClosedTabs = @import("terminal/closedtabs.zig").LastClosedTabs; /// General purpose allocator alloc: Allocator, @@ -31,6 +32,9 @@ alloc: Allocator, /// The list of surfaces that are currently active. surfaces: SurfaceList, +/// Storage for recently closed tabs +last_closed_tabs: LastClosedTabs = .{}, + /// This is true if the app that Ghostty is in is focused. This may /// mean that no surfaces (terminals) are focused but the app is still /// focused, i.e. may an about window. On macOS, this concept is known @@ -101,6 +105,7 @@ pub fn create( .quit = false, .font_grid_set = font_grid_set, .config_conditional_state = .{}, + .last_closed_tabs = .{}, }; errdefer app.surfaces.deinit(alloc); @@ -112,6 +117,9 @@ pub fn destroy(self: *App) void { for (self.surfaces.items) |surface| surface.deinit(); self.surfaces.deinit(self.alloc); + // Clean up our last closed tabs + self.last_closed_tabs.deinit(self.alloc); + // Clean up our font group cache // We should have zero items in the grid set at this point because // destroy only gets called when the app is shutting down and this @@ -444,7 +452,6 @@ pub fn performAction( .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), .toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}), .toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}), - .reopen_last_tab => try rt_app.performAction(.app, .reopen_last_tab, {}), } } diff --git a/src/Surface.zig b/src/Surface.zig index eedeb4fb5..4283a5d54 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -724,6 +724,25 @@ pub fn deinit(self: *Surface) void { /// Close this surface. This will trigger the runtime to start the /// close process, which should ultimately deinitialize this surface. pub fn close(self: *Surface) void { + // Save tab data before closing + const cwd = self.io.terminal.getPwd(); + const cwd_copy = if (cwd) |c| self.alloc.dupe(u8, c) catch null else null; + + const title = self.rt_surface.getTitle(); + const title_copy = if (title) |t| self.alloc.dupe(u8, t) catch null else null; + + // Save to last closed tabs + self.app.last_closed_tabs.push(.{ + .title = title_copy, + .cwd = cwd_copy, + }, self.alloc); + + log.debug("closing tab - pwd: {s}, title: {s}", .{ + cwd_copy orelse "(null)", + title_copy orelse "(null)", + }); + + log.debug("close from surface ptr={X}", .{@intFromPtr(self)}); self.rt_surface.close(self.needsConfirmQuit()); } @@ -4007,6 +4026,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .reopen_last_tab => try self.rt_app.performAction( + .{ .surface = self }, + .reopen_last_tab, + {}, + ), + inline .previous_tab, .next_tab, .last_tab, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index e73cabbf8..ccdd0f313 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -22,8 +22,7 @@ const CoreApp = @import("../App.zig"); const CoreSurface = @import("../Surface.zig"); const configpkg = @import("../config.zig"); const Config = @import("../config.zig").Config; - -const LastClosedTabs = @import("../terminal/closedtabs.zig").LastClosedTabs; +const LastClosedTab = @import("../terminal/closedtabs.zig").LastClosedTab; // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ @@ -40,9 +39,6 @@ pub const App = struct { /// Mac-specific state. darwin: if (Darwin.enabled) Darwin else void, - /// Store information about the last closed tabs - last_closed_tabs: LastClosedTabs = .{}, - pub const Options = struct {}; pub fn init(core_app: *CoreApp, _: Options) !App { @@ -110,7 +106,6 @@ pub const App = struct { } pub fn terminate(self: *App) void { - self.last_closed_tabs.deinit(self.app.alloc); self.config.deinit(); glfw.terminate(); } @@ -165,7 +160,10 @@ pub const App = struct { .surface => |v| v, }), - .reopen_last_tab => try self.reopenLastTab(), + .reopen_last_tab => try self.reopenLastTab(switch (target) { + .app => null, + .surface => |v| v, + }), .size_limit => switch (target) { .app => {}, @@ -325,26 +323,8 @@ pub const App = struct { win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0); } - /// Log that a reopen last tab action was triggered - fn reopenLastTab(self: *App) !void { + fn addTab(self: *App, parent: *CoreSurface, window: *Surface) !void { _ = self; - std.log.debug("Reopen last tab action triggered", .{}); - } - - /// Create a new tab in the parent surface. - fn newTab(self: *App, parent_: ?*CoreSurface) !void { - if (!Darwin.enabled) { - log.warn("tabbing is not supported on this platform", .{}); - return; - } - - const parent = parent_ orelse { - _ = try self.newSurface(null); - return; - }; - - // Create the new window - const window = try self.newSurface(parent); // Add the new window the parent window const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?; @@ -370,6 +350,66 @@ pub const App = struct { }; } + /// Log that a reopen last tab action was triggered + fn reopenLastTab(self: *App, parent_: ?*CoreSurface) !void { + if (!Darwin.enabled) { + log.warn("tabbing is not supported on this platform", .{}); + return; + } + + const parent: *CoreSurface = parent_ orelse { + log.warn("No parent surface found", .{}); + return; + }; + + // Get the last closed tab from the app-level storage + const last_tab: *LastClosedTab = parent.app.last_closed_tabs.getLast() orelse { + log.warn("No last closed tab found", .{}); + return; + }; + + // Create a new tab + const window = try self.newSurface(parent); + + // Set the working directory and title if available + if (last_tab.cwd) |cwd| { + try window.core_surface.io.terminal.setPwd(cwd); + } + if (last_tab.title) |title| { + // Ensure we have a null-terminated string for the title + const title_z = try window.core_surface.alloc.dupeZ(u8, title); + errdefer window.core_surface.alloc.free(title_z); + try window.core_surface.rt_surface.setTitle(title_z); + } + + log.debug("Reopening last tab - pwd: {s}, title: {s}", .{ + last_tab.cwd orelse "(null)", + last_tab.title orelse "(null)", + }); + + try self.addTab(parent, window); + } + + /// Create a new tab in the parent surface. + fn newTab(self: *App, parent_: ?*CoreSurface) !void { + if (!Darwin.enabled) { + log.warn("tabbing is not supported on this platform", .{}); + return; + } + + const parent = parent_ orelse { + _ = try self.newSurface(null); + return; + }; + + log.debug("New tab: {?}", .{parent}); + + // Create the new window + const window = try self.newSurface(parent); + + try self.addTab(parent, window); + } + fn newSurface(self: *App, parent_: ?*CoreSurface) !*Surface { // Grab a surface allocation because we're going to need it. var surface = try self.app.alloc.create(Surface); @@ -610,26 +650,6 @@ pub const Surface = struct { } pub fn deinit(self: *Surface) void { - // Save the closing tab information - const title = if (self.title_text) |t| - self.core_surface.alloc.dupe(u8, t) catch null - else - null; - errdefer if (title) |t| self.core_surface.alloc.free(t); - - const cwd = self.core_surface.alloc.dupe(u8, "~") catch null; - errdefer if (cwd) |c| self.core_surface.alloc.free(c); - - self.app.last_closed_tabs.push(.{ - .title = title, - .cwd = cwd, - }, self.core_surface.alloc); - - log.debug("all last closed tab: {?}", .{self.app.last_closed_tabs.this}); - log.debug("last closed tab: {?}", .{self.app.last_closed_tabs.getLast()}); - - if (self.title_text) |t| self.core_surface.alloc.free(t); - // Remove ourselves from the list of known surfaces in the app. self.app.app.deleteSurface(self); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 9128c8b10..9d637cf45 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -459,6 +459,7 @@ pub fn performAction( .toggle_fullscreen => self.toggleFullscreen(target, value), .new_tab => try self.newTab(target), + .reopen_last_tab => try self.reopenLastTab(target), .goto_tab => self.gotoTab(target, value), .move_tab => self.moveTab(target, value), .new_split => try self.newSplit(target, value), @@ -514,6 +515,23 @@ fn newTab(_: *App, target: apprt.Target) !void { } } +fn reopenLastTab(_: *App, target: apprt.Target) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "reopen_last_tab invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + window.reopenLastTab(); + }, + } +} + fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void { switch (target) { .app => {}, @@ -927,6 +945,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("win.close", .{ .close_surface = {} }); try self.syncActionAccelerator("win.new_window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} }); + try self.syncActionAccelerator("win.reopen_last_tab", .{ .reopen_last_tab = {} }); try self.syncActionAccelerator("win.split_right", .{ .new_split = .right }); try self.syncActionAccelerator("win.split_down", .{ .new_split = .down }); try self.syncActionAccelerator("win.split_left", .{ .new_split = .left }); @@ -1591,6 +1610,7 @@ fn initMenu(self: *App) void { c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); c.g_menu_append(section, "New Window", "win.new_window"); c.g_menu_append(section, "New Tab", "win.new_tab"); + c.g_menu_append(section, "Reopen Last Tab", "win.reopen_last_tab"); c.g_menu_append(section, "Split Right", "win.split_right"); c.g_menu_append(section, "Split Down", "win.split_down"); c.g_menu_append(section, "Close Window", "win.close"); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 0ad09ab74..cf4c8d11b 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -129,6 +129,7 @@ pub fn init(self: *Window, app: *App) !void { const tab_overview = c.adw_tab_overview_new(); c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view); c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); + c.adw_tab_overview_set_enable_reopen_last_tab(@ptrCast(tab_overview), 1); _ = c.g_signal_connect_data( tab_overview, "create-tab", @@ -382,6 +383,7 @@ fn initActions(self: *Window) void { .{ "close", >kActionClose }, .{ "new_window", >kActionNewWindow }, .{ "new_tab", >kActionNewTab }, + .{ "reopen_last_tab", >kActionReopenLastTab }, .{ "split_right", >kActionSplitRight }, .{ "split_down", >kActionSplitDown }, .{ "split_left", >kActionSplitLeft }, @@ -436,6 +438,12 @@ pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { // does not (cursor doesn't blink) unless reactivated by refocusing. } +pub fn reopenLastTab(self: *Window) void { + _ = self; + + log.debug("reopen last tab", .{}); +} + /// Close the tab for the given notebook page. This will automatically /// handle closing the window if there are no more tabs. pub fn closeTab(self: *Window, tab: *Tab) void { @@ -810,6 +818,15 @@ fn gtkActionNewTab( gtkTabNewClick(undefined, ud); } +fn gtkActionReopenLastTab( + _: *c.GSimpleAction, + _: *c.GVariant, + ud: ?*anyopaque, +) callconv(.C) void { + const self: *Window = @ptrCast(@alignCast(ud orelse return)); + self.reopenLastTab(); +} + fn gtkActionSplitRight( _: *c.GSimpleAction, _: *c.GVariant, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 577746038..c43828c89 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -628,7 +628,6 @@ pub const Action = union(enum) { .quit, .toggle_quick_terminal, .toggle_visibility, - .reopen_last_tab, => .app, // These are app but can be special-cased in a surface context. @@ -673,6 +672,7 @@ pub const Action = union(enum) { // come from. For example `new_window` needs to be sourced to // a surface so inheritance can be done correctly. .new_tab, + .reopen_last_tab, .previous_tab, .next_tab, .last_tab, @@ -896,6 +896,7 @@ pub const Key = enum(c_int) { paste_from_clipboard, new_tab, new_window, + reopen_last_tab, }; /// Trigger is the associated key state that can trigger an action.