bd44b5aa08
Test / build-libghostty-vt (x86_64-windows-gnu) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-ios) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt-macos (x86_64-macos) (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-dist (push) Has been cancelled
Test / GTK x11=true wayland=true (push) Has been cancelled
Test / Build -Dsimd=false (push) Has been cancelled
Test / Build -Dsimd=true (push) Has been cancelled
Test / Build -Dsentry=false (push) Has been cancelled
Test / Build -Dsentry=true (push) Has been cancelled
Test / zig-fmt (push) Has been cancelled
Test / GitHub Actions Pins (push) Has been cancelled
Test / prettier (push) Has been cancelled
Test / swiftlint (push) Has been cancelled
Test / alejandra (push) Has been cancelled
Test / typos (push) Has been cancelled
Nix / Required Checks: Nix (push) Has been cancelled
Nix / check-zig-cache-hash (push) Has been cancelled
Test / skip (push) Has been cancelled
Test / Required Checks: Test (push) Has been cancelled
Test / build-bench (push) Has been cancelled
Test / list-examples (push) Has been cancelled
Test / Example ${{ matrix.dir }} (Windows) (push) Has been cancelled
Test / build-cmake (push) Has been cancelled
Test / test-lib-vt-pkgconfig (push) Has been cancelled
Test / build-flatpak (push) Has been cancelled
Test / build-snap (push) Has been cancelled
Test / build-libghostty-vt (aarch64-linux) (push) Has been cancelled
Test / build-libghostty-vt (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt (wasm32-freestanding) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux-musl) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-macos) (push) Has been cancelled
Test / build-libghostty-vt-android (aarch64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-android (arm-linux-androideabi) (push) Has been cancelled
Test / build-libghostty-vt-android (x86_64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-windows (push) Has been cancelled
Test / build-libghostty-windows-gnu (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-linux-libghostty (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-dist-lib-vt (push) Has been cancelled
Test / trigger-snap (push) Has been cancelled
Test / trigger-flatpak (push) Has been cancelled
Test / build-macos (push) Has been cancelled
Test / build-macos-freetype (push) Has been cancelled
Test / test (push) Has been cancelled
Test / test-lib-vt (push) Has been cancelled
Test / GTK x11=false wayland=false (push) Has been cancelled
Test / GTK x11=true wayland=false (push) Has been cancelled
Test / GTK x11=false wayland=true (push) Has been cancelled
Test / test-macos (push) Has been cancelled
Test / test-windows (push) Has been cancelled
Test / Build -Di18n=false (push) Has been cancelled
Test / Build -Di18n=true (push) Has been cancelled
Test / Build test/fuzz-libghostty (push) Has been cancelled
Test / shellcheck (push) Has been cancelled
Test / translations (push) Has been cancelled
Test / blueprint-compiler (push) Has been cancelled
Test / Test pkg/wuffs (push) Has been cancelled
Test / Test build on Debian 13 (push) Has been cancelled
Test / valgrind (push) Has been cancelled
Test / Example ${{ matrix.dir }} (push) Has been cancelled
121 lines
4.7 KiB
Swift
121 lines
4.7 KiB
Swift
import Foundation
|
|
import Cocoa
|
|
|
|
/// Manages cached window state per screen for the quick terminal.
|
|
///
|
|
/// This cache tracks the last closed window frame for each screen, allowing the quick terminal
|
|
/// to restore to its previous size and position when reopened. It uses stable display UUIDs
|
|
/// to survive NSScreen garbage collection and automatically prunes stale entries.
|
|
class QuickTerminalScreenStateCache {
|
|
typealias Entries = [UUID: DisplayEntry]
|
|
|
|
/// The maximum number of saved screen states we retain. This is to avoid some kind of
|
|
/// pathological memory growth in case we get our screen state serializing wrong. I don't
|
|
/// know anyone with more than 10 screens, so let's just arbitrarily go with that.
|
|
private static let maxSavedScreens = 10
|
|
|
|
/// Time-to-live for screen entries that are no longer present (14 days).
|
|
private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60
|
|
|
|
/// Keyed by display UUID to survive NSScreen garbage collection.
|
|
private(set) var stateByDisplay: Entries = [:]
|
|
|
|
init(stateByDisplay: Entries = [:]) {
|
|
self.stateByDisplay = stateByDisplay
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(onScreensChanged(_:)),
|
|
name: NSApplication.didChangeScreenParametersNotification,
|
|
object: nil)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
/// Save the window frame for a screen.
|
|
func save(frame: NSRect, for screen: NSScreen) {
|
|
guard let key = screen.displayUUID else { return }
|
|
let entry = DisplayEntry(
|
|
frame: frame,
|
|
screenSize: screen.frame.size,
|
|
scale: screen.backingScaleFactor,
|
|
lastSeen: Date()
|
|
)
|
|
stateByDisplay[key] = entry
|
|
pruneCapacity()
|
|
}
|
|
|
|
/// Retrieve the last closed frame for a screen, if valid.
|
|
func frame(for screen: NSScreen) -> NSRect? {
|
|
guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil }
|
|
|
|
// Drop on dimension/scale change that makes the entry invalid
|
|
if !entry.isValid(for: screen) {
|
|
stateByDisplay.removeValue(forKey: key)
|
|
return nil
|
|
}
|
|
|
|
entry.lastSeen = Date()
|
|
stateByDisplay[key] = entry
|
|
return entry.frame
|
|
}
|
|
|
|
@objc private func onScreensChanged(_ note: Notification) {
|
|
let screens = NSScreen.screens
|
|
let now = Date()
|
|
let currentIDs = Set(screens.compactMap { $0.displayUUID })
|
|
|
|
for screen in screens {
|
|
guard let key = screen.displayUUID else { continue }
|
|
if var entry = stateByDisplay[key] {
|
|
// Drop on dimension/scale change that makes the entry invalid. The
|
|
// saved frame is only meaningful for the exact geometry it was
|
|
// captured on, so a present screen that no longer matches is dropped.
|
|
if !entry.isValid(for: screen) {
|
|
stateByDisplay.removeValue(forKey: key)
|
|
} else {
|
|
entry.lastSeen = now
|
|
stateByDisplay[key] = entry
|
|
}
|
|
}
|
|
}
|
|
|
|
// TTL prune for non-present screens
|
|
stateByDisplay = stateByDisplay.filter { key, entry in
|
|
currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL
|
|
}
|
|
|
|
pruneCapacity()
|
|
}
|
|
|
|
private func pruneCapacity() {
|
|
guard stateByDisplay.count > Self.maxSavedScreens else { return }
|
|
let toRemove = stateByDisplay
|
|
.sorted { $0.value.lastSeen < $1.value.lastSeen }
|
|
.prefix(stateByDisplay.count - Self.maxSavedScreens)
|
|
for (key, _) in toRemove {
|
|
stateByDisplay.removeValue(forKey: key)
|
|
}
|
|
}
|
|
|
|
struct DisplayEntry: Codable {
|
|
var frame: NSRect
|
|
var screenSize: CGSize
|
|
var scale: CGFloat
|
|
var lastSeen: Date
|
|
|
|
/// Returns true if this entry is still valid for the given screen.
|
|
///
|
|
/// An entry is only valid for the exact screen geometry it was captured on: both the
|
|
/// backing scale factor and the frame size must match. A saved frame is meaningless once
|
|
/// the display's resolution changes, which commonly happens when an external display is
|
|
/// disconnected and later reconnected at a different resolution (e.g. after travel). In
|
|
/// that case we drop the entry and fall back to the configured `quick-terminal-size`,
|
|
/// rather than restoring a stale frame that no longer fills the screen as expected.
|
|
func isValid(for screen: NSScreen) -> Bool {
|
|
scale == screen.backingScaleFactor && screenSize == screen.frame.size
|
|
}
|
|
}
|
|
}
|