feat: add handling logic to store and retrieve closed tabs

This commit is contained in:
pnodet
2024-12-29 01:35:23 +01:00
parent 5051a19c7f
commit 2052b8df6c
8 changed files with 150 additions and 49 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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, {}),
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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");

View File

@ -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", &gtkActionClose },
.{ "new_window", &gtkActionNewWindow },
.{ "new_tab", &gtkActionNewTab },
.{ "reopen_last_tab", &gtkActionReopenLastTab },
.{ "split_right", &gtkActionSplitRight },
.{ "split_down", &gtkActionSplitDown },
.{ "split_left", &gtkActionSplitLeft },
@ -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,

View File

@ -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.