vOOice/VoiceInk/Services/BrowserURLService.swift

152 lines
5.1 KiB
Swift

import Foundation
import AppKit
import os
enum BrowserType {
case safari
case arc
case chrome
case edge
case firefox
case brave
case opera
case vivaldi
case orion
case zen
var scriptName: String {
switch self {
case .safari: return "safariURL"
case .arc: return "arcURL"
case .chrome: return "chromeURL"
case .edge: return "edgeURL"
case .firefox: return "firefoxURL"
case .brave: return "braveURL"
case .opera: return "operaURL"
case .vivaldi: return "vivaldiURL"
case .orion: return "orionURL"
case .zen: return "zenURL"
}
}
var bundleIdentifier: String {
switch self {
case .safari: return "com.apple.Safari"
case .arc: return "company.thebrowser.Browser"
case .chrome: return "com.google.Chrome"
case .edge: return "com.microsoft.edgemac"
case .firefox: return "org.mozilla.firefox"
case .brave: return "com.brave.Browser"
case .opera: return "com.operasoftware.Opera"
case .vivaldi: return "com.vivaldi.Vivaldi"
case .orion: return "com.kagi.kagimacOS"
case .zen: return "app.zen-browser.zen"
}
}
var displayName: String {
switch self {
case .safari: return "Safari"
case .arc: return "Arc"
case .chrome: return "Google Chrome"
case .edge: return "Microsoft Edge"
case .firefox: return "Firefox"
case .brave: return "Brave"
case .opera: return "Opera"
case .vivaldi: return "Vivaldi"
case .orion: return "Orion"
case .zen: return "Zen Browser"
}
}
static var allCases: [BrowserType] {
[.safari, .arc, .chrome, .edge, .firefox, .brave, .opera, .vivaldi, .orion, .zen]
}
static var installedBrowsers: [BrowserType] {
allCases.filter { browser in
let workspace = NSWorkspace.shared
return workspace.urlForApplication(withBundleIdentifier: browser.bundleIdentifier) != nil
}
}
}
enum BrowserURLError: Error {
case scriptNotFound
case executionFailed
case browserNotRunning
case noActiveWindow
case noActiveTab
}
class BrowserURLService {
static let shared = BrowserURLService()
private let logger = Logger(
subsystem: "com.prakashjoshipax.VoiceInk",
category: "browser.applescript"
)
private init() {}
func getCurrentURL(from browser: BrowserType) async throws -> String {
guard let scriptURL = Bundle.main.url(forResource: browser.scriptName, withExtension: "scpt") else {
logger.error("❌ AppleScript file not found: \(browser.scriptName).scpt")
throw BrowserURLError.scriptNotFound
}
logger.debug("🔍 Attempting to execute AppleScript for \(browser.displayName)")
// Check if browser is running
if !isRunning(browser) {
logger.error("❌ Browser not running: \(browser.displayName)")
throw BrowserURLError.browserNotRunning
}
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = [scriptURL.path]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
do {
logger.debug("▶️ Executing AppleScript for \(browser.displayName)")
try task.run()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
if output.isEmpty {
logger.error("❌ Empty output from AppleScript for \(browser.displayName)")
throw BrowserURLError.noActiveTab
}
// Check if output contains error messages
if output.lowercased().contains("error") {
logger.error("❌ AppleScript error for \(browser.displayName): \(output)")
throw BrowserURLError.executionFailed
}
logger.debug("✅ Successfully retrieved URL from \(browser.displayName): \(output)")
return output
} else {
logger.error("❌ Failed to decode output from AppleScript for \(browser.displayName)")
throw BrowserURLError.executionFailed
}
} catch {
logger.error("❌ AppleScript execution failed for \(browser.displayName): \(error.localizedDescription)")
throw BrowserURLError.executionFailed
}
}
func isRunning(_ browser: BrowserType) -> Bool {
let workspace = NSWorkspace.shared
let runningApps = workspace.runningApplications
let isRunning = runningApps.contains { $0.bundleIdentifier == browser.bundleIdentifier }
logger.debug("\(browser.displayName) running status: \(isRunning)")
return isRunning
}
}