From 524575787515d2720893c16a7de0514cf6bb24eb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 13:02:45 -0800 Subject: [PATCH] macos: all sorts of cleanups --- .../App/macOS/AppDelegate+Ghostty.swift | 5 + .../Splits/TerminalSplitTreeView.swift | 9 +- .../Terminal/BaseTerminalController.swift | 111 ++++++++++-------- .../Terminal/TerminalController.swift | 2 +- 4 files changed, 77 insertions(+), 50 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift index 7cc74ba7d..4d798a1a5 100644 --- a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -1,11 +1,16 @@ import AppKit +// MARK: Ghostty Delegate + +/// This implements the Ghostty app delegate protocol which is used by the Ghostty +/// APIs for app-global information. extension AppDelegate: Ghostty.Delegate { func ghosttySurface(id: UUID) -> Ghostty.SurfaceView? { for window in NSApp.windows { guard let controller = window.windowController as? BaseTerminalController else { continue } + for surface in controller.surfaceTree { if surface.id == id { return surface diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 12a9b990b..cffab8c6c 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -1,6 +1,9 @@ import SwiftUI -import os +/// A single operation within the split tree. +/// +/// Rather than binding the split tree (which is immutable), any mutable operations are +/// exposed via this enum to the embedder to handle. enum TerminalSplitOperation { case resize(Resize) case drop(Drop) @@ -41,7 +44,7 @@ struct TerminalSplitTreeView: View { } } -struct TerminalSplitSubtreeView: View { +fileprivate struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App let node: SplitTree.Node @@ -83,7 +86,7 @@ struct TerminalSplitSubtreeView: View { } } -struct TerminalSplitLeaf: View { +fileprivate struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool let action: (TerminalSplitOperation) -> Void diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 20e0d6b4f..e278f653b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -466,33 +466,33 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: newView, from: oldView) } } - + // Setup our undo - if let undoManager { - if let undoAction { - undoManager.setActionName(undoAction) + guard let undoManager else { return } + if let undoAction { + undoManager.setActionName(undoAction) + } + + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration + ) { target in + target.surfaceTree = oldTree + if let oldView { + DispatchQueue.main.async { + Ghostty.moveFocus(to: oldView, from: target.focusedSurface) + } } + undoManager.registerUndo( - withTarget: self, - expiresAfter: undoExpiration + withTarget: target, + expiresAfter: target.undoExpiration ) { target in - target.surfaceTree = oldTree - if let oldView { - DispatchQueue.main.async { - Ghostty.moveFocus(to: oldView, from: target.focusedSurface) - } - } - - undoManager.registerUndo( - withTarget: target, - expiresAfter: target.undoExpiration - ) { target in - target.replaceSurfaceTree( - newTree, - moveFocusTo: newView, - moveFocusFrom: target.focusedSurface, - undoAction: undoAction) - } + target.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: target.focusedSurface, + undoAction: undoAction) } } } @@ -835,7 +835,11 @@ class BaseTerminalController: NSWindowController, } } - private func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) { + private func splitDidDrop( + source: Ghostty.SurfaceView, + destination: Ghostty.SurfaceView, + zone: TerminalSplitDropZone + ) { // Map drop zone to split direction let direction: SplitTree.NewDirection = switch zone { case .top: .up @@ -843,12 +847,12 @@ class BaseTerminalController: NSWindowController, case .left: .left case .right: .right } - + // Check if source is in our tree if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move let treeWithoutSource = surfaceTree.remove(sourceNode) - + do { let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) replaceSurfaceTree( @@ -859,10 +863,10 @@ class BaseTerminalController: NSWindowController, } catch { Ghostty.logger.warning("failed to insert surface during drop: \(error)") } - + return } - + // Source is not in our tree - search other windows var sourceController: BaseTerminalController? var sourceNode: SplitTree.Node? @@ -875,33 +879,48 @@ class BaseTerminalController: NSWindowController, break } } - + guard let sourceController, let sourceNode else { Ghostty.logger.warning("source surface not found in any window during drop") return } - - // TODO: Undo for cross window move. - - // Remove from source controller's tree + + // Remove from source controller's tree and add it to our tree. + // We do this first because if there is an error then we can + // abort. let sourceTreeWithoutNode = sourceController.surfaceTree.remove(sourceNode) - sourceController.replaceSurfaceTree( - sourceTreeWithoutNode, - moveFocusTo: nil, - moveFocusFrom: nil, - undoAction: nil) - - // Insert into our tree + let newTree: SplitTree do { - let newTree = try surfaceTree.insert(view: source, at: destination, direction: direction) - replaceSurfaceTree( - newTree, - moveFocusTo: source, - moveFocusFrom: focusedSurface, - undoAction: "Move Split") + newTree = try surfaceTree.insert(view: source, at: destination, direction: direction) } catch { Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") + return } + + // If our old sourceTree became empty, disable undo, because this will + // close the window and we don't have a way to restore that currently. + if sourceTreeWithoutNode.isEmpty { + undoManager?.disableUndoRegistration() + } + defer { + if sourceTreeWithoutNode.isEmpty { + undoManager?.enableUndoRegistration() + } + } + + // Treat our undo below as a full group. + undoManager?.beginUndoGrouping() + undoManager?.setActionName("Move Split") + defer { + undoManager?.endUndoGrouping() + } + + sourceController.replaceSurfaceTree( + sourceTreeWithoutNode) + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface) } func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c5481851b..ae0b44e4a 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -671,7 +671,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr /// Closes the current window (including any other tabs) immediately and without /// confirmation. This will setup proper undo state so the action can be undone. - private func closeWindowImmediately() { + func closeWindowImmediately() { guard let window = window else { return } registerUndoForCloseWindow()