mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #436 from Ebullioscopic/bugfixes/terminal
Bugfixes/terminal
This commit is contained in:
commit
a6d5251842
9 changed files with 279 additions and 28 deletions
|
|
@ -105,8 +105,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-argument-parser",
|
"location" : "https://github.com/apple/swift-argument-parser",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615",
|
"revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b",
|
||||||
"version" : "1.7.0"
|
"version" : "1.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -132,8 +132,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/migueldeicaza/SwiftTerm",
|
"location" : "https://github.com/migueldeicaza/SwiftTerm",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b1262db5b6bea699a8260a8c66999436c508ca56",
|
"revision" : "8e7a1e154f470e19c709a00a8768df348ba5fc43",
|
||||||
"version" : "1.11.2"
|
"version" : "1.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,7 @@ struct ContentView: View {
|
||||||
// When sneak peek finishes, check if user is still hovering and open notch if needed
|
// When sneak peek finishes, check if user is still hovering and open notch if needed
|
||||||
if !sneakPeekShowing {
|
if !sneakPeekShowing {
|
||||||
runAfter(0.2) {
|
runAfter(0.2) {
|
||||||
if isHovering && vm.notchState == .closed {
|
if isHovering && vm.notchState == .closed && !coordinator.isHoverOpenSuppressed {
|
||||||
openNotch()
|
openNotch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1799,6 +1799,7 @@ struct ContentView: View {
|
||||||
guard let vm, let lockScreenManager else { return }
|
guard let vm, let lockScreenManager else { return }
|
||||||
guard !lockScreenManager.isLocked else { return }
|
guard !lockScreenManager.isLocked else { return }
|
||||||
guard vm.notchState == .closed else { return }
|
guard vm.notchState == .closed else { return }
|
||||||
|
guard !self.coordinator.isHoverOpenSuppressed else { return }
|
||||||
guard self.isHovering else { return }
|
guard self.isHovering else { return }
|
||||||
guard !self.handleClosedMusicWaveformTapIfNeeded() else { return }
|
guard !self.handleClosedMusicWaveformTapIfNeeded() else { return }
|
||||||
if Defaults[.enableHaptics] {
|
if Defaults[.enableHaptics] {
|
||||||
|
|
@ -1850,6 +1851,10 @@ struct ContentView: View {
|
||||||
stickyTerminalClickMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak vm] _ in
|
stickyTerminalClickMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak vm] _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let vm, vm.notchState == .open else { return }
|
guard let vm, vm.notchState == .open else { return }
|
||||||
|
let clickLocation = NSEvent.mouseLocation
|
||||||
|
if self.isPointInsideNotchWindow(clickLocation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1902,7 +1907,8 @@ struct ContentView: View {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard self.vm.notchState == .closed,
|
guard self.vm.notchState == .closed,
|
||||||
self.isHovering,
|
self.isHovering,
|
||||||
!self.isSneakPeekVisibleOnCurrentScreen else { return }
|
!self.isSneakPeekVisibleOnCurrentScreen,
|
||||||
|
!self.coordinator.isHoverOpenSuppressed else { return }
|
||||||
|
|
||||||
if shouldFocusTimerTab {
|
if shouldFocusTimerTab {
|
||||||
withAnimation(.smooth) {
|
withAnimation(.smooth) {
|
||||||
|
|
@ -1927,14 +1933,27 @@ struct ContentView: View {
|
||||||
} else if self.vm.notchState == .open
|
} else if self.vm.notchState == .open
|
||||||
&& Defaults[.terminalStickyMode]
|
&& Defaults[.terminalStickyMode]
|
||||||
&& self.coordinator.currentView == .terminal {
|
&& self.coordinator.currentView == .terminal {
|
||||||
// Terminal sticky mode kept the notch open — install a
|
// Re-sync monitor state through one code path to avoid
|
||||||
// global click monitor so a click outside closes it.
|
// monitor lifecycle races between hover and state updates.
|
||||||
self.installStickyTerminalClickMonitor()
|
self.syncStickyTerminalOutsideClickMonitor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isPointInsideNotchWindow(_ point: CGPoint) -> Bool {
|
||||||
|
if let appDelegate = AppDelegate.shared {
|
||||||
|
if Defaults[.showOnAllDisplays] {
|
||||||
|
return appDelegate.windows.values.contains(where: { $0.frame.contains(point) })
|
||||||
|
}
|
||||||
|
if let window = appDelegate.window {
|
||||||
|
return window.frame.contains(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSApp.windows.contains(where: { $0.frame.contains(point) })
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to check if any popovers are active
|
// Helper function to check if any popovers are active
|
||||||
private func hasAnyActivePopovers() -> Bool {
|
private func hasAnyActivePopovers() -> Bool {
|
||||||
|
|
|
||||||
|
|
@ -1046,13 +1046,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
closeNotchWorkItem = nil
|
closeNotchWorkItem = nil
|
||||||
vm.open()
|
vm.open()
|
||||||
coordinator.currentView = .terminal
|
coordinator.currentView = .terminal
|
||||||
|
TerminalManager.shared.refreshTerminalAppearanceIfNeeded()
|
||||||
|
TerminalManager.shared.focusTerminalIfPossible()
|
||||||
|
TerminalManager.shared.refreshTerminalAppearanceIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
if coordinator.currentView == .terminal {
|
if coordinator.currentView == .terminal {
|
||||||
|
coordinator.suppressHoverOpen()
|
||||||
|
TerminalManager.shared.resignTerminalFirstResponderIfNeeded()
|
||||||
vm.close()
|
vm.close()
|
||||||
} else {
|
} else {
|
||||||
closeNotchWorkItem?.cancel()
|
closeNotchWorkItem?.cancel()
|
||||||
closeNotchWorkItem = nil
|
closeNotchWorkItem = nil
|
||||||
coordinator.currentView = .terminal
|
coordinator.currentView = .terminal
|
||||||
|
TerminalManager.shared.refreshTerminalAppearanceIfNeeded()
|
||||||
|
TerminalManager.shared.focusTerminalIfPossible()
|
||||||
|
TerminalManager.shared.refreshTerminalAppearanceIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ struct ExpandedItem {
|
||||||
class DynamicIslandViewCoordinator: ObservableObject {
|
class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
static let shared = DynamicIslandViewCoordinator()
|
static let shared = DynamicIslandViewCoordinator()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var hoverOpenSuppressedUntil: Date = .distantPast
|
||||||
|
|
||||||
private static let tabOrder: [NotchViews] = [.home, .shelf, .timer, .stats, .colorPicker, .notes, .clipboard, .terminal, .extensionExperience]
|
private static let tabOrder: [NotchViews] = [.home, .shelf, .timer, .stats, .colorPicker, .notes, .clipboard, .terminal, .extensionExperience]
|
||||||
|
|
||||||
|
|
@ -246,6 +247,14 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
enforceMinimumNotchWidth()
|
enforceMinimumNotchWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isHoverOpenSuppressed: Bool {
|
||||||
|
Date() < hoverOpenSuppressedUntil
|
||||||
|
}
|
||||||
|
|
||||||
|
func suppressHoverOpen(for duration: TimeInterval = 0.35) {
|
||||||
|
hoverOpenSuppressedUntil = Date().addingTimeInterval(max(0, duration))
|
||||||
|
}
|
||||||
|
|
||||||
private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) {
|
private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) {
|
||||||
guard oldValue != newValue else { return }
|
guard oldValue != newValue else { return }
|
||||||
if newValue == .stats && Defaults[.enableStatsFeature] {
|
if newValue == .stats && Defaults[.enableStatsFeature] {
|
||||||
|
|
|
||||||
|
|
@ -5622,6 +5622,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Amazon Music" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Animation" : {
|
"Animation" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|
@ -41026,6 +41029,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Terminal opacity only affects the terminal backdrop; text stays fully opaque. Blur uses the system material behind the window." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Text" : {
|
"Text" : {
|
||||||
"comment" : "Name of a clipboard item type that contains plain text.",
|
"comment" : "Name of a clipboard item type that contains plain text.",
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,39 @@ struct NotchTerminalView: View {
|
||||||
@ObservedObject var terminalManager = TerminalManager.shared
|
@ObservedObject var terminalManager = TerminalManager.shared
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@Default(.enableTerminalFeature) var enableTerminalFeature
|
@Default(.enableTerminalFeature) var enableTerminalFeature
|
||||||
|
@Default(.cornerRadiusScaling) var cornerRadiusScaling
|
||||||
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
@State private var suppressionToken = UUID()
|
@State private var suppressionToken = UUID()
|
||||||
@State private var isSuppressing = false
|
@State private var isSuppressing = false
|
||||||
|
|
||||||
|
/// Top corners stay mild; bottom follows shell − padding (see `notchTerminalBottomCornerRadii`).
|
||||||
|
private static let terminalClipTopCornerRadius: CGFloat = 6
|
||||||
|
|
||||||
|
private var currentScreenName: String {
|
||||||
|
vm.screen ?? DynamicIslandViewCoordinator.shared.selectedScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isDynamicIslandMode: Bool {
|
||||||
|
shouldUseDynamicIslandMode(for: currentScreenName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var terminalClipShape: UnevenRoundedRectangle {
|
||||||
|
let innerBottom = notchTerminalBottomCornerRadii(
|
||||||
|
isDynamicIslandMode: isDynamicIslandMode,
|
||||||
|
notchState: vm.notchState,
|
||||||
|
cornerRadiusScaling: cornerRadiusScaling,
|
||||||
|
enableMinimalisticUI: enableMinimalisticUI,
|
||||||
|
closedNotchHeight: vm.closedNotchSize.height
|
||||||
|
).innerBottom
|
||||||
|
return UnevenRoundedRectangle(
|
||||||
|
topLeadingRadius: Self.terminalClipTopCornerRadius,
|
||||||
|
bottomLeadingRadius: innerBottom,
|
||||||
|
bottomTrailingRadius: innerBottom,
|
||||||
|
topTrailingRadius: Self.terminalClipTopCornerRadius,
|
||||||
|
style: .continuous
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if enableTerminalFeature {
|
if enableTerminalFeature {
|
||||||
|
|
@ -121,15 +151,20 @@ struct NotchTerminalView: View {
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, notchTerminalContentEdgePadding.horizontal)
|
||||||
|
|
||||||
// Terminal content — the containerView is stable across
|
// Terminal content — the containerView is stable across
|
||||||
// notch close/open cycles; updateNSView handles restart.
|
// notch close/open cycles; updateNSView handles restart.
|
||||||
TerminalNSViewRepresentable(terminalManager: terminalManager)
|
TerminalNSViewRepresentable(terminalManager: terminalManager)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
.clipShape(terminalClipShape)
|
||||||
.padding(.horizontal, 8)
|
.padding(
|
||||||
.padding(.bottom, 8)
|
EdgeInsets(
|
||||||
.padding(.top, 4)
|
top: notchTerminalContentEdgePadding.top,
|
||||||
|
leading: notchTerminalContentEdgePadding.horizontal,
|
||||||
|
bottom: notchTerminalContentEdgePadding.bottom,
|
||||||
|
trailing: notchTerminalContentEdgePadding.horizontal
|
||||||
|
)
|
||||||
|
)
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
updateSuppression(for: hovering)
|
updateSuppression(for: hovering)
|
||||||
}
|
}
|
||||||
|
|
@ -154,6 +189,10 @@ struct NotchTerminalView: View {
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
updateSuppression(for: false)
|
updateSuppression(for: false)
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
terminalManager.refreshTerminalAppearanceIfNeeded()
|
||||||
|
terminalManager.focusTerminalIfPossible()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSuppression(for hovering: Bool) {
|
private func updateSuppression(for hovering: Bool) {
|
||||||
|
|
|
||||||
|
|
@ -897,7 +897,7 @@ struct SettingsView: View {
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Enable terminal", keywords: ["terminal", "guake", "shell"], highlightID: SettingsTab.terminal.highlightID(for: "Enable terminal")),
|
SettingsSearchEntry(tab: .terminal, title: "Enable terminal", keywords: ["terminal", "guake", "shell"], highlightID: SettingsTab.terminal.highlightID(for: "Enable terminal")),
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Shell path", keywords: ["shell", "zsh", "bash", "terminal"], highlightID: SettingsTab.terminal.highlightID(for: "Shell path")),
|
SettingsSearchEntry(tab: .terminal, title: "Shell path", keywords: ["shell", "zsh", "bash", "terminal"], highlightID: SettingsTab.terminal.highlightID(for: "Shell path")),
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Font size", keywords: ["terminal", "font", "text size"], highlightID: SettingsTab.terminal.highlightID(for: "Font size")),
|
SettingsSearchEntry(tab: .terminal, title: "Font size", keywords: ["terminal", "font", "text size"], highlightID: SettingsTab.terminal.highlightID(for: "Font size")),
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Terminal opacity", keywords: ["terminal", "opacity", "transparency"], highlightID: SettingsTab.terminal.highlightID(for: "Terminal opacity")),
|
SettingsSearchEntry(tab: .terminal, title: "Terminal opacity", keywords: ["terminal", "opacity", "transparency", "blur", "background"], highlightID: SettingsTab.terminal.highlightID(for: "Terminal opacity")),
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Maximum height", keywords: ["terminal", "height", "size"], highlightID: SettingsTab.terminal.highlightID(for: "Maximum height")),
|
SettingsSearchEntry(tab: .terminal, title: "Maximum height", keywords: ["terminal", "height", "size"], highlightID: SettingsTab.terminal.highlightID(for: "Maximum height")),
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Background color", keywords: ["terminal", "background", "color", "theme"], highlightID: SettingsTab.terminal.highlightID(for: "Background color")),
|
SettingsSearchEntry(tab: .terminal, title: "Background color", keywords: ["terminal", "background", "color", "theme"], highlightID: SettingsTab.terminal.highlightID(for: "Background color")),
|
||||||
SettingsSearchEntry(tab: .terminal, title: "Foreground color", keywords: ["terminal", "foreground", "text color", "theme"], highlightID: SettingsTab.terminal.highlightID(for: "Foreground color")),
|
SettingsSearchEntry(tab: .terminal, title: "Foreground color", keywords: ["terminal", "foreground", "text color", "theme"], highlightID: SettingsTab.terminal.highlightID(for: "Foreground color")),
|
||||||
|
|
@ -7875,6 +7875,10 @@ struct TerminalSettings: View {
|
||||||
.settingsHighlight(id: highlightID("Maximum height"))
|
.settingsHighlight(id: highlightID("Maximum height"))
|
||||||
} header: {
|
} header: {
|
||||||
Text("Appearance")
|
Text("Appearance")
|
||||||
|
} footer: {
|
||||||
|
Text("Terminal opacity only affects the terminal backdrop; text stays fully opaque. Blur uses the system material behind the window.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Colors
|
// MARK: Colors
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import AppKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftTerm
|
import SwiftTerm
|
||||||
|
|
@ -59,6 +60,9 @@ final class StableTerminalContainerView: NSView {
|
||||||
for child in subviews {
|
for child in subviews {
|
||||||
child.needsDisplay = true
|
child.needsDisplay = true
|
||||||
}
|
}
|
||||||
|
Task { @MainActor in
|
||||||
|
TerminalManager.shared.refreshTerminalAppearanceIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,12 +93,24 @@ class TerminalManager: ObservableObject {
|
||||||
let v = StableTerminalContainerView(frame: .zero)
|
let v = StableTerminalContainerView(frame: .zero)
|
||||||
v.autoresizingMask = [.width, .height]
|
v.autoresizingMask = [.width, .height]
|
||||||
v.wantsLayer = true
|
v.wantsLayer = true
|
||||||
|
v.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
return v
|
return v
|
||||||
}()
|
}()
|
||||||
|
|
||||||
/// The actual terminal view (child of `containerView`).
|
/// The actual terminal view (child of `containerView`).
|
||||||
private(set) var terminalView: LocalProcessTerminalView?
|
private(set) var terminalView: LocalProcessTerminalView?
|
||||||
|
|
||||||
|
/// Frosted blur of desktop/content behind the terminal; always below `terminalView`.
|
||||||
|
private lazy var terminalBackgroundEffectView: NSVisualEffectView = {
|
||||||
|
let v = NSVisualEffectView()
|
||||||
|
// Minimal frosted look with very light blur/vibrancy.
|
||||||
|
v.material = .underWindowBackground
|
||||||
|
v.blendingMode = .behindWindow
|
||||||
|
v.state = .active
|
||||||
|
v.autoresizingMask = [.width, .height]
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
@ -105,9 +121,8 @@ class TerminalManager: ObservableObject {
|
||||||
if let existing = terminalView, existing.superview === containerView {
|
if let existing = terminalView, existing.superview === containerView {
|
||||||
// Already mounted — just re-wire the delegate in case the coordinator changed.
|
// Already mounted — just re-wire the delegate in case the coordinator changed.
|
||||||
existing.processDelegate = delegate
|
existing.processDelegate = delegate
|
||||||
// Force a redraw — the view may have been off-screen (notch closed)
|
// Re-apply opacity/translucency after hide/show cycles (e.g. shortcut toggle).
|
||||||
// and needs to re-render the terminal buffer.
|
refreshTerminalOpacityAndTranslucency(for: existing)
|
||||||
existing.needsDisplay = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +134,9 @@ class TerminalManager: ObservableObject {
|
||||||
? containerView.bounds
|
? containerView.bounds
|
||||||
: CGRect(x: 0, y: 0, width: 400, height: 300)
|
: CGRect(x: 0, y: 0, width: 400, height: 300)
|
||||||
|
|
||||||
|
// Replace only the terminal view - keep the blur underlay between restarts.
|
||||||
|
terminalView?.removeFromSuperview()
|
||||||
|
|
||||||
let view = LocalProcessTerminalView(frame: initialFrame)
|
let view = LocalProcessTerminalView(frame: initialFrame)
|
||||||
view.autoresizingMask = [.width, .height]
|
view.autoresizingMask = [.width, .height]
|
||||||
|
|
||||||
|
|
@ -127,16 +145,27 @@ class TerminalManager: ObservableObject {
|
||||||
|
|
||||||
view.processDelegate = delegate
|
view.processDelegate = delegate
|
||||||
|
|
||||||
// Mount inside the stable container
|
if terminalBackgroundEffectView.superview == nil {
|
||||||
containerView.subviews.forEach { $0.removeFromSuperview() }
|
containerView.addSubview(terminalBackgroundEffectView)
|
||||||
|
}
|
||||||
containerView.addSubview(view)
|
containerView.addSubview(view)
|
||||||
terminalView = view
|
terminalView = view
|
||||||
|
|
||||||
// If the container already has a valid size, snap the child to it.
|
// If the container already has a valid size, snap the child to it.
|
||||||
let containerSize = containerView.bounds.size
|
let containerSize = containerView.bounds.size
|
||||||
if containerSize.width >= 10, containerSize.height >= 10 {
|
if containerSize.width >= 10, containerSize.height >= 10 {
|
||||||
|
terminalBackgroundEffectView.frame = containerView.bounds
|
||||||
view.frame = containerView.bounds
|
view.frame = containerView.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply translucency immediately and again on the next run loop tick.
|
||||||
|
// The deferred pass handles AppKit/SwiftUI mount timing where layer
|
||||||
|
// changes can otherwise be lost on first open.
|
||||||
|
refreshTerminalOpacityAndTranslucency(for: view)
|
||||||
|
DispatchQueue.main.async { [weak self, weak view] in
|
||||||
|
guard let self, let view, self.terminalView === view else { return }
|
||||||
|
self.refreshTerminalOpacityAndTranslucency(for: view)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the shell process if not already running.
|
/// Starts the shell process if not already running.
|
||||||
|
|
@ -196,6 +225,46 @@ class TerminalManager: ObservableObject {
|
||||||
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Background color with opacity slider applied to alpha only (glyphs stay fully opaque).
|
||||||
|
private func resolvedTerminalBackgroundNSColor(
|
||||||
|
baseColor: NSColor? = nil,
|
||||||
|
opacitySlider: CGFloat? = nil
|
||||||
|
) -> NSColor {
|
||||||
|
let raw = baseColor ?? NSColor(Defaults[.terminalBackgroundColor])
|
||||||
|
let rgb = raw.usingColorSpace(.deviceRGB) ?? raw.usingColorSpace(.sRGB) ?? raw
|
||||||
|
let opacity = opacitySlider ?? CGFloat(Defaults[.terminalOpacity])
|
||||||
|
let alpha = CGFloat(rgb.cgColor.alpha) * opacity
|
||||||
|
return rgb.withAlphaComponent(alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composites the terminal buffer over `terminalBackgroundEffectView` without dimming glyphs.
|
||||||
|
/// SwiftTerm’s `TerminalView.isOpaque` is read-only; translucency relies on alpha in `nativeBackgroundColor`.
|
||||||
|
private func applyTerminalBackgroundAppearance(to view: LocalProcessTerminalView) {
|
||||||
|
view.layer?.opacity = 1
|
||||||
|
view.nativeBackgroundColor = resolvedTerminalBackgroundNSColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upstream `TerminalView.setupOptions()` assigns `layer.backgroundColor` from `nativeBackgroundColor`, which
|
||||||
|
/// prevents the `NSVisualEffectView` under the terminal from showing through. Re-clear after our color updates.
|
||||||
|
/// Default-cell alpha and attribute-cache invalidation on native color changes may still need a SwiftTerm PR
|
||||||
|
private func applyTerminalLayerTranslucencyHacks(to view: LocalProcessTerminalView) {
|
||||||
|
view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
view.layer?.isOpaque = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Best-effort redraw. `TerminalView.installColors(_:)` could clear SwiftTerm color caches but needs 16
|
||||||
|
/// `SwiftTerm.Color` values; `Terminal.ansiColors` is not public, so we do not duplicate a palette here.
|
||||||
|
private func synchronizeTerminalTranslucencyPresentation(to view: LocalProcessTerminalView) {
|
||||||
|
applyTerminalLayerTranslucencyHacks(to: view)
|
||||||
|
view.setNeedsDisplay(view.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-applies backdrop opacity + translucency hacks after remount/reopen paths.
|
||||||
|
private func refreshTerminalOpacityAndTranslucency(for view: LocalProcessTerminalView) {
|
||||||
|
applyTerminalBackgroundAppearance(to: view)
|
||||||
|
synchronizeTerminalTranslucencyPresentation(to: view)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Settings Application
|
// MARK: - Settings Application
|
||||||
|
|
||||||
/// Applies all persisted settings to a terminal view at creation time.
|
/// Applies all persisted settings to a terminal view at creation time.
|
||||||
|
|
@ -205,11 +274,8 @@ class TerminalManager: ObservableObject {
|
||||||
let fontFamily = Defaults[.terminalFontFamily]
|
let fontFamily = Defaults[.terminalFontFamily]
|
||||||
view.font = resolveFont(family: fontFamily, size: fontSize)
|
view.font = resolveFont(family: fontFamily, size: fontSize)
|
||||||
|
|
||||||
// Opacity
|
// Colors (background opacity is separate from terminal opacity slider — see `applyTerminalBackgroundAppearance`)
|
||||||
view.layer?.opacity = Float(Defaults[.terminalOpacity])
|
applyTerminalBackgroundAppearance(to: view)
|
||||||
|
|
||||||
// Colors
|
|
||||||
view.nativeBackgroundColor = NSColor(Defaults[.terminalBackgroundColor])
|
|
||||||
view.nativeForegroundColor = NSColor(Defaults[.terminalForegroundColor])
|
view.nativeForegroundColor = NSColor(Defaults[.terminalForegroundColor])
|
||||||
view.caretColor = NSColor(Defaults[.terminalCursorColor])
|
view.caretColor = NSColor(Defaults[.terminalCursorColor])
|
||||||
|
|
||||||
|
|
@ -219,6 +285,7 @@ class TerminalManager: ObservableObject {
|
||||||
view.getTerminal().setCursorStyle(cursorStyle.swiftTermStyle)
|
view.getTerminal().setCursorStyle(cursorStyle.swiftTermStyle)
|
||||||
|
|
||||||
// Keep drawing the selected cursor style when TerminalView.hasFocus is false; SwiftTerm
|
// Keep drawing the selected cursor style when TerminalView.hasFocus is false; SwiftTerm
|
||||||
|
// otherwise uses an unfocused/outline caret while tracksFocus is true.
|
||||||
view.caretViewTracksFocus = false
|
view.caretViewTracksFocus = false
|
||||||
|
|
||||||
// Scrollback
|
// Scrollback
|
||||||
|
|
@ -232,6 +299,8 @@ class TerminalManager: ObservableObject {
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
view.useBrightColors = Defaults[.terminalBoldAsBright]
|
view.useBrightColors = Defaults[.terminalBoldAsBright]
|
||||||
|
|
||||||
|
refreshTerminalOpacityAndTranslucency(for: view)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates font size on the live terminal view.
|
/// Updates font size on the live terminal view.
|
||||||
|
|
@ -248,10 +317,12 @@ class TerminalManager: ObservableObject {
|
||||||
view.font = resolveFont(family: family, size: fontSize)
|
view.font = resolveFont(family: family, size: fontSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates opacity on the live terminal view.
|
/// Updates background opacity on the live terminal view (glyphs stay fully opaque).
|
||||||
func applyOpacity(_ opacity: Double) {
|
func applyOpacity(_ opacity: Double) {
|
||||||
guard let view = terminalView else { return }
|
guard let view = terminalView else { return }
|
||||||
view.layer?.opacity = Float(opacity)
|
view.layer?.opacity = 1
|
||||||
|
view.nativeBackgroundColor = resolvedTerminalBackgroundNSColor(opacitySlider: CGFloat(opacity))
|
||||||
|
refreshTerminalOpacityAndTranslucency(for: view)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates cursor style on the live terminal view.
|
/// Updates cursor style on the live terminal view.
|
||||||
|
|
@ -289,13 +360,68 @@ class TerminalManager: ObservableObject {
|
||||||
/// Updates background color on the live terminal view.
|
/// Updates background color on the live terminal view.
|
||||||
func applyBackgroundColor(_ color: SwiftUI.Color) {
|
func applyBackgroundColor(_ color: SwiftUI.Color) {
|
||||||
guard let view = terminalView else { return }
|
guard let view = terminalView else { return }
|
||||||
view.nativeBackgroundColor = NSColor(color)
|
view.layer?.opacity = 1
|
||||||
|
view.nativeBackgroundColor = resolvedTerminalBackgroundNSColor(baseColor: NSColor(color))
|
||||||
|
refreshTerminalOpacityAndTranslucency(for: view)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates foreground color on the live terminal view.
|
/// Updates foreground color on the live terminal view.
|
||||||
func applyForegroundColor(_ color: SwiftUI.Color) {
|
func applyForegroundColor(_ color: SwiftUI.Color) {
|
||||||
guard let view = terminalView else { return }
|
guard let view = terminalView else { return }
|
||||||
view.nativeForegroundColor = NSColor(color)
|
view.nativeForegroundColor = NSColor(color)
|
||||||
|
synchronizeTerminalTranslucencyPresentation(to: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by UI lifecycle hooks after tab re-appearance to keep opacity stable.
|
||||||
|
func refreshTerminalAppearanceIfNeeded() {
|
||||||
|
guard let view = terminalView else { return }
|
||||||
|
refreshTerminalOpacityAndTranslucency(for: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes terminal view first responder when terminal tab opens.
|
||||||
|
/// Retries a few times to cover mount/animation timing so typing works immediately.
|
||||||
|
func focusTerminalIfPossible() {
|
||||||
|
focusTerminalIfPossible(attemptsRemaining: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func focusTerminalIfPossible(attemptsRemaining: Int) {
|
||||||
|
guard let view = terminalView else { return }
|
||||||
|
guard let window = containerView.window ?? view.window else {
|
||||||
|
guard attemptsRemaining > 0 else { return }
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
||||||
|
self?.focusTerminalIfPossible(attemptsRemaining: attemptsRemaining - 1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !NSApp.isActive {
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
|
if !window.isKeyWindow {
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard window.firstResponder !== view else { return }
|
||||||
|
if !window.makeFirstResponder(view), attemptsRemaining > 0 {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
||||||
|
self?.focusTerminalIfPossible(attemptsRemaining: attemptsRemaining - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resigns terminal focus before closing notch to avoid stale responder state.
|
||||||
|
func resignTerminalFirstResponderIfNeeded() {
|
||||||
|
guard let view = terminalView else { return }
|
||||||
|
guard let window = containerView.window ?? view.window else { return }
|
||||||
|
guard window.firstResponder === view else { return }
|
||||||
|
_ = window.makeFirstResponder(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the terminal view is currently first responder.
|
||||||
|
func isTerminalFirstResponder() -> Bool {
|
||||||
|
guard let view = terminalView else { return false }
|
||||||
|
guard let window = containerView.window ?? view.window else { return false }
|
||||||
|
return window.firstResponder === view
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates cursor color on the live terminal view.
|
/// Updates cursor color on the live terminal view.
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,46 @@ var minimalisticOpenNotchSize: CGSize {
|
||||||
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
|
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
|
||||||
let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed)
|
let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed)
|
||||||
|
|
||||||
|
// MARK: - Terminal tab clip (notch surface)
|
||||||
|
|
||||||
|
/// Padding on the terminal block inside the notch. Inner corner radius = outer shell radius on that edge, minus the matching edge padding.
|
||||||
|
let notchTerminalContentEdgePadding: (top: CGFloat, horizontal: CGFloat, bottom: CGFloat) = (4, 8, 8)
|
||||||
|
|
||||||
|
/// Bottom radii for the shell (outer) and the terminal ``clipShape`` (inner), per design: inner = outer shell bottom radius − `notchTerminalContentEdgePadding.bottom`.
|
||||||
|
func notchTerminalBottomCornerRadii(
|
||||||
|
isDynamicIslandMode: Bool,
|
||||||
|
notchState: NotchState,
|
||||||
|
cornerRadiusScaling: Bool,
|
||||||
|
enableMinimalisticUI: Bool,
|
||||||
|
closedNotchHeight: CGFloat
|
||||||
|
) -> (outerBottom: CGFloat, innerBottom: CGFloat) {
|
||||||
|
let p = notchTerminalContentEdgePadding.bottom
|
||||||
|
if isDynamicIslandMode {
|
||||||
|
let outer: CGFloat
|
||||||
|
if notchState == .open {
|
||||||
|
outer = enableMinimalisticUI
|
||||||
|
? minimalisticCornerRadiusInsets.opened.top
|
||||||
|
: dynamicIslandPillCornerRadiusInsets.opened
|
||||||
|
} else {
|
||||||
|
outer = max(closedNotchHeight / 2, dynamicIslandPillCornerRadiusInsets.closed.standard)
|
||||||
|
}
|
||||||
|
return (outer, max(0, outer - p))
|
||||||
|
}
|
||||||
|
let active: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = {
|
||||||
|
if enableMinimalisticUI {
|
||||||
|
return (opened: minimalisticCornerRadiusInsets.opened, closed: cornerRadiusInsets.closed)
|
||||||
|
}
|
||||||
|
return cornerRadiusInsets
|
||||||
|
}()
|
||||||
|
let outerBottom: CGFloat
|
||||||
|
if notchState == .open && cornerRadiusScaling {
|
||||||
|
outerBottom = active.opened.bottom
|
||||||
|
} else {
|
||||||
|
outerBottom = active.closed.bottom
|
||||||
|
}
|
||||||
|
return (outerBottom, max(0, outerBottom - p))
|
||||||
|
}
|
||||||
|
|
||||||
func statsAdjustedNotchSize(
|
func statsAdjustedNotchSize(
|
||||||
from baseSize: CGSize,
|
from baseSize: CGSize,
|
||||||
isStatsTabActive: Bool,
|
isStatsTabActive: Bool,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue