Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Sources/ProGhosttyApp/UI/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ final class AppModel: ObservableObject {

}

enum SessionClosedAction: Equatable {
case closePane(workspaceID: UUID, paneID: UUID)
case closeWorkspace(workspaceID: UUID)
case none
}

@Published var workspaceRuntimes: [WorkspaceRuntime] = []
@Published var activeWorkspaceID: UUID?
@Published var isWorkspaceSwitcherPresented = false
Expand Down Expand Up @@ -1580,6 +1586,8 @@ final class AppModel: ObservableObject {

private func handle(_ event: TerminalEvent) {
switch event {
case .sessionClosed(let session):
reconcileClosedSession(session)
case .output:
break
case .cwdChanged(let session, let cwd):
Expand Down Expand Up @@ -1624,6 +1632,61 @@ final class AppModel: ObservableObject {
}
}

static func sessionClosedAction(
for session: TerminalSessionID,
in runtimes: [WorkspaceRuntime]
) -> SessionClosedAction {
guard let runtime = runtimes.first(where: { runtime in
PaneTreeReducer.listLeaves(in: runtime.layout.root).contains { $0.sessionId == session }
}) else {
return .none
}

let leaves = PaneTreeReducer.listLeaves(in: runtime.layout.root)
guard let pane = leaves.first(where: { $0.sessionId == session }) else {
return .none
}
return leaves.count > 1
? .closePane(workspaceID: runtime.id, paneID: pane.paneId)
: .closeWorkspace(workspaceID: runtime.id)
}

private func reconcileClosedSession(_ session: TerminalSessionID) {
switch Self.sessionClosedAction(for: session, in: workspaceRuntimes) {
case .closePane(let workspaceID, let paneID):
guard let index = workspaceRuntimes.firstIndex(where: { $0.id == workspaceID }) else { return }
do {
guard let closed = try paneWorkspaceController.closePane(workspaceID: workspaceID, paneID: paneID),
let updatedLayout = paneWorkspaceController.workspaceLayout(id: workspaceID)
else {
return
}
var runtime = workspaceRuntimes[index]
runtime.layout = updatedLayout
runtime.cwdBySession[closed.sessionId] = nil
runtime.reportedTitleBySession[closed.sessionId] = nil
paneSplitAvailabilityController.remove(paneID: closed.paneId)
workspaceRuntimes[index] = runtime
persistWorkspaceRuntime(at: index)
applyTerminalWindowMinimumContentSize(for: runtime)
applyFocusedTerminalSurface()
} catch {
DebugLog.write("reconcileClosedSession closePane failed session=\(session) error=\(error)")
}
case .closeWorkspace(let workspaceID):
_ = closeWorkspaceRuntime(id: workspaceID)
closeTerminalWindowIfNoWorkspace()
case .none:
break
}
}

private func closeTerminalWindowIfNoWorkspace() {
guard workspaceRuntimes.isEmpty else { return }
guard let window = NSApp.windows.first(where: { !($0 is NSPanel) && $0 !== settingsWindow }) else { return }
window.performClose(nil)
}

private func performNotificationActions(_ actions: [TerminalNotificationAction], session: TerminalSessionID) {
for action in actions {
switch action {
Expand Down
50 changes: 50 additions & 0 deletions Tests/ProGhosttyAppTests/AppModelSessionClosedActionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation
import ProGhosttyCore
import Testing

@testable import ProGhosttyApp

@Suite("AppModel session closed action")
@MainActor
struct AppModelSessionClosedActionTests {
@Test func choosesClosePaneWhenWorkspaceStillHasSiblingPanes() {
let targetSession = TerminalSessionID()
let targetPane = TerminalPane(sessionId: targetSession, title: "zsh")
let siblingPane = TerminalPane(sessionId: TerminalSessionID(), title: "zsh")
let runtime = runtime(
root: .split(SplitPane(axis: .horizontal, first: .leaf(targetPane), second: .leaf(siblingPane)))
)

#expect(
AppModel.sessionClosedAction(for: targetSession, in: [runtime])
== .closePane(workspaceID: runtime.id, paneID: targetPane.paneId)
)
}

@Test func choosesCloseWorkspaceWhenClosedSessionIsLastPane() {
let targetSession = TerminalSessionID()
let runtime = runtime(root: .leaf(TerminalPane(sessionId: targetSession, title: "zsh")))

#expect(
AppModel.sessionClosedAction(for: targetSession, in: [runtime])
== .closeWorkspace(workspaceID: runtime.id)
)
}

@Test func returnsNoneWhenSessionDoesNotBelongToAnyRuntime() {
let runtime = runtime(root: .leaf(TerminalPane(sessionId: TerminalSessionID(), title: "zsh")))

#expect(
AppModel.sessionClosedAction(for: TerminalSessionID(), in: [runtime])
== .none
)
}

private func runtime(root: PaneNode) -> AppModel.WorkspaceRuntime {
AppModel.WorkspaceRuntime(
layout: WorkspaceLayout(title: "Workspace", root: root),
workspace: nil,
cwdBySession: [:]
)
}
}
Loading