mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +00:00
Update stuff
This commit is contained in:
parent
e59d35d89a
commit
1441f83388
6 changed files with 456 additions and 30 deletions
|
|
@ -21,11 +21,53 @@ import SwiftUI
|
|||
import SwiftTerm
|
||||
import Defaults
|
||||
|
||||
// MARK: - Stable Container
|
||||
|
||||
/// Container NSView that shields its terminal child from zero-sized frames.
|
||||
///
|
||||
/// When the notch closes, SwiftUI removes the `NSViewRepresentable` from the
|
||||
/// hierarchy — but later re-adds the same `containerView` on reopen. During
|
||||
/// that insertion, SwiftUI momentarily sets the frame to `.zero`. Without
|
||||
/// protection, AppKit's autoresizing mask propagates the zero frame to the
|
||||
/// terminal child, causing SwiftTerm's `processSizeChange` to resize the
|
||||
/// emulator to 2 cols × 1 row (the enforced minimum), which destroys the
|
||||
/// scrollback buffer.
|
||||
///
|
||||
/// We bypass `super.resizeSubviews(withOldSize:)` entirely and manually set
|
||||
/// children to fill the container's bounds. This avoids the autoresizing
|
||||
/// calculation which produces corrupt geometry when `oldSize` was `.zero`
|
||||
/// (recorded during the transient removal) but the child kept its previous
|
||||
/// large frame.
|
||||
final class StableTerminalContainerView: NSView {
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
let size = bounds.size
|
||||
// Block degenerate sizes — the terminal would collapse to 2×1
|
||||
guard size.width >= 10, size.height >= 10 else { return }
|
||||
// Manually fill children to bounds — bypasses autoresizing math
|
||||
// that breaks when oldSize was zero but child kept its old frame.
|
||||
for child in subviews {
|
||||
child.frame = bounds
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
guard window != nil else { return }
|
||||
// When re-inserted into a window (notch reopened), force a full
|
||||
// redraw so SwiftTerm re-renders the terminal buffer contents.
|
||||
for child in subviews {
|
||||
child.needsDisplay = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal Manager
|
||||
|
||||
/// Manages the Guake-style dropdown terminal session lifecycle.
|
||||
/// The terminal is lazily created when the user first switches to the terminal tab,
|
||||
/// and the process is kept alive across notch open/close cycles.
|
||||
///
|
||||
/// Uses a stable `containerView` (NSView) as the host so that SwiftUI's
|
||||
/// Uses a stable `StableTerminalContainerView` as the host so that SwiftUI's
|
||||
/// `NSViewRepresentable` lifecycle (make/update/dismantle) never tears down
|
||||
/// the actual terminal. The `LocalProcessTerminalView` is added as a subview
|
||||
/// of the container and survives notch close/open cycles.
|
||||
|
|
@ -39,12 +81,11 @@ class TerminalManager: ObservableObject {
|
|||
/// The current terminal title reported by the shell.
|
||||
@Published var terminalTitle: String = "Terminal"
|
||||
|
||||
/// Bumped on restart so the `NSViewRepresentable` can recreate via `.id()`.
|
||||
@Published var sessionGeneration: Int = 0
|
||||
|
||||
/// Stable container returned to SwiftUI — never deallocated.
|
||||
let containerView: NSView = {
|
||||
let v = NSView(frame: .zero)
|
||||
/// Uses `StableTerminalContainerView` to prevent zero-frame transients
|
||||
/// from destroying the scrollback buffer.
|
||||
let containerView: StableTerminalContainerView = {
|
||||
let v = StableTerminalContainerView(frame: .zero)
|
||||
v.autoresizingMask = [.width, .height]
|
||||
v.wantsLayer = true
|
||||
return v
|
||||
|
|
@ -63,24 +104,25 @@ class TerminalManager: ObservableObject {
|
|||
if let existing = terminalView, existing.superview === containerView {
|
||||
// Already mounted — just re-wire the delegate in case the coordinator changed.
|
||||
existing.processDelegate = delegate
|
||||
// Force a redraw — the view may have been off-screen (notch closed)
|
||||
// and needs to re-render the terminal buffer.
|
||||
existing.needsDisplay = true
|
||||
return
|
||||
}
|
||||
|
||||
let view = LocalProcessTerminalView(frame: containerView.bounds)
|
||||
// Use the container's current bounds if valid, otherwise a reasonable
|
||||
// default. SwiftTerm's init calls setupOptions() which reads the
|
||||
// frame — a zero frame creates a 2×1 terminal that is corrected
|
||||
// once the container gets its proper layout size.
|
||||
let initialFrame = containerView.bounds.size.width >= 10
|
||||
? containerView.bounds
|
||||
: CGRect(x: 0, y: 0, width: 400, height: 300)
|
||||
|
||||
let view = LocalProcessTerminalView(frame: initialFrame)
|
||||
view.autoresizingMask = [.width, .height]
|
||||
|
||||
// Apply font size from settings
|
||||
let fontSize = CGFloat(Defaults[.terminalFontSize])
|
||||
if let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) as NSFont? {
|
||||
view.font = font
|
||||
}
|
||||
|
||||
// Apply opacity
|
||||
view.layer?.opacity = Float(Defaults[.terminalOpacity])
|
||||
|
||||
// Configure the dark terminal appearance
|
||||
view.nativeBackgroundColor = .black
|
||||
view.nativeForegroundColor = .white
|
||||
// Apply all settings from Defaults
|
||||
applyAllSettings(to: view)
|
||||
|
||||
view.processDelegate = delegate
|
||||
|
||||
|
|
@ -88,6 +130,12 @@ class TerminalManager: ObservableObject {
|
|||
containerView.subviews.forEach { $0.removeFromSuperview() }
|
||||
containerView.addSubview(view)
|
||||
terminalView = view
|
||||
|
||||
// If the container already has a valid size, snap the child to it.
|
||||
let containerSize = containerView.bounds.size
|
||||
if containerSize.width >= 10, containerSize.height >= 10 {
|
||||
view.frame = containerView.bounds
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the shell process if not already running.
|
||||
|
|
@ -112,6 +160,12 @@ class TerminalManager: ObservableObject {
|
|||
}
|
||||
|
||||
/// Restarts the shell by tearing down the old terminal and creating a fresh one.
|
||||
///
|
||||
/// Instead of bumping a generation counter (which forced SwiftUI to destroy
|
||||
/// and recreate the `NSViewRepresentable` — recycling the same `containerView`
|
||||
/// across identities which broke layout), we simply nil out `terminalView`
|
||||
/// and change `@Published` state. SwiftUI's `updateNSView` will fire,
|
||||
/// see the nil terminal, and call `ensureTerminalView` to mount a new one.
|
||||
func restartShell() {
|
||||
// Terminate the running process gracefully
|
||||
terminalView?.terminate()
|
||||
|
|
@ -120,8 +174,6 @@ class TerminalManager: ObservableObject {
|
|||
terminalView = nil
|
||||
isProcessRunning = false
|
||||
terminalTitle = "Terminal"
|
||||
// Bump generation so the SwiftUI representable re-mounts
|
||||
sessionGeneration += 1
|
||||
}
|
||||
|
||||
/// Updates the terminal title from the shell escape sequence.
|
||||
|
|
@ -129,6 +181,42 @@ class TerminalManager: ObservableObject {
|
|||
terminalTitle = title
|
||||
}
|
||||
|
||||
// MARK: - Settings Application
|
||||
|
||||
/// Applies all persisted settings to a terminal view at creation time.
|
||||
private func applyAllSettings(to view: LocalProcessTerminalView) {
|
||||
// Font
|
||||
let fontSize = CGFloat(Defaults[.terminalFontSize])
|
||||
if let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) as NSFont? {
|
||||
view.font = font
|
||||
}
|
||||
|
||||
// Opacity
|
||||
view.layer?.opacity = Float(Defaults[.terminalOpacity])
|
||||
|
||||
// Colors
|
||||
view.nativeBackgroundColor = NSColor(Defaults[.terminalBackgroundColor])
|
||||
view.nativeForegroundColor = NSColor(Defaults[.terminalForegroundColor])
|
||||
view.caretColor = NSColor(Defaults[.terminalCursorColor])
|
||||
|
||||
// Cursor style
|
||||
let cursorStyle = TerminalCursorStyleOption(rawValue: Defaults[.terminalCursorStyle])
|
||||
?? .blinkBlock
|
||||
view.getTerminal().options.cursorStyle = cursorStyle.swiftTermStyle
|
||||
|
||||
// Scrollback
|
||||
let scrollback = Defaults[.terminalScrollbackLines]
|
||||
view.getTerminal().buffer.changeHistorySize(scrollback)
|
||||
view.getTerminal().options.scrollback = scrollback
|
||||
|
||||
// Input behavior
|
||||
view.optionAsMetaKey = Defaults[.terminalOptionAsMeta]
|
||||
view.allowMouseReporting = Defaults[.terminalMouseReporting]
|
||||
|
||||
// Rendering
|
||||
view.useBrightColors = Defaults[.terminalBoldAsBright]
|
||||
}
|
||||
|
||||
/// Updates font size on the live terminal view.
|
||||
func applyFontSize(_ size: Double) {
|
||||
guard let view = terminalView else { return }
|
||||
|
|
@ -143,6 +231,56 @@ class TerminalManager: ObservableObject {
|
|||
view.layer?.opacity = Float(opacity)
|
||||
}
|
||||
|
||||
/// Updates cursor style on the live terminal view.
|
||||
func applyCursorStyle(_ style: TerminalCursorStyleOption) {
|
||||
guard let view = terminalView else { return }
|
||||
view.getTerminal().options.cursorStyle = style.swiftTermStyle
|
||||
view.setNeedsDisplay(view.bounds)
|
||||
}
|
||||
|
||||
/// Updates scrollback buffer size on the live terminal view.
|
||||
func applyScrollback(_ lines: Int) {
|
||||
guard let view = terminalView else { return }
|
||||
view.getTerminal().buffer.changeHistorySize(lines)
|
||||
view.getTerminal().options.scrollback = lines
|
||||
}
|
||||
|
||||
/// Updates option-as-meta on the live terminal view.
|
||||
func applyOptionAsMeta(_ enabled: Bool) {
|
||||
guard let view = terminalView else { return }
|
||||
view.optionAsMetaKey = enabled
|
||||
}
|
||||
|
||||
/// Updates mouse reporting on the live terminal view.
|
||||
func applyMouseReporting(_ enabled: Bool) {
|
||||
guard let view = terminalView else { return }
|
||||
view.allowMouseReporting = enabled
|
||||
}
|
||||
|
||||
/// Updates bold-as-bright on the live terminal view.
|
||||
func applyBoldAsBright(_ enabled: Bool) {
|
||||
guard let view = terminalView else { return }
|
||||
view.useBrightColors = enabled
|
||||
}
|
||||
|
||||
/// Updates background color on the live terminal view.
|
||||
func applyBackgroundColor(_ color: SwiftUI.Color) {
|
||||
guard let view = terminalView else { return }
|
||||
view.nativeBackgroundColor = NSColor(color)
|
||||
}
|
||||
|
||||
/// Updates foreground color on the live terminal view.
|
||||
func applyForegroundColor(_ color: SwiftUI.Color) {
|
||||
guard let view = terminalView else { return }
|
||||
view.nativeForegroundColor = NSColor(color)
|
||||
}
|
||||
|
||||
/// Updates cursor color on the live terminal view.
|
||||
func applyCursorColor(_ color: SwiftUI.Color) {
|
||||
guard let view = terminalView else { return }
|
||||
view.caretColor = NSColor(color)
|
||||
}
|
||||
|
||||
// MARK: - Environment
|
||||
|
||||
/// Builds the environment for the child shell process.
|
||||
|
|
@ -155,3 +293,37 @@ class TerminalManager: ObservableObject {
|
|||
return env.map { "\($0.key)=\($0.value)" }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cursor Style Bridge
|
||||
|
||||
/// Codable-friendly cursor style enum that bridges to SwiftTerm's `CursorStyle`.
|
||||
enum TerminalCursorStyleOption: String, CaseIterable, Defaults.Serializable {
|
||||
case blinkBlock = "blinkBlock"
|
||||
case steadyBlock = "steadyBlock"
|
||||
case blinkUnderline = "blinkUnderline"
|
||||
case steadyUnderline = "steadyUnderline"
|
||||
case blinkBar = "blinkBar"
|
||||
case steadyBar = "steadyBar"
|
||||
|
||||
var swiftTermStyle: CursorStyle {
|
||||
switch self {
|
||||
case .blinkBlock: return .blinkBlock
|
||||
case .steadyBlock: return .steadyBlock
|
||||
case .blinkUnderline: return .blinkUnderline
|
||||
case .steadyUnderline: return .steadyUnderline
|
||||
case .blinkBar: return .blinkBar
|
||||
case .steadyBar: return .steadyBar
|
||||
}
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .blinkBlock: return "Block (blinking)"
|
||||
case .steadyBlock: return "Block (steady)"
|
||||
case .blinkUnderline: return "Underline (blinking)"
|
||||
case .steadyUnderline: return "Underline (steady)"
|
||||
case .blinkBar: return "Bar (blinking)"
|
||||
case .steadyBar: return "Bar (steady)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue