Merge 85545ed7d1 into 46d54ed673
commit
2fd64b745c
|
|
@ -582,6 +582,7 @@ typedef enum {
|
|||
GHOSTTY_GOTO_SPLIT_LEFT,
|
||||
GHOSTTY_GOTO_SPLIT_DOWN,
|
||||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
GHOSTTY_GOTO_SPLIT_RECENT,
|
||||
} ghostty_action_goto_split_e;
|
||||
|
||||
// apprt.action.GotoWindow
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ class BaseTerminalController: NSWindowController,
|
|||
/// The cancellables related to our focused surface.
|
||||
private var focusedSurfaceCancellables: Set<AnyCancellable> = []
|
||||
|
||||
/// The most recently focused surface used for toggling focus.
|
||||
private var recentFocusedSurface: Ghostty.SurfaceView? = nil
|
||||
|
||||
/// Cancellable for aggregating bell state across all surfaces in this controller.
|
||||
private var bellStateCancellable: AnyCancellable?
|
||||
|
||||
|
|
@ -292,6 +295,8 @@ class BaseTerminalController: NSWindowController,
|
|||
if to.isEmpty {
|
||||
focusedSurface = nil
|
||||
}
|
||||
|
||||
pruneRecentFocusedSurface(tree: to)
|
||||
}
|
||||
|
||||
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
||||
|
|
@ -427,6 +432,56 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
// MARK: Split Tree Management
|
||||
|
||||
private func containsInSurfaceTree(
|
||||
_ surface: Ghostty.SurfaceView?,
|
||||
tree: SplitTree<Ghostty.SurfaceView>? = nil
|
||||
) -> Bool {
|
||||
guard let surface else { return false }
|
||||
let activeTree = tree ?? surfaceTree
|
||||
return activeTree.contains(surface)
|
||||
}
|
||||
|
||||
private func pruneRecentFocusedSurface(tree: SplitTree<Ghostty.SurfaceView>? = nil) {
|
||||
guard recentFocusedSurface != nil else { return }
|
||||
|
||||
if !containsInSurfaceTree(recentFocusedSurface, tree: tree) {
|
||||
recentFocusedSurface = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRecentFocusedSurface(
|
||||
previous: Ghostty.SurfaceView?,
|
||||
current: Ghostty.SurfaceView?
|
||||
) {
|
||||
guard let previous, let current else { return }
|
||||
guard previous !== current else { return }
|
||||
guard containsInSurfaceTree(previous), containsInSurfaceTree(current) else { return }
|
||||
|
||||
recentFocusedSurface = previous
|
||||
}
|
||||
|
||||
func splitFocusTarget(
|
||||
for direction: Ghostty.SplitFocusDirection,
|
||||
from target: Ghostty.SurfaceView
|
||||
) -> Ghostty.SurfaceView? {
|
||||
// Find the node for the target surface
|
||||
guard let targetNode = surfaceTree.root?.node(view: target) else { return nil }
|
||||
|
||||
switch direction {
|
||||
case .recent:
|
||||
pruneRecentFocusedSurface()
|
||||
|
||||
guard let recentSurface = recentFocusedSurface, recentSurface !== target else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return containsInSurfaceTree(recentSurface) ? recentSurface : nil
|
||||
|
||||
default:
|
||||
return surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the next surface to focus when a node is being closed.
|
||||
/// Goes to previous split unless we're the leftmost leaf, then goes to next.
|
||||
private func findNextFocusTargetAfterClosing(node: SplitTree<Ghostty.SurfaceView>.Node) -> Ghostty.SurfaceView? {
|
||||
|
|
@ -633,11 +688,8 @@ class BaseTerminalController: NSWindowController,
|
|||
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return }
|
||||
guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return }
|
||||
|
||||
// Find the node for the target surface
|
||||
guard let targetNode = surfaceTree.root?.node(view: target) else { return }
|
||||
|
||||
// Find the next surface to focus
|
||||
guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else {
|
||||
guard let nextSurface = splitFocusTarget(for: direction, from: target) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -805,6 +857,9 @@ class BaseTerminalController: NSWindowController,
|
|||
let lastFocusedSurface = focusedSurface
|
||||
focusedSurface = to
|
||||
|
||||
updateRecentFocusedSurface(previous: lastFocusedSurface, current: focusedSurface)
|
||||
pruneRecentFocusedSurface()
|
||||
|
||||
// Important to cancel any prior subscriptions
|
||||
focusedSurfaceCancellables = []
|
||||
|
||||
|
|
@ -853,6 +908,7 @@ class BaseTerminalController: NSWindowController,
|
|||
window.title = lastComputedTitle
|
||||
}
|
||||
|
||||
|
||||
func pwdDidChange(to: URL?) {
|
||||
guard let window else { return }
|
||||
|
||||
|
|
|
|||
|
|
@ -1178,16 +1178,8 @@ extension Ghostty {
|
|||
// Convert the C API direction to our Swift type
|
||||
guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return false }
|
||||
|
||||
// Find the current node in the tree
|
||||
guard let targetNode = controller.surfaceTree.root?.node(view: surfaceView) else { return false }
|
||||
|
||||
// Check if a split actually exists in the target direction before
|
||||
// returning true. This ensures performable keybinds only consume
|
||||
// the key event when we actually perform navigation.
|
||||
let focusDirection: SplitTree<Ghostty.SurfaceView>.FocusDirection = splitDirection.toSplitTreeFocusDirection()
|
||||
guard controller.surfaceTree.focusTarget(for: focusDirection, from: targetNode) != nil else {
|
||||
return false
|
||||
}
|
||||
// Validate there is a target split before claiming the action is performable.
|
||||
guard controller.splitFocusTarget(for: splitDirection, from: surfaceView) != nil else { return false }
|
||||
|
||||
// We have a valid target, post the notification to perform the navigation
|
||||
NotificationCenter.default.post(
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ extension Ghostty {
|
|||
|
||||
/// An enum that is used for the directions that a split focus event can change.
|
||||
enum SplitFocusDirection {
|
||||
case previous, next, up, down, left, right
|
||||
case previous, next, up, down, left, right, recent
|
||||
|
||||
/// Initialize from a Ghostty API enum.
|
||||
static func from(direction: ghostty_action_goto_split_e) -> Self? {
|
||||
|
|
@ -142,6 +142,9 @@ extension Ghostty {
|
|||
case GHOSTTY_GOTO_SPLIT_NEXT:
|
||||
return .next
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_RECENT:
|
||||
return .recent
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_UP:
|
||||
return .up
|
||||
|
||||
|
|
@ -167,6 +170,9 @@ extension Ghostty {
|
|||
case .next:
|
||||
return GHOSTTY_GOTO_SPLIT_NEXT
|
||||
|
||||
case .recent:
|
||||
return GHOSTTY_GOTO_SPLIT_RECENT
|
||||
|
||||
case .up:
|
||||
return GHOSTTY_GOTO_SPLIT_UP
|
||||
|
||||
|
|
@ -240,6 +246,9 @@ extension Ghostty.SplitFocusDirection {
|
|||
|
||||
case .right:
|
||||
return .spatial(.right)
|
||||
|
||||
case .recent:
|
||||
fatalError("recent is not a valid SplitTree FocusDirection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -511,6 +511,7 @@ pub const GotoSplit = enum(c_int) {
|
|||
left,
|
||||
down,
|
||||
right,
|
||||
recent,
|
||||
|
||||
test "ghostty.h GotoSplit" {
|
||||
try lib.checkGhosttyHEnum(GotoSplit, "GHOSTTY_GOTO_SPLIT_");
|
||||
|
|
|
|||
|
|
@ -2022,14 +2022,7 @@ const Action = struct {
|
|||
return false;
|
||||
};
|
||||
|
||||
return tree.goto(switch (to) {
|
||||
.previous => .previous_wrapped,
|
||||
.next => .next_wrapped,
|
||||
.up => .{ .spatial = .up },
|
||||
.down => .{ .spatial = .down },
|
||||
.left => .{ .spatial = .left },
|
||||
.right => .{ .spatial = .right },
|
||||
});
|
||||
return tree.gotoSplit(to);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@ pub const SplitTree = extern struct {
|
|||
/// tree change states.
|
||||
last_focused: WeakRef(Surface) = .empty,
|
||||
|
||||
/// Previously focused surface, used for recent navigation.
|
||||
recent_focused: WeakRef(Surface) = .empty,
|
||||
|
||||
/// The source that we use to rebuild the tree. This is also
|
||||
/// used to debounce updates.
|
||||
rebuild_source: ?c_uint = null,
|
||||
|
|
@ -386,6 +389,37 @@ pub const SplitTree = extern struct {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Move focus to the most recently focused surface (if any).
|
||||
fn gotoRecent(self: *Self) bool {
|
||||
const tree = self.getTree() orelse return false;
|
||||
const active = self.getActiveSurfaceHandle() orelse return false;
|
||||
|
||||
const recent_surface = self.private().recent_focused.get() orelse return false;
|
||||
defer recent_surface.unref();
|
||||
|
||||
const recent_handle = handleForSurface(tree, recent_surface) orelse return false;
|
||||
|
||||
// Don't do anything if we're already on the recent surface.
|
||||
if (recent_handle == active) return false;
|
||||
|
||||
const surface = tree.nodes[recent_handle.idx()].leaf;
|
||||
surface.grabFocus();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Move focus according to an apprt GotoSplit direction.
|
||||
pub fn gotoSplit(self: *Self, to: apprt.action.GotoSplit) bool {
|
||||
return switch (to) {
|
||||
.recent => self.gotoRecent(),
|
||||
.previous => self.goto(.previous_wrapped),
|
||||
.next => self.goto(.next_wrapped),
|
||||
.up => self.goto(.{ .spatial = .up }),
|
||||
.down => self.goto(.{ .spatial = .down }),
|
||||
.left => self.goto(.{ .spatial = .left }),
|
||||
.right => self.goto(.{ .spatial = .right }),
|
||||
};
|
||||
}
|
||||
|
||||
fn disconnectSurfaceHandlers(self: *Self) void {
|
||||
const tree = self.getTree() orelse return;
|
||||
var it = tree.iterator();
|
||||
|
|
@ -492,6 +526,41 @@ pub const SplitTree = extern struct {
|
|||
return surface;
|
||||
}
|
||||
|
||||
fn handleForSurface(
|
||||
tree: *const Surface.Tree,
|
||||
surface: *Surface,
|
||||
) ?Surface.Tree.Node.Handle {
|
||||
var it = tree.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.view == surface) return entry.handle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn pruneCachedFocus(self: *Self, tree: ?*Surface.Tree) void {
|
||||
const priv = self.private();
|
||||
const active_tree = tree orelse {
|
||||
priv.last_focused.set(null);
|
||||
priv.recent_focused.set(null);
|
||||
return;
|
||||
};
|
||||
|
||||
if (priv.last_focused.get()) |v| {
|
||||
defer v.unref();
|
||||
if (handleForSurface(active_tree, v) == null) {
|
||||
priv.last_focused.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (priv.recent_focused.get()) |v| {
|
||||
defer v.unref();
|
||||
if (handleForSurface(active_tree, v) == null) {
|
||||
priv.recent_focused.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getHasSurfaces(self: *Self) bool {
|
||||
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
|
||||
return !tree.isEmpty();
|
||||
|
|
@ -544,6 +613,7 @@ pub const SplitTree = extern struct {
|
|||
self.connectSurfaceHandlers();
|
||||
}
|
||||
|
||||
self.pruneCachedFocus(priv.tree);
|
||||
self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
|
||||
}
|
||||
|
||||
|
|
@ -579,6 +649,7 @@ pub const SplitTree = extern struct {
|
|||
fn dispose(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
priv.last_focused.set(null);
|
||||
priv.recent_focused.set(null);
|
||||
if (priv.rebuild_source) |v| {
|
||||
if (glib.Source.remove(v) == 0) {
|
||||
log.warn("unable to remove rebuild source", .{});
|
||||
|
|
@ -776,11 +847,22 @@ pub const SplitTree = extern struct {
|
|||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
|
||||
// We never CLEAR our last_focused because the property is specifically
|
||||
// the last focused surface. We let the weakref clear itself when
|
||||
// the surface is destroyed.
|
||||
if (!surface.getFocused()) return;
|
||||
self.private().last_focused.set(surface);
|
||||
const last_focused = priv.last_focused.get();
|
||||
defer if (last_focused) |v| v.unref();
|
||||
|
||||
if (last_focused) |v| {
|
||||
if (v != surface) {
|
||||
priv.recent_focused.set(v);
|
||||
}
|
||||
}
|
||||
|
||||
priv.last_focused.set(surface);
|
||||
|
||||
// Our active surface probably changed
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ pub const Action = union(enum) {
|
|||
|
||||
/// Focus on a split either in the specified direction (`right`, `down`,
|
||||
/// `left` and `up`), or in the adjacent split in the order of creation
|
||||
/// (`previous` and `next`).
|
||||
/// (`previous` and `next`), or the most recently used split (`recent`).
|
||||
goto_split: SplitFocusDirection,
|
||||
|
||||
/// Focus on either the previous window or the next one ('previous', 'next')
|
||||
|
|
@ -1047,6 +1047,7 @@ pub const Action = union(enum) {
|
|||
left,
|
||||
down,
|
||||
right,
|
||||
recent,
|
||||
|
||||
pub fn parse(input: []const u8) !SplitFocusDirection {
|
||||
return std.meta.stringToEnum(SplitFocusDirection, input) orelse {
|
||||
|
|
@ -1072,6 +1073,7 @@ pub const Action = union(enum) {
|
|||
try testing.expectEqual(.left, try SplitFocusDirection.parse("left"));
|
||||
try testing.expectEqual(.down, try SplitFocusDirection.parse("down"));
|
||||
try testing.expectEqual(.right, try SplitFocusDirection.parse("right"));
|
||||
try testing.expectEqual(.recent, try SplitFocusDirection.parse("recent"));
|
||||
|
||||
try testing.expectEqual(.up, try SplitFocusDirection.parse("top"));
|
||||
try testing.expectEqual(.down, try SplitFocusDirection.parse("bottom"));
|
||||
|
|
|
|||
|
|
@ -484,6 +484,11 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
.title = "Focus Split: Next",
|
||||
.description = "Focus the next split, if any.",
|
||||
},
|
||||
.{
|
||||
.action = .{ .goto_split = .recent },
|
||||
.title = "Focus Split: Recent",
|
||||
.description = "Focus the most recently used split, if any.",
|
||||
},
|
||||
.{
|
||||
.action = .{ .goto_split = .left },
|
||||
.title = "Focus Split: Left",
|
||||
|
|
|
|||
Loading…
Reference in New Issue