fix: show Dock icon during onboarding
This commit is contained in:
@@ -390,6 +390,7 @@ enum AppStateStore {
|
|||||||
@MainActor
|
@MainActor
|
||||||
enum AppActivationPolicy {
|
enum AppActivationPolicy {
|
||||||
static func apply(showDockIcon: Bool) {
|
static func apply(showDockIcon: Bool) {
|
||||||
NSApp.setActivationPolicy(showDockIcon ? .regular : .accessory)
|
_ = showDockIcon
|
||||||
|
DockIconManager.shared.updateDockVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
apps/macos/Sources/Clawdis/DockIconManager.swift
Normal file
117
apps/macos/Sources/Clawdis/DockIconManager.swift
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import AppKit
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
/// Central manager for Dock icon visibility.
|
||||||
|
/// Shows the Dock icon while any windows are visible, regardless of user preference.
|
||||||
|
final class DockIconManager: NSObject, @unchecked Sendable {
|
||||||
|
static let shared = DockIconManager()
|
||||||
|
|
||||||
|
private var windowsObservation: NSKeyValueObservation?
|
||||||
|
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "DockIconManager")
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
self.setupObservers()
|
||||||
|
Task { @MainActor in
|
||||||
|
self.updateDockVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.windowsObservation?.invalidate()
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDockVisibility() {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard NSApp != nil else {
|
||||||
|
self.logger.warning("NSApp not ready, skipping Dock visibility update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let userWantsDockHidden = !UserDefaults.standard.bool(forKey: showDockIconKey)
|
||||||
|
let visibleWindows = NSApp?.windows.filter { window in
|
||||||
|
window.isVisible &&
|
||||||
|
window.frame.width > 1 &&
|
||||||
|
window.frame.height > 1 &&
|
||||||
|
!window.isKind(of: NSPanel.self) &&
|
||||||
|
"\(type(of: window))" != "NSPopupMenuWindow" &&
|
||||||
|
window.contentViewController != nil
|
||||||
|
} ?? []
|
||||||
|
|
||||||
|
let hasVisibleWindows = !visibleWindows.isEmpty
|
||||||
|
if !userWantsDockHidden || hasVisibleWindows {
|
||||||
|
NSApp?.setActivationPolicy(.regular)
|
||||||
|
} else {
|
||||||
|
NSApp?.setActivationPolicy(.accessory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func temporarilyShowDock() {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard NSApp != nil else {
|
||||||
|
self.logger.warning("NSApp not ready, cannot show Dock icon")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
NSApp.setActivationPolicy(.regular)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupObservers() {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let app = NSApp else {
|
||||||
|
self.logger.warning("NSApp not ready, delaying Dock observers")
|
||||||
|
try? await Task.sleep(for: .milliseconds(200))
|
||||||
|
self.setupObservers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.windowsObservation = app.observe(\.windows, options: [.new]) { [weak self] _, _ in
|
||||||
|
Task { @MainActor in
|
||||||
|
try? await Task.sleep(for: .milliseconds(50))
|
||||||
|
self?.updateDockVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(self.windowVisibilityChanged),
|
||||||
|
name: NSWindow.didBecomeKeyNotification,
|
||||||
|
object: nil)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(self.windowVisibilityChanged),
|
||||||
|
name: NSWindow.didResignKeyNotification,
|
||||||
|
object: nil)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(self.windowVisibilityChanged),
|
||||||
|
name: NSWindow.willCloseNotification,
|
||||||
|
object: nil)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(self.dockPreferenceChanged),
|
||||||
|
name: UserDefaults.didChangeNotification,
|
||||||
|
object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func windowVisibilityChanged(_: Notification) {
|
||||||
|
Task { @MainActor in
|
||||||
|
self.updateDockVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func dockPreferenceChanged(_ notification: Notification) {
|
||||||
|
guard let userDefaults = notification.object as? UserDefaults,
|
||||||
|
userDefaults == UserDefaults.standard
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
self.updateDockVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ final class OnboardingController {
|
|||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
if let window {
|
if let window {
|
||||||
|
DockIconManager.shared.temporarilyShowDock()
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
return
|
return
|
||||||
@@ -28,6 +29,7 @@ final class OnboardingController {
|
|||||||
window.titleVisibility = .hidden
|
window.titleVisibility = .hidden
|
||||||
window.isMovableByWindowBackground = true
|
window.isMovableByWindowBackground = true
|
||||||
window.center()
|
window.center()
|
||||||
|
DockIconManager.shared.temporarilyShowDock()
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
self.window = window
|
self.window = window
|
||||||
@@ -630,7 +632,7 @@ struct OnboardingView: View {
|
|||||||
.font(.largeTitle.weight(.semibold))
|
.font(.largeTitle.weight(.semibold))
|
||||||
Text(
|
Text(
|
||||||
"The Gateway is the WebSocket service that keeps Clawdis connected. " +
|
"The Gateway is the WebSocket service that keeps Clawdis connected. " +
|
||||||
"We’ll install/update the `clawdis` npm package and verify Node is available.")
|
"Clawdis bundles it and runs it via launchd so it stays running.")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -674,7 +676,7 @@ struct OnboardingView: View {
|
|||||||
if self.gatewayInstalling {
|
if self.gatewayInstalling {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else {
|
} else {
|
||||||
Text("Install or update gateway")
|
Text("Enable Gateway daemon")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
@@ -692,7 +694,7 @@ struct OnboardingView: View {
|
|||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"Runs `npm install -g clawdis@<version>` on your PATH. " +
|
"Installs a per-user LaunchAgent (\(gatewayLaunchdLabel)). " +
|
||||||
"The gateway listens on port 18789.")
|
"The gateway listens on port 18789.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -1248,10 +1250,10 @@ struct OnboardingView: View {
|
|||||||
self.gatewayInstalling = true
|
self.gatewayInstalling = true
|
||||||
defer { self.gatewayInstalling = false }
|
defer { self.gatewayInstalling = false }
|
||||||
self.gatewayInstallMessage = nil
|
self.gatewayInstallMessage = nil
|
||||||
let expected = GatewayEnvironment.expectedGatewayVersion()
|
let port = GatewayEnvironment.gatewayPort()
|
||||||
await GatewayEnvironment.installGlobal(version: expected) { message in
|
let bundlePath = Bundle.main.bundleURL.path
|
||||||
Task { @MainActor in self.gatewayInstallMessage = message }
|
let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port)
|
||||||
}
|
self.gatewayInstallMessage = err ?? "Gateway enabled and started on port \(port)"
|
||||||
self.refreshGatewayStatus()
|
self.refreshGatewayStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user