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
21 changes: 21 additions & 0 deletions Sources/termio/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: work)
}

func windowDidDeminiaturize(_ notification: Notification) {
// Restoring from the Dock can bring the window back under its content minimum, and leaves a
// tickless terminal surface (and a mid-load WKWebView in the Issues pane) unpainted — the
// "minimize while a GitHub issue loads → tiny + black" report. Clamp a shrunken frame back
// up (setFrame ignores contentMinSize), force a relayout, and nudge the surface to repaint.
guard let window else { return }
let minFrameHeight = window.frameRect(forContentRect:
NSRect(origin: .zero, size: window.contentMinSize)).height
if window.frame.height < minFrameHeight {
var frame = window.frame
frame.origin.y -= (minFrameHeight - frame.height) // grow from the top, keep it fixed
frame.size.height = minFrameHeight
window.setFrame(frame, display: true)
}
window.contentView?.layoutSubtreeIfNeeded()
store.repaintSelectedSurface()
}

/// Lets the right inspector grow with the window: its max width is the golden ratio (0.618)
/// of the current content width, floored at 420pt so it never shrinks below the old fixed cap
/// on narrow windows. A static `maximumThickness` capped the inspector at 420pt regardless of
Expand Down Expand Up @@ -998,6 +1016,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} else {
maximizedDetailHost?.removeFromSuperview()
maximizedDetailHost = nil
// Removing the host re-exposes the terminal, but a tickless surface won't repaint
// itself — nudge it so a switch out of a maximized detail can't leave a blank.
store.repaintSelectedSurface()
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/termio/Git/GitModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SwiftUI
/// Which inspector pane the trailing column is showing — the file tree, the file
/// search, the git changes list, the issue tracker, or the session Info pane.
/// Drives the segmented switch at the top of `FileBrowserView`.
enum InspectorTab: Hashable, Sendable {
enum InspectorTab: String, Hashable, Sendable, Codable {
case files, search, changes, issues, info
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/termio/Issues/IssuesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct IssuesView: View {
// editor and diff), driven by `store.openIssueDetail`, not pushed in here.
listPane
.task(id: repoRoot) {
store.issuesModel = model
store.registerIssuesModel(model)
await model.start()
}
// Selection IS the open gesture; route it to the center overlay. Follow the
Expand Down Expand Up @@ -624,6 +624,11 @@ struct IssueDetailView: View {
),
background: settings.terminalBackgroundColor
)
// Fill like the error/progress branches below: without this, SwiftUI can size the
// representable from the WKWebView's intrinsic (near-zero while it's mid-load), which
// collapses the detail to a sliver — and the empty area paints the window background
// (reads as a black window, most visibly across a minimize/restore relayout).
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = model.detailError {
ContentUnavailableView("Couldn’t Load", huge: .github, description: Text(error))
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand Down
10 changes: 7 additions & 3 deletions Sources/termio/Sidebar/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1119,9 +1119,13 @@ private struct SessionRow: View {
// click-to-select doesn't commit for seconds (the drag machinery swallows
// the mouseDown), which shipped as v0.19.0's dead sidebar clicks.
.simultaneousGesture(TapGesture().onEnded {
// Tapping a session always returns to its terminal — close the file
// editor even when this row is already selected (no change to react to).
store.openFileURL = nil
// Re-tapping the row you're already on returns to its terminal, closing its
// open file editor. Tapping a *different* session must NOT clear here: the
// selection didSet captures the outgoing session's inspector layout first
// (issue #160), so clearing pre-capture would drop its open file.
if store.selectedSessionID == session.id {
store.openFileURL = nil
}
store.selectedSessionID = session.id
// Re-tapping the row you're already on still clears a resting
// done/attention dot (the selection didSet only reacts to a change).
Expand Down
11 changes: 4 additions & 7 deletions Sources/termio/Terminal/TerminalPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,10 @@ struct TerminalPane: View {
if let id, !activated.contains(id) {
activated.append(id)
}
// Switching sessions returns to the terminal: dismiss any open file editor so the
// newly selected session's surface is what's shown (the overlay's `.onDisappear`
// flushes any pending auto-save first). The diff overlay is dismissed for the same reason.
store.openFileURL = nil
store.openDiff = nil
store.openTrace = nil
store.openIssueDetail = nil
// The inspector layout now follows the session: `TermioStore.selectedSessionID`
// saves the outgoing session's open detail and restores the incoming one's
// (issue #160), so we no longer clear the overlays here. The editor overlay's
// `.onDisappear` still flushes any pending auto-save when a restore replaces it.
requestSelectedTerminalFocus(reason: .selectionChanged)
}
// The toolbar's close button posts this; tear the overlay down the same way the overlay's
Expand Down
15 changes: 15 additions & 0 deletions Sources/termio/TermioStore/StateFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ struct StateFile {
/// The split groups (see `TermioStore.splitGroups`). Optional so state
/// files written before groups existed still decode.
var splitGroups: [SplitNode]?
/// Each session's inspector layout, keyed by session `id.uuidString`. Only the
/// durable subset is written — the tab and the open *file* — since a diff / PR /
/// trace is a snapshot of data that gets re-fetched (see `TermioStore.InspectorState`).
/// Optional so older state files still decode.
var inspectorLayouts: [String: InspectorLayout]?
}

/// The persisted slice of a session's inspector layout: which tab, and the file it
/// had open (validated for existence on restore, since the file may have been deleted
/// or its worktree removed while the app was closed).
struct InspectorLayout: Codable {
var tab: InspectorTab
var filePath: String?
var fileLine: Int?
var fileReadOnly: Bool?
}

let url = AppChannel.supportDirectory
Expand Down
10 changes: 10 additions & 0 deletions Sources/termio/TermioStore/TermioStore+TerminalSurface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,16 @@ extension TermioStore {
/// than waiting for the next PTY-output wakeup. A fixed short pump is enough: a
/// color/font reconfigure needs no reply-gated handshake the way a cold spawn
/// does, so a handful of frames flush the new look.
/// Repaints the selected session's surface for a brief window. Used when a full-window
/// overlay — the maximized inspector detail — is torn down and re-exposes the terminal:
/// this embedding has no continuous tick (see `warmUpRendering`), so an uncovered surface
/// would otherwise sit on its stale last frame (a blank on a fresh session) until the next
/// PTY-output or focus event. This is issue #160's fullscreen blank-on-tab-switch.
func repaintSelectedSurface() {
guard let id = selectedSessionID, let state = surfaces[id] else { return }
pumpRendering(state, duration: 0.25)
}

private func pumpRendering(_ state: TerminalViewState, duration: TimeInterval) {
let started = Date()
let timer = Timer(timeInterval: 1.0 / 60.0, repeats: true) { [weak state] timer in
Expand Down
Loading