Skip to content
Open
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
67 changes: 55 additions & 12 deletions VeloxClip/App/VeloxClipApp.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import SwiftUI

extension Notification.Name {
static let veloxClipOpenSettings = Notification.Name("com.antigravity.veloxclip.openSettings")
}

@main
struct VeloxClipApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var monitor = ClipboardMonitor()

@Environment(\.openWindow) var openWindow
@StateObject private var settings = AppSettings.shared

var body: some Scene {
// No WindowGroup for main app, but we need one for Settings
WindowGroup(id: "settings") {
// Standard Preferences scene — opens via the system "showSettingsWindow:" selector
// from anywhere (menu, AppDelegate, distributed-notification handler).
Settings {
SettingsView()
}
.windowResizability(.contentSize)
.defaultSize(width: 500, height: 350)

MenuBarExtra("Velox Clip", systemImage: "paperclip.circle.fill") {

// On cold start, AppSettings loads asynchronously from SQLite; the icon may briefly
// appear with the default `true` before the persisted value is applied (~200ms).
MenuBarExtra(
"Velox Clip",
systemImage: "paperclip.circle.fill",
isInserted: $settings.showMenuBarIcon
) {
Button("Show Clipboard") {
WindowManager.shared.toggleWindow()
}
Expand All @@ -28,8 +36,8 @@ struct VeloxClipApp: App {
Divider()

Button("Preferences...") {
openWindow(id: "settings")
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
.keyboardShortcut(",", modifiers: .command)

Expand All @@ -49,15 +57,50 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if isAnotherInstanceRunning() {
print("⚠️ Another instance of VeloxClip is already running. Activating it and quitting this instance.")
activateExistingInstance()
DistributedNotificationCenter.default().postNotificationName(
.veloxClipOpenSettings,
object: nil,
userInfo: nil,
deliverImmediately: true
)
NSApplication.shared.terminate(nil)
return
}

// Register all global shortcuts
ShortcutManager.shared.registerAllShortcuts()

// Note: Window will be shown when user presses the shortcut or clicks menu item
// Removed auto-show on launch to avoid interrupting user workflow

// Listen for "open settings" requests from a duplicate launch attempt.
DistributedNotificationCenter.default().addObserver(
forName: .veloxClipOpenSettings,
object: nil,
queue: .main
) { _ in
Task { @MainActor in
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
}

// Cold-start fallback: if the menu bar icon is hidden, the user has no visible
// entry point. Wait for AppSettings to finish its initial DB load (up to 5s),
// then open Settings so they can re-enable it or quit.
Task { @MainActor in
let deadline = Date().addingTimeInterval(5)
while !AppSettings.shared.isLoaded && Date() < deadline {
do {
try await Task.sleep(nanoseconds: 50_000_000) // 50ms poll
} catch {
return // task cancelled (app terminating); abort
}
}
if !AppSettings.shared.showMenuBarIcon {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
}

// Note: Main window will be shown when user presses the shortcut or clicks the menu item.
}

private func isAnotherInstanceRunning() -> Bool {
Expand Down
30 changes: 28 additions & 2 deletions VeloxClip/Models/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ class AppSettings: ObservableObject {
}
}

@Published var showMenuBarIcon: Bool {
didSet {
guard !isInitializing else { return }
Task {
try? await dbManager.setSetting(key: "showMenuBarIcon", value: String(showMenuBarIcon))
}
}
}

@Published var globalShortcut: String {
didSet {
if !isInitializing {
Expand Down Expand Up @@ -96,13 +105,16 @@ class AppSettings: ObservableObject {
}
}
}


@Published private(set) var isLoaded: Bool = false

private var isInitializing = true

private init() {
// Initialize with default values first
self.historyLimit = 100
self.launchAtLogin = false
self.showMenuBarIcon = true
self.globalShortcut = "cmd+shift+v"
self.screenshotShortcut = "f1"
self.pasteImageShortcut = "f3"
Expand Down Expand Up @@ -151,7 +163,16 @@ class AppSettings: ObservableObject {
} else {
try? await dbManager.setSetting(key: "launchAtLogin", value: "false")
}


// Load showMenuBarIcon
if let showMenuBarIconStr = await dbManager.getSetting(key: "showMenuBarIcon") {
await MainActor.run {
self.showMenuBarIcon = showMenuBarIconStr == "true"
}
} else {
try? await dbManager.setSetting(key: "showMenuBarIcon", value: "true")
}

// Load globalShortcut
if let shortcut = await dbManager.getSetting(key: "globalShortcut") {
await MainActor.run {
Expand Down Expand Up @@ -221,6 +242,11 @@ class AppSettings: ObservableObject {
} else {
try? await dbManager.setSetting(key: "openRouterModel", value: "tngtech/deepseek-r1t2-chimera:free")
}

// Signal that the initial DB load completed.
await MainActor.run {
self.isLoaded = true
}
}

private func updateLaunchAtLogin() {
Expand Down
3 changes: 3 additions & 0 deletions VeloxClip/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ struct GeneralSettingsView: View {

Toggle("Launch at Login", isOn: $settings.launchAtLogin)
.help("Automatically start Velox Clip when you log in")

Toggle("Show Menu Bar Icon", isOn: $settings.showMenuBarIcon)
.help("When hidden, re-launch Velox Clip to open Preferences.")
}

Section("AI Settings") {
Expand Down
Loading