chore: adiciona .gitignore e commit.command

This commit is contained in:
João Henrique
2026-08-18 08:25:29 -04:00
parent 68958fde00
commit 8fca456ceb
215 changed files with 65752 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
import Combine
import ComposableArchitecture
import Inject
import Sparkle
import SwiftUI
@Observable
@MainActor
final class CheckForUpdatesViewModel {
init() {
anyCancellable = controller.updater.publisher(for: \.canCheckForUpdates)
.sink(receiveValue: { self.canCheckForUpdates = $0 })
}
static let shared = CheckForUpdatesViewModel()
let controller = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
var anyCancellable: AnyCancellable?
var canCheckForUpdates = false
func checkForUpdates() {
controller.updater.checkForUpdates()
}
}
struct CheckForUpdatesView: View {
@State var viewModel = CheckForUpdatesViewModel.shared
@ObserveInjection var inject
var body: some View {
Button("Check for Updates…", action: viewModel.checkForUpdates)
.disabled(!viewModel.canCheckForUpdates)
.enableInjection()
}
}
+13
View File
@@ -0,0 +1,13 @@
import HexCore
import OSLog
enum DiagnosticsLogging {
private static var isBootstrapped = false
private static let logger = Logger(subsystem: HexLog.subsystem, category: "Diagnostics")
static func bootstrapIfNeeded() {
guard !isBootstrapped else { return }
logger.notice("Diagnostics logging initialized")
isBootstrapped = true
}
}
+54
View File
@@ -0,0 +1,54 @@
import ComposableArchitecture
import Inject
import Sparkle
import AppKit
import SwiftUI
@main
struct HexApp: App {
static let appStore = Store(initialState: AppFeature.State()) {
AppFeature()
}
@NSApplicationDelegateAdaptor(HexAppDelegate.self) var appDelegate
var body: some Scene {
MenuBarExtra {
MenuBarCopyLastTranscriptButton()
Button("Settings…") {
appDelegate.presentSettingsView()
}.keyboardShortcut(",")
CheckForUpdatesView()
Divider()
Button("Quit Hex") {
NSApplication.shared.terminate(nil)
}.keyboardShortcut("q")
} label: {
if let image = NSImage(named: "HexIcon").map({
let ratio = $0.size.height / $0.size.width
$0.size.height = 18
$0.size.width = 18 / ratio
return $0
}) {
Image(nsImage: image)
} else {
Image(systemName: "hexagon")
}
}
.commands {
CommandGroup(after: .appInfo) {
CheckForUpdatesView()
Button("Settings…") {
appDelegate.presentSettingsView()
}.keyboardShortcut(",")
}
CommandGroup(replacing: .help) {}
}
}
}
+186
View File
@@ -0,0 +1,186 @@
import ComposableArchitecture
import HexCore
import SwiftUI
private let appLogger = HexLog.app
private let cacheLogger = HexLog.caches
class HexAppDelegate: NSObject, NSApplicationDelegate {
var invisibleWindow: InvisibleWindow?
var settingsWindow: NSWindow?
var statusItem: NSStatusItem!
private var launchedAtLogin = false
@Dependency(\.soundEffects) var soundEffect
@Dependency(\.recording) var recording
@Shared(.hexSettings) var hexSettings: HexSettings
func applicationDidFinishLaunching(_: Notification) {
DiagnosticsLogging.bootstrapIfNeeded()
// Ensure Parakeet/FluidAudio caches live under Application Support, not ~/.cache
configureLocalCaches()
if isTesting {
appLogger.debug("Running in testing mode")
return
}
Task {
await soundEffect.preloadSounds()
await soundEffect.setEnabled(hexSettings.soundEffectsEnabled)
}
launchedAtLogin = wasLaunchedAtLogin()
appLogger.info("Application did finish launching")
appLogger.notice("launchedAtLogin = \(self.launchedAtLogin)")
// Set activation policy first
updateAppMode()
// Add notification observer
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppModeUpdate),
name: .updateAppMode,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handlePresentSettingsWindow),
name: .presentSettingsWindow,
object: nil
)
// Start long-running app effects (global hotkeys, permissions, etc.)
startLifecycleTasksIfNeeded()
// Then present main views
presentMainView()
guard shouldOpenForegroundUIOnLaunch else {
appLogger.notice("Suppressing foreground windows for login launch")
return
}
presentSettingsView()
NSApp.activate(ignoringOtherApps: true)
}
private var shouldOpenForegroundUIOnLaunch: Bool {
// When Hex launches at login, stay quietly in the menu bar regardless of
// the dock-icon preference. Users who enabled "Open on Login" expect a
// background launch; the Settings window can be opened later from the
// menu bar item or ⌘, when needed.
!launchedAtLogin
}
private func wasLaunchedAtLogin() -> Bool {
guard let event = NSAppleEventManager.shared().currentAppleEvent else {
return false
}
return event.eventID == AEEventID(kAEOpenApplication)
&& event.paramDescriptor(forKeyword: AEKeyword(keyAEPropData))?.enumCodeValue == AEEventClass(keyAELaunchedAsLogInItem)
}
private func startLifecycleTasksIfNeeded() {
Task { @MainActor in
await HexApp.appStore.send(.task).finish()
}
}
/// Sets XDG_CACHE_HOME so FluidAudio stores models under our app's
/// Application Support folder, keeping everything in one place.
private func configureLocalCaches() {
do {
let cache = try URL.hexApplicationSupport.appendingPathComponent("cache", isDirectory: true)
try FileManager.default.createDirectory(at: cache, withIntermediateDirectories: true)
setenv("XDG_CACHE_HOME", cache.path, 1)
cacheLogger.info("XDG_CACHE_HOME set to \(cache.path)")
} catch {
cacheLogger.error("Failed to configure local caches: \(error.localizedDescription)")
}
}
func presentMainView() {
guard invisibleWindow == nil else {
return
}
let transcriptionStore = HexApp.appStore.scope(state: \.transcription, action: \.transcription)
let transcriptionView = TranscriptionView(store: transcriptionStore).padding().padding(.top).padding(.top)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
invisibleWindow = InvisibleWindow.fromView(transcriptionView)
invisibleWindow?.orderFrontRegardless()
}
func presentSettingsView() {
if let settingsWindow = settingsWindow {
settingsWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return
}
let settingsView = AppView(store: HexApp.appStore)
let settingsWindow = NSWindow(
contentRect: .init(x: 0, y: 0, width: 700, height: 700),
styleMask: [.titled, .fullSizeContentView, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
settingsWindow.titleVisibility = .visible
settingsWindow.contentView = NSHostingView(rootView: settingsView)
settingsWindow.isReleasedWhenClosed = false
settingsWindow.minSize = .init(width: 620, height: 560)
settingsWindow.setFrameAutosaveName("Settings")
settingsWindow.center()
settingsWindow.toolbarStyle = NSWindow.ToolbarStyle.unified
settingsWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
self.settingsWindow = settingsWindow
}
@objc private func handleAppModeUpdate() {
Task {
await updateAppMode()
}
}
@objc private func handlePresentSettingsWindow() {
presentSettingsView()
}
@MainActor
private func updateAppMode() {
appLogger.debug("showDockIcon = \(self.hexSettings.showDockIcon)")
if self.hexSettings.showDockIcon {
NSApp.setActivationPolicy(.regular)
} else {
NSApp.setActivationPolicy(.accessory)
}
}
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
presentSettingsView()
return true
}
func applicationWillTerminate(_: Notification) {
// Wait for audio teardown before the process exits: a fire-and-forget Task here
// raced process exit, crashing inside AVAudioEngine teardown while tap callbacks
// were still in flight (#245). Pump the main run loop while waiting instead of
// blocking outright - cleanup() hops to the main actor/main queue (media-key
// resume, Core Audio listener removal), which a blocked main thread would deadlock.
let recording = recording
let semaphore = DispatchSemaphore(value: 0)
Task.detached {
await recording.cleanup()
semaphore.signal()
}
let deadline = Date().addingTimeInterval(3)
while semaphore.wait(timeout: .now()) == .timedOut {
guard Date() < deadline else {
appLogger.error("Recording cleanup timed out during app termination")
return
}
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
}
}
}
@@ -0,0 +1,97 @@
import SwiftUI
import ComposableArchitecture
import Dependencies
import AppKit
import HexCore
import Sauce
struct MenuBarCopyLastTranscriptButton: View {
@Shared(.transcriptionHistory) var transcriptionHistory: TranscriptionHistory
@Shared(.hexSettings) var hexSettings: HexSettings
@Dependency(\.pasteboard) var pasteboard
var body: some View {
let lastText = transcriptionHistory.history.first?.text
let button = Button("Paste Last Transcript") {
if let text = lastText {
Task { await pasteboard.paste(text) }
}
}
.disabled(lastText == nil)
if let hotkey = hexSettings.pasteLastTranscriptHotkey,
let key = hotkey.key,
let keyEquivalent = toKeyEquivalent(key) {
button.keyboardShortcut(keyEquivalent, modifiers: toEventModifiers(hotkey.modifiers))
} else {
button
}
}
private func toKeyEquivalent(_ key: Key) -> KeyEquivalent? {
switch key.rawValue {
case "a": return "a"
case "b": return "b"
case "c": return "c"
case "d": return "d"
case "e": return "e"
case "f": return "f"
case "g": return "g"
case "h": return "h"
case "i": return "i"
case "j": return "j"
case "k": return "k"
case "l": return "l"
case "m": return "m"
case "n": return "n"
case "o": return "o"
case "p": return "p"
case "q": return "q"
case "r": return "r"
case "s": return "s"
case "t": return "t"
case "u": return "u"
case "v": return "v"
case "w": return "w"
case "x": return "x"
case "y": return "y"
case "z": return "z"
case "0": return "0"
case "1": return "1"
case "2": return "2"
case "3": return "3"
case "4": return "4"
case "5": return "5"
case "6": return "6"
case "7": return "7"
case "8": return "8"
case "9": return "9"
case ",": return ","
case ".": return "."
case "/": return "/"
case "\\": return "\\"
case "'": return "'"
case ";": return ";"
case "[": return "["
case "]": return "]"
case "-": return "-"
case "=": return "="
case "`": return "`"
default: return nil
}
}
private func toEventModifiers(_ modifiers: Modifiers) -> SwiftUI.EventModifiers {
var result: SwiftUI.EventModifiers = []
if modifiers.contains(kind: .command) { result.insert(.command) }
if modifiers.contains(kind: .option) { result.insert(.option) }
if modifiers.contains(kind: .shift) { result.insert(.shift) }
if modifiers.contains(kind: .control) { result.insert(.control) }
return result
}
}
#Preview {
MenuBarCopyLastTranscriptButton()
}
+7
View File
@@ -0,0 +1,7 @@
import Foundation
extension NSNotification.Name {
/// Posted when app mode settings change (dock icon visibility, etc.)
static let updateAppMode = NSNotification.Name("UpdateAppMode")
static let presentSettingsWindow = NSNotification.Name("PresentSettingsWindow")
}