class Element {
    let rawElement: AXUIElement
    let role: String
    let url: URL?
    var clippedFrame: NSRect?
    static func initialize(rawElement: AXUIElement) -> Element? {
        let uiElement = UIElement(rawElement)
        let valuesOptional = try? uiElement.getMultipleAttributes([.role, .url])
        guard let values = valuesOptional else { return nil }
        guard let role: String = values[Attribute.role] as! String? else { return nil }
        return Element(rawElement: rawElement, role: role, url: values[Attribute.url] as? URL)
    }
    init(rawElement: AXUIElement, role: String, url: URL?) {
        self.rawElement = rawElement
        self.role = role
        self.url = url
    }
}

import AXSwift
import Cocoa
import RxSwift

class QueryWebAreaService {
    let windowElement: Element

    init(windowElement: Element) {
        self.windowElement = windowElement
    }

    func perform() throws -> Element? {
        try fetchScrollArea()
    }

    private func fetchScrollArea() throws -> Element? {
        var stack: [Element] = [windowElement]

        while stack.count > 0 {
            let element = stack.popLast()!

            if element.role == "AXWebArea",
               element.url?.scheme != "chrome-extension"
            {
                return element
            }

            let children = try fetchChildren(element) ?? []
            for child in children {
                stack.append(child)
            }
        }

        return nil
    }

    private func fetchChildren(_ element: Element) throws -> [Element]? {
        let rawElementsOptional: [AXUIElement]? = try {
            if element.role == "AXTable" || element.role == "AXOutline" {
                return try UIElement(element.rawElement).attribute(.visibleRows)
            }
            return try UIElement(element.rawElement).attribute(.children)
        }()

        guard let rawElements = rawElementsOptional else {
            return nil
        }

        return rawElements
            .map { Element.initialize(rawElement: $0) }
            .compactMap { $0 }
    }
}

import AXSwift
import Cocoa

extension UIElement {
    func getCursorRectInfo() -> (rect: CGRect, isContainer: Bool)? {
        guard let focusedElement: UIElement = try? attribute(.focusedUIElement),
              Self.isInputContainer(focusedElement),
              let inputAreaRect = Self.findInputAreaRect(focusedElement)
        else { return nil }

        if let cursorRect = Self.findCursorRect(focusedElement),
           inputAreaRect.contains(cursorRect)
        {
            return (rect: cursorRect, isContainer: false)
        } else {
            return (rect: inputAreaRect, isContainer: true)
        }
    }
}

extension UIElement {
    static func isInputContainer(_ elm: UIElement?) -> Bool {
        guard let elm = elm,
              let role = try? elm.role()
        else { return false }

        return role == .textArea || role == .textField || role == .comboBox
    }

    static func findInputAreaRect(_ focusedElement: UIElement) -> CGRect? {
        if let parent: UIElement = try? focusedElement.attribute(.parent),
           let role = try? parent.role(),
           role == .scrollArea,
           let origin: CGPoint = try? parent.attribute(.position),
           let size: CGSize = try? parent.attribute(.size)
        {
            return NSScreen.convertFromQuartz(CGRect(origin: origin, size: size))
        }

        if let origin: CGPoint = try? focusedElement.attribute(.position),
           let size: CGSize = try? focusedElement.attribute(.size)
        {
            return NSScreen.convertFromQuartz(CGRect(origin: origin, size: size))
        }

        return nil
    }
}

extension UIElement {
    static func findCursorRect(_ focusedElement: UIElement) -> CGRect? {
        return findWebAreaCursor(focusedElement) ?? findNativeInputAreaCursor(focusedElement)
    }

    static func findWebAreaCursor(_ focusedElement: UIElement) -> CGRect? {
        guard let range: AXTextMarkerRange = try? focusedElement.attribute("AXSelectedTextMarkerRange"),
              let bounds: CGRect = try? focusedElement.parameterizedAttribute("AXBoundsForTextMarkerRange", param: range)
        else { return nil }

        return NSScreen.convertFromQuartz(bounds)
    }

    static func findNativeInputAreaCursor(_ focusedElement: UIElement) -> CGRect? {
        guard let selectedRange: CFRange = try? focusedElement.attribute(.selectedTextRange),
              let visibleRange: CFRange = try? focusedElement.attribute(.visibleCharacterRange),
              let rawValue: AnyObject = try? focusedElement.attribute(.value),
              CFGetTypeID(rawValue) == CFStringGetTypeID(),
              let value = rawValue as? String
        else { return nil }

        func getBounds(cursor location: Int) -> CGRect? {
            return try? focusedElement.parameterizedAttribute(
                kAXBoundsForRangeParameterizedAttribute,
                param: AXValue.range(CFRange(location: max(location, 0), length: 1))
            )
            .flatMap(NSScreen.convertFromQuartz)
        }

        func getCursorBounds() -> CGRect? {
            let lastCursor = visibleRange.location + visibleRange.length
            // Notes 最后存在两个换行符时会有问题
            // let isLastCursor = selectedRange.location >= (lastCursor - 1)
            let isLastCursor = selectedRange.location >= lastCursor
            let location = selectedRange.location - (isLastCursor ? 1 : 0)

            guard let bounds = getBounds(cursor: location)
            else { return nil }

            if isLastCursor, value.string(at: location) == "\n" {
                if location > 0 {
                    for offsetDiff in 1 ... location {
                        let offset = location - offsetDiff

                        if value.string(at: offset + 1) == "\n",
                           let prevNewLineBounds = getBounds(cursor: offset)
                        {
                            return CGRect(
                                origin: CGPoint(
                                    x: prevNewLineBounds.origin.x,
                                    y: prevNewLineBounds.minY - prevNewLineBounds.height
                                ),
                                size: bounds.size
                            )
                        }
                    }

                    return nil
                } else {
                    return nil
                }
            } else {
                return bounds
            }
        }

        func getLineBounds() -> CGRect? {
            guard let cursorLine: Int = try? focusedElement.attribute(.insertionPointLineNumber),
                  let lineRange: CFRange = try? focusedElement.parameterizedAttribute("AXRangeForLine", param: cursorLine),
                  let bounds: CGRect = try? focusedElement.parameterizedAttribute(
                      kAXBoundsForRangeParameterizedAttribute,
                      param: AXValue.range(lineRange)
                  )
            else { return nil }

            return NSScreen.convertFromQuartz(bounds)
        }

        return getCursorBounds() ?? getLineBounds()
    }
}

extension UIElement {
    func children() -> [UIElement]? {
        guard let children: [AXUIElement] = try? attribute(.children)
        else { return nil }

        return children.map { .init($0) }
    }
}

extension Role {
    static let validInputElms: [Role] = [.comboBox, .textArea, .textField]

    static let allCases: [Role] = [
        .unknown,
        .button,
        .radioButton,
        .checkBox,
        .slider,
        .tabGroup,
        .textField,
        .staticText,
        .textArea,
        .scrollArea,
        .popUpButton,
        .menuButton,
        .table,
        .application,
        .group,
        .radioGroup,
        .list,
        .scrollBar,
        .valueIndicator,
        .image,
        .menuBar,
        .menu,
        .menuItem,
        .column,
        .row,
        .toolbar,
        .busyIndicator,
        .progressIndicator,
        .window,
        .drawer,
        .systemWide,
        .outline,
        .incrementor,
        .browser,
        .comboBox,
        .splitGroup,
        .splitter,
        .colorWell,
        .growArea,
        .sheet,
        .helpTag,
        .matte,
        .ruler,
        .rulerMarker,
        .link,
        .disclosureTriangle,
        .grid,
        .relevanceIndicator,
        .levelIndicator,
        .cell,
        .popover,
        .layoutArea,
        .layoutItem,
        .handle,
    ]
}

extension AXNotification {
    static let allCases: [AXNotification] = [
        .mainWindowChanged,
        .focusedWindowChanged,
        .focusedUIElementChanged,
        .applicationActivated,
        .applicationDeactivated,
        .applicationHidden,
        .applicationShown,
        .windowCreated,
        .windowMoved,
        .windowResized,
        .windowMiniaturized,
        .windowDeminiaturized,
        .drawerCreated,
        .sheetCreated,
        .uiElementDestroyed,
        .valueChanged,
        .titleChanged,
        .resized,
        .moved,
        .created,
        .layoutChanged,
        .helpTagCreated,
        .selectedTextChanged,
        .rowCountChanged,
        .selectedChildrenChanged,
        .selectedRowsChanged,
        .selectedColumnsChanged,
        .rowExpanded,
        .rowCollapsed,
        .selectedCellsChanged,
        .unitsChanged,
        .selectedChildrenMoved,
        .announcementRequested,
    ]
}

import AppKit
import AXSwift
import Combine

private func notify(
    _ observer: Observer?,
    _ validRoles: [Role],
    _ element: UIElement,
    _ notifications: [AXNotification],
    _ indetify: String?
) -> AnyCancellable {
    guard let role = try? element.role(),
          validRoles.contains(role)
    else { return AnyCancellable {} }

    do {
        try notifications.forEach {
            try observer?.addNotification($0, forElement: element)
        }

        return AnyCancellable {
            for notification in notifications {
                try? observer?.removeNotification(notification, forElement: element)
            }
        }
    } catch {
        let role = role.rawValue
        let msg = "\(indetify ?? "nil"): Could not watch [\(element)] with role \(role): \(error)"

        #if DEBUG
            print(msg)
        #endif

        return AnyCancellable {}
    }
}

extension NSRunningApplication {
    typealias WatchAXOutput = (runningApp: NSRunningApplication, notification: AXNotification)

    func watchAX(
        elm: UIElement? = nil,
        _ notifications: [AXNotification],
        _ validRoles: [Role]
    ) -> AnyPublisher<WatchAXOutput, Never> {
        return Timer.delay(seconds: 0.5)
            .receive(on: DispatchQueue.main)
            .flatMapLatest { _ -> AnyPublisher<WatchAXOutput, Never> in
                AnyPublisher.create { [weak self] observer in
                    let cancelBag = CancelBag()

                    self?.activateAccessibilities()

                    guard
                        let runningApplication = self,
                        let app = Application(runningApplication)
                    else { return AnyCancellable {} }

                    let appObserver = app.createObserver { (_ appObserver: Observer, _ element: UIElement, _ event: AXNotification) in
                        // Watch events on new windows
                        if event == .windowCreated {
                            notify(
                                appObserver,
                                validRoles,
                                element,
                                notifications,
                                runningApplication.bundleIdentifier
                            )
                            .store(in: cancelBag)
                        }

                        observer.send((runningApplication, event))
                    }

                    notify(
                        appObserver,
                        validRoles,
                        elm ?? app,
                        notifications,
                        runningApplication.bundleIdentifier
                    )
                    .store(in: cancelBag)

                    return AnyCancellable {
                        cancelBag.cancel()
                        appObserver?.stop()
                    }
                }
            }
            .eraseToAnyPublisher()
    }
}

extension NSRunningApplication {
    func bundleId() -> String? {
        if let bundleIdentifier = bundleIdentifier {
            return bundleIdentifier
        }

        if let url = bundleURL ?? executableURL {
            return url.bundleId()
        }

        return nil
    }
}

extension NSRunningApplication {
    // For performance reasons Chromium only makes the webview accessible when there it detects voiceover through the `AXEnhancedUserInterface` attribute on the Chrome application itself:
    // http://dev.chromium.org/developers/design-documents/accessibility
    // Similarly, electron uses `AXManualAccessibility`:
    // https://electronjs.org/docs/tutorial/accessibility#assistive-technology
    func activateAccessibilities() {
        guard bundleIdentifier?.starts(with: "com.apple.") != true else { return }

        activateAccessibility(attribute: "AXEnhancedUserInterface")
        activateAccessibility(attribute: "AXManualAccessibility")
    }

    func activateAccessibility(attribute: String) {
        if let application = Application(self),
           let isSettable = try? application.attributeIsSettable(attribute),
           isSettable
        {
            if let rawValue: AnyObject = try? application.attribute(attribute),
               CFBooleanGetTypeID() == CFGetTypeID(rawValue),
               let enabled = rawValue as? Bool, enabled
            {
                return
            }

            try? application.setAttribute(attribute, value: true)
        }
    }
}

import AppKit
import AXSwift
import Combine
import Foundation

@MainActor
enum AppKind {
    typealias BrowserInfo = (
        focusedElement: UIElement?,
        isFocusOnInputContainer: Bool,

        url: URL,
        rule: BrowserRule?,
        isFocusedOnAddressBar: Bool
    )

    typealias NormalInfo = (
        focusedElement: UIElement?,
        isFocusOnInputContainer: Bool
    )

    case normal(app: NSRunningApplication, info: NormalInfo)
    case browser(app: NSRunningApplication, info: BrowserInfo)

    func getId() -> String? {
        switch self {
        case let .normal(app, _):
            return app.bundleId()
        case let .browser(app, info):
            if !info.isFocusedOnAddressBar,
               info.url != .newtab,
               let bundleId = app.bundleId(),
               let addressId = info.rule?.id() ?? info.url.host
            {
                return "\(bundleId)_\(addressId)"
            } else {
                return nil
            }
        }
    }

    func getApp() -> NSRunningApplication {
        switch self {
        case let .normal(app, _):
            return app
        case let .browser(app, _):
            return app
        }
    }

    func getBrowserInfo() -> BrowserInfo? {
        switch self {
        case .normal:
            return nil
        case let .browser(_, info):
            return info
        }
    }

    func isFocusOnInputContainer() -> Bool {
        switch self {
        case let .normal(_, info):
            return info.isFocusOnInputContainer
        case let .browser(_, info):
            return info.isFocusOnInputContainer
        }
    }

    func getFocusedElement() -> UIElement? {
        switch self {
        case let .normal(_, info):
            return info.focusedElement
        case let .browser(_, info):
            return info.focusedElement
        }
    }

    func isSameAppOrWebsite(with otherKind: AppKind?, detectAddressBar: Bool = false) -> Bool {
        guard let otherKind = otherKind else { return false }
        guard getApp() == otherKind.getApp() else { return false }

        let isSameAddress = getBrowserInfo()?.url == otherKind.getBrowserInfo()?.url
        let isSameAddressBar = getBrowserInfo()?.isFocusedOnAddressBar == otherKind.getBrowserInfo()?.isFocusedOnAddressBar

        return detectAddressBar ? (isSameAddressBar && isSameAddress) : isSameAddress
    }
}

// MARK: - From

extension AppKind {
    static func from(_ app: NSRunningApplication?, preferencesVM: PreferencesVM) -> AppKind? {
        if let app = app {
            return .from(app, preferencesVM: preferencesVM)
        } else {
            return nil
        }
    }

    static func from(_ app: NSRunningApplication, preferencesVM: PreferencesVM) -> AppKind {
        let application = app.getApplication(preferencesVM: preferencesVM)
        let focusedElement = app.focuedUIElement(application: application)
        let isFocusOnInputContainer = UIElement.isInputContainer(focusedElement)

        if let url = preferencesVM.getBrowserURL(app.bundleIdentifier, application: application)?.removeFragment() {
            let rule = preferencesVM.getBrowserRule(url: url)
            let isFocusOnBrowserAddress = preferencesVM.isFocusOnBrowserAddress(app: app, focusedElement: focusedElement)

            return .browser(
                app: app,
                info: (
                    focusedElement,
                    isFocusOnInputContainer,
                    url,
                    rule,
                    isFocusOnBrowserAddress
                )
            )
        } else {
            return .normal(
                app: app,
                info: (
                    focusedElement,
                    isFocusOnInputContainer
                )
            )
        }
    }
}

import AppKit
import AXSwift

// Update BrowserThatCanWatchBrowserAddressFocus as well
enum Browser: String, CaseIterable {
    case Safari = "com.apple.Safari"
    case SafariTechnologyPreview = "com.apple.SafariTechnologyPreview"
    case Chrome = "com.google.Chrome"
    case Chromium = "org.chromium.Chromium"
    case Arc = "company.thebrowser.Browser"
    case Edge = "com.microsoft.edgemac"
    case Brave = "com.brave.Browser"
    case BraveBeta = "com.brave.Browser.beta"
    case BraveNightly = "com.brave.Browser.nightly"
    case Vivaldi = "com.vivaldi.Vivaldi"
    case Opera = "com.operasoftware.Opera"
    case Thorium = "org.chromium.Thorium"
    case Firefox = "org.mozilla.firefox"
    case FirefoxDeveloperEdition = "org.mozilla.firefoxdeveloperedition"
    case FirefoxNightly = "org.mozilla.nightly"
    case Zen = "app.zen-browser.zen"
    case Dia = "company.thebrowser.dia"

    var displayName: String {
        switch self {
        case .Safari:
            return "Safari"
        case .SafariTechnologyPreview:
            return "Safari Technology Preview"
        case .Chrome:
            return "Chrome"
        case .Chromium:
            return "Chromium"
        case .Arc:
            return "Arc"
        case .Edge:
            return "Edge"
        case .Brave:
            return "Brave"
        case .BraveBeta:
            return "Brave Beta"
        case .BraveNightly:
            return "Brave Nightly"
        case .Vivaldi:
            return "Vivaldi"
        case .Opera:
            return "Opera"
        case .Thorium:
            return "Thorium"
        case .Firefox:
            return "Firefox"
        case .FirefoxDeveloperEdition:
            return "Firefox Developer Edition"
        case .FirefoxNightly:
            return "Firefox Nightly"
        case .Zen:
            return "Zen"
        case .Dia:
            return "Dia"
        }
    }
}

extension Browser {
    func getCurrentTabURL(focusedWindow: UIElement) -> URL? {
        guard let windowElement = Element.initialize(rawElement: focusedWindow.element),
              let webArea = (try? QueryWebAreaService(windowElement: windowElement).perform()),
              let url = webArea.url
        else { return nil }

        if [
            "favorites://",
            "edge://newtab/",
            "chrome://newtab/",
            "chrome://new-tab-page/",
            "chrome://vivaldi-webui/",
            "about:newtab", // Firefox
        ].contains(where: { url.absoluteString.contains($0) }) {
            return .newtab
        } else {
            return url
        }
    }
}

extension Browser {
    static func isSupportedBrowser(bundleId: String) -> Bool {
        return allCases.contains { $0.rawValue == bundleId }
    }
}

import AXSwift
import Cocoa

extension UIElement {
    func domIdentifier() -> String? {
        return safeString(attribute: .identifier)
    }

    func firefoxDomIdentifier() -> String? {
        return safeString(attribute: "AXDOMIdentifier")
    }

    func safeString(attribute attr: Attribute) -> String? {
        if let anyObject: AnyObject = try? attribute(attr),
           CFGetTypeID(anyObject) == CFStringGetTypeID()
        {
            return anyObject as? String
        } else {
            return nil
        }
    }

    func safeString(attribute attr: String) -> String? {
        if let anyObject: AnyObject = try? attribute(attr),
           CFGetTypeID(anyObject) == CFStringGetTypeID()
        {
            return anyObject as? String
        } else {
            return nil
        }
    }

    func domClassList() -> [String] {
        if let rawDOMClassList: AnyObject = try? attribute("AXDOMClassList"),
           CFGetTypeID(rawDOMClassList) == CFArrayGetTypeID(),
           let domClassList1 = rawDOMClassList as? [AnyObject],
           let rawDOMClass = domClassList1.first,
           CFGetTypeID(rawDOMClass) == CFStringGetTypeID(),
           let domClassList = domClassList1 as? [String]
        {
            return domClassList
        } else {
            return []
        }
    }
}

import AXSwift
import Cocoa

extension NSRunningApplication {
    @MainActor
    func focusedUIElement(preferencesVM: PreferencesVM?) -> UIElement? {
        return focuedUIElement(application: getApplication(preferencesVM: preferencesVM))
    }

    @MainActor
    func focuedUIElement(application: Application?) -> UIElement? {
        if let application = application {
            return try? application.attribute(.focusedUIElement)
        } else {
            return nil
        }
    }

    @MainActor
    func getApplication(preferencesVM: PreferencesVM?) -> Application? {
        if let preferencesVM = preferencesVM,
           preferencesVM.preferences.isEnhancedModeEnabled,
           !NSApplication.isFloatingApp(bundleIdentifier)
        {
            return Application(self)
        } else {
            return nil
        }
    }
}

import AppKit
import AXSwift
import Combine
import CombineExt
import os

@MainActor
final class ApplicationVM: ObservableObject {
    @Published private(set) var appKind: AppKind? = nil
    @Published private(set) var appsDiff: AppsDiff = .empty

    let logger = ISPLogger(category: String(describing: ApplicationVM.self))

    let cancelBag = CancelBag()
    let preferencesVM: PreferencesVM

    lazy var windowAXNotificationPublisher = ApplicationVM
        .createWindowAXNotificationPublisher(preferencesVM: preferencesVM)

    init(preferencesVM: PreferencesVM) {
        self.preferencesVM = preferencesVM
        appKind = .from(NSWorkspace.shared.frontmostApplication, preferencesVM: preferencesVM)

        activateAccessibilitiesForCurrentApp()
        watchApplicationChange()
        watchAppsDiffChange()
    }
}

extension ApplicationVM {
    private func watchApplicationChange() {
        let axNotification = windowAXNotificationPublisher.mapToVoid()

        let workspaceNotification = NSWorkspace.shared.notificationCenter
            .publisher(for: NSWorkspace.didActivateApplicationNotification, object: NSWorkspace.shared)
            .mapToVoid()

        Publishers
            .Merge(axNotification, workspaceNotification)
            .compactMap { [weak self] _ -> NSRunningApplication? in
                guard self?.preferencesVM.preferences.isEnhancedModeEnabled == true,
                      let elm: UIElement = try? systemWideElement.attribute(.focusedApplication),
                      let pid = try? elm.pid()
                else { return NSWorkspace.shared.frontmostApplication }
                return NSRunningApplication(processIdentifier: pid)
            }
            .removeDuplicates()
            .flatMapLatest { [weak self] (app: NSRunningApplication) -> AnyPublisher<AppKind, Never> in
                guard let preferencesVM = self?.preferencesVM
                else { return Empty().eraseToAnyPublisher() }

                guard NSApplication.isBrowser(app)
                else { return Just(.from(app, preferencesVM: preferencesVM)).eraseToAnyPublisher() }

                return Timer
                    .interval(seconds: 1)
                    .prepend(Date())
                    .compactMap { _ in app.focusedUIElement(preferencesVM: preferencesVM) }
                    .first()
                    .flatMapLatest { _ in
                        app.watchAX([
                            .focusedUIElementChanged,
                            .titleChanged,
                            .windowCreated,
                        ], [.application, .window])
                            .filter { $0.notification != .windowCreated }
                            .map { event in event.runningApp }
                    }
                    .prepend(app)
                    .compactMap { app -> AppKind? in .from(app, preferencesVM: preferencesVM) }
                    .eraseToAnyPublisher()
            }
            .removeDuplicates(by: { $0.isSameAppOrWebsite(with: $1, detectAddressBar: true) })
            .sink { [weak self] in self?.appKind = $0 }
            .store(in: cancelBag)
    }
}

extension ApplicationVM {
    private func watchAppsDiffChange() {
        AppsDiff
            .publisher(preferencesVM: preferencesVM)
            .assign(to: &$appsDiff)
    }

    private func activateAccessibilitiesForCurrentApp() {
        $appKind
            .compactMap { $0 }
            .filter { [weak self] _ in self?.preferencesVM.preferences.isEnhancedModeEnabled == true }
            .filter { [weak self] in self?.preferencesVM.isHideIndicator($0) != true }
            .sink { $0.getApp().activateAccessibilities() }
            .store(in: cancelBag)
    }
}

import AppKit
import AXSwift
import Combine
import CombineExt

// MARK: - Default indicator: auto hide

extension IndicatorWindowController {
    func autoHidePublisher(
        event: IndicatorVM.ActivateEvent,
        inputSource: InputSource,
        appKind: AppKind
    ) -> AnyPublisher<Void, Never> {
        return Just(event)
            .tap { [weak self] in self?.updateIndicator(event: $0, inputSource: inputSource) }
            .flatMapLatest { [weak self] _ -> AnyPublisher<Void, Never> in
                guard let self = self,
                      let appSize = self.getAppSize()
                else { return Empty().eraseToAnyPublisher() }

                return self.preferencesVM
                    .getIndicatorPositionPublisher(appSize: appSize, app: appKind.getApp())
                    .compactMap { $0 }
                    .first()
                    .tap { self.moveIndicator(position: $0) }
                    .flatMapLatest { _ -> AnyPublisher<Bool, Never> in
                        Publishers.Merge(
                            Timer.delay(seconds: 1).mapToVoid(),
                            // hover event
                            self.indicatorVC.hoverableView.hoverdPublisher
                                .filter { $0 }
                                .first()
                                .flatMapLatest { _ in
                                    Timer.delay(seconds: 0.15)
                                }
                                .mapToVoid()
                        )
                        .first()
                        .mapTo(false)
                        .prepend(true)
                        .tap { isActive in
                            self.isActive = isActive
                        }
                        .eraseToAnyPublisher()
                    }
                    .mapToVoid()
                    .eraseToAnyPublisher()
            }
            .mapToVoid()
            .eraseToAnyPublisher()
    }
}

// MARK: - Default indicator: auto show

extension IndicatorWindowController {
    func autoShowPublisher(
        event: IndicatorVM.ActivateEvent,
        inputSource: InputSource,
        appKind: AppKind
    ) -> AnyPublisher<Void, Never> {
        let app = appKind.getApp()
        let application = app.getApplication(preferencesVM: preferencesVM)

        let needActivateAtFirstTime = {
            if preferencesVM.preferences.isActiveWhenSwitchApp {
                return true
            }

            if preferencesVM.preferences.isActiveWhenFocusedElementChangesEnabled,
               let focusedUIElement = app.focuedUIElement(application: application),
               UIElement.isInputContainer(focusedUIElement)
            {
                return true
            }

            return false
        }()

        if !needActivateAtFirstTime, isActive {
            isActive = false
        }

        return app
            .watchAX([.focusedUIElementChanged], [.application, .window])
            .compactMap { _ in app.focuedUIElement(application: application) }
            .removeDuplicates()
            .filter { UIElement.isInputContainer($0) }
            .mapTo(true)
            .prepend(needActivateAtFirstTime)
            .filter { $0 }
            .compactMap { [weak self] _ in self?.autoHidePublisher(event: event, inputSource: inputSource, appKind: appKind) }
            .switchToLatest()
            .eraseToAnyPublisher()
    }
}

// MARK: - Just hide indicator

extension IndicatorWindowController {
    func justHidePublisher() -> AnyPublisher<Void, Never> {
        Just(true)
            .tap { [weak self] _ in self?.isActive = false }
            .mapToVoid()
            .eraseToAnyPublisher()
    }
}

// MARK: - AlwaysOn

extension IndicatorWindowController {
    @MainActor
    enum AlwaysOn {
        enum Event {
            case cursorMoved, showAlwaysOnIndicator, scrollStart, scrollEnd
        }

        struct State {
            typealias Changes = (current: State, prev: State)

            static let initial = State(isShowAlwaysOnIndicator: false, isScrolling: false)

            var isShowAlwaysOnIndicator: Bool
            var isScrolling: Bool

            func reducer(_ event: Event) -> State {
                switch event {
                case .scrollStart:
                    return update {
                        $0.isScrolling = true
                    }
                case .scrollEnd:
                    return update {
                        $0.isScrolling = false
                    }
                case .showAlwaysOnIndicator:
                    return update {
                        $0.isShowAlwaysOnIndicator = true
                    }
                case .cursorMoved:
                    return self
                }
            }

            func update(_ change: (inout State) -> Void) -> State {
                var draft = self

                change(&draft)

                return draft
            }
        }

        static func statePublisher(app: NSRunningApplication) -> AnyPublisher<State.Changes, Never> {
            let show = app.watchAX(
                [.selectedTextChanged],
                [.application, .window] + Role.validInputElms
            )
            .mapTo(Event.cursorMoved)

            let checkIfUnfocusedTimer = Timer.interval(seconds: 1)
                .mapTo(Event.cursorMoved)

            let showAlwaysOnIndicatorTimer = Timer.delay(seconds: 0.8)
                .mapTo(Event.showAlwaysOnIndicator)

            let hide = NSEvent.watch(matching: [.scrollWheel])
                .flatMapLatest { _ in Timer
                    .delay(seconds: 0.3)
                    .mapTo(Event.scrollEnd)
                    .prepend(Event.scrollStart)
                }
                .removeDuplicates()
                .eraseToAnyPublisher()

            return Publishers.MergeMany([show, hide, checkIfUnfocusedTimer, showAlwaysOnIndicatorTimer])
                .prepend(.cursorMoved)
                .scan((State.initial, State.initial)) { changes, event -> State.Changes in
                    (changes.current.reducer(event), changes.current)
                }
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    }

    func alwaysOnPublisher(
        event: IndicatorVM.ActivateEvent,
        inputSource: InputSource,
        appKind: AppKind
    ) -> AnyPublisher<Void, Never> {
        typealias Action = () -> Void

        let app = appKind.getApp()
        var isAlwaysOnIndicatorShowed = false

        updateIndicator(
            event: event,
            inputSource: inputSource
        )

        return AlwaysOn
            .statePublisher(app: app)
            .flatMapLatest { [weak self] state -> AnyPublisher<Action, Never> in
                let ACTION_HIDE: Action = { self?.isActive = false }
                let ACTION_SHOW: Action = { self?.isActive = true }
                let ACTION_SHOW_ALWAYS_ON_INDICATOR: Action = { self?.indicatorVC.showAlwaysOnView() }

                if !state.current.isScrolling,
                   let self = self,
                   let appSize = self.getAppSize()
                {
                    return self.preferencesVM.getIndicatorPositionPublisher(appSize: appSize, app: app)
                        .map { position -> Action in
                            guard let position = position
                            else { return ACTION_HIDE }

                            return {
                                if state.current.isShowAlwaysOnIndicator,
                                   !isAlwaysOnIndicatorShowed
                                {
                                    isAlwaysOnIndicatorShowed = true
                                    ACTION_SHOW_ALWAYS_ON_INDICATOR()
                                }

                                if position.kind.isInputArea {
                                    self.moveIndicator(position: position)
                                    ACTION_SHOW()
                                } else {
                                    if state.current.isShowAlwaysOnIndicator {
                                        ACTION_HIDE()
                                    } else {
                                        self.moveIndicator(position: position)
                                        ACTION_SHOW()
                                    }
                                }
                            }
                        }
                        .eraseToAnyPublisher()
                } else {
                    return Just(ACTION_HIDE).eraseToAnyPublisher()
                }
            }
            .tap { $0() }
            .mapToVoid()
            .eraseToAnyPublisher()
    }
}

import AppKit
import AXSwift
import Combine
import CombineExt
import LaunchAtLogin
import Sparkle
import SwiftUI

private enum UpdateFeed {
    static let stable = URL(string: "https://inputsource.pro/stable/appcast.xml")!
    static let beta = URL(string: "https://inputsource.pro/beta/appcast.xml")!
}

@MainActor
final class PreferencesVM: NSObject, ObservableObject {
    @Published
    var preferences = Preferences()

    @Published
    var keyboardConfigs: [KeyboardConfig] = []

    var permissionsVM: PermissionsVM

    var updaterController: SPUStandardUpdaterController?

    var cancelBag = CancelBag()

    var appKeyboardCache = AppKeyboardCache()

    let container: NSPersistentContainer
    let mainStorage: MainStorage

    let versionStr = "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")"

    let buildStr = "\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown")"

    @Published
    var automaticallyChecksForUpdates = false

    @Published
    var canChecksForUpdates = false

    init(permissionsVM: PermissionsVM) {
        self.permissionsVM = permissionsVM
        container = NSPersistentContainer(name: "Main")
        mainStorage = MainStorage(container: container)

        super.init();

        setupAutoUpdate()

        // TODO: - Move to MainStorage
        container.loadPersistentStores { _, error in
            if let error = error {
                print("Core Data 'Main' failed to load: \(error.localizedDescription)")
            } else {
                self.mainStorage.refresh()
            }
        }

        if preferences.prevInstalledBuildVersion == 0 {
            for filterApp in filterApps(NSWorkspace.shared.runningApplications) {
                addAppCustomization(filterApp)
            }
        }

        cleanRemovedAppCustomizationIfNeed()
        migratePreferncesIfNeed()
        migrateBoutiqueIfNeed()
        watchKeyboardConfigsChange()
        watchPreferenceChanges()
    }

    func update(_ change: (inout Preferences) -> Void) {
        var draft = preferences

        change(&draft)

        preferences = draft
    }

    func saveContext(_ callback: (() -> Void)? = nil) {
        if let callback = callback {
            container.viewContext.performAndWait {
                callback()
                save()
            }
        } else {
            save()
        }

        func save() {
            do {
                try container.viewContext.save()
            } catch {
                print("saveAppCustomization error: \(error.localizedDescription)")
            }
        }
    }
}

extension PreferencesVM {
    private func watchPreferenceChanges() {
        $preferences
            .map(\.isLaunchAtLogin)
            .removeDuplicates()
            .sink { LaunchAtLogin.isEnabled = $0 }
            .store(in: cancelBag)
    }

    private func watchKeyboardConfigsChange() {
        mainStorage.keyboardConfigs
            .assign(to: &$keyboardConfigs)
    }
}

extension PreferencesVM: @preconcurrency SPUUpdaterDelegate {
    private func setupAutoUpdate() {
        updaterController = SPUStandardUpdaterController(
            startingUpdater: true,
            updaterDelegate: self,
            userDriverDelegate: nil
        )

        updaterController?.updater
            .publisher(for: \.automaticallyChecksForUpdates)
            .removeDuplicates()
            .assign(to: &$automaticallyChecksForUpdates)

        $automaticallyChecksForUpdates
            .dropFirst()
            .sink { [weak self] in self?.updaterController?.updater.automaticallyChecksForUpdates = $0 }
            .store(in: cancelBag)

        updaterController?.updater
            .publisher(for: \.canCheckForUpdates)
            .debounce(for: 0.5, scheduler: DispatchQueue.main)
            .assign(to: &$canChecksForUpdates)
    }

    func checkUpdates() {
        updaterController?.updater.checkForUpdates()
    }

    func feedURLString(for updater: SPUUpdater) -> String? {
        let url = preferences.receiveBetaUpdates ? UpdateFeed.beta : UpdateFeed.stable
        return url.absoluteString
    }
}

extension PreferencesVM {
    func getBrowserURL(_ bundleIdentifier: String?, application: Application?) -> URL? {
        guard
            permissionsVM.isAccessibilityEnabled,
            let bundleIdentifier = bundleIdentifier,
            let browser = Browser(rawValue: bundleIdentifier)
        else { return nil }

        switch browser {
        case .Safari:
            guard preferences.isEnableURLSwitchForSafari else { return nil }
        case .SafariTechnologyPreview:
            guard preferences.isEnableURLSwitchForSafariTechnologyPreview else { return nil }
        case .Chrome:
            guard preferences.isEnableURLSwitchForChrome else { return nil }
        case .Chromium:
            guard preferences.isEnableURLSwitchForChromium else { return nil }
        case .Arc:
            guard preferences.isEnableURLSwitchForArc else { return nil }
        case .Edge:
            guard preferences.isEnableURLSwitchForEdge else { return nil }
        case .Brave:
            guard preferences.isEnableURLSwitchForBrave else { return nil }
        case .BraveBeta:
            guard preferences.isEnableURLSwitchForBraveBeta else { return nil }
        case .BraveNightly:
            guard preferences.isEnableURLSwitchForBraveNightly else { return nil }
        case .Vivaldi:
            guard preferences.isEnableURLSwitchForVivaldi else { return nil }
        case .Opera:
            guard preferences.isEnableURLSwitchForOpera else { return nil }
        case .Thorium:
            guard preferences.isEnableURLSwitchForThorium else { return nil }
        case .Firefox:
            guard preferences.isEnableURLSwitchForFirefox else { return nil }
        case .FirefoxDeveloperEdition:
            guard preferences.isEnableURLSwitchForFirefoxDeveloperEdition else { return nil }
        case .FirefoxNightly:
            guard preferences.isEnableURLSwitchForFirefoxNightly else { return nil }
        case .Zen:
            guard preferences.isEnableURLSwitchForZen else { return nil }
        case .Dia:
            guard preferences.isEnableURLSwitchForDia else { return nil }
        }

        if let application = application,
           let focusedWindow: UIElement = try? application.attribute(.focusedWindow),
           let url = browser.getCurrentTabURL(focusedWindow: focusedWindow)
        {
            return url
        } else {
            if browser == .Safari || browser == .SafariTechnologyPreview {
                return .newtab
            } else {
                return nil
            }
        }
    }

    func filterApps(_ apps: [NSRunningApplication]) -> [NSRunningApplication] {
        let isDetectSpotlightLikeApp = preferences.isEnhancedModeEnabled

        return apps.filter { app in
            (isDetectSpotlightLikeApp && NSApplication.isFloatingApp(app.bundleIdentifier))
                || app.activationPolicy == .regular
        }
    }
}

struct Preferences {
    enum AppearanceMode: String, CaseIterable, Codable, Equatable {
        case dark = "Dark"
        case light = "Light"
    }

    private enum Key {
        static let prevInstalledBuildVersion = "prevInstalledBuildVersion"

        static let isLaunchAtLogin = "isLaunchAtLogin"
        static let isShowIconInMenuBar = "isShowIconInMenuBar"
        static let isEnhancedModeEnabled = "isDetectSpotlightLikeApp"
        static let isCJKVFixEnabled = "isCJKVFixEnabled"

        static let systemWideDefaultKeyboardId = "systemWideDefaultKeyboardId"

        static let browserAddressDefaultKeyboardId = "browserAddressDefaultKeyboardId"
        static let isActiveWhenLongpressLeftMouse = "isActiveWhenLongpressLeftMouse"
        static let isActiveWhenFocusedElementChanges = "isActiveWhenFocusedElementChanges"
        static let isActiveWhenSwitchApp = "isActiveWhenSwitchApp"
        static let isHideWhenSwitchAppWithForceKeyboard = "isHideWhenSwitchAppWithForceKeyboard"
        static let isActiveWhenSwitchInputSource = "isActiveWhenSwitchInputSource"

        static let isRestorePreviouslyUsedInputSource = "isRestorePreviouslyUsedInputSource"

        static let isEnableURLSwitchForSafari = "isEnableURLSwitchForSafari"
        static let isEnableURLSwitchForSafariTechnologyPreview = "isEnableURLSwitchForSafariTechnologyPreview"
        static let isEnableURLSwitchForChrome = "isEnableURLSwitchForChrome"
        static let isEnableURLSwitchForChromium = "isEnableURLSwitchForChromium"
        static let isEnableURLSwitchForArc = "isEnableURLSwitchForArc"
        static let isEnableURLSwitchForEdge = "isEnableURLSwitchForEdge"
        static let isEnableURLSwitchForBrave = "isEnableURLSwitchForBrave"
        static let isEnableURLSwitchForBraveBeta = "isEnableURLSwitchForBraveBeta"
        static let isEnableURLSwitchForBraveNightly = "isEnableURLSwitchForBraveNightly"
        static let isEnableURLSwitchForVivaldi = "isEnableURLSwitchForVivaldi"
        static let isEnableURLSwitchForOpera = "isEnableURLSwitchForOpera"
        static let isEnableURLSwitchForThorium = "isEnableURLSwitchForThorium"
        static let isEnableURLSwitchForFirefox = "isEnableURLSwitchForFirefox"
        static let isEnableURLSwitchForFirefoxDeveloperEdition = "isEnableURLSwitchForFirefoxDeveloperEdition"
        static let isEnableURLSwitchForFirefoxNightly = "isEnableURLSwitchForFirefoxNightly"
        static let isEnableURLSwitchForZen = "isEnableURLSwitchForZen"
        static let isEnableURLSwitchForDia = "isEnableURLSwitchForDia"

        static let isAutoAppearanceMode = "isAutoAppearanceMode"
        static let appearanceMode = "appearanceMode"
        static let isShowInputSourcesLabel = "isShowInputSourcesLabel"
        static let indicatorBackground = "indicatorBackground"
        static let indicatorForgeground = "indicatorForgeground"

        static let tryToDisplayIndicatorNearCursor = "tryToDisplayIndicatorNearCursor"
        static let isEnableAlwaysOnIndicator = "isEnableAlwaysOnIndicator"

        static let indicatorPosition = "indicatorPosition"
        static let indicatorPositionAlignment = "indicatorPositionAlignment"
        static let indicatorPositionSpacing = "indicatorPositionSpacing"

        static let indicatorSize = "indicatorSize"
        static let indicatorInfo = "indicatorInfo"

        static let receiveBetaUpdates = "receiveBetaUpdates"
    }

    fileprivate init() {}

    @UserDefault(Preferences.Key.prevInstalledBuildVersion)
    var prevInstalledBuildVersion = 0

    // MARK: - General

    @UserDefault(Preferences.Key.isLaunchAtLogin)
    var isLaunchAtLogin = LaunchAtLogin.isEnabled

    @UserDefault(Preferences.Key.isShowIconInMenuBar)
    var isShowIconInMenuBar = true

    @UserDefault(Preferences.Key.isEnhancedModeEnabled)
    var isEnhancedModeEnabled = false

    @UserDefault(Preferences.Key.isCJKVFixEnabled)
    var isCJKVFixEnabled = false

    @UserDefault(Preferences.Key.receiveBetaUpdates)
    var receiveBetaUpdates = false

    // MARK: - Triggers

    @UserDefault(Preferences.Key.isActiveWhenLongpressLeftMouse)
    var isActiveWhenLongpressLeftMouse = true

    @UserDefault(Preferences.Key.isActiveWhenSwitchApp)
    var isActiveWhenSwitchApp = true

    @UserDefault(Preferences.Key.isHideWhenSwitchAppWithForceKeyboard)
    var isHideWhenSwitchAppWithForceKeyboard = false

    @UserDefault(Preferences.Key.isActiveWhenSwitchInputSource)
    var isActiveWhenSwitchInputSource = true

    @UserDefault(Preferences.Key.isActiveWhenFocusedElementChanges)
    var isActiveWhenFocusedElementChanges = true

    // MARK: - Input Source

    @UserDefault(Preferences.Key.isRestorePreviouslyUsedInputSource)
    var isRestorePreviouslyUsedInputSource = false

    // MARK: - App Rules

    @UserDefault(Preferences.Key.systemWideDefaultKeyboardId)
    var systemWideDefaultKeyboardId = ""

    // MARK: - Browser Rules

    @UserDefault(Preferences.Key.browserAddressDefaultKeyboardId)
    var browserAddressDefaultKeyboardId = ""

    @UserDefault(Preferences.Key.isEnableURLSwitchForSafari)
    var isEnableURLSwitchForSafari = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForSafariTechnologyPreview)
    var isEnableURLSwitchForSafariTechnologyPreview = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForChrome)
    var isEnableURLSwitchForChrome = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForChromium)
    var isEnableURLSwitchForChromium = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForArc)
    var isEnableURLSwitchForArc = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForEdge)
    var isEnableURLSwitchForEdge = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForBrave)
    var isEnableURLSwitchForBrave = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForBraveBeta)
    var isEnableURLSwitchForBraveBeta = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForBraveNightly)
    var isEnableURLSwitchForBraveNightly = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForVivaldi)
    var isEnableURLSwitchForVivaldi = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForOpera)
    var isEnableURLSwitchForOpera = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForThorium)
    var isEnableURLSwitchForThorium = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForFirefox)
    var isEnableURLSwitchForFirefox = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForFirefoxDeveloperEdition)
    var isEnableURLSwitchForFirefoxDeveloperEdition = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForFirefoxNightly)
    var isEnableURLSwitchForFirefoxNightly = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForZen)
    var isEnableURLSwitchForZen = false

    @UserDefault(Preferences.Key.isEnableURLSwitchForDia)
    var isEnableURLSwitchForDia = false

    // MARK: - Appearance

    @available(*, deprecated, message: "Use indicatorInfo instead")
    @UserDefault(Preferences.Key.isShowInputSourcesLabel)
    var isShowInputSourcesLabel = true

    @CodableUserDefault(Preferences.Key.indicatorInfo)
    var indicatorInfo = IndicatorInfo.iconAndTitle

    @CodableUserDefault(Preferences.Key.indicatorSize)
    var indicatorSize = IndicatorSize.medium

    @UserDefault(Preferences.Key.isAutoAppearanceMode)
    var isAutoAppearanceMode = true

    @CodableUserDefault(Preferences.Key.appearanceMode)
    var appearanceMode = AppearanceMode.dark

    @CodableUserDefault(Preferences.Key.indicatorBackground)
    var indicatorBackground = IndicatorColor.background

    @CodableUserDefault(Preferences.Key.indicatorForgeground)
    var indicatorForgeground = IndicatorColor.forgeground

    @UserDefault(Preferences.Key.tryToDisplayIndicatorNearCursor)
    var tryToDisplayIndicatorNearCursor = true

    @UserDefault(Preferences.Key.isEnableAlwaysOnIndicator)
    var isEnableAlwaysOnIndicator = false

    @CodableUserDefault(Preferences.Key.indicatorPosition)
    var indicatorPosition = IndicatorPosition.nearMouse

    @CodableUserDefault(Preferences.Key.indicatorPositionAlignment)
    var indicatorPositionAlignment = IndicatorPosition.Alignment.bottomRight

    @CodableUserDefault(Preferences.Key.indicatorPositionSpacing)
    var indicatorPositionSpacing = IndicatorPosition.Spacing.s
}

extension Preferences {
    var shortVersion: String {
        Bundle.main.shortVersion
    }

    var buildVersion: Int {
        Bundle.main.buildVersion
    }

    var isActiveWhenFocusedElementChangesEnabled: Bool {
        return isEnhancedModeEnabled && isActiveWhenFocusedElementChanges
    }

    var indicatorKind: IndicatorKind {
        guard let indicatorInfo = indicatorInfo else { return .iconAndTitle }

        switch indicatorInfo {
        case .iconAndTitle:
            return .iconAndTitle
        case .iconOnly:
            return .icon
        case .titleOnly:
            return .title
        }
    }

    var indicatorBackgroundColor: Color {
        get {
            switch appearanceMode {
            case .dark?:
                return indicatorBackground?.dark ?? IndicatorColor.background.dark
            default:
                return indicatorBackground?.light ?? IndicatorColor.background.light
            }
        }

        set {
            guard let appearanceMode = appearanceMode,
                  let indicatorBackground = indicatorBackground
            else { return }

            switch appearanceMode {
            case .dark:
                self.indicatorBackground = IndicatorColor(
                    light: indicatorBackground.light,
                    dark: newValue
                )
            case .light:
                self.indicatorBackground = IndicatorColor(
                    light: newValue,
                    dark: indicatorBackground.dark
                )
            }
        }
    }

    var indicatorForgegroundColor: Color {
        get {
            switch appearanceMode {
            case .dark?:
                return indicatorForgeground?.dark ?? IndicatorColor.forgeground.dark
            default:
                return indicatorForgeground?.light ?? IndicatorColor.forgeground.light
            }
        }

        set {
            guard let appearanceMode = appearanceMode,
                  let indicatorForgeground = indicatorForgeground
            else { return }

            switch appearanceMode {
            case .dark:
                self.indicatorForgeground = IndicatorColor(
                    light: indicatorForgeground.light,
                    dark: newValue
                )
            case .light:
                self.indicatorForgeground = IndicatorColor(
                    light: newValue,
                    dark: indicatorForgeground.dark
                )
            }
        }
    }
}

extension PreferencesVM {
    var systemWideDefaultKeyboard: InputSource? {
        return InputSource.sources.first { $0.id == preferences.systemWideDefaultKeyboardId }
    }

    var browserAddressDefaultKeyboard: InputSource? {
        return InputSource.sources.first { $0.id == preferences.browserAddressDefaultKeyboardId }
    }
}

extension PreferencesVM {
    func isUseCJKVFix() -> Bool {
        return preferences.isEnhancedModeEnabled && preferences.isCJKVFixEnabled
    }

    func isAbleToQueryLocation(_ app: NSRunningApplication) -> Bool {
        if app.bundleIdentifier == "com.tencent.WeWorkMac" {
            return false
        } else {
            return true
        }
    }

    func isShowAlwaysOnIndicator(app: NSRunningApplication) -> Bool {
        if preferences.isEnableAlwaysOnIndicator,
           isAbleToQueryLocation(app)
        {
            return true
        } else {
            return false
        }
    }

    func needDetectFocusedFieldChanges(app: NSRunningApplication) -> Bool {
        if preferences.isActiveWhenFocusedElementChangesEnabled,
           isAbleToQueryLocation(app)
        {
            return true
        } else {
            return false
        }
    }

    func isHideIndicator(_ appKind: AppKind) -> Bool {
        if let browserRule = appKind.getBrowserInfo()?.rule,
           browserRule.hideIndicator
        {
            return true
        }

        return getAppCustomization(app: appKind.getApp())?.hideIndicator == true
    }

    func needDisplayEnhancedModePrompt(bundleIdentifier: String?) -> Bool {
        NSApplication.isFloatingApp(bundleIdentifier) && !preferences.isEnhancedModeEnabled
    }
}

import AppKit
import AXSwift
import Carbon
import Combine
import Foundation

@MainActor
class InputSourceVM: ObservableObject {
    let preferencesVM: PreferencesVM

    private var _isProgrammaticChange = false

    private var cancelBag = CancelBag()

    private let selectInputSourceSubject = PassthroughSubject<InputSource, Never>()

    private let inputSourceChangesSubject = PassthroughSubject<InputSource, Never>()

    let inputSourceChangesPublisher: AnyPublisher<InputSource, Never>

    init(preferencesVM: PreferencesVM) {
        self.preferencesVM = preferencesVM
        inputSourceChangesPublisher = inputSourceChangesSubject.eraseToAnyPublisher()

        watchSystemNotification()

        selectInputSourceSubject
            .tap { [weak self] inputSource in
                guard let preferencesVM = self?.preferencesVM
                else { return }

                inputSource.select(useCJKVFix: preferencesVM.isUseCJKVFix())
            }
            .flatMapLatest { _ in
                Timer.interval(seconds: 0.2)
                    .map { _ in InputSource.getCurrentInputSource() }
                    .withPrevious()
                    .filter { previous, current in
                        guard let previous = previous else { return false }
                        return previous.id == current.id
                    }
                    .map { _, current in current }
                    .prefix(1)
            }
            .sink { [weak self] in
                self?.inputSourceChangesSubject.send($0)
                self?._isProgrammaticChange = false
            }
            .store(in: cancelBag)
    }

    func select(inputSource: InputSource) {
        _isProgrammaticChange = true
        selectInputSourceSubject.send(inputSource)
    }

    private func watchSystemNotification() {
        DistributedNotificationCenter.default()
            .publisher(for: Notification.Name(rawValue: kTISNotifySelectedKeyboardInputSourceChanged as String))
            .receive(on: DispatchQueue.main)
            .filter { [weak self] _ in self?._isProgrammaticChange == false }
            .map { _ in InputSource.getCurrentInputSource() }
            .sink { [weak self] in self?.inputSourceChangesSubject.send($0) }
            .store(in: cancelBag)
    }

    private func getInputSourceFromMenu() -> InputSource? {
        for app in NSWorkspace.shared.runningApplications {
            if app.bundleIdentifier == "com.apple.TextInputMenuAgent" {
                if let application = app.getApplication(preferencesVM: preferencesVM),
                   let menuBar: UIElement = try? application.attribute(.extrasMenuBar)
                {
                    for child in menuBar.children() ?? [] {
                        if let description: String = try? child.attribute(.description),
                           let inputSource = InputSource.sources.first(where: { $0.name == description })
                        {
                            return inputSource
                        }
                    }
                }

                return nil
            }
        }

        return nil
    }
}

import Cocoa

enum BrowserRuleType: Int32, CaseIterable {
    case domainSuffix = 0
    case domain = 1
    case urlRegex = 2

    var name: String {
        switch self {
        case .domainSuffix:
            return "DOMAIN-SUFFIX"
        case .domain:
            return "DOMAIN"
        case .urlRegex:
            return "URL-REGEX"
        }
    }

    var explanation: String {
        return "\(name) Explanation".i18n()
    }
}

extension BrowserRule {
    @MainActor
    var forcedKeyboard: InputSource? {
        guard let inputSourceId = inputSourceId else { return nil }

        return InputSource.sources.first { $0.id == inputSourceId }
    }

    var type: BrowserRuleType {
        get {
            return BrowserRuleType(rawValue: typeValue) ?? .domainSuffix
        }

        set {
            typeValue = newValue.rawValue
        }
    }

    func validate(url: URL) -> Bool {
        return Self.validate(type: type, url: url, value: value)
    }

    func id() -> String {
        return "\(type.name)_\(value ?? "")_\(createdAt?.timeIntervalSince1970 ?? 0)"
    }
}

extension BrowserRule {
    static func validate(type: BrowserRuleType, url: URL, value: String?) -> Bool {
        switch type {
        case .domainSuffix:
            if let suffix = value,
               let host = url.host,
               let regex = try? NSRegularExpression(pattern: "\(suffix)$", options: .caseInsensitive)
            {
                return regex.matches(host)
            }
            return false
        case .domain:
            return url.host == value
        case .urlRegex:
            if let regex = try? NSRegularExpression(pattern: value ?? "", options: .caseInsensitive) {
                return regex.matches(url.absoluteString)
            }
            return false
        }
    }
}

extension BrowserRule {
    var keyboardRestoreStrategy: KeyboardRestoreStrategy? {
        guard let rawValue = keyboardRestoreStrategyRaw
        else { return nil }

        return KeyboardRestoreStrategy(rawValue: rawValue)
    }
}

import AppKit
import AXSwift

private let chromiumSearchBarDescMap = [
    "Aadressi- ja otsinguriba": true,
    "Address and search bar": true,
    "Address bar at bar sa paghahanap": true,
    "Adres ve arama çubuğu": true,
    "Adres- en soekbalk": true,
    "Adres- en zoekbalk": true,
    "Adreses un meklēšanas josla": true,
    "Adresna traka i traka za pretraživanje": true,
    "Adresní a vyhledávací řádek": true,
    "Adreso ir paieškos juosta": true,
    "Adress- och sökfält": true,
    "Adress- und Suchleiste": true,
    "Adresse og søgelinje": true,
    "Adresse- og søkefelt": true,
    "Bar alamat dan carian": true,
    "Bar cyfeiriad a chwilio": true,
    "Bara de adrese și de căutare": true,
    "Barra d'adreces i de cerca": true,
    "Barra de direcciones y de búsqueda": true,
    "Barra de direcciones y de búsqueda ": true,
    "Barra de enderezos e de busca": true,
    "Barra de endereço e de pesquisa": true,
    "Barra de pesquisa e endereço": true,
    "Barra degli indirizzi e di ricerca": true,
    "Barre d'adresse et de recherche": true,
    "Bilah penelusuran dan alamat": true,
    "Cím- és keresősáv": true,
    "Helbide- eta bilaketa-barra": true,
    "Ikheli nebha yosesho": true,
    "Manzil va qidiruv paneli": true,
    "Naslovna in iskalna vrstica": true,
    "Osoite- ja hakupalkki": true,
    "Panel s adresou a vyhľadávací panel": true,
    "Pasek adresu i wyszukiwania": true,
    "Shiriti i adresës dhe i kërkimit": true,
    "Thanh địa chỉ và tìm kiếm": true,
    "Traka za adresu i pretragu": true,
    "Traka za adresu i pretraživanje": true,
    "Upau wa anwani na utafutaji": true,
    "Veffanga- og leitarstika": true,
    "Ünvan və axtarış paneli": true,
    "Γραμμή διευθύνσεων και αναζήτησης": true,
    "Адрасны радок і панэль пошуку": true,
    "Адресная строка и строка поиска": true,
    "Адресний і пошуковий рядок": true,
    "Дарек жана издөө тилкеси": true,
    "Лента за адреса и за пребарување": true,
    "Лента за адреси и за търсене": true,
    "Мекенжайы және іздеу жолағы": true,
    "Трака за адресу и претрагу": true,
    "Хаяг ба хайлтын цонх": true,
    "Հասցեագոտի և որոնման գոտի": true,
    "שורת חיפוש וכתובות אתרים": true,
    "شريط العناوين والبحث": true,
    "نوار جستجو و آدرس": true,
    "پتہ اور تلاش بار": true,
    "ठेगाना र खोज पट्टी": true,
    "पता और सर्च बार": true,
    "पत्ता आणि शोध बार": true,
    "ঠিকনা আৰু সন্ধানৰ বাৰ": true,
    "ঠিকানা এবং সার্চ দণ্ড": true,
    "ਪਤਾ ਅਤੇ ਖੋਜ ਬਾਰ": true,
    "સરનામું અને શોધ બાર": true,
    "ଠିକଣା ଏବଂ ସନ୍ଧାନ ବାର୍": true,
    "முகவரி மற்றும் தேடல் பட்டி": true,
    "అడ్రస్ మరియు శోధన బార్": true,
    "ವಿಳಾಸ ಹಾಗೂ ಹುಡುಕಾಟ ಪಟ್ಟಿ": true,
    "വിലാസവും തിരയൽ ബാറും": true,
    "ලිපිනය සහ සෙවීම් බාර් එක": true,
    "ที่อยู่และแถบค้นหา": true,
    "ແຖບທີ່ຢູ່ ແລະຄົ້ນຫາ": true,
    "လိပ်စာ နှင့် ရှာဖွေရေး ဘား": true,
    "მისამართი და ძიების ზოლი": true,
    "የአድራሻ እና ፍለጋ አሞሌ": true,
    "អាសយដ្ឋាន និងរបាស្វែងរក": true,
    "アドレス検索バー": true,
    "地址和搜索栏": true,
    "網址與搜尋列": true,
    "주소창 및 검색창": true,
]

enum BrowserThatCanWatchBrowserAddressFocus: CaseIterable {
    static var allBundleIdentifiers = allCases.map { $0.bundleIdentifier }

    static func createBy(bundleIdentifier: String?) -> BrowserThatCanWatchBrowserAddressFocus? {
        if let bundleIdentifier = bundleIdentifier {
            for item in allCases {
                if item.bundleIdentifier == bundleIdentifier {
                    return item
                }
            }
        }

        return nil
    }

    case Safari, SafariTechnologyPreview, Chrome, Chromium, Brave, BraveBeta, BraveNightly, Edge, Vivaldi, Arc, Opera, Firefox, FirefoxNightly, FirefoxDeveloperEdition, Zen, Dia

    var bundleIdentifier: String {
        return browser.rawValue
    }

    var browser: Browser {
        switch self {
        case .Safari:
            return .Safari
        case .SafariTechnologyPreview:
            return .SafariTechnologyPreview
        case .Chrome:
            return .Chrome
        case .Chromium:
            return .Chromium
        case .Brave:
            return .Brave
        case .BraveBeta:
            return .BraveBeta
        case .BraveNightly:
            return .BraveNightly
        case .Edge:
            return .Edge
        case .Vivaldi:
            return .Vivaldi
        case .Arc:
            return .Arc
        case .Opera:
            return .Opera
        case .Firefox:
            return .Firefox
        case .FirefoxNightly:
            return .FirefoxNightly
        case .FirefoxDeveloperEdition:
            return .FirefoxDeveloperEdition
        case .Zen:
            return .Zen
        case .Dia:
            return .Dia
        }
    }

    func isFocusOnBrowserAddress(focusedElement: UIElement?) -> Bool {
        guard let focusedElement = focusedElement
        else { return false }

        switch self {
        case .Safari, .SafariTechnologyPreview:
            return focusedElement.domIdentifier() == "WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD"

        case .Arc:
            return focusedElement.domIdentifier() == "commandBarTextField"

        case .Vivaldi:
            let classList = focusedElement.domClassList()
            return classList.contains("UrlBar-UrlField") && classList.contains("vivaldi-addressfield")

        case .Opera:
            return focusedElement.domClassList().contains("AddressTextfieldView")

        case .Chromium, .Chrome, .Brave, .BraveBeta, .BraveNightly, .Edge, .Dia:
            if focusedElement.domClassList().contains("OmniboxViewViews") {
                if let description = focusedElement.safeString(attribute: .description),
                   chromiumSearchBarDescMap[description] == true
                {
                    return true
                }

                if let title = focusedElement.safeString(attribute: .title),
                   chromiumSearchBarDescMap[title] == true
                {
                    return true
                }

                return false
            } else {
                return false
            }

        case .Firefox, .FirefoxNightly, .FirefoxDeveloperEdition, .Zen:
            return focusedElement.firefoxDomIdentifier() == "urlbar-input"
        }
    }
}

extension PreferencesVM {
    func isFocusOnBrowserAddress(app: NSRunningApplication?, focusedElement: UIElement?) -> Bool {
        return BrowserThatCanWatchBrowserAddressFocus
            .createBy(bundleIdentifier: app?.bundleIdentifier)?
            .isFocusOnBrowserAddress(focusedElement: focusedElement) ?? false
    }
}

import Carbon
import Cocoa
import Combine
import CoreGraphics
import CryptoKit

@MainActor
class InputSource {
    static let logger = ISPLogger(
        category: "🤖 " + String(describing: InputSource.self),
        disabled: true
    )

    let tisInputSource: TISInputSource

    let icon: NSImage?

    var id: String { tisInputSource.id }
    var name: String { tisInputSource.name }

    var isCJKVR: Bool {
        guard let lang = tisInputSource.sourceLanguages.first else { return false }

        return lang == "ru" || lang == "ko" || lang == "ja" || lang == "vi" || lang.hasPrefix("zh")
    }

    init(tisInputSource: TISInputSource) {
        self.tisInputSource = tisInputSource

        icon = { () -> NSImage? in
            if let imgName = Self.iconMap[String(tisInputSource.id.sha256().prefix(8))],
               let image = NSImage(named: imgName)
            {
                return image.markTemplateIfGrayScaleOrPdf()
            }

            guard let imageURL = tisInputSource.iconImageURL else {
                if #available(macOS 11.0, *) {
                    return nil
                } else {
                    if let iconRef = tisInputSource.iconRef {
                        return NSImage(iconRef: iconRef).markTemplateIfGrayScaleOrPdf()
                    } else {
                        return nil
                    }
                }
            }

            for url in [imageURL.retinaImageURL, imageURL.tiffImageURL, imageURL] {
                if let image = NSImage(contentsOf: url) {
                    return image.markTemplateIfGrayScaleOrPdf(url: url)
                }
            }

            if let baseURL = imageURL.baseURL,
               baseURL.lastPathComponent.hasSuffix(".app")
            {
                return NSWorkspace.shared.icon(forFile: baseURL.relativePath)
            }

            return nil
        }()
    }

    func select(useCJKVFix: Bool) {
        Self.logger.debug { "Select \(id)" }

        guard Self.getCurrentInputSource().id != id else {
            Self.logger.debug { "Skip Select \(id)" }
            return
        }

        let updateStrategy: String = {
            if isCJKVR {
                // https://stackoverflow.com/a/60375569
                if useCJKVFix,
                   PermissionsVM.checkAccessibility(prompt: false),
                   let selectPreviousShortcut = Self.getSelectPreviousShortcut()
                {
                    TISSelectInputSource(tisInputSource)

                    // Workaround for TIS CJKV layout bug:
                    // when it's CJKV, select nonCJKV input first and then return
                    if let nonCJKV = Self.nonCJKVSource() {
                        Self.logger.debug { "S1: Start" }
                        TISSelectInputSource(nonCJKV.tisInputSource)
                        Self.logger.debug { "S1: selectPrevious" }
                        Self.selectPrevious(shortcut: selectPreviousShortcut)
                        Self.logger.debug { "S1: Done" }
                    }

                    return "S1"
                } else {
                    TISSelectInputSource(tisInputSource)
                    return "S0-2"
                }
            } else {
                TISSelectInputSource(tisInputSource)

                return "S0-2"
            }
        }()

        Self.logger.debug { "Select by \(updateStrategy)" }
    }
}

extension InputSource: @preconcurrency Equatable {
    static func == (lhs: InputSource, rhs: InputSource) -> Bool {
        return lhs.id == rhs.id
    }
}

extension InputSource {
    private static var cancelBag = CancelBag()

    @MainActor
    static func getCurrentInputSource() -> InputSource {
        return InputSource(tisInputSource: TISCopyCurrentKeyboardInputSource().takeRetainedValue())
    }
}

extension InputSource {
    static var sources: [InputSource] {
        let inputSourceNSArray = TISCreateInputSourceList(nil, false).takeRetainedValue() as NSArray
        let inputSourceList = inputSourceNSArray as! [TISInputSource]

        return inputSourceList
            .filter { $0.category == TISInputSource.Category.keyboardInputSource && $0.isSelectable }
            .map { InputSource(tisInputSource: $0) }
    }

    static func nonCJKVSource() -> InputSource? {
        return sources.first(where: { !$0.isCJKVR })
    }

    static func anotherCJKVSource(current: InputSource) -> InputSource? {
        return sources.first(where: { $0 != current && $0.isCJKVR })
    }

    static func selectPrevious(shortcut: (Int, UInt64)) {
        let src = CGEventSource(stateID: .hidSystemState)

        let key = CGKeyCode(shortcut.0)
        let flag = CGEventFlags(rawValue: shortcut.1)

        let down = CGEvent(keyboardEventSource: src, virtualKey: key, keyDown: true)!
        let up = CGEvent(keyboardEventSource: src, virtualKey: key, keyDown: false)!

        down.flags = flag
        up.flags = flag

        let ctrlDown = CGEvent(keyboardEventSource: nil, virtualKey: UInt16(kVK_Control), keyDown: true)
        let ctrlUp = CGEvent(keyboardEventSource: nil, virtualKey: UInt16(kVK_Control), keyDown: false)

        ctrlDown?.post(tap: .cghidEventTap)
        down.post(tap: .cghidEventTap)
        up.post(tap: .cghidEventTap)
        ctrlUp?.post(tap: .cghidEventTap)
    }

    // from read-symbolichotkeys script of Karabiner
    // github.com/tekezo/Karabiner/blob/master/src/util/read-symbolichotkeys/read-symbolichotkeys/main.m
    static func getSelectPreviousShortcut() -> (Int, UInt64)? {
        guard let dict = UserDefaults.standard.persistentDomain(forName: "com.apple.symbolichotkeys") else {
            return nil
        }
        guard let symbolichotkeys = dict["AppleSymbolicHotKeys"] as! NSDictionary? else {
            return nil
        }
        guard let symbolichotkey = symbolichotkeys["60"] as! NSDictionary? else {
            return nil
        }
        if (symbolichotkey["enabled"] as! NSNumber).intValue != 1 {
            return nil
        }
        guard let value = symbolichotkey["value"] as! NSDictionary? else {
            return nil
        }
        guard let parameters = value["parameters"] as! NSArray? else {
            return nil
        }
        return (
            (parameters[1] as! NSNumber).intValue,
            (parameters[2] as! NSNumber).uint64Value
        )
    }
}

private extension URL {
    var retinaImageURL: URL {
        var components = pathComponents
        let filename: String = components.removeLast()
        let ext: String = pathExtension
        let retinaFilename = filename.replacingOccurrences(of: "." + ext, with: "@2x." + ext)
        return NSURL.fileURL(withPathComponents: components + [retinaFilename])!
    }

    var tiffImageURL: URL {
        return deletingPathExtension().appendingPathExtension("tiff")
    }
}

extension InputSource: @preconcurrency CustomStringConvertible {
    var description: String {
        id
    }
}

import AppKit
import AXSwift
import Foundation

extension PreferencesVM {
    func addBrowserRule(
        type: BrowserRuleType,
        value: String,
        sample: String,
        inputSourceId: String,
        hideIndicator: Bool,
        keyboardRestoreStrategy: KeyboardRestoreStrategy?
    ) {
        let rule = BrowserRule(context: container.viewContext)

        rule.createdAt = Date()
        rule.type = type
        rule.value = value
        rule.sample = sample
        rule.inputSourceId = inputSourceId
        rule.keyboardRestoreStrategyRaw = keyboardRestoreStrategy?.rawValue
        rule.hideIndicator = hideIndicator
        rule.disabled = false

        saveContext()
    }

    func deleteBrowserRule(_ rule: BrowserRule) {
        container.viewContext.delete(rule)
        saveContext()
    }

    func updateBrowserRule(
        _ rule: BrowserRule,
        type: BrowserRuleType,
        value: String,
        sample: String,
        inputSourceId: String,
        hideIndicator: Bool,
        keyboardRestoreStrategy: KeyboardRestoreStrategy?
    ) {
        saveContext {
            rule.type = type
            rule.value = value
            rule.sample = sample
            rule.inputSourceId = inputSourceId
            rule.hideIndicator = hideIndicator
            rule.keyboardRestoreStrategyRaw = keyboardRestoreStrategy?.rawValue
        }
    }

    func toggleBrowserRule(_ rule: BrowserRule) {
        saveContext {
            rule.disabled.toggle()
        }
    }

    private func getBrowserRules() -> [BrowserRule] {
        let request = NSFetchRequest<BrowserRule>(entityName: "BrowserRule")

        do {
            return try container.viewContext.fetch(request)
        } catch {
            print("getBrowserRules() error: \(error.localizedDescription)")
            return []
        }
    }

    func getBrowserRule(url: URL) -> BrowserRule? {
        return getBrowserRules().first { $0.validate(url: url) }
    }
}

extension PreferencesVM {
    var isNeedToDetectBrowserTabChanges: Bool {
        guard preferences.isEnhancedModeEnabled else { return false }

        return Browser.allCases.contains(where: isBrowserEnabled)
    }

    func isBrowserAndEnabled(_ app: NSRunningApplication) -> Bool {
        guard let browser = NSApplication.getBrowser(app)
        else { return false }

        return isBrowserEnabled(browser)
    }

    func isBrowserEnabled(_ browser: Browser) -> Bool {
        switch browser {
        case .Safari:
            return preferences.isEnableURLSwitchForSafari
        case .SafariTechnologyPreview:
            return preferences.isEnableURLSwitchForSafariTechnologyPreview
        case .Chrome:
            return preferences.isEnableURLSwitchForChrome
        case .Chromium:
            return preferences.isEnableURLSwitchForChromium
        case .Arc:
            return preferences.isEnableURLSwitchForArc
        case .Edge:
            return preferences.isEnableURLSwitchForEdge
        case .Brave:
            return preferences.isEnableURLSwitchForBrave
        case .BraveBeta:
            return preferences.isEnableURLSwitchForBraveBeta
        case .BraveNightly:
            return preferences.isEnableURLSwitchForBraveNightly
        case .Vivaldi:
            return preferences.isEnableURLSwitchForVivaldi
        case .Opera:
            return preferences.isEnableURLSwitchForOpera
        case .Thorium:
            return preferences.isEnableURLSwitchForThorium
        case .Firefox:
            return preferences.isEnableURLSwitchForFirefox
        case .FirefoxDeveloperEdition:
            return preferences.isEnableURLSwitchForFirefoxDeveloperEdition
        case .FirefoxNightly:
            return preferences.isEnableURLSwitchForFirefoxNightly
        case .Zen:
            return preferences.isEnableURLSwitchForZen
        case .Dia:
            return preferences.isEnableURLSwitchForDia
        }
    }

    func getBrowserInfo(app: NSRunningApplication) -> (url: URL, rule: BrowserRule?)? {
        guard let url = getBrowserURL(app.bundleIdentifier, application: Application(app)) else { return nil }

        return (url, getBrowserRule(url: url))
    }
}

import AppKit
import Combine

extension Publisher {
    func tap(_ receiveOutput: @escaping (Output) -> Void) -> AnyPublisher<Output, Failure> {
        handleEvents(receiveOutput: { output in
            receiveOutput(output)
        })
        .eraseToAnyPublisher()
    }
}

extension Publisher {
    func mapTo<T>(_ value: T) -> AnyPublisher<T, Failure> {
        map { _ in value }
            .eraseToAnyPublisher()
    }
}

extension Publisher {
    typealias WithPrevious<T> = (previous: T?, current: T)

    /// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
    /// The first time the upstream publisher emits an element, the previous element will be `nil`.
    ///
    /// ```
    /// let range = (1...5)
    /// let subscription = range.publisher
    ///   .pairwise()
    ///   .sink { print("(\($0.previous), \($0.current))", terminator: " ") }
    /// ```
    /// Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5)".
    ///
    /// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
    ///
    /// - Note: Based on <https://stackoverflow.com/a/67133582/3532505>.
    func withPrevious() -> AnyPublisher<WithPrevious<Output>, Failure> {
        // `scan()` needs an initial value, which is `nil` in our case.
        // Therefore we have to return an optional here and use `compactMap()` below the remove the optional type.
        scan(nil) { previousPair, currentElement -> WithPrevious<Output>? in
            WithPrevious(previous: previousPair?.current, current: currentElement)
        }
        .compactMap { $0 }
        .eraseToAnyPublisher()
    }
}

import AppKit

extension NSImage {
    func markTemplateIfGrayScaleOrPdf(url: URL? = nil) -> NSImage {
        let image = copy() as! NSImage

        if url?.pathExtension == "pdf" || isGrayScale() {
            image.isTemplate = true
        }

        return image
    }

    func isGrayScale() -> Bool {
        guard let imageRef = cgImage(),
              let colorSpace = imageRef.colorSpace
        else { return false }

        if colorSpace.model == .monochrome {
            return true
        }

        guard let imageData = imageRef.dataProvider?.data,
              let rawData = CFDataGetBytePtr(imageData)
        else { return false }

        var byteIndex = 0

        for _ in 0 ..< imageRef.width * imageRef.height {
            let r = rawData[byteIndex]
            let g = rawData[byteIndex + 1]
            let b = rawData[byteIndex + 2]

            if r == g && g == b {
                byteIndex += 4
            } else {
                return false
            }
        }

        return true
    }

    func cgImage() -> CGImage? {
        var rect = CGRect(origin: .zero, size: size)
        return cgImage(forProposedRect: &rect, context: nil, hints: nil)
    }
}

import AppKit
import AXSwift
import Combine
import CombineExt

extension PreferencesVM {
    func calcSpacing(minLength: CGFloat) -> CGFloat {
        guard let spacing = preferences.indicatorPositionSpacing else { return 0 }

        switch spacing {
        case .none:
            return 0
        case .xs:
            return minLength * 0.02
        case .s:
            return minLength * 0.05
        case .m:
            return minLength * 0.08
        case .l:
            return minLength * 0.13
        case .xl:
            return minLength * 0.21
        }
    }

    typealias IndicatorPositionInfo = (kind: IndicatorActuallyPositionKind, point: CGPoint)

    func getIndicatorPositionPublisher(
        appSize: CGSize,
        app: NSRunningApplication
    ) -> AnyPublisher<IndicatorPositionInfo?, Never> {
        Just(preferences.indicatorPosition)
            .compactMap { $0 }
            .flatMapLatest { [weak self] position -> AnyPublisher<IndicatorPositionInfo?, Never> in
                let DEFAULT = self?.getIndicatorBasePosition(
                    appSize: appSize,
                    app: app,
                    position: position
                ) ?? Empty(completeImmediately: true).eraseToAnyPublisher()

                guard let self = self
                else { return DEFAULT }

                return self.getPositionAroundFloatingWindow(app, size: appSize)
                    .flatMapLatest { positionForFloatingWindow -> AnyPublisher<IndicatorPositionInfo?, Never> in
                        if let positionForFloatingWindow = positionForFloatingWindow {
                            return Just((.floatingApp, positionForFloatingWindow)).eraseToAnyPublisher()
                        }

                        if self.preferences.isEnhancedModeEnabled,
                           self.preferences.tryToDisplayIndicatorNearCursor == true,
                           self.isAbleToQueryLocation(app)
                        {
                            return self.getPositionAroundInputCursor(size: appSize)
                                .map { cursorPosition -> AnyPublisher<IndicatorPositionInfo?, Never> in
                                    guard let cursorPosition = cursorPosition else { return DEFAULT }

                                    return Just((cursorPosition.isContainer ? .inputRect : .inputCursor, cursorPosition.point))
                                        .eraseToAnyPublisher()
                                }
                                .switchToLatest()
                                .eraseToAnyPublisher()
                        }

                        return DEFAULT
                    }
                    .eraseToAnyPublisher()
            }
            .eraseToAnyPublisher()
    }

    func getIndicatorBasePosition(
        appSize: CGSize,
        app: NSRunningApplication,
        position: IndicatorPosition
    ) -> AnyPublisher<IndicatorPositionInfo?, Never> {
        Just(position)
            .flatMapLatest { [weak self] _ -> AnyPublisher<IndicatorPositionInfo?, Never> in
                guard let self = self else { return Just(nil).eraseToAnyPublisher() }

                switch position {
                case .nearMouse:
                    return self.getPositionNearMouse(size: appSize)
                        .map {
                            guard let position = $0 else { return nil }
                            return (.nearMouse, position)
                        }
                        .eraseToAnyPublisher()
                case .windowCorner:
                    return self.getPositionRelativeToAppWindow(size: appSize, app)
                        .map {
                            guard let position = $0 else { return nil }
                            return (.windowCorner, position)
                        }
                        .eraseToAnyPublisher()
                case .screenCorner:
                    return self.getPositionRelativeToScreen(size: appSize, app)
                        .map {
                            guard let position = $0 else { return nil }
                            return (.screenCorner, position)
                        }
                        .eraseToAnyPublisher()
                }
            }
            .eraseToAnyPublisher()
    }
}

private extension PreferencesVM {
    func getPositionAroundInputCursor(
        size _: CGSize
    ) -> AnyPublisher<(point: CGPoint, isContainer: Bool)?, Never> {
        Future { promise in
            DispatchQueue.global().async {
                guard let rectInfo = systemWideElement.getCursorRectInfo(),
                      let screen = NSScreen.getScreenInclude(rect: rectInfo.rect)
                else { return promise(.success(nil)) }

                if rectInfo.isContainer,
                   rectInfo.rect.width / screen.frame.width > 0.7 &&
                   rectInfo.rect.height / screen.frame.height > 0.7
                {
                    return promise(.success(nil))
                }

                let offset: CGFloat = 6

                return promise(.success((
                    CGPoint(x: rectInfo.rect.minX, y: rectInfo.rect.maxY + offset),
                    rectInfo.isContainer
                )))
            }
        }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
    }

    func getPositionNearMouse(size: CGSize) -> AnyPublisher<CGPoint?, Never> {
        AnyPublisher.create { observer in
            guard let screen = NSScreen.getScreenWithMouse() else { return AnyCancellable {} }

            let offset: CGFloat = 12
            let padding: CGFloat = 5
            let maxXPoint = screen.visibleFrame.origin.x + screen.visibleFrame.width

            var mousePoint = CGPoint(x: NSEvent.mouseLocation.x, y: NSEvent.mouseLocation.y)

            // default offset
            mousePoint.x += offset
            mousePoint.y -= offset

            // move app to cursor's right/bottom edge
            mousePoint.y -= size.height

            // avoid overflow
            mousePoint.x = min(maxXPoint - size.width - padding, mousePoint.x)
            mousePoint.y = max(padding, mousePoint.y)

            observer.send(mousePoint)
            observer.send(completion: .finished)

            return AnyCancellable {}
        }
    }

    func getPositionRelativeToAppWindow(
        size: CGSize,
        _ app: NSRunningApplication
    ) -> AnyPublisher<CGPoint?, Never> {
        app.getWindowInfoPublisher()
            .map { [weak self] windowInfo -> CGPoint? in
                guard let self = self,
                      let windowBounds = windowInfo?.bounds,
                      let screen = NSScreen.getScreenInclude(rect: windowBounds)
                else { return nil }

                let rect = windowBounds.relativeTo(screen: screen)

                return self.getPositionWithin(
                    rect: rect,
                    size: size,
                    alignment: self.preferences.indicatorPositionAlignment ?? .bottomRight
                )
            }
            .eraseToAnyPublisher()
    }

    func getPositionRelativeToScreen(
        size: CGSize,
        _ app: NSRunningApplication
    ) -> AnyPublisher<CGPoint?, Never> {
        app.getWindowInfoPublisher()
            .map { [weak self] windowInfo -> CGPoint? in
                guard let self = self,
                      let windowBounds = windowInfo?.bounds,
                      let screen = NSScreen.getScreenInclude(rect: windowBounds) ??
                      NSScreen.getScreenWithMouse() ??
                      NSScreen.main
                else { return nil }

                return self.getPositionWithin(
                    rect: screen.visibleFrame,
                    size: size,
                    alignment: self.preferences.indicatorPositionAlignment ?? .bottomRight
                )
            }
            .eraseToAnyPublisher()
    }

    func getPositionAround(rect: CGRect) -> (NSScreen, CGPoint)? {
        guard let screen = NSScreen.getScreenInclude(rect: rect) else { return nil }

        return (screen, rect.relativeTo(screen: screen).origin)
    }

    func getPositionAroundFloatingWindow(
        _ app: NSRunningApplication, size: CGSize
    ) -> AnyPublisher<CGPoint?, Never> {
        guard NSApplication.isSpotlightLikeApp(app.bundleIdentifier) else { return Just(nil).eraseToAnyPublisher() }

        return app.getWindowInfoPublisher()
            .map { [weak self] windowInfo -> CGPoint? in
                guard let self = self,
                      let rect = windowInfo?.bounds,
                      let (screen, point) = self.getPositionAround(rect: rect)
                else { return nil }

                let offset: CGFloat = 6

                let position = CGPoint(
                    x: point.x,
                    y: point.y + rect.height + offset
                )

                if screen.frame.contains(CGRect(origin: position, size: size)) {
                    return position
                } else {
                    return nil
                }
            }
            .eraseToAnyPublisher()
    }

    func getPositionWithin(
        rect: NSRect,
        size: CGSize,
        alignment: IndicatorPosition.Alignment
    ) -> CGPoint {
        let spacing = calcSpacing(minLength: min(rect.width, rect.height))

        switch alignment {
        case .topLeft:
            return CGPoint(
                x: rect.minX + spacing,
                y: rect.maxY - size.height - spacing
            )
        case .topCenter:
            return CGPoint(
                x: rect.midX - size.width / 2,
                y: rect.maxY - size.height - spacing
            )
        case .topRight:
            return CGPoint(
                x: rect.maxX - size.width - spacing,
                y: rect.maxY - size.height - spacing
            )
        case .center:
            return CGPoint(
                x: rect.midX - size.width / 2,
                y: rect.midY + size.height / 2
            )
        case .centerLeft:
            return CGPoint(
                x: rect.minX + spacing,
                y: rect.midY - size.height / 2
            )
        case .centerRight:
            return CGPoint(
                x: rect.maxX - size.width - spacing,
                y: rect.midY - size.height / 2
            )
        case .bottomLeft:
            return CGPoint(
                x: rect.minX + spacing,
                y: rect.minY + spacing
            )
        case .bottomCenter:
            return CGPoint(
                x: rect.midX - size.width / 2,
                y: rect.minY + spacing
            )
        case .bottomRight:
            return CGPoint(
                x: rect.maxX - size.width - spacing,
                y: rect.minY + spacing
            )
        }
    }
}

import Cocoa
import Combine

@MainActor
class StatusItemController {
    let navigationVM: NavigationVM
    let permissionsVM: PermissionsVM
    let preferencesVM: PreferencesVM
    let applicationVM: ApplicationVM
    let indicatorVM: IndicatorVM
    let feedbackVM: FeedbackVM
    let inputSourceVM: InputSourceVM

    let cancelBag = CancelBag()

    var statusItem: NSStatusItem?
    var hasPreferencesShown = false

    var addRuleMenu: NSMenuItem? {
        guard let app = applicationVM.appKind?.getApp() else { return nil }

        var items: [NSMenuItem] = [
            AppRuleMenuItem(app: app, preferencesVM: preferencesVM, inputSource: nil),
            NSMenuItem.separator(),
        ]

        items[0].toolTip = "Default Keyboard Tooltip".i18n()

        items += InputSource.sources.map {
            AppRuleMenuItem(app: app, preferencesVM: preferencesVM, inputSource: $0)
        }

        items
            .filter { (($0 as? AppRuleMenuItem) != nil) ? true : false }
            .enumerated()
            .forEach { index, item in
                if index < 10 {
                    item.keyEquivalent = "\(index)"
                    item.keyEquivalentModifierMask = .command
                }
            }

        let menu = NSMenuItem(
            title: String(format: "Default Keyboard for %@".i18n(), app.localizedName ?? "Current App".i18n()),
            submenuItems: items
        )

        menu.toolTip = app.bundleIdentifier

        return menu
    }

    var addBrowserRuleMenu: NSMenuItem? {
        guard let browserInfo = applicationVM.appKind?.getBrowserInfo(),
              let host = browserInfo.url.host
        else { return nil }

        var items: [NSMenuItem] = [
            BrowserRuleMenuItem(url: browserInfo.url, preferencesVM: preferencesVM, inputSource: nil),
            NSMenuItem.separator(),
        ]

        items[0].toolTip = "Default Keyboard Tooltip".i18n()

        items += InputSource.sources.map {
            BrowserRuleMenuItem(url: browserInfo.url, preferencesVM: preferencesVM, inputSource: $0)
        }

        items
            .filter { (($0 as? BrowserRuleMenuItem) != nil) ? true : false }
            .enumerated()
            .forEach { index, item in
                if index < 10 {
                    item.keyEquivalent = "\(index)"
                    item.keyEquivalentModifierMask = .command
                }
            }

        return NSMenuItem(
            title: String(format: "Default Keyboard for %@".i18n(), host),
            submenuItems: items
        )
    }

    var settingsMenu: NSMenuItem? {
        NSMenuItem(
            title: "Preferences".i18n() + "...",
            target: self,
            action: #selector(openPreferences),
            keyEquivalent: ","
        )
    }

    var changelogMenu: NSMenuItem? {
        NSMenuItem(
            title: "Changelog".i18n(),
            target: self,
            action: #selector(openChangelog),
            keyEquivalent: ""
        )
    }

    var checkUpdatesMenu: NSMenuItem? {
        NSMenuItem(
            title: "Check for Updates".i18n() + "...",
            target: self,
            action: #selector(checkForUpdates),
            keyEquivalent: ""
        )
    }

    var menu: NSMenu {
        let menu = NSMenu()

        let items = [
            NSMenuItem(title: "Input Source Pro".i18n(), action: nil, keyEquivalent: ""),
            addBrowserRuleMenu ?? addRuleMenu,
            NSMenuItem.separator(),
            changelogMenu,
            checkUpdatesMenu,
            settingsMenu,
            NSMenuItem.separator(),
            NSMenuItem(
                title: "Quit".i18n(),
                action: #selector(NSApplication.shared.terminate(_:)),
                keyEquivalent: "q"
            ),
        ]
        .compactMap { $0 }

        items.forEach { menu.addItem($0) }

        return menu
    }

    lazy var preferencesWindowController = PreferencesWindowController(
        navigationVM: self.navigationVM,
        permissionsVM: self.permissionsVM,
        preferencesVM: self.preferencesVM,
        indicatorVM: self.indicatorVM,
        feedbackVM: self.feedbackVM,
        inputSourceVM: self.inputSourceVM
    )

    init(
        navigationVM: NavigationVM,
        permissionsVM: PermissionsVM,
        preferencesVM: PreferencesVM,
        applicationVM: ApplicationVM,
        indicatorVM: IndicatorVM,
        feedbackVM: FeedbackVM,
        inputSourceVM: InputSourceVM
    ) {
        self.navigationVM = navigationVM
        self.permissionsVM = permissionsVM
        self.preferencesVM = preferencesVM
        self.applicationVM = applicationVM
        self.indicatorVM = indicatorVM
        self.feedbackVM = feedbackVM
        self.inputSourceVM = inputSourceVM

        preferencesVM.$preferences
            .map(\.isShowIconInMenuBar)
            .removeDuplicates()
            .map { isShow -> AnyPublisher<Void, Never> in
                if isShow {
                    return AnyPublisher.create { [unowned self] _ in
                        statusItem = NSStatusBar.system.statusItem(
                            withLength: NSStatusItem.squareLength
                        )

                        statusItem?.button?.image = NSImage(named: "MenuBarIcon")
                        statusItem?.button?.image?.size = NSSize(width: 16, height: 16)
                        statusItem?.button?.image?.isTemplate = true
                        statusItem?.button?.target = self
                        statusItem?.button?.action = #selector(self.displayMenu)

                        return AnyCancellable { [unowned self] in
                            self.statusItem = nil
                        }
                    }
                } else {
                    return Empty<Void, Never>().eraseToAnyPublisher()
                }
            }
            .switchToLatest()
            .sink {}
            .store(in: cancelBag)
    }

    @objc func displayMenu() {
        // https://stackoverflow.com/a/57612963
        statusItem?.menu = menu
        statusItem?.button?.performClick(nil)
        statusItem?.menu = nil
    }

    @objc func openPreferences() {
        NSApp.setActivationPolicy(.regular)
        NSApplication.shared.activate(ignoringOtherApps: true)
        if let window = preferencesWindowController.window {
            DispatchQueue.main.async {
                if !self.hasPreferencesShown {
                    self.hasPreferencesShown = true
                    window.center()
                }

                window.makeKeyAndOrderFront(nil)
            }
        }
    }

    @objc func openChangelog() {
        if let url = URL(string: "https://inputsource.pro/changelog") {
            NSWorkspace.shared.open(url)
        }
    }

    @objc func checkForUpdates() {
        preferencesVM.checkUpdates()
    }

    @objc func openFeedback() {
        feedbackVM.show()
        navigationVM.selection = .troubleshooting
        openPreferences()
    }
}

import AppKit
import AXSwift
import Combine
import Foundation

extension ApplicationVM {
    static func createWindowAXNotificationPublisher(
        preferencesVM: PreferencesVM
    ) -> AnyPublisher<NSRunningApplication.WatchAXOutput, Never> {
        return preferencesVM.$preferences
            .flatMapLatest { preferences -> AnyPublisher<NSRunningApplication.WatchAXOutput, Never> in
                guard preferences.isEnhancedModeEnabled
                else { return Empty<NSRunningApplication.WatchAXOutput, Never>().eraseToAnyPublisher() }

                return NSWorkspace.shared
                    .publisher(for: \.runningApplications)
                    .map { $0.filter { NSApplication.isFloatingApp($0.bundleIdentifier) } }
                    .removeDuplicates()
                    .flatMapLatest { apps in
                        Publishers.MergeMany(apps
                            .map {
                                $0.watchAX(
                                    [.windowCreated, .uiElementDestroyed],
                                    [.application, .window]
                                )
                            }
                        )
                    }
                    .eraseToAnyPublisher()
            }
            .share()
            .eraseToAnyPublisher()
    }
}

import AppKit
import Foundation

extension PreferencesVM {
    @discardableResult
    func addAppCustomization(_ app: NSRunningApplication) -> AppRule? {
        guard let url = app.bundleURL ?? app.executableURL,
              let bundleId = app.bundleId()
        else { return nil }

        return addAppCustomization(url, bundleId: bundleId)
    }

    func addAppCustomization(_ url: URL, bundleId: String) -> AppRule {
        if let appCustomization = getAppCustomization(bundleId: bundleId) {
            return appCustomization
        }

        let appCustomization = AppRule(context: container.viewContext)

        appCustomization.createdAt = Date()
        appCustomization.url = url
        appCustomization.bundleId = url.bundleId()
        appCustomization.bundleName = FileManager.default.displayName(atPath: url.path)

        saveContext()

        return appCustomization
    }

    func removeAppCustomization(_ appCustomization: AppRule) {
        container.viewContext.delete(appCustomization)
        saveContext()
    }

    func setForceKeyboard(_ appCustomization: AppRule?, _ inputSourceId: String?) {
        guard let appCustomization = appCustomization else { return }

        saveContext {
            appCustomization.inputSourceId = inputSourceId
        }
    }

    func setDoRestoreKeyboard(_ appCustomization: AppRule?, _ doRestoreKeyboard: Bool) {
        guard let appCustomization = appCustomization else { return }

        saveContext {
            appCustomization.doRestoreKeyboard = doRestoreKeyboard
        }
    }

    func setDoNotRestoreKeyboard(_ appCustomization: AppRule?, _ doNotRestoreKeyboard: Bool) {
        guard let appCustomization = appCustomization else { return }

        saveContext {
            appCustomization.doNotRestoreKeyboard = doNotRestoreKeyboard
        }
    }

    func setHideIndicator(_ appCustomization: AppRule?, _ hideIndicator: Bool) {
        guard let appCustomization = appCustomization else { return }

        saveContext {
            appCustomization.hideIndicator = hideIndicator
        }
    }

    func getAppCustomization(app: NSRunningApplication) -> AppRule? {
        return getAppCustomization(bundleId: app.bundleId())
    }

    func getAppCustomization(bundleId: String?) -> AppRule? {
        guard let bundleId = bundleId else { return nil }

        let request = AppRule.fetchRequest()

        request.predicate = NSPredicate(format: "bundleId == %@", bundleId)

        do {
            return try container.viewContext.fetch(request).first
        } catch {
            print("getAppCustomization(bundleId) error: \(error.localizedDescription)")
            return nil
        }
    }

    func cleanRemovedAppCustomizationIfNeed() {
        guard preferences.prevInstalledBuildVersion < 308 else { return }

        let request = AppRule.fetchRequest()

        request.predicate = NSPredicate(format: "removed == %@", "1")

        do {
            let appRules = try container.viewContext.fetch(request)
            appRules.forEach { removeAppCustomization($0) }
        } catch {
            print("cleanRemovedAppCustomization error: \(error.localizedDescription)")
        }
    }
}

import AppKit

@MainActor
class BrowserRuleMenuItem: NSMenuItem {
    let url: URL
    let preferencesVM: PreferencesVM
    let inputSource: InputSource?

    var cancelBag = CancelBag()

    var browserRule: BrowserRule? {
        preferencesVM.getBrowserRule(url: url)
    }

    init(url: URL, preferencesVM: PreferencesVM, inputSource: InputSource?) {
        self.url = url
        self.preferencesVM = preferencesVM
        self.inputSource = inputSource

        super.init(title: inputSource?.name ?? "", action: #selector(forceKeyboard(_:)), keyEquivalent: "")

        target = self

        updateState()
        watchChanges()
    }

    @available(*, unavailable)
    required init(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc func forceKeyboard(_: Any) {
        let inputSourceId = inputSource?.id ?? ""
        let host = url.host ?? ""

        if let browserRule = browserRule {
            preferencesVM.updateBrowserRule(
                browserRule,
                type: .domain,
                value: host,
                sample: url.absoluteString,
                inputSourceId: inputSourceId,
                hideIndicator: browserRule.hideIndicator,
                keyboardRestoreStrategy: browserRule.keyboardRestoreStrategy
            )
        } else {
            preferencesVM.addBrowserRule(
                type: .domain,
                value: host,
                sample: url.absoluteString,
                inputSourceId: inputSourceId,
                hideIndicator: false,
                keyboardRestoreStrategy: nil
            )
        }

        inputSource?.select(useCJKVFix: preferencesVM.isUseCJKVFix())

        watchChanges()
    }

    func watchChanges() {
        cancelBag.cancel()

        browserRule?.publisher(for: \.inputSourceId)
            .sink { [weak self] _ in self?.updateState() }
            .store(in: cancelBag)
    }

    func updateState() {
        state = browserRule?.inputSourceId == inputSource?.id ? .on : .off
    }
}

import AppKit
import AXSwift
import Combine
import CombineExt

extension IndicatorVM {
    @MainActor
    enum ActivateEvent {
        case justHide
        case longMouseDown
        case appChanges(current: AppKind?, prev: AppKind?)
        case inputSourceChanges(InputSource, InputSourceChangeReason)

        func isAppChangesWithSameAppOrWebsite() -> Bool {
            switch self {
            case let .appChanges(current, prev):
                return current?.isSameAppOrWebsite(with: prev) == true
            case .inputSourceChanges:
                return false
            case .longMouseDown:
                return false
            case .justHide:
                return false
            }
        }

        var isJustHide: Bool {
            switch self {
            case .justHide: return true
            default: return false
            }
        }
    }

    func longMouseDownPublisher() -> AnyPublisher<ActivateEvent, Never> {
        AnyPublisher<NSEvent, Never>
            .create { observer in
                let monitor = NSEvent.addGlobalMonitorForEvents(
                    matching: [.leftMouseDown, .leftMouseUp, .leftMouseDragged],
                    handler: { observer.send($0) }
                )

                return AnyCancellable { NSEvent.removeMonitor(monitor!) }
            }
            .flatMapLatest { event -> AnyPublisher<Void, Never> in
                if event.type == .leftMouseDown {
                    return Timer
                        .delay(seconds: 0.35)
                        .mapToVoid()
                        .eraseToAnyPublisher()
                } else {
                    return Empty<Void, Never>().eraseToAnyPublisher()
                }
            }
            .filter { [weak self] _ in
                self?.preferencesVM.preferences.isActiveWhenLongpressLeftMouse ?? false
            }
            .mapTo(.longMouseDown)
    }

    func stateChangesPublisher() -> AnyPublisher<ActivateEvent, Never> {
        $state
            .withPrevious()
            .map { [weak self] previous, current -> ActivateEvent in
                if let preferencesVM = self?.preferencesVM {
                    if previous?.appKind?.getId() != current.appKind?.getId() {
                        let event = ActivateEvent.appChanges(current: current.appKind, prev: previous?.appKind)

                        if preferencesVM.preferences.isActiveWhenSwitchApp || preferencesVM.preferences.isActiveWhenFocusedElementChangesEnabled {
                            if preferencesVM.preferences.isHideWhenSwitchAppWithForceKeyboard {
                                switch current.inputSourceChangeReason {
                                case let .appSpecified(status):
                                    switch status {
                                    case .cached:
                                        return event
                                    case .specified:
                                        return .justHide
                                    }
                                default:
                                    return .justHide
                                }
                            } else {
                                return event
                            }
                        } else {
                            return .justHide
                        }
                    }

                    if previous?.inputSource.id != current.inputSource.id {
                        switch current.inputSourceChangeReason {
                        case .noChanges:
                            return .justHide
                        case .system, .shortcut, .appSpecified:
                            guard preferencesVM.preferences.isActiveWhenSwitchInputSource else { return .justHide }
                            return .inputSourceChanges(current.inputSource, current.inputSourceChangeReason)
                        }
                    }
                }

                return .justHide
            }
            .eraseToAnyPublisher()
    }
}

extension IndicatorVM.ActivateEvent: @preconcurrency CustomStringConvertible {
    var description: String {
        switch self {
        case let .appChanges(current, prev):
            return "appChanges(\(String(describing: current)), \(String(describing: prev))"
        case .inputSourceChanges:
            return "inputSourceChanges"
        case .longMouseDown:
            return "longMouseDown"
        case .justHide:
            return "justHide"
        }
    }
}

import Carbon
import Foundation

extension TISInputSource {
    enum Category {
        static var keyboardInputSource: String {
            return kTISCategoryKeyboardInputSource as String
        }
    }

    private func getProperty(_ key: CFString) -> AnyObject? {
        let cfType = TISGetInputSourceProperty(self, key)
        if cfType != nil {
            return Unmanaged<AnyObject>.fromOpaque(cfType!).takeUnretainedValue()
        } else {
            return nil
        }
    }

    var id: String {
        return getProperty(kTISPropertyInputSourceID) as! String
    }

    var name: String {
        return getProperty(kTISPropertyLocalizedName) as! String
    }

    var category: String? {
        return getProperty(kTISPropertyInputSourceCategory) as? String
    }

    var isSelectable: Bool {
        return getProperty(kTISPropertyInputSourceIsSelectCapable) as? Bool ?? false
    }

    var sourceLanguages: [String] {
        return getProperty(kTISPropertyInputSourceLanguages) as? [String] ?? []
    }

    var iconImageURL: URL? {
        return getProperty(kTISPropertyIconImageURL) as? URL
    }

    var iconRef: IconRef? {
        return OpaquePointer(TISGetInputSourceProperty(self, kTISPropertyIconRef))
    }
}

import AppKit

// MARK: - isFloatingApp

private let floatingAppIdentifier: [(
    identifier: String,
    maxWindowLayer: Int,
    isSpotlightLikeApp: Bool,
    isValid: (((windowLayer: Int?, windowBounds: CGRect?)) -> Bool)?
)] = [
    ("com.apple.Spotlight", 30, true, nil),
    ("com.runningwithcrayons.Alfred", 30, true, nil),
    ("at.obdev.LaunchBar", 30, true, nil),
    ("com.raycast.macos", 30, true, nil),
    ("com.googlecode.iterm2", 30, false, nil),
    ("com.xunyong.hapigo", 30, true, nil),
    ("com.hezongyidev.Bob", 30, false, nil),
    ("com.ripperhe.Bob", 30, false, nil),
    ("org.yuanli.utools", 30, false, nil),
    ("com.1password.1password", 1000, true, nil),
    ("com.eusoft.eudic.LightPeek", 1000, true, nil),
    ("com.contextsformac.Contexts", 1000, true, { $0.windowLayer != 20 }),
]

extension NSApplication {
    static func isFloatingApp(_ bundleIdentifier: String?, windowLayer: Int? = nil, windowBounds: CGRect? = nil) -> Bool {
        guard let bundleIdentifier = bundleIdentifier else { return false }

        return floatingAppIdentifier.contains {
            guard $0.0 == bundleIdentifier else { return false }
            guard let windowLayer = windowLayer else { return true }
            guard windowLayer < $0.1, windowLayer > 0 else { return false }

            return $0.isValid?((windowLayer, windowBounds)) ?? true
        }
    }

    static func isSpotlightLikeApp(_ bundleIdentifier: String?) -> Bool {
        guard let bundleIdentifier = bundleIdentifier else { return false }

        return floatingAppIdentifier.contains(where: { $0.isSpotlightLikeApp && $0.identifier == bundleIdentifier })
    }

    static func isBrowser(_ app: NSRunningApplication) -> Bool {
        return getBrowser(app) != nil
    }

    static func getBrowser(_ app: NSRunningApplication) -> Browser? {
        if let bundleIdentifier = app.bundleIdentifier,
           let browser = Browser(rawValue: bundleIdentifier)
        {
            return browser
        } else {
            return nil
        }
    }
}

// MARK: - isBrowserApp

private var browserAppIdentifier: Set<String> = {
    let array1 = LSCopyAllRoleHandlersForContentType(
        "public.html" as CFString, .viewer
    )?.takeRetainedValue() as? [String] ?? []
    let array2 = LSCopyAllHandlersForURLScheme(
        "https" as CFString
    )?.takeRetainedValue() as? [String] ?? []

    let set1 = Set(array1)
    let set2 = Set(array2)

    return set1.intersection(set2)
}()

extension NSApplication {
    static func isBrowserApp(_ bundleIdentifier: String?) -> Bool {
        guard let bundleIdentifier = bundleIdentifier else { return false }

        return browserAppIdentifier.contains(bundleIdentifier)
    }

    static func isBrowserInstalled(_ bundleIdentifier: String) -> Bool {
        return isBrowserApp(bundleIdentifier)
    }
}

import AppKit
import ApplicationServices

extension AXValue {
    private func get<T>(_ type: AXValueType, initial: T) -> T? {
        var result = initial
        return AXValueGetValue(self, type, &result) ? result : nil
    }

    var asPoint: CGPoint? { return get(.cgPoint, initial: .zero) }
    var asSize: CGSize? { return get(.cgSize, initial: .zero) }
    var asRect: CGRect? { return get(.cgRect, initial: .zero) }
    var asRange: CFRange? { return get(.cfRange, initial: CFRange()) }
    var asError: AXError? { return get(.axError, initial: .success) }

    private static func create<T>(_ type: AXValueType, _ value: T) -> AXValue {
        var value = value
        return AXValueCreate(type, &value)!
    }

    static func point(_ v: CGPoint) -> AXValue { return create(.cgPoint, v) }
    static func size(_ v: CGSize) -> AXValue { return create(.cgSize, v) }
    static func rect(_ v: CGRect) -> AXValue { return create(.cgRect, v) }
    static func range(_ v: CFRange) -> AXValue { return create(.cfRange, v) }
    static func error(_ v: AXError) -> AXValue { return create(.axError, v) }
}

import AppKit

extension NSWorkspace {
    func openAccessibilityPreferences() {
        open(
            URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
        )
    }

    func openAutomationPreferences() {
        open(
            URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")!
        )
    }

    func openKeyboardPreferences() {
        open(
            URL(fileURLWithPath: "/System/Library/PreferencePanes/Keyboard.prefPane")
        )
    }
}

extension NSWorkspace {
    func desktopImage() -> NSImage? {
        guard let mainScreen = NSScreen.main,
              let url = desktopImageURL(for: mainScreen),
              let image = NSImage(contentsOf: url)
        else { return nil }

        return image
    }
}

import Cocoa
import Combine

@propertyWrapper
final class CodableUserDefault<T: Codable>: NSObject {
    var wrappedValue: T? {
        get {
            guard let data = userDefaults.data(forKey: key) else { return nil }
            return try? JSONDecoder().decode(T.self, from: data)
        }

        set {
            do {
                let data = try JSONEncoder().encode(newValue)

                userDefaults.setValue(data, forKey: key)
            } catch {
                print("Unable to Encode (\(error))")
            }
        }
    }

    private let key: String
    private let userDefaults: UserDefaults
    private var observerContext = 0
    private let subject: CurrentValueSubject<T?, Never>

    init(wrappedValue defaultValue: T, _ key: String, userDefaults: UserDefaults = .standard) {
        self.key = key
        self.userDefaults = userDefaults
        subject = CurrentValueSubject(defaultValue)

        super.init()

        do {
            try userDefaults.register(defaults: [key: JSONEncoder().encode(defaultValue)])
            // This fulfills requirement 4. Some implementations use NSUserDefaultsDidChangeNotification
            // but that is sent every time any value is updated in UserDefaults.
            userDefaults.addObserver(self, forKeyPath: key, options: .new, context: &observerContext)
        } catch {
            print("Unable to register (\(error))")
        }

        subject.value = wrappedValue
    }

    override func observeValue(
        forKeyPath keyPath: String?,
        of object: Any?,
        change: [NSKeyValueChangeKey: Any]?,
        context: UnsafeMutableRawPointer?
    ) {
        if context == &observerContext {
            subject.value = wrappedValue
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    deinit {
        userDefaults.removeObserver(self, forKeyPath: key, context: &observerContext)
    }
}

import AppKit
import Bodega
import Boutique
import Foundation
import SwiftUI

extension PreferencesVM {
    func addKeyboardConfig(_ inputSource: InputSource) -> KeyboardConfig {
        if let config = getKeyboardConfig(inputSource) {
            return config
        } else {
            let config = KeyboardConfig(context: container.viewContext)

            config.id = inputSource.id

            saveContext()

            return config
        }
    }

    func update(_ config: KeyboardConfig, textColor: Color, bgColor: Color) {
        saveContext {
            config.textColor = textColor == self.preferences.indicatorForgegroundColor ? nil : textColor
            config.bgColor = bgColor == self.preferences.indicatorBackgroundColor ? nil : bgColor
        }
    }

    func getKeyboardConfig(_ inputSource: InputSource) -> KeyboardConfig? {
        return keyboardConfigs.first(where: { $0.id == inputSource.id })
    }

    func getOrCreateKeyboardConfig(_ inputSource: InputSource) -> KeyboardConfig {
        return getKeyboardConfig(inputSource) ?? addKeyboardConfig(inputSource)
    }
}

extension PreferencesVM {
    func getTextNSColor(_ inputSource: InputSource) -> NSColor? {
        let isAutoAppearanceMode = preferences.isAutoAppearanceMode

        if let keyboardColor = getKeyboardConfig(inputSource)?.textColor {
            return NSColor(keyboardColor)
        } else {
            return isAutoAppearanceMode
                ? preferences.indicatorForgeground?.dynamicColor
                : NSColor(preferences.indicatorForgegroundColor)
        }
    }

    func getTextColor(_ inputSource: InputSource) -> Color {
        if let nsColor = getTextNSColor(inputSource) {
            return Color(nsColor)
        } else {
            return preferences.indicatorForgegroundColor
        }
    }

    func getBgNSColor(_ inputSource: InputSource) -> NSColor? {
        let isAutoAppearanceMode = preferences.isAutoAppearanceMode

        if let bgColor = getKeyboardConfig(inputSource)?.bgColor {
            return NSColor(bgColor)
        } else {
            return isAutoAppearanceMode
                ? preferences.indicatorBackground?.dynamicColor
                : NSColor(preferences.indicatorBackgroundColor)
        }
    }

    func getBgColor(_ inputSource: InputSource) -> Color {
        if let nsColor = getBgNSColor(inputSource) {
            return Color(nsColor)
        } else {
            return preferences.indicatorBackgroundColor
        }
    }
}

extension PreferencesVM {
    struct DeprecatedKeyboardSettings: Codable & Equatable & Identifiable {
        let id: String

        var textColorHex: String?
        var bgColorHex: String?
    }

    func migratePreferncesIfNeed() {
        if preferences.prevInstalledBuildVersion <= 462 {
            update {
                $0.indicatorInfo = $0.isShowInputSourcesLabel ? .iconAndTitle : .iconOnly
            }
        }
    }

    func migrateBoutiqueIfNeed() {
        let storagePath = Store<DeprecatedKeyboardSettings>.documentsDirectory(appendingPath: "KeyboardSettings")

        guard preferences.prevInstalledBuildVersion == 316,
              FileManager.default.fileExists(atPath: storagePath.path) else { return }

        let store = Store<DeprecatedKeyboardSettings>(storagePath: storagePath)
        let inputSources = InputSource.sources

        store.$items
            .filter { $0.count > 0 }
            .first()
            .sink { [weak self] items in
                self?.saveContext {
                    for item in items {
                        guard let inputSource = inputSources.first(where: { $0.id == item.id }),
                              let config = self?.getOrCreateKeyboardConfig(inputSource)
                        else { continue }

                        config.textColorHex = item.textColorHex
                        config.bgColorHex = item.bgColorHex
                    }
                }

                do {
                    try FileManager.default.removeItem(at: storagePath)
                } catch {
                    print("Boutique migration error: \(error.localizedDescription)")
                }
            }
            .store(in: cancelBag)
    }
}

import SwiftUI

struct GeneralSettingsView: View {
    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var permissionsVM: PermissionsVM

    @State var isDetectSpotlightLikeApp = false

    var items: [PickerItem] {
        [PickerItem.empty]
            + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) }
    }

    var body: some View {
        let keyboardRestoreStrategyBinding = Binding(
            get: { preferencesVM.preferences.isRestorePreviouslyUsedInputSource ?
                KeyboardRestoreStrategy.RestorePreviouslyUsedOne :
                KeyboardRestoreStrategy.UseDefaultKeyboardInstead
            },
            set: { newValue in
                preferencesVM.update {
                    switch newValue {
                    case .RestorePreviouslyUsedOne:
                        $0.isRestorePreviouslyUsedInputSource = true
                    case .UseDefaultKeyboardInstead:
                        $0.isRestorePreviouslyUsedInputSource = false
                    }
                }
            }
        )

        ScrollView {
            VStack(alignment: .leading, spacing: 18) {
                SettingsSection(title: "") {
                    PromotionBadge()
                }

                SettingsSection(title: "") {
                    EnhancedModeToggle()
                }

                SettingsSection(title: "Default Keyboard") {
                    HStack {
                        Text("For All Apps and Websites".i18n())

                        PopUpButtonPicker<PickerItem?>(
                            items: items,
                            isItemSelected: { $0?.id == preferencesVM.preferences.systemWideDefaultKeyboardId },
                            getTitle: { $0?.title ?? "" },
                            getToolTip: { $0?.toolTip },
                            onSelect: handleSystemWideDefaultKeyboardSelect
                        )
                    }
                    .padding()
                    .border(width: 1, edges: [.bottom], color: NSColor.border2.color)
                }

                SettingsSection(title: "Keyboard Restore Strategy") {
                    VStack(alignment: .leading) {
                        Text("When Switching Back to the App or Website".i18n() + ":")

                        Picker("Keyboard Restore Strategy", selection: keyboardRestoreStrategyBinding) {
                            ForEach(KeyboardRestoreStrategy.allCases) { item in
                                Text(item.name).tag(item)
                            }
                        }
                        .pickerStyle(.segmented)
                    }
                    .padding()
                }

                Group {
                    SettingsSection(title: "Indicator Triggers") {
                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isActiveWhenLongpressLeftMouse)

                            Text("isActiveWhenLongpressLeftMouse".i18n())

                            Spacer()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isActiveWhenSwitchInputSource)

                            Text("isActiveWhenSwitchInputSource".i18n())

                            Spacer()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isActiveWhenSwitchApp)

                            Text("isActiveWhenSwitchApp".i18n())

                            Spacer()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isActiveWhenFocusedElementChanges)
                                .disabled(!preferencesVM.preferences.isEnhancedModeEnabled)

                            Text("isActiveWhenFocusedElementChanges".i18n())

                            Spacer()

                            EnhancedModeRequiredBadge()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)
                    }

                    SettingsSection(title: "") {
                        HStack {
                            Toggle("",
                                   isOn: $preferencesVM.preferences.isHideWhenSwitchAppWithForceKeyboard)
                                .disabled(!(
                                    preferencesVM.preferences.isActiveWhenSwitchApp ||
                                        preferencesVM.preferences.isActiveWhenSwitchInputSource ||
                                        preferencesVM.preferences.isActiveWhenFocusedElementChanges
                                ))

                            Text("isHideWhenSwitchAppWithForceKeyboard")

                            Spacer()
                        }
                        .padding()
                    }
                }

                Group {
                    SettingsSection(title: "System") {
                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isLaunchAtLogin)
                            Text("Launch at Login".i18n())
                            Spacer()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isShowIconInMenuBar)
                            Text("Display Icon in Menu Bar".i18n())
                            Spacer()
                        }
                        .padding()
                    }

                    SettingsSection(title: "Updates") {
                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.receiveBetaUpdates)
                            Text("Receive Beta Updates".i18n())
                            Spacer()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        Button(action: { preferencesVM.checkUpdates() }, label: {
                            HStack {
                                Text("Check for Updates".i18n() + "...")

                                Spacer()

                                Text(" \(preferencesVM.versionStr) (\(preferencesVM.buildStr))")
                                    .foregroundColor(Color.primary.opacity(0.5))
                            }
                        })
                        .buttonStyle(SectionButtonStyle())
                    }
                }

                Group {
                    SettingsSection(title: "Find Us", tips: Text("Right click each section to copy link").font(.subheadline).opacity(0.5)) {
                        Button(action: { URL.website.open() }, label: {
                            HStack {
                                Text("Website".i18n())
                                    .foregroundColor(Color.primary)

                                Spacer()

                                Text(URL.website.absoluteString)
                            }
                        })
                        .buttonStyle(SectionButtonStyle())
                        .contextMenu {
                            Button("Copy") {
                                NSPasteboard.general.clearContents()
                                NSPasteboard.general.setString(URL.website.absoluteString, forType: .string)
                            }
                        }
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        Button(action: { URL.twitter.open() }, label: {
                            HStack {
                                Text("Twitter")
                                    .foregroundColor(Color.primary)

                                Spacer()

                                Text("@Runjuu")
                            }
                        })
                        .buttonStyle(SectionButtonStyle())
                        .contextMenu {
                            Button("Copy") {
                                NSPasteboard.general.clearContents()
                                NSPasteboard.general.setString(URL.twitter.absoluteString, forType: .string)
                            }
                        }
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        Button(action: { URL.email.open() }, label: {
                            HStack {
                                Text("Email")
                                    .foregroundColor(Color.primary)

                                Spacer()

                                Text(URL.emailString)
                            }
                        })
                        .buttonStyle(SectionButtonStyle())
                        .contextMenu {
                            Button("Copy") {
                                NSPasteboard.general.clearContents()
                                NSPasteboard.general.setString(URL.emailString, forType: .string)
                            }
                        }
                    }

                    SettingsSection(title: "") {
                        FeedbackButton()
                    }
                }

                SettingsSection(title: "Privacy") {
                    HStack {
                        Text("Privacy Content".i18n())
                            .multilineTextAlignment(.leading)
                            .padding()
                            .opacity(0.8)

                        Spacer(minLength: 0)
                    }
                }

                HStack {
                    Spacer()
                    Text("Created by Runjuu & Die2")
                    Spacer()
                }
                .font(.footnote)
                .opacity(0.5)
            }
            .padding()
        }
        .labelsHidden()
        .toggleStyle(.switch)
        .background(NSColor.background1.color)
        .onAppear(perform: disableIsDetectSpotlightLikeAppIfNeed)
    }

    func disableIsDetectSpotlightLikeAppIfNeed() {
        if !permissionsVM.isAccessibilityEnabled && preferencesVM.preferences.isEnhancedModeEnabled {
            preferencesVM.update {
                $0.isEnhancedModeEnabled = false
            }
        }
    }

    func handleSystemWideDefaultKeyboardSelect(_ index: Int) {
        let defaultKeyboard = items[index]

        preferencesVM.update {
            $0.systemWideDefaultKeyboardId = defaultKeyboard.id
        }
    }
}

import AppKit
import AXSwift
import Carbon
import Combine
import CombineExt
import KeyboardShortcuts
import os

@MainActor
final class IndicatorVM: ObservableObject {
    private var cancelBag = CancelBag()

    let applicationVM: ApplicationVM
    let preferencesVM: PreferencesVM
    let inputSourceVM: InputSourceVM
    let permissionsVM: PermissionsVM

    let logger = ISPLogger(category: String(describing: IndicatorVM.self))

    @Published
    private(set) var state: State

    var actionSubject = PassthroughSubject<Action, Never>()

    var refreshShortcutSubject = PassthroughSubject<Void, Never>()

    private(set) lazy var activateEventPublisher = Publishers.MergeMany([
        longMouseDownPublisher(),
        stateChangesPublisher(),
    ])
    .share()

    private(set) lazy var screenIsLockedPublisher = Publishers.MergeMany([
        DistributedNotificationCenter.default()
            .publisher(for: Notification.Name(rawValue: "com.apple.screenIsLocked"))
            .mapTo(true),

        DistributedNotificationCenter.default()
            .publisher(for: NSWorkspace.willSleepNotification)
            .mapTo(true),

        DistributedNotificationCenter.default()
            .publisher(for: Notification.Name(rawValue: "com.apple.screenIsUnlocked"))
            .mapTo(false),

        DistributedNotificationCenter.default()
            .publisher(for: NSWorkspace.didWakeNotification)
            .mapTo(false),
    ])
    .receive(on: DispatchQueue.main)
    .prepend(false)
    .removeDuplicates()
    .share()

    init(
        permissionsVM: PermissionsVM,
        preferencesVM: PreferencesVM,
        applicationVM: ApplicationVM,
        inputSourceVM: InputSourceVM
    ) {
        self.permissionsVM = permissionsVM
        self.preferencesVM = preferencesVM
        self.applicationVM = applicationVM
        self.inputSourceVM = inputSourceVM
        state = .from(
            preferencesVM: preferencesVM,
            inputSourceChangeReason: .system,
            applicationVM.appKind,
            InputSource.getCurrentInputSource()
        )

        clearAppKeyboardCacheIfNeed()
        watchState()
    }

    private func clearAppKeyboardCacheIfNeed() {
        applicationVM.$appsDiff
            .sink { [weak self] appsDiff in
                appsDiff.removed
                    .compactMap { $0.bundleIdentifier }
                    .forEach { bundleId in
                        self?.preferencesVM.removeKeyboardCacheFor(bundleId: bundleId)
                    }
            }
            .store(in: cancelBag)

        preferencesVM.$preferences
            .map(\.isRestorePreviouslyUsedInputSource)
            .filter { $0 == false }
            .sink { [weak self] _ in
                self?.preferencesVM.clearKeyboardCache()
            }
            .store(in: cancelBag)
    }
}

extension IndicatorVM {
    enum InputSourceChangeReason {
        case noChanges, system, shortcut, appSpecified(PreferencesVM.AppAutoSwitchKeyboardStatus)
    }

    @MainActor
    struct State {
        let appKind: AppKind?
        let inputSource: InputSource
        let inputSourceChangeReason: InputSourceChangeReason

        func isSame(with other: State) -> Bool {
            return State.isSame(self, other)
        }

        static func isSame(_ lhs: IndicatorVM.State, _ rhs: IndicatorVM.State) -> Bool {
            guard let appKind1 = lhs.appKind, let appKind2 = rhs.appKind
            else { return lhs.appKind == nil && rhs.appKind == nil }

            guard appKind1.isSameAppOrWebsite(with: appKind2, detectAddressBar: true)
            else { return false }

            guard lhs.inputSource.id == rhs.inputSource.id
            else { return false }

            return true
        }

        static func from(
            preferencesVM _: PreferencesVM,
            inputSourceChangeReason: InputSourceChangeReason,
            _ appKind: AppKind?,
            _ inputSource: InputSource
        ) -> State {
            return .init(
                appKind: appKind,
                inputSource: inputSource,
                inputSourceChangeReason: inputSourceChangeReason
            )
        }
    }

    enum Action {
        case start
        case appChanged(AppKind)
        case switchInputSourceByShortcut(InputSource)
        case inputSourceChanged(InputSource)
    }

    func send(_ action: Action) {
        actionSubject.send(action)
    }

    func refreshShortcut() {
        refreshShortcutSubject.send(())
    }

    func watchState() {
        actionSubject
            .scan(state) { [weak self] state, action -> State in
                guard let preferencesVM = self?.preferencesVM,
                      let inputSourceVM = self?.inputSourceVM
                else { return state }

                @MainActor
                func updateState(appKind: AppKind?, inputSource: InputSource, inputSourceChangeReason: InputSourceChangeReason) -> State {
                    // TODO: Move to outside
                    if let appKind = appKind {
                        preferencesVM.cacheKeyboardFor(appKind, keyboard: inputSource)
                    }

                    return .from(
                        preferencesVM: preferencesVM,
                        inputSourceChangeReason: inputSourceChangeReason,
                        appKind,
                        inputSource
                    )
                }

                switch action {
                case .start:
                    return state
                case let .appChanged(appKind):
                    if let status = preferencesVM.getAppAutoSwitchKeyboard(appKind) {
                        inputSourceVM.select(inputSource: status.inputSource)

                        return updateState(
                            appKind: appKind,
                            inputSource: status.inputSource,
                            inputSourceChangeReason: .appSpecified(status)
                        )
                    } else {
                        return updateState(
                            appKind: appKind,
                            inputSource: state.inputSource,
                            inputSourceChangeReason: .noChanges
                        )
                    }
                case let .inputSourceChanged(inputSource):
                    guard inputSource.id != state.inputSource.id else { return state }

                    return updateState(appKind: state.appKind, inputSource: inputSource, inputSourceChangeReason: .system)
                case let .switchInputSourceByShortcut(inputSource):
                    inputSourceVM.select(inputSource: inputSource)

                    return updateState(appKind: state.appKind, inputSource: inputSource, inputSourceChangeReason: .shortcut)
                }
            }
            .removeDuplicates(by: { $0.isSame(with: $1) })
            .assign(to: &$state)

        applicationVM.$appKind
            .compactMap { $0 }
            .sink(receiveValue: { [weak self] in self?.send(.appChanged($0)) })
            .store(in: cancelBag)

        inputSourceVM.inputSourceChangesPublisher
            .sink(receiveValue: { [weak self] in self?.send(.inputSourceChanged($0)) })
            .store(in: cancelBag)

        refreshShortcutSubject
            .sink { [weak self] _ in
                KeyboardShortcuts.removeAllHandlers()

                for inputSource in InputSource.sources {
                    KeyboardShortcuts.onKeyUp(for: .init(inputSource.id)) {
                        self?.send(.switchInputSourceByShortcut(inputSource))
                    }
                }

                self?.preferencesVM.getHotKeyGroups().forEach { group in
                    KeyboardShortcuts.onKeyUp(for: .init(group.id!)) {
                        guard group.inputSources.count > 0 else { return }

                        let currIps = InputSource.getCurrentInputSource()
                        let nextIdx = (
                            (group.inputSources.firstIndex(where: { currIps.id == $0.id }) ?? -1) + 1
                        ) % group.inputSources.count

                        self?.send(.switchInputSourceByShortcut(group.inputSources[nextIdx]))
                    }
                }
            }
            .store(in: cancelBag)

        refreshShortcut()
        send(.start)
    }
}

// https://gist.github.com/simonbs/61c8269e1b0550feab606ee9890fa72b
import Cocoa
import Combine

@propertyWrapper
final class UserDefault<T>: NSObject {
    // This ensures requirement 1 is fulfilled. The wrapped value is stored in user defaults.
    var wrappedValue: T {
        get {
            return userDefaults.object(forKey: key) as! T
        }
        set {
            userDefaults.setValue(newValue, forKey: key)
        }
    }

    var projectedValue: AnyPublisher<T, Never> {
        return subject.eraseToAnyPublisher()
    }

    private let key: String
    private let userDefaults: UserDefaults
    private var observerContext = 0
    private let subject: CurrentValueSubject<T, Never>

    init(wrappedValue defaultValue: T, _ key: String, userDefaults: UserDefaults = .standard) {
        self.key = key
        self.userDefaults = userDefaults
        subject = CurrentValueSubject(defaultValue)
        super.init()
        userDefaults.register(defaults: [key: defaultValue])
        // This fulfills requirement 4. Some implementations use NSUserDefaultsDidChangeNotification
        // but that is sent every time any value is updated in UserDefaults.
        userDefaults.addObserver(self, forKeyPath: key, options: .new, context: &observerContext)
        subject.value = wrappedValue
    }

    override func observeValue(
        forKeyPath keyPath: String?,
        of object: Any?,
        change: [NSKeyValueChangeKey: Any]?,
        context: UnsafeMutableRawPointer?
    ) {
        if context == &observerContext {
            subject.value = wrappedValue
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    deinit {
        userDefaults.removeObserver(self, forKeyPath: key, context: &observerContext)
    }
}

import Cocoa

extension AppRule {
    var image: NSImage? {
        guard let path = url else { return nil }

        return NSWorkspace.shared.icon(forFile: path.path)
    }
}

extension AppRule {
    @MainActor
    var forcedKeyboard: InputSource? {
        guard let inputSourceId = inputSourceId else { return nil }

        return InputSource.sources.first { $0.id == inputSourceId }
    }
}

import SwiftUI

struct BrowserRuleEditView: View {
    @State var value: String = ""
    @State var sampleValue: String = ""
    @State var ruleType = BrowserRuleType.domainSuffix
    @State var inputSourceItem = PickerItem.empty
    @State var restoreStrategyItem = PickerItem.empty
    @State var hideIndicator = false

    @State var isPopover = false

    @State private var width = CGFloat.zero

    @Binding var isPresented: Bool

    @EnvironmentObject var preferencesVM: PreferencesVM

    let rule: BrowserRule?

    var ruleTypes: [BrowserRuleType] {
        BrowserRuleType.allCases
    }

    var inputSourceItems: [PickerItem] {
        [PickerItem.empty]
            + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) }
    }

    var restoreStrategyItems: [PickerItem] {
        [PickerItem.empty]
            + KeyboardRestoreStrategy.allCases.map { PickerItem(id: $0.rawValue, title: $0.name, toolTip: nil) }
    }

    var sampleURL: URL? {
        guard !sampleValue.isEmpty else { return nil }

        if sampleValue.starts(with: "http://") || sampleValue.starts(with: "https://") {
            return URL(string: sampleValue)
        } else {
            return URL(string: "http://\(sampleValue)")
        }
    }

    var body: some View {
        VStack(spacing: 0) {
            VStack(alignment: .leading, spacing: 5) {
                Text("Match".i18n().uppercased())
                    .font(.footnote)
                    .fontWeight(.medium)
                    .opacity(0.5)

                VStack {
                    HStack {
                        Text("Sample URL".i18n() + ":")
                            .alignedView(width: $width, alignment: .trailing)
                        TextField("https://www.twitter.com/runjuuu", text: $sampleValue)
                    }

                    HStack {
                        Text("Rule Type".i18n() + ":")
                            .alignedView(width: $width, alignment: .trailing)
                        HStack {
                            PopUpButtonPicker<BrowserRuleType>(
                                items: ruleTypes,
                                isItemSelected: { $0 == ruleType },
                                getTitle: { $0.name },
                                getToolTip: { $0.explanation },
                                onSelect: handleSelectRule
                            )

                            Button(action: { isPopover.toggle() }) {
                                SwiftUI.Image(systemName: "questionmark")
                            }
                            .font(.system(size: 10).weight(.bold))
                            .frame(width: 18, height: 18)
                            .clipShape(RoundedRectangle(cornerRadius: 99))
                            .popover(
                                isPresented: self.$isPopover,
                                arrowEdge: .bottom
                            ) {
                                VStack {
                                    Text(.init(ruleType.explanation))
                                        .lineSpacing(3)
                                }
                                .frame(width: 280)
                                .padding()
                            }
                        }
                    }

                    HStack {
                        Text("Domain".i18n() + ":")
                            .alignedView(width: $width, alignment: .trailing)
                        HStack {
                            TextField("twitter.com", text: $value)
                            if let url = sampleURL, !value.isEmpty {
                                if BrowserRule.validate(type: ruleType, url: url, value: value) {
                                    Image(systemName: "checkmark.circle.fill")
                                        .foregroundColor(.green)
                                        .frame(width: 18, height: 18)
                                } else {
                                    Image(systemName: "x.circle.fill")
                                        .foregroundColor(.red)
                                        .frame(width: 18, height: 18)
                                }
                            }
                        }
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .clipShape(RoundedRectangle(cornerRadius: 8))
                .padding(.bottom)
            }

            VStack(alignment: .leading, spacing: 5) {
                Text("Keyboard".i18n().uppercased())
                    .font(.footnote)
                    .fontWeight(.medium)
                    .opacity(0.5)

                VStack {
                    HStack {
                        Text("Default Keyboard".i18n() + ":")
                            .alignedView(width: $width, alignment: .trailing)
                        PopUpButtonPicker<PickerItem?>(
                            items: inputSourceItems,
                            isItemSelected: { $0 == inputSourceItem },
                            getTitle: { $0?.title ?? "" },
                            getToolTip: { $0?.toolTip },
                            onSelect: handleSelectInputSource
                        )
                    }

                    HStack {
                        Text("Restore Strategy".i18n() + ":")
                            .alignedView(width: $width, alignment: .trailing)
                        PopUpButtonPicker<PickerItem?>(
                            items: restoreStrategyItems,
                            isItemSelected: { $0 == restoreStrategyItem },
                            getTitle: { $0?.title ?? "" },
                            getToolTip: { $0?.toolTip },
                            onSelect: handleRestoreStrategy
                        )
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .clipShape(RoundedRectangle(cornerRadius: 8))
                .padding(.bottom)
            }

            VStack(alignment: .leading, spacing: 5) {
                Text("Tooltip".i18n().uppercased())
                    .font(.footnote)
                    .fontWeight(.medium)
                    .opacity(0.5)

                VStack {
                    HStack {
                        HStack(spacing: 4) {
                            Image(systemName: "eye.slash.circle.fill")
                                .foregroundColor(.gray)
                            Text("Hide Indicator".i18n() + ":")
                        }
                        .alignedView(width: $width, alignment: .trailing)

                        Toggle("", isOn: $hideIndicator)
                            .toggleStyle(.switch)

                        Spacer()
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .clipShape(RoundedRectangle(cornerRadius: 8))
                .padding(.bottom)
            }

            HStack {
                Spacer()

                Button("Cancel".i18n(), action: cancel)
                    .keyboardShortcut(.cancelAction)

                Button("\(rule == nil ? "Add" : "Save")".i18n(), action: save)
                    .disabled(value.isEmpty)
                    .keyboardShortcut(.defaultAction)
            }
            .padding(.top, 6)
        }
        .padding()
        .frame(width: 480)
        .onAppear {
            value = rule?.value ?? ""
            sampleValue = rule?.sample ?? ""
            ruleType = rule?.type ?? .domainSuffix
            hideIndicator = rule?.hideIndicator ?? false

            if let inputSource = rule?.forcedKeyboard {
                inputSourceItem = PickerItem(id: inputSource.id, title: inputSource.name, toolTip: inputSource.id)
            }

            if let keyboardRestoreStrategy = rule?.keyboardRestoreStrategy {
                restoreStrategyItem = PickerItem(
                    id: keyboardRestoreStrategy.rawValue,
                    title: keyboardRestoreStrategy.name,
                    toolTip: nil
                )
            }
        }
    }

    func handleSelectInputSource(_ index: Int) {
        inputSourceItem = inputSourceItems[index]
    }

    func handleRestoreStrategy(_ index: Int) {
        restoreStrategyItem = restoreStrategyItems[index]
    }

    func handleSelectRule(_ index: Int) {
        ruleType = ruleTypes[index]
    }

    func cancel() {
        isPresented = false
    }

    func save() {
        guard !value.isEmpty else { return }

        let keyboardRestoreStrategy = restoreStrategyItem == .empty ? nil : KeyboardRestoreStrategy(rawValue: restoreStrategyItem.id)

        if let rule = rule {
            preferencesVM.updateBrowserRule(
                rule,
                type: ruleType,
                value: value,
                sample: sampleValue,
                inputSourceId: inputSourceItem.id,
                hideIndicator: hideIndicator,
                keyboardRestoreStrategy: keyboardRestoreStrategy
            )
        } else {
            preferencesVM.addBrowserRule(
                type: ruleType,
                value: value,
                sample: sampleValue,
                inputSourceId: inputSourceItem.id,
                hideIndicator: hideIndicator,
                keyboardRestoreStrategy: keyboardRestoreStrategy
            )
        }

        isPresented = false
    }
}

import AppKit
import Combine

@MainActor
final class PermissionsVM: ObservableObject {
    @discardableResult
    static func checkAccessibility(prompt: Bool) -> Bool {
        let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString
        return AXIsProcessTrustedWithOptions([checkOptPrompt: prompt] as CFDictionary?)
    }

    @Published var isAccessibilityEnabled = PermissionsVM.checkAccessibility(prompt: false)

    init() {
        watchAccessibilityChange()
    }

    private func watchAccessibilityChange() {
        guard !isAccessibilityEnabled else { return }

        Timer
            .interval(seconds: 1)
            .map { _ in Self.checkAccessibility(prompt: false) }
            .filter { $0 }
            .first()
            .assign(to: &$isAccessibilityEnabled)
    }
}

import AppKit

enum IndicatorActuallyPositionKind {
    case floatingApp
    case inputCursor
    case inputRect
    case nearMouse
    case windowCorner
    case screenCorner

    var isInputArea: Bool {
        self == .inputRect || self == .inputCursor
    }
}

enum IndicatorPosition: Int32, CaseIterable, Identifiable {
    case nearMouse = 0
    case windowCorner = 1
    case screenCorner = 2

    var id: Self { self }

    enum Alignment: Int32, CaseIterable {
        case topLeft = 0
        case topCenter = 5
        case topRight = 1
        case centerLeft = 7
        case center = 2
        case centerRight = 8
        case bottomLeft = 3
        case bottomCenter = 6
        case bottomRight = 4
    }

    enum Spacing: Int32, CaseIterable {
        case none = 0
        case xs = 1
        case s = 2
        case m = 3
        case l = 4
        case xl = 5
    }
}

extension IndicatorPosition {
    var name: String {
        switch self {
        case .nearMouse:
            return "Follow Mouse".i18n()
        case .windowCorner:
            return "Relative to App".i18n()
        case .screenCorner:
            return "Relative to Screen".i18n()
        }
    }
}

extension IndicatorPosition.Alignment {
    var name: String {
        switch self {
        case .topLeft:
            return "Top-Left Corner".i18n()
        case .topRight:
            return "Top-Right Corner".i18n()
        case .topCenter:
            return "Top-Center".i18n()
        case .center:
            return "Center".i18n()
        case .centerLeft:
            return "Center-Left".i18n()
        case .centerRight:
            return "Center-Right".i18n()
        case .bottomLeft:
            return "Bottom-Left Corner".i18n()
        case .bottomCenter:
            return "Bottom-Center".i18n()
        case .bottomRight:
            return "Bottom-Right Corner".i18n()
        }
    }
}

extension IndicatorPosition.Spacing {
    static func fromSlide(value: Double) -> Self {
        return .init(rawValue: Int32(value)) ?? .m
    }

    var name: String {
        switch self {
        case .none:
            return "Nope".i18n()
        case .xs:
            return "XS".i18n()
        case .s:
            return "S".i18n()
        case .m:
            return "M".i18n()
        case .l:
            return "L".i18n()
        case .xl:
            return "XL".i18n()
        }
    }
}

extension IndicatorPosition: Codable {
    private enum CodingKeys: String, CodingKey {
        case base
    }

    private enum Base: String, Codable {
        case nearMouse, windowCorner, screenCorner
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        switch self {
        case .nearMouse:
            try container.encode(Base.nearMouse, forKey: .base)
        case .windowCorner:
            try container.encode(Base.windowCorner, forKey: .base)
        case .screenCorner:
            try container.encode(Base.screenCorner, forKey: .base)
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let base = try container.decode(Base.self, forKey: .base)

        switch base {
        case .nearMouse:
            self = .nearMouse
        case .windowCorner:
            self = .windowCorner
        case .screenCorner:
            self = .screenCorner
        }
    }
}

extension IndicatorPosition.Alignment: Codable {
    private enum CodingKeys: String, CodingKey {
        case base
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(rawValue, forKey: .base)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let rawValue = try container.decode(Int32.self, forKey: .base)

        self = Self(rawValue: rawValue)!
    }
}

extension IndicatorPosition.Spacing: Codable {
    private enum CodingKeys: String, CodingKey {
        case base
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(rawValue, forKey: .base)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let rawValue = try container.decode(Int32.self, forKey: .base)

        self = Self(rawValue: rawValue)!
    }
}

import AppKit
import Foundation
import os

@MainActor
class AppKeyboardCache {
    private var cache = [String: String]()

    let logger = ISPLogger(category: String(describing: AppKeyboardCache.self))

    func remove(_ kind: AppKind) {
        if let id = kind.getId(), cache[id] != nil {
            logger.debug { "Remove #\(id)" }
            cache[id] = nil
        }
    }

    func save(_ kind: AppKind, keyboard: InputSource?) {
        guard let id = kind.getId() else { return }

        if let keyboardId = keyboard?.id {
            logger.debug { "Save \(id)#\(keyboardId)" }
            cache[id] = keyboardId
        }
    }

    func retrieve(_ kind: AppKind) -> InputSource? {
        guard let id = kind.getId(),
              let keyboardId = cache[id]
        else { return nil }

        logger.debug { "Retrieve \(id)#\(keyboardId)" }

        return InputSource.sources.first { $0.id == keyboardId }
    }

    func clear() {
        // FIXME: - Some apps/websites where 'restore' is selected should be ignored
        logger.debug { "Clear All" }
        cache.removeAll()
    }

    func remove(byBundleId bundleId: String) {
        for key in cache.keys {
            if key.starts(with: "\(bundleId)_") {
                logger.debug { "Remove \(bundleId)#\(key)" }
                cache[key] = nil
            }
        }
    }
}

import AppKit
import Combine
import Foundation

private func isValidWindow(windowAlpha: CGFloat, windowBounds: CGRect) -> Bool {
    // ----------------------------------------
    // Ignore windows.
    //
    // There are well known windows that we need ignore:
    //   * Google Chrome has some transparent windows.
    //   * Google Chrome's status bar which is shown when mouse cursor is on links.
    //   * Karabiner's status message windows.
    //
    // Do not forget to treat this situations:
    //   * Do not ignore menubar.
    //   * Do not ignore popup menu.
    //   * Do not ignore alert window on web browsers.
    //   * Do not ignore iTunes's preferences window which has some special behavior.

    // Ignore transparent windows.
    let transparentThreshold: CGFloat = 0.001

    if windowAlpha < transparentThreshold {
        return false
    }

    // Ignore small windows. (For example, a status bar of Google Chrome.)
    let windowSizeThreshold: CGFloat = 40
    if windowBounds.size.width < windowSizeThreshold ||
        windowBounds.size.height < windowSizeThreshold
    {
        return false
    }

    // Xcode and some app have some invisable window at fullscreen mode
    if let screen = NSScreen.getScreenInclude(rect: windowBounds),
       windowBounds.width == screen.frame.width,
       windowBounds.height < 70
    {
        return false
    }

    return true
}

extension NSRunningApplication {
    static func getWindowInfoPublisher(processIdentifier: pid_t) -> AnyPublisher<WindowInfo?, Never> {
        AnyPublisher.create { observer in
            let thread = Thread(block: {
                observer.send(getWindowInfo(processIdentifier: processIdentifier))
                observer.send(completion: .finished)
            })

            thread.start()

            return AnyCancellable { thread.cancel() }
        }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
    }

    func getWindowInfoPublisher() -> AnyPublisher<WindowInfo?, Never> {
        return NSRunningApplication.getWindowInfoPublisher(processIdentifier: processIdentifier)
    }
}

struct WindowInfo {
    let bounds: CGRect
    let layer: Int
}

private func getWindowInfo(processIdentifier: pid_t) -> WindowInfo? {
    guard let windows = CGWindowListCopyWindowInfo(.optionOnScreenAboveWindow, kCGNullWindowID) as? [[String: Any]]
    else { return nil }

    for window in windows {
        // Target windows:
        //   * frontmostApplication
        //   * loginwindow (shutdown dialog)
        //   * Launchpad
        //
        // Limitations:
        //   * There is not reliable way to judge whether Dashboard is shown.

        guard
            let windowOwnerPID = window["kCGWindowOwnerPID"] as? pid_t,
            let windowLayer = window["kCGWindowLayer"] as? Int,
            let windowAlpha = window["kCGWindowAlpha"] as? CGFloat,
            let windowBounds = CGRect(dictionaryRepresentation: window["kCGWindowBounds"] as! CFDictionary)
        else { continue }

        guard windowOwnerPID == processIdentifier
        else { continue }

        if isValidWindow(windowAlpha: windowAlpha, windowBounds: windowBounds) {
            return .init(bounds: windowBounds, layer: windowLayer)
        }
    }

    return nil
}

import AppKit
import SwiftUI

struct PickerItem: Equatable, Identifiable {
    static let mixed = PickerItem(id: "mixed", title: "Mixed", toolTip: nil)
    static let empty = PickerItem(id: "", title: "", toolTip: nil)

    let id: String
    let title: String
    let toolTip: String?
}

struct PopUpButtonPicker<Item: Equatable>: NSViewRepresentable {
    final class Coordinator: NSObject {
        private let parent: PopUpButtonPicker

        init(parent: PopUpButtonPicker) {
            self.parent = parent
        }

        @IBAction
        func selectItem(_ sender: NSPopUpButton) {
            parent.onSelect(sender.indexOfSelectedItem)
        }
    }

    let items: [Item]
    var width: CGFloat? = nil
    var isItemEnabled: (Item) -> Bool = { _ in true }
    let isItemSelected: (Item) -> Bool
    let getTitle: (Item) -> String
    let getToolTip: (Item) -> String?
    let onSelect: (Int) -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeNSView(context: Self.Context) -> NSPopUpButton {
        let popUpButton = NSPopUpButton(frame: .zero, pullsDown: false)
        popUpButton.autoenablesItems = false
        popUpButton.target = context.coordinator
        popUpButton.action = #selector(Coordinator.selectItem(_:))

        if let width = width {
            popUpButton.snp.makeConstraints { make in
                make.width.equalTo(width)
            }
        }

        return popUpButton
    }

    func updateNSView(_ popUpButton: NSPopUpButton, context _: Self.Context) {
        popUpButton.removeAllItems()

        for item in items {
            let menuItem = NSMenuItem()

            // in order for this to work, autoenablesItems must be set to false
            menuItem.isEnabled = isItemEnabled(item)
            menuItem.title = getTitle(item)
            menuItem.toolTip = getToolTip(item)

            popUpButton.menu?.addItem(menuItem)
        }

        if let selectedIndex = items.firstIndex(where: isItemSelected) {
            popUpButton.selectItem(at: selectedIndex)
        }
    }
}

import AppKit
import AVFoundation
import SwiftUI

struct PlayerView: NSViewRepresentable {
    let url: URL

    func updateNSView(_: NSViewType, context _: Context) {}

    func makeNSView(context _: Context) -> some NSView {
        return LoopingPlayerUIView(url: url)
    }
}

class LoopingPlayerUIView: NSView {
    private let playerLayer = AVPlayerLayer()

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    init(url: URL) {
        super.init(frame: .zero)

        // Setup the player
        let player = AVPlayer(url: url)
        playerLayer.player = player
        playerLayer.videoGravity = .resizeAspectFill

        wantsLayer = true
        layer?.addSublayer(playerLayer)

        // Setup looping
        player.actionAtItemEnd = .none
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(playerItemDidReachEnd(notification:)),
            name: .AVPlayerItemDidPlayToEndTime,
            object: player.currentItem
        )

        // Start the movie
        player.play()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc
    func playerItemDidReachEnd(notification _: Notification) {
        playerLayer.player?.seek(to: CMTime.zero)
    }

    override func layout() {
        super.layout()
        playerLayer.frame = bounds
    }
}

import AppKit

enum IndicatorSize: Int32, CaseIterable, Identifiable {
    case small = 1
    case medium = 2
    case large = 3

    var displayName: String {
        switch self {
        case .small:
            return "Small".i18n()
        case .medium:
            return "Medium".i18n()
        case .large:
            return "Large".i18n()
        }
    }

    var id: Self { self }
}

extension IndicatorSize: Codable {
    private enum CodingKeys: String, CodingKey {
        case base
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(rawValue, forKey: .base)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let rawValue = try container.decode(Int32.self, forKey: .base)

        self = Self(rawValue: rawValue)!
    }
}

import AppKit
import AXSwift
import Carbon
import Combine
import CombineExt
import SnapKit

@MainActor
class IndicatorWindowController: FloatWindowController {
    let permissionsVM: PermissionsVM
    let preferencesVM: PreferencesVM
    let indicatorVM: IndicatorVM
    let applicationVM: ApplicationVM
    let inputSourceVM: InputSourceVM

    let indicatorVC = IndicatorViewController()

    var isActive = false {
        didSet {
            if isActive {
                indicatorVC.view.animator().alphaValue = 1
                window?.displayIfNeeded()
                active()
            } else {
                indicatorVC.view.animator().alphaValue = 0
                deactive()
            }
        }
    }

    var cancelBag = CancelBag()

    init(
        permissionsVM: PermissionsVM,
        preferencesVM: PreferencesVM,
        indicatorVM: IndicatorVM,
        applicationVM: ApplicationVM,
        inputSourceVM: InputSourceVM
    ) {
        self.permissionsVM = permissionsVM
        self.preferencesVM = preferencesVM
        self.indicatorVM = indicatorVM
        self.applicationVM = applicationVM
        self.inputSourceVM = inputSourceVM

        super.init()

        contentViewController = indicatorVC

        let indicatorPublisher = indicatorVM.activateEventPublisher
            .receive(on: DispatchQueue.main)
            .map { (event: $0, inputSource: self.indicatorVM.state.inputSource) }
            .flatMapLatest { [weak self] params -> AnyPublisher<Void, Never> in
                let event = params.event
                let inputSource = params.inputSource

                guard let self = self else { return Empty().eraseToAnyPublisher() }
                guard let appKind = self.applicationVM.appKind,
                      !event.isJustHide,
                      !preferencesVM.isHideIndicator(appKind)
                else { return self.justHidePublisher() }

                let app = appKind.getApp()

                if preferencesVM.isShowAlwaysOnIndicator(app: app) {
                    return self.alwaysOnPublisher(event: event, inputSource: inputSource, appKind: appKind)
                } else if preferencesVM.needDetectFocusedFieldChanges(app: app) {
                    return self.autoShowPublisher(event: event, inputSource: inputSource, appKind: appKind)
                } else {
                    return self.autoHidePublisher(event: event, inputSource: inputSource, appKind: appKind)
                }
            }
            .eraseToAnyPublisher()

        indicatorVM.screenIsLockedPublisher
            .flatMapLatest { isLocked in isLocked ? Empty().eraseToAnyPublisher() : indicatorPublisher }
            .sink { _ in }
            .store(in: cancelBag)
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

import AppKit

extension URL {
    static let newtab = URL(string: "isp://newtab")!

    func open() {
        NSWorkspace.shared.open(self)
    }

    func bundleId() -> String {
        Bundle(url: self)?.bundleIdentifier ?? dataRepresentation.md5()
    }

    func removeFragment() -> URL {
        var components = URLComponents(url: self, resolvingAgainstBaseURL: false)

        components?.fragment = nil

        return components?.url ?? self
    }
}

extension URL {
    static let website = URL(
        string: "https://inputsource.pro"
    )!

    static let purchase = URL(
        string: "https://inputsource.pro/purchase"
    )!

    static let twitter = URL(
        string: "https://twitter.com/runjuuu"
    )!

    static let emailString = "support@inputsource.pro"

    static let email = URL(string: "mailto:\(emailString)")!

    static let howToEnableAccessbility = URL(
        string: "https://inputsource.pro/help/enable-spotlight-like-apps-support"
    )!

    static let howToEnableBrowserRule = URL(
        string: "https://inputsource.pro/help/enable-browser-rule"
    )!
}

// https://github.com/nbasham/BlackLabsSwiftUIColor/blob/main/Sources/BlackLabsSwiftUIColor/BlackLabsSwiftUIColor.swift

import SwiftUI

public extension Color {
    var hex: String { NSColor(self).hex }
    var hexWithAlpha: String { NSColor(self).hexWithAlpha }

    func hexDescription(_ includeAlpha: Bool = false) -> String {
        NSColor(self).hexDescription(includeAlpha)
    }
}

public extension Color {
    /**
     Creates an immuatble `Color` instance specified by a hex string, CSS color name, or nil.

     - parameter hex: A case insensitive `String`? representing a hex or CSS value e.g.

     - **"abc"**
     - **"abc7"**
     - **"#abc7"**
     - **"00FFFF"**
     - **"#00FFFF"**
     - **"00FFFF77"**
     - **"Orange", "Azure", "Tomato"** Modern browsers support 140 color names (<http://www.w3schools.com/cssref/css_colornames.asp>)
     - **"Clear"** [NSColor clearColor]
     - **"Transparent"** [NSColor clearColor]
     - **nil** [NSColor clearColor]
     - **empty string** [NSColor clearColor]
     */
    init(hex: String?) {
        self.init(NSColor(hex: hex))
    }
}

extension NSColor {
    /**
     Creates an immuatble `NSColor` instance specified by a hex string, CSS color name, or nil.

     - parameter hex: A case insensitive `String`? representing a hex or CSS value e.g.

     - **"abc"**
     - **"abc7"**
     - **"#abc7"**
     - **"00FFFF"**
     - **"#00FFFF"**
     - **"00FFFF77"**
     - **"Orange", "Azure", "Tomato"** Modern browsers support 140 color names (<http://www.w3schools.com/cssref/css_colornames.asp>)
     - **"Clear"** [NSColor clearColor]
     - **"Transparent"** [NSColor clearColor]
     - **nil** [NSColor clearColor]
     - **empty string** [NSColor clearColor]
     */
    convenience init(hex: String?) {
        let normalizedHexString: String = NSColor.normalize(hex)
        var c: UInt64 = 0
        Scanner(string: normalizedHexString).scanHexInt64(&c)

        let cc = CUnsignedInt(c)

        self.init(
            red: NSColorMasks.redValue(cc),
            green: NSColorMasks.greenValue(cc),
            blue: NSColorMasks.blueValue(cc),
            alpha: NSColorMasks.alphaValue(cc)
        )
    }

    var hex: String { hexDescription(false) }
    var hexWithAlpha: String { hexDescription(true) }

    /**
     Returns a hex equivalent of this `NSColor`.

     - Parameter includeAlpha:   Optional parameter to include the alpha hex, defaults to `false`.

     `color.hexDescription() -> "ff0000"`

     `color.hexDescription(true) -> "ff0000aa"`

     - Returns: A new string with `String` with the color's hexidecimal value.
     */
    func hexDescription(_ includeAlpha: Bool = false) -> String {
        guard cgColor.numberOfComponents == 4,
              let a = cgColor.components?.map({ Int($0 * CGFloat(255)) })
        else { return "Color not RGB." }

        let color = String(format: "%02x%02x%02x", a[0], a[1], a[2])

        if includeAlpha {
            let alpha = String(format: "%02x", a[3])
            return "\(color)\(alpha)"
        }

        return color
    }

    fileprivate enum NSColorMasks: CUnsignedInt {
        case redMask = 0xFF00_0000
        case greenMask = 0x00FF_0000
        case blueMask = 0x0000_FF00
        case alphaMask = 0x0000_00FF

        static func redValue(_ value: CUnsignedInt) -> CGFloat {
            return CGFloat((value & redMask.rawValue) >> 24) / 255.0
        }

        static func greenValue(_ value: CUnsignedInt) -> CGFloat {
            return CGFloat((value & greenMask.rawValue) >> 16) / 255.0
        }

        static func blueValue(_ value: CUnsignedInt) -> CGFloat {
            return CGFloat((value & blueMask.rawValue) >> 8) / 255.0
        }

        static func alphaValue(_ value: CUnsignedInt) -> CGFloat {
            return CGFloat(value & alphaMask.rawValue) / 255.0
        }
    }

    fileprivate static func normalize(_ hex: String?) -> String {
        guard var hexString = hex else {
            return "00000000"
        }
        if let cssColor = cssToHexDictionary[hexString.uppercased()] {
            return cssColor.count == 8 ? cssColor : cssColor + "ff"
        }
        if hexString.hasPrefix("#") {
            hexString = String(hexString.dropFirst())
        }
        if hexString.count == 3 || hexString.count == 4 {
            hexString = hexString.map { "\($0)\($0)" }.joined()
        }
        let hasAlpha = hexString.count > 7
        if !hasAlpha {
            hexString += "ff"
        }
        return hexString
    }

    /**
     All modern browsers support the following 140 color names (see http://www.w3schools.com/cssref/css_colornames.asp)
     */
    fileprivate static func hexFromCssName(_ cssName: String) -> String {
        let key = cssName.uppercased()
        if let hex = cssToHexDictionary[key] {
            return hex
        }
        return cssName
    }

    static let cssToHexDictionary: [String: String] = [
        "CLEAR": "00000000",
        "TRANSPARENT": "00000000",
        "": "00000000",
        "ALICEBLUE": "F0F8FF",
        "ANTIQUEWHITE": "FAEBD7",
        "AQUA": "00FFFF",
        "AQUAMARINE": "7FFFD4",
        "AZURE": "F0FFFF",
        "BEIGE": "F5F5DC",
        "BISQUE": "FFE4C4",
        "BLACK": "000000",
        "BLANCHEDALMOND": "FFEBCD",
        "BLUE": "0000FF",
        "BLUEVIOLET": "8A2BE2",
        "BROWN": "A52A2A",
        "BURLYWOOD": "DEB887",
        "CADETBLUE": "5F9EA0",
        "CHARTREUSE": "7FFF00",
        "CHOCOLATE": "D2691E",
        "CORAL": "FF7F50",
        "CORNFLOWERBLUE": "6495ED",
        "CORNSILK": "FFF8DC",
        "CRIMSON": "DC143C",
        "CYAN": "00FFFF",
        "DARKBLUE": "00008B",
        "DARKCYAN": "008B8B",
        "DARKGOLDENROD": "B8860B",
        "DARKGRAY": "A9A9A9",
        "DARKGREY": "A9A9A9",
        "DARKGREEN": "006400",
        "DARKKHAKI": "BDB76B",
        "DARKMAGENTA": "8B008B",
        "DARKOLIVEGREEN": "556B2F",
        "DARKORANGE": "FF8C00",
        "DARKORCHID": "9932CC",
        "DARKRED": "8B0000",
        "DARKSALMON": "E9967A",
        "DARKSEAGREEN": "8FBC8F",
        "DARKSLATEBLUE": "483D8B",
        "DARKSLATEGRAY": "2F4F4F",
        "DARKSLATEGREY": "2F4F4F",
        "DARKTURQUOISE": "00CED1",
        "DARKVIOLET": "9400D3",
        "DEEPPINK": "FF1493",
        "DEEPSKYBLUE": "00BFFF",
        "DIMGRAY": "696969",
        "DIMGREY": "696969",
        "DODGERBLUE": "1E90FF",
        "FIREBRICK": "B22222",
        "FLORALWHITE": "FFFAF0",
        "FORESTGREEN": "228B22",
        "FUCHSIA": "FF00FF",
        "GAINSBORO": "DCDCDC",
        "GHOSTWHITE": "F8F8FF",
        "GOLD": "FFD700",
        "GOLDENROD": "DAA520",
        "GRAY": "808080",
        "GREY": "808080",
        "GREEN": "008000",
        "GREENYELLOW": "ADFF2F",
        "HONEYDEW": "F0FFF0",
        "HOTPINK": "FF69B4",
        "INDIANRED": "CD5C5C",
        "INDIGO": "4B0082",
        "IVORY": "FFFFF0",
        "KHAKI": "F0E68C",
        "LAVENDER": "E6E6FA",
        "LAVENDERBLUSH": "FFF0F5",
        "LAWNGREEN": "7CFC00",
        "LEMONCHIFFON": "FFFACD",
        "LIGHTBLUE": "ADD8E6",
        "LIGHTCORAL": "F08080",
        "LIGHTCYAN": "E0FFFF",
        "LIGHTGOLDENRODYELLOW": "FAFAD2",
        "LIGHTGRAY": "D3D3D3",
        "LIGHTGREY": "D3D3D3",
        "LIGHTGREEN": "90EE90",
        "LIGHTPINK": "FFB6C1",
        "LIGHTSALMON": "FFA07A",
        "LIGHTSEAGREEN": "20B2AA",
        "LIGHTSKYBLUE": "87CEFA",
        "LIGHTSLATEGRAY": "778899",
        "LIGHTSLATEGREY": "778899",
        "LIGHTSTEELBLUE": "B0C4DE",
        "LIGHTYELLOW": "FFFFE0",
        "LIME": "00FF00",
        "LIMEGREEN": "32CD32",
        "LINEN": "FAF0E6",
        "MAGENTA": "FF00FF",
        "MAROON": "800000",
        "MEDIUMAQUAMARINE": "66CDAA",
        "MEDIUMBLUE": "0000CD",
        "MEDIUMORCHID": "BA55D3",
        "MEDIUMPURPLE": "9370DB",
        "MEDIUMSEAGREEN": "3CB371",
        "MEDIUMSLATEBLUE": "7B68EE",
        "MEDIUMSPRINGGREEN": "00FA9A",
        "MEDIUMTURQUOISE": "48D1CC",
        "MEDIUMVIOLETRED": "C71585",
        "MIDNIGHTBLUE": "191970",
        "MINTCREAM": "F5FFFA",
        "MISTYROSE": "FFE4E1",
        "MOCCASIN": "FFE4B5",
        "NAVAJOWHITE": "FFDEAD",
        "NAVY": "000080",
        "OLDLACE": "FDF5E6",
        "OLIVE": "808000",
        "OLIVEDRAB": "6B8E23",
        "ORANGE": "FFA500",
        "ORANGERED": "FF4500",
        "ORCHID": "DA70D6",
        "PALEGOLDENROD": "EEE8AA",
        "PALEGREEN": "98FB98",
        "PALETURQUOISE": "AFEEEE",
        "PALEVIOLETRED": "DB7093",
        "PAPAYAWHIP": "FFEFD5",
        "PEACHPUFF": "FFDAB9",
        "PERU": "CD853F",
        "PINK": "FFC0CB",
        "PLUM": "DDA0DD",
        "POWDERBLUE": "B0E0E6",
        "PURPLE": "800080",
        "RED": "FF0000",
        "ROSYBROWN": "BC8F8F",
        "ROYALBLUE": "4169E1",
        "SADDLEBROWN": "8B4513",
        "SALMON": "FA8072",
        "SANDYBROWN": "F4A460",
        "SEAGREEN": "2E8B57",
        "SEASHELL": "FFF5EE",
        "SIENNA": "A0522D",
        "SILVER": "C0C0C0",
        "SKYBLUE": "87CEEB",
        "SLATEBLUE": "6A5ACD",
        "SLATEGRAY": "708090",
        "SLATEGREY": "708090",
        "SNOW": "FFFAFA",
        "SPRINGGREEN": "00FF7F",
        "STEELBLUE": "4682B4",
        "TAN": "D2B48C",
        "TEAL": "008080",
        "THISTLE": "D8BFD8",
        "TOMATO": "FF6347",
        "TURQUOISE": "40E0D0",
        "VIOLET": "EE82EE",
        "WHEAT": "F5DEB3",
        "WHITE": "FFFFFF",
        "WHITESMOKE": "F5F5F5",
        "YELLOW": "FFFF00",
        "YELLOWGREEN": "9ACD32",
    ]
}

import AppKit

enum IndicatorKind {
    case icon
    case title
    case iconAndTitle
    case alwaysOn
}

@MainActor
struct IndicatorViewConfig {
    let inputSource: InputSource
    let kind: IndicatorKind
    let size: IndicatorSize
    let bgColor: NSColor?
    let textColor: NSColor?

    func render() -> NSView? {
        switch kind {
        case .iconAndTitle:
            return renderWithLabel()
        case .icon:
            return renderWithoutLabel()
        case .title:
            return renderOnlyLabel()
        case .alwaysOn:
            return renderAlwaysOn()
        }
    }

    func renderAlwaysOn() -> NSView? {
        let containerView = getContainerView()

        containerView.layer?.cornerRadius = 8

        containerView.snp.makeConstraints {
            $0.width.height.equalTo(8)
        }

        return containerView
    }

    private func renderWithLabel() -> NSView? {
        guard let imageView = getImageView(inputSource)
        else { return renderOnlyLabel() }

        let containerView = getContainerView()
        let labelView = NSTextField(labelWithString: inputSource.name)
        let stackView = NSStackView(views: [imageView, labelView])

        switch size {
        case .small:
            containerView.layer?.cornerRadius = 3
            stackView.spacing = 3
            labelView.font = .systemFont(ofSize: 10)
        case .medium:
            containerView.layer?.cornerRadius = 4
            stackView.spacing = 5
            labelView.font = .systemFont(ofSize: 12.6)
        case .large:
            containerView.layer?.cornerRadius = 6
            stackView.spacing = 8
            labelView.font = .systemFont(ofSize: 20)
        }

        labelView.textColor = textColor
        stackView.alignment = .centerY
        containerView.addSubview(stackView)

        stackView.snp.makeConstraints { make in
            switch size {
            case .small:
                make.edges.equalToSuperview().inset(NSEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))
            case .medium:
                make.edges.equalToSuperview().inset(NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4))
            case .large:
                make.edges.equalToSuperview().inset(NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6))
            }
        }

        return containerView
    }

    private func renderWithoutLabel() -> NSView? {
        guard let imageView = getImageView(inputSource)
        else { return renderOnlyLabel() }

        let containerView = getContainerView()
        let stackView = NSStackView(views: [imageView])

        containerView.layer?.cornerRadius = 4
        containerView.addSubview(stackView)

        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(NSEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))
        }

        return containerView
    }

    private func renderOnlyLabel() -> NSView? {
        let containerView = getContainerView()
        let labelView = NSTextField(labelWithString: inputSource.name)
        let stackView = NSStackView(views: [labelView])

        switch size {
        case .small:
            containerView.layer?.cornerRadius = 3
            stackView.spacing = 3
            labelView.font = .systemFont(ofSize: 10)
        case .medium:
            containerView.layer?.cornerRadius = 4
            stackView.spacing = 5
            labelView.font = .systemFont(ofSize: 12.6)
        case .large:
            containerView.layer?.cornerRadius = 6
            stackView.spacing = 8
            labelView.font = .systemFont(ofSize: 20)
        }

        labelView.textColor = textColor
        stackView.alignment = .centerY
        containerView.addSubview(stackView)

        stackView.snp.makeConstraints { make in
            switch size {
            case .small:
                make.edges.equalToSuperview().inset(NSEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))
            case .medium:
                make.edges.equalToSuperview().inset(NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4))
            case .large:
                make.edges.equalToSuperview().inset(NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6))
            }
        }

        return containerView
    }

    private func getImageView(_ inputSource: InputSource) -> NSView? {
//    if let textImage = getTextImageView(inputSource) { return textImage }

        guard let image = inputSource.icon
        else { return nil }

        let imageView = NSImageView(image: image)

        imageView.contentTintColor = textColor

        imageView.snp.makeConstraints { make in
            let size = imageView.image?.size ?? .zero
            let ratio = size.height / size.width
            let width: CGFloat = {
                switch self.size {
                case .small:
                    return 12
                case .medium:
                    return 16
                case .large:
                    return 24
                }
            }()
            let height = ratio * width

            make.size.equalTo(CGSize(width: width, height: height))
        }

        return imageView
    }

    private func getTextImageView(_ inputSource: InputSource) -> NSView? {
        guard let labelName = inputSource.getSystemLabelName()
        else { return nil }

        let labelView = NSTextField(labelWithString: labelName)
        let view = NSView()

        view.addSubview(labelView)
        view.wantsLayer = true
        view.layer?.backgroundColor = textColor?.cgColor
        view.layer?.cornerRadius = 2

        labelView.snp.makeConstraints {
            $0.center.equalTo(view)
        }

        view.snp.makeConstraints {
//      $0.width.equalTo(22)
            $0.width.height.equalTo(16)
        }

        labelView.textColor = bgColor
        labelView.font = .systemFont(ofSize: labelName.count > 1 ? 10 : 11, weight: .regular)

        return view
    }

    private func getContainerView() -> NSView {
        let containerView = NSView()

        containerView.wantsLayer = true
        containerView.setValue(bgColor, forKey: "backgroundColor")

        containerView.layer?.borderWidth = 1
        containerView.layer?.borderColor = NSColor.black.withAlphaComponent(0.1 * (bgColor?.alphaComponent ?? 1)).cgColor

        return containerView
    }
}

import AppKit

extension PreferencesVM {
    func appNeedCacheKeyboard(_ appKind: AppKind) -> Bool {
        if let browserRule = appKind.getBrowserInfo()?.rule,
           let keyboardRestoreStrategy = browserRule.keyboardRestoreStrategy
        {
            switch keyboardRestoreStrategy {
            case .RestorePreviouslyUsedOne:
                return true
            case .UseDefaultKeyboardInstead:
                return false
            }
        }

        let appRule = getAppCustomization(app: appKind.getApp())

        if preferences.isRestorePreviouslyUsedInputSource,
           appRule?.doNotRestoreKeyboard != true
        {
            return true
        }

        if !preferences.isRestorePreviouslyUsedInputSource,
           appRule?.doRestoreKeyboard == true
        {
            return true
        }

        return false
    }

    func cacheKeyboardFor(_ appKind: AppKind, keyboard: InputSource) {
        let defaultKeyboard = getAppDefaultKeyboard(appKind)

        if appNeedCacheKeyboard(appKind),
           defaultKeyboard?.id != keyboard.id
        {
            appKeyboardCache.save(appKind, keyboard: keyboard)
        } else {
            appKeyboardCache.remove(appKind)
        }
    }

    func removeKeyboardCacheFor(bundleId: String) {
        appKeyboardCache.remove(byBundleId: bundleId)
    }

    func clearKeyboardCache() {
        appKeyboardCache.clear()
    }

    enum AppAutoSwitchKeyboardStatus {
        case cached(InputSource), specified(InputSource)

        var inputSource: InputSource {
            switch self {
            case let .cached(i): return i
            case let .specified(i): return i
            }
        }
    }

    func getAppAutoSwitchKeyboard(
        _ appKind: AppKind
    ) -> AppAutoSwitchKeyboardStatus? {
        if let cachedKeyboard = getAppCachedKeyboard(appKind) {
            return .cached(cachedKeyboard)
        }

        if let defaultKeyboard = getAppDefaultKeyboard(appKind) {
            return .specified(defaultKeyboard)
        }

        // Fallback to system-wide default keyboard to prevent input method getting stuck
        // This ensures that apps without specific keyboard configuration will always
        // switch to the system default, resolving issues like ChatGPT → Terminal switching
        if let systemDefaultKeyboard = systemWideDefaultKeyboard {
            return .specified(systemDefaultKeyboard)
        }

        return nil
    }

    func getAppCachedKeyboard(_ appKind: AppKind) -> InputSource? {
        guard appNeedCacheKeyboard(appKind) else { return nil }
        return appKeyboardCache.retrieve(appKind)
    }

    func getAppDefaultKeyboard(_ appKind: AppKind) -> InputSource? {
        if appKind.getBrowserInfo()?.isFocusedOnAddressBar == true,
           let browserAddressKeyboard = browserAddressDefaultKeyboard
        {
            return browserAddressKeyboard
        }

        if let inputSource = appKind.getBrowserInfo()?.rule?.forcedKeyboard {
            return inputSource
        }

        return getAppCustomization(app: appKind.getApp())?.forcedKeyboard ?? systemWideDefaultKeyboard
    }
}

import SwiftUI

struct IndicatorColor {
    let light: Color
    let dark: Color
}

extension IndicatorColor {
    var dynamicColor: NSColor {
        return NSColor(name: nil) { appearance in
            switch appearance.bestMatch(from: [.darkAqua]) {
            case .darkAqua?:
                return NSColor(self.dark)
            default:
                return NSColor(self.light)
            }
        }
    }
}

extension IndicatorColor {
    static let background = IndicatorColor(
        light: .white.opacity(0.95),
        dark: .black
    )

    static let forgeground = IndicatorColor(
        light: .black,
        dark: .white
    )
}

extension IndicatorColor: Codable {
    private enum CodingKeys: String, CodingKey { case light, dark }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let lightStr = try container.decode(String.self, forKey: .light)
        let darkStr = try container.decode(String.self, forKey: .dark)

        light = Color(hex: lightStr)
        dark = Color(hex: darkStr)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(light.hexWithAlpha, forKey: .light)
        try container.encode(dark.hexWithAlpha, forKey: .dark)
    }
}

import AppKit
import Combine

class MainStorage: NSObject {
    var keyboardConfigs = CurrentValueSubject<[KeyboardConfig], Never>([])

    let container: NSPersistentContainer

    private lazy var keyboardConfigsFRC: NSFetchedResultsController<KeyboardConfig> = {
        let fetchRequest = KeyboardConfig.fetchRequest()

        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]

        keyboardConfigsFRC = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: container.viewContext,
            sectionNameKeyPath: nil,
            cacheName: nil
        )

        keyboardConfigsFRC.delegate = self

        return keyboardConfigsFRC
    }()

    init(container: NSPersistentContainer) {
        self.container = container

        super.init()
    }

    func refresh() {
        do {
            try keyboardConfigsFRC.performFetch()

            if let configs = keyboardConfigsFRC.fetchedObjects {
                keyboardConfigs.send(configs)
            }
        } catch {
            print("MainStorage refresh error: \(error.localizedDescription)")
        }
    }
}

extension MainStorage: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        if let configs = controller.fetchedObjects as? [KeyboardConfig] {
            keyboardConfigs.send(configs)
        }
    }
}

import Foundation

extension String {
    func string(at atIndex: Int) -> String {
        guard !isEmpty else { return self }

        let offset = max(min(count - 1, atIndex), 0)

        return String(self[index(startIndex, offsetBy: offset)])
    }
}

extension String {
    func runCommand(
        requireSudo: Bool,
        completion: @escaping (_ output: String, _ errorOutput: String, _ exitCode: Int32) -> Void
    ) {
        let process = Process()
        let stdoutPipe = Pipe()
        let stderrPipe = Pipe()

        if requireSudo {
            let appleScript = """
            do shell script "\(self.replacingOccurrences(of: "\"", with: "\\\""))" with administrator privileges
            """
            process.launchPath = "/usr/bin/osascript"
            process.arguments = ["-e", appleScript]
        } else {
            process.launchPath = "/bin/bash"
            process.arguments = ["-c", self]
        }

        process.standardOutput = stdoutPipe
        process.standardError = stderrPipe

        process.terminationHandler = { proc in
            let outputData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
            let errorData = stderrPipe.fileHandleForReading.readDataToEndOfFile()

            let output = String(data: outputData, encoding: .utf8) ?? ""
            let errorOutput = String(data: errorData, encoding: .utf8) ?? ""
            let exitCode = proc.terminationStatus

            DispatchQueue.main.async {
                completion(output, errorOutput, exitCode)
            }
        }

        do {
            try process.run()
        } catch {
            DispatchQueue.main.async {
                completion("", "Process failed to start: \(error)", -1)
            }
        }
    }
}

import AppKit

@MainActor
class AppRuleMenuItem: NSMenuItem {
    let app: NSRunningApplication
    let preferencesVM: PreferencesVM
    let inputSource: InputSource?

    var cancelBag = CancelBag()

    var appCustomization: AppRule? {
        preferencesVM.getAppCustomization(app: app)
    }

    init(app: NSRunningApplication, preferencesVM: PreferencesVM, inputSource: InputSource?) {
        self.app = app
        self.preferencesVM = preferencesVM
        self.inputSource = inputSource

        super.init(title: inputSource?.name ?? "", action: #selector(forceKeyboard(_:)), keyEquivalent: "")

        target = self

        updateState()
        watchChanges()
    }

    @available(*, unavailable)
    required init(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc func forceKeyboard(_: Any) {
        let inputSourceId = inputSource?.id ?? ""

        if let appCustomization = appCustomization {
            preferencesVM.setForceKeyboard(appCustomization, inputSourceId)
        } else {
            preferencesVM.setForceKeyboard(
                preferencesVM.addAppCustomization(app),
                inputSourceId
            )
        }

        inputSource?.select(useCJKVFix: preferencesVM.isUseCJKVFix())

        watchChanges()
    }

    func watchChanges() {
        cancelBag.cancel()

        appCustomization?.publisher(for: \.inputSourceId)
            .sink { [weak self] _ in self?.updateState() }
            .store(in: cancelBag)
    }

    func updateState() {
        state = appCustomization?.inputSourceId == inputSource?.id ? .on : .off
    }
}

import SwiftUI

struct RunningApplicationsPicker: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    let appIconSize: CGFloat = 18

    let onSelect: (NSRunningApplication) -> Void

    init(onSelect: @escaping (NSRunningApplication) -> Void) {
        self.onSelect = onSelect
    }

    var body: some View {
        Menu("Add Running Apps") {
            ForEach(preferencesVM.filterApps(NSWorkspace.shared.runningApplications), id: \.processIdentifier) { app in
                Button(action: { onSelect(app) }) {
                    Text(app.localizedName ?? app.bundleId() ?? app.description)

                    if let url = app.bundleURL {
                        let image = NSWorkspace.shared.icon(forFile: url.path)

                        SwiftUI.Image(nsImage: image)
                            .resizable()
                            .frame(width: appIconSize, height: appIconSize)
                    } else {
                        SwiftUI.Image(systemName: "app.dashed")
                            .resizable()
                            .frame(width: appIconSize, height: appIconSize)
                    }
                }
            }
        }
    }
}

import AppKit

extension PreferencesWindowController: NSSharingServicePickerToolbarItemDelegate {
    func items(for _: NSSharingServicePickerToolbarItem) -> [Any] {
        // Compose an array of items that are sharable such as text, URLs, etc.
        // depending on the context of your application (i.e. what the user
        // current has selected in the app and/or they tab they're in).
        let sharableItems = [URL(string: "https://inputsource.pro/")!] as [Any]

        return sharableItems
    }
}

extension PreferencesWindowController: NSToolbarDelegate {
    func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
        return []
//    return [.toolbarShareButtonItem]
    }

    func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [.toolbarShareButtonItem]
    }

    func toolbar(
        _: NSToolbar,
        itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
        willBeInsertedIntoToolbar _: Bool
    ) -> NSToolbarItem? {
        if itemIdentifier == NSToolbarItem.Identifier.toolbarShareButtonItem {
            let shareItem = NSSharingServicePickerToolbarItem(itemIdentifier: itemIdentifier)

            shareItem.toolTip = "Share"
            shareItem.delegate = self

            if #available(macOS 11.0, *) {
                shareItem.menuFormRepresentation?.image = NSImage(
                    systemSymbolName: "square.and.arrow.up",
                    accessibilityDescription: nil
                )
            }

            return shareItem
        }

        return nil
    }
}

extension PreferencesWindowController: NSToolbarItemValidation {
    func validateToolbarItem(_ item: NSToolbarItem) -> Bool {
        if item.itemIdentifier == .toolbarShareButtonItem {
            return true
        }

        return false
    }
}

import AppKit

extension NSScreen {
    static func getScreenWithMouse() -> NSScreen? {
        let mouseLocation = NSEvent.mouseLocation
        let screens = NSScreen.screens

        return screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) })
    }

    static func getScreenInclude(rect: CGRect) -> NSScreen? {
        return NSScreen.screens
            .map { screen in (screen, screen.frame.intersection(rect)) }
            .filter { _, intersect in !intersect.isNull }
            .map { screen, intersect in (screen, intersect.size.width * intersect.size.height) }
            .max { lhs, rhs in lhs.1 < rhs.1 }?.0
    }
}

extension NSScreen {
    /// The screen whose bottom left is at (0, 0).
    static var primary: NSScreen? {
        return NSScreen.screens.first
    }

    /// Converts the rectangle from Quartz "display space" to Cocoa "screen space".
    /// <http://stackoverflow.com/a/19887161/23649>
    static func convertFromQuartz(_ rect: CGRect) -> CGRect? {
        return NSScreen.primary.map { screen in
            var result = rect
            result.origin.y = screen.frame.maxY - result.maxY
            return result
        }
    }
}

import SwiftUI

struct SettingsSection<Content: View, Tips: View>: View {
    let title: String

    let tips: Tips?

    let noI18N: Bool

    let content: Content

    init(title: String, noI18N: Bool = false, tips: Tips? = nil, @ViewBuilder _ content: () -> Content) {
        self.title = title
        self.noI18N = noI18N
        self.tips = tips
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading) {
            if !title.isEmpty || tips != nil {
                HStack {
                    if !title.isEmpty {
                        Text(noI18N ? title : title.i18n())
                            .opacity(0.8)
                    }

                    tips
                }
                .padding(.horizontal)
            }

            VStack(alignment: .leading, spacing: 0) {
                content
            }
            .frame(maxWidth: .infinity)
            .background(NSColor.background2.color)
            .clipShape(RoundedRectangle(cornerRadius: 8))
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(NSColor.border2.color, lineWidth: 1)
            )
        }
    }
}

// Support optional footer
extension SettingsSection where Tips == EmptyView {
    init(title: String, noI18N: Bool = false, @ViewBuilder _ content: () -> Content) {
        self.title = title
        self.noI18N = noI18N
        tips = nil
        self.content = content()
    }
}

import AppKit
import SnapKit

@MainActor
class IndicatorViewController: NSViewController {
    let hoverableView = NSViewHoverable(frame: .zero)

    private(set) var config: IndicatorViewConfig? = nil {
        didSet {
            nextAlwaysOnView = config?.renderAlwaysOn()
            nextNormalView = config?.render()

            if normalView == nil || alwaysOnView == nil {
                refresh()
            }
        }
    }

    var fittingSize: CGSize? {
        nextNormalView?.fittingSize ?? normalView?.fittingSize
    }

    private(set) var nextNormalView: NSView? = nil
    private(set) var nextAlwaysOnView: NSView? = nil

    private(set) var normalView: NSView? = nil {
        didSet {
            oldValue?.removeFromSuperview()

            if let normalView = normalView {
                view.addSubview(normalView)

                normalView.snp.makeConstraints { make in
                    let size = normalView.fittingSize

                    make.edges.equalToSuperview()
                    make.width.equalTo(size.width)
                    make.height.equalTo(size.height)
                }
            }
        }
    }

    private(set) var alwaysOnView: NSView? = nil {
        didSet {
            oldValue?.removeFromSuperview()

            if let alwaysOnView = alwaysOnView {
                alwaysOnView.alphaValue = 0
                view.addSubview(alwaysOnView)

                alwaysOnView.snp.makeConstraints { make in
                    make.leading.bottom.equalToSuperview()
                }
            }
        }
    }

    init() {
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func prepare(config: IndicatorViewConfig) {
        self.config = config
    }

    func refresh() {
        if let nextNormalView = nextNormalView {
            normalView = nextNormalView
            self.nextNormalView = nil
        }

        if let nextAlwaysOnView = nextAlwaysOnView {
            alwaysOnView = nextAlwaysOnView
            self.nextAlwaysOnView = nil
        }
    }

    func showAlwaysOnView() {
        normalView?.animator().alphaValue = 0
        alwaysOnView?.animator().alphaValue = 1
    }

    override func loadView() {
        view = hoverableView
    }
}

import Cocoa
import Combine
import SwiftUI
import Alamofire
import LaunchAtLogin

class AppDelegate: NSObject, NSApplicationDelegate {
    var navigationVM: NavigationVM!
    var indicatorVM: IndicatorVM!
    var preferencesVM: PreferencesVM!
    var permissionsVM: PermissionsVM!
    var applicationVM: ApplicationVM!
    var inputSourceVM: InputSourceVM!
    var feedbackVM: FeedbackVM!
    var indicatorWindowController: IndicatorWindowController!
    var statusItemController: StatusItemController!

    func applicationDidFinishLaunching(_: Notification) {
        feedbackVM = FeedbackVM()
        navigationVM = NavigationVM()
        permissionsVM = PermissionsVM()
        preferencesVM = PreferencesVM(permissionsVM: permissionsVM)
        applicationVM = ApplicationVM(preferencesVM: preferencesVM)
        inputSourceVM = InputSourceVM(preferencesVM: preferencesVM)
        indicatorVM = IndicatorVM(permissionsVM: permissionsVM, preferencesVM: preferencesVM, applicationVM: applicationVM, inputSourceVM: inputSourceVM)

        indicatorWindowController = IndicatorWindowController(
            permissionsVM: permissionsVM,
            preferencesVM: preferencesVM,
            indicatorVM: indicatorVM,
            applicationVM: applicationVM,
            inputSourceVM: inputSourceVM
        )

        statusItemController = StatusItemController(
            navigationVM: navigationVM,
            permissionsVM: permissionsVM,
            preferencesVM: preferencesVM,
            applicationVM: applicationVM,
            indicatorVM: indicatorVM,
            feedbackVM: feedbackVM,
            inputSourceVM: inputSourceVM
        )

        LaunchAtLogin.migrateIfNeeded()
        openPreferencesAtFirstLaunch()
        sendLaunchPing()
        updateInstallVersionInfo()
    }

    func applicationDidBecomeActive(_: Notification) {
        statusItemController.openPreferences()
    }

    @MainActor
    func openPreferencesAtFirstLaunch() {
        if preferencesVM.preferences.prevInstalledBuildVersion != preferencesVM.preferences.buildVersion {
            statusItemController.openPreferences()
        }
    }

    @MainActor
    func updateInstallVersionInfo() {
        preferencesVM.preferences.prevInstalledBuildVersion = preferencesVM.preferences.buildVersion
    }

    @MainActor
    func sendLaunchPing() {
        let url = "https://inputsource.pro/api/launch"
        let launchData: [String: String] = [
            "prevInstalledBuildVersion": "\(preferencesVM.preferences.prevInstalledBuildVersion)",
            "shortVersion": Bundle.main.shortVersion,
            "buildVersion": "\(Bundle.main.buildVersion)",
            "osVersion": ProcessInfo.processInfo.operatingSystemVersionString
        ]

        AF.request(
            url,
            method: .post,
            parameters: launchData,
            encoding: JSONEncoding.default
        )
        .response { response in
            switch response.result {
            case .success:
                print("Launch ping sent successfully.")
            case let .failure(error):
                print("Failed to send launch ping:", error)
            }
        }
    }
}

import SwiftUI

struct RulesSettingsView: View {
    @State var selectedApp = Set<AppRule>()

    @EnvironmentObject var preferencesVM: PreferencesVM

    var items: [PickerItem] {
        [PickerItem.empty]
            + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(spacing: 0) {
                VStack(alignment: .leading, spacing: 0) {
                    ApplicationPicker(selectedApp: $selectedApp)
                        .border(width: 1, edges: [.bottom], color: Color(NSColor.gridColor))

                    HStack(spacing: 5) {
                        Button(action: selectApp) {
                            SwiftUI.Image(systemName: "plus")
                        }

                        Button(action: removeApp) {
                            SwiftUI.Image(systemName: "minus")
                        }
                        .disabled(selectedApp.isEmpty)

                        RunningApplicationsPicker(onSelect: selectRunningApp)
                    }
                    .padding(10)
                }
                .frame(width: 245)
                .border(width: 1, edges: [.trailing], color: Color(NSColor.gridColor))

                ApplicationDetail(selectedApp: $selectedApp)
                    .padding()

                Spacer(minLength: 0)
            }
        }
    }

    func selectApp() {
        let panel = NSOpenPanel()

        panel.allowsMultipleSelection = true
        panel.canChooseDirectories = false
        panel.allowedContentTypes = [.application]

        if let applicationPath = NSSearchPathForDirectoriesInDomains(
            .applicationDirectory,
            .localDomainMask,
            true
        ).first {
            panel.directoryURL = URL(fileURLWithPath: applicationPath, isDirectory: true)
        }

        if panel.runModal() == .OK {
            selectedApp = Set(panel.urls.map {
                preferencesVM.addAppCustomization($0, bundleId: $0.bundleId())
            })
        }
    }

    func removeApp() {
        for app in selectedApp {
            preferencesVM.removeAppCustomization(app)
        }
        selectedApp.removeAll()
    }

    func selectRunningApp(_ app: NSRunningApplication) {
        if let appRule = preferencesVM.addAppCustomization(app) {
            selectedApp = Set([appRule])
        }
    }
}

import Foundation

enum IndicatorInfo: Int32, CaseIterable, Identifiable {
    case iconAndTitle = 0
    case iconOnly = 1
    case titleOnly = 2

    var id: Self { self }

    var name: String {
        switch self {
        case .iconAndTitle: return "Icon and Title".i18n()
        case .iconOnly: return "Icon".i18n()
        case .titleOnly: return "Title".i18n()
        }
    }
}

extension IndicatorInfo: Codable {
    private enum CodingKeys: String, CodingKey {
        case base
    }

    private enum Base: String, Codable {
        case iconAndTitle, iconOnly, titleOnly
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        switch self {
        case .iconAndTitle:
            try container.encode(Base.iconAndTitle, forKey: .base)
        case .iconOnly:
            try container.encode(Base.iconOnly, forKey: .base)
        case .titleOnly:
            try container.encode(Base.titleOnly, forKey: .base)
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let base = try container.decode(Base.self, forKey: .base)

        switch base {
        case .iconAndTitle:
            self = .iconAndTitle
        case .iconOnly:
            self = .iconOnly
        case .titleOnly:
            self = .titleOnly
        }
    }
}

import SwiftUI

@MainActor
class NavigationVM: ObservableObject {
    enum Nav: String, CaseIterable, Identifiable {
        var id: String { rawValue }

        case general = "General"

        case appRules = "App Rules"
        case browserRules = "Browser Rules"

        case position = "Position"
        case appearance = "Appearance"

        case inputSourcesColorScheme = "Input Sources Color Scheme"
        case keyboardShortcut = "Hot Keys"

        case troubleshooting = "Troubleshooting"

        static var grouped: [(id: String, title: String, nav: [Nav])] {
            [
                ("general", "", [.general]),
                ("rule", "Rules", [.appRules, .browserRules]),
                ("theme", "Indicator", [.position, .appearance]),
                ("keyboard", "Keyboard", [.inputSourcesColorScheme, .keyboardShortcut]),
                ("others", "Others", [.troubleshooting]),
            ]
        }
    }

    @Published var selection: Nav = .general
}

import SwiftUI

// 新增 ToggleLabel 组件
struct ToggleLabel: View {
    let systemImageName: String
    let text: String
    var body: some View {
        HStack(spacing: 6) {
            Image(systemName: systemImageName)
            Text(text)
        }
    }
}

struct ApplicationDetail: View {
    @Binding var selectedApp: Set<AppRule>

    @EnvironmentObject var preferencesVM: PreferencesVM

    @State var forceKeyboard: PickerItem?
    @State var doRestoreKeyboardState = NSToggleViewState.off
    @State var doNotRestoreKeyboardState = NSToggleViewState.off
    @State var hideIndicator = NSToggleViewState.off

    var mixed: Bool {
        Set(selectedApp.map { $0.forcedKeyboard?.id }).count > 1
    }

    var items: [PickerItem] {
        [mixed ? PickerItem.mixed : nil, PickerItem.empty].compactMap { $0 }
            + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) }
    }

    var body: some View {
        VStack(alignment: .leading) {
            Text(String(format: "%@ App(s) Selected".i18n(), "\(selectedApp.count)"))
                .font(.subheadline.monospacedDigit())
                .opacity(0.5)
                .padding(.bottom, 5)

            VStack(alignment: .leading) {
                Text("Default Keyboard".i18n())
                    .fontWeight(.medium)

                PopUpButtonPicker<PickerItem?>(
                    items: items,
                    isItemEnabled: { $0?.id != "mixed" },
                    isItemSelected: { $0 == forceKeyboard },
                    getTitle: { $0?.title ?? "" },
                    getToolTip: { $0?.toolTip },
                    onSelect: handleSelect
                )
            }

            Divider()
                .padding(.vertical, 4)

            VStack(alignment: .leading) {
                Text("Keyboard Restore Strategy".i18n())
                    .fontWeight(.medium)

                HStack {
                    Image(systemName: "d.circle.fill")
                        .foregroundColor(.green)
                    NSToggleView(
                        label: restoreStrategyName(strategy: .UseDefaultKeyboardInstead),
                        state: preferencesVM.preferences.isRestorePreviouslyUsedInputSource
                            ? doNotRestoreKeyboardState
                            : .on,
                        onStateUpdate: handleToggleDoNotRestoreKeyboard
                    )
                    .fixedSize()
                    .disabled(!preferencesVM.preferences.isRestorePreviouslyUsedInputSource)
                }

                HStack {
                    Image(systemName: "arrow.uturn.left.circle.fill")
                        .foregroundColor(.blue)
                    NSToggleView(
                        label: restoreStrategyName(strategy: .RestorePreviouslyUsedOne),
                        state: preferencesVM.preferences.isRestorePreviouslyUsedInputSource
                            ? .on
                            : doRestoreKeyboardState,
                        onStateUpdate: handleToggleDoRestoreKeyboard
                    )
                    .fixedSize()
                    .disabled(preferencesVM.preferences.isRestorePreviouslyUsedInputSource)
                }
            }

            Divider()
                .padding(.vertical, 4)

            VStack(alignment: .leading) {
                Text("Indicator".i18n())
                    .fontWeight(.medium)
                HStack {
                    Image(systemName: "eye.slash.circle.fill")
                        .foregroundColor(.gray)
                    NSToggleView(
                        label: "Hide Indicator".i18n(),
                        state: hideIndicator,
                        onStateUpdate: handleToggleHideIndicator
                    )
                    .fixedSize()
                }
            }

            if selectedApp.contains(where: { preferencesVM.needDisplayEnhancedModePrompt(bundleIdentifier: $0.bundleId) }) {
                Divider().padding(.vertical, 4)

                EnhancedModeRequiredBadge()
            }

            Spacer()
        }
        .disabled(selectedApp.isEmpty)
        .onChange(of: selectedApp) { _ in
            updateForceKeyboardId()
            updateDoRestoreKeyboardState()
            updateDoNotRestoreKeyboardState()
            updateHideIndicatorState()
        }
    }

    func updateForceKeyboardId() {
        if mixed {
            forceKeyboard = PickerItem.mixed
        } else if let keyboard = selectedApp.first?.forcedKeyboard {
            forceKeyboard = PickerItem(id: keyboard.id, title: keyboard.name, toolTip: keyboard.id)
        } else {
            forceKeyboard = PickerItem.empty
        }
    }

    func updateDoRestoreKeyboardState() {
        let stateSet = Set(selectedApp.map { $0.doRestoreKeyboard })

        if stateSet.count > 1 {
            doRestoreKeyboardState = .mixed
        } else {
            doRestoreKeyboardState = stateSet.first == true ? .on : .off
        }
    }

    func updateDoNotRestoreKeyboardState() {
        let stateSet = Set(selectedApp.map { $0.doNotRestoreKeyboard })

        if stateSet.count > 1 {
            doNotRestoreKeyboardState = .mixed
        } else {
            doNotRestoreKeyboardState = stateSet.first == true ? .on : .off
        }
    }

    func updateHideIndicatorState() {
        let stateSet = Set(selectedApp.map { $0.hideIndicator })

        if stateSet.count > 1 {
            hideIndicator = .mixed
        } else {
            hideIndicator = stateSet.first == true ? .on : .off
        }
    }

    func handleSelect(_ index: Int) {
        forceKeyboard = items[index]

        for app in selectedApp {
            preferencesVM.setForceKeyboard(app, forceKeyboard?.id)
        }
    }

    func handleToggleDoNotRestoreKeyboard() -> NSControl.StateValue {
        switch doNotRestoreKeyboardState {
        case .on:
            selectedApp.forEach { preferencesVM.setDoNotRestoreKeyboard($0, false) }
            doNotRestoreKeyboardState = .off
            return .off
        case .off, .mixed:
            selectedApp.forEach { preferencesVM.setDoNotRestoreKeyboard($0, true) }
            doNotRestoreKeyboardState = .on
            return .on
        }
    }

    func handleToggleDoRestoreKeyboard() -> NSControl.StateValue {
        switch doRestoreKeyboardState {
        case .on:
            selectedApp.forEach { preferencesVM.setDoRestoreKeyboard($0, false) }
            doRestoreKeyboardState = .off
            return .off
        case .off, .mixed:
            selectedApp.forEach { preferencesVM.setDoRestoreKeyboard($0, true) }
            doRestoreKeyboardState = .on
            return .on
        }
    }

    func handleToggleHideIndicator() -> NSControl.StateValue {
        switch hideIndicator {
        case .on:
            selectedApp.forEach { preferencesVM.setHideIndicator($0, false) }
            hideIndicator = .off
            return .off
        case .off, .mixed:
            selectedApp.forEach { preferencesVM.setHideIndicator($0, true) }
            hideIndicator = .on
            return .on
        }
    }

    func restoreStrategyName(strategy: KeyboardRestoreStrategy) -> String {
        strategy.name + restoreStrategyTips(strategy: strategy)
    }

    func restoreStrategyTips(strategy: KeyboardRestoreStrategy) -> String {
        switch strategy {
        case .RestorePreviouslyUsedOne:
            return preferencesVM.preferences.isRestorePreviouslyUsedInputSource ? " (\("Default".i18n()))" : ""
        case .UseDefaultKeyboardInstead:
            return !preferencesVM.preferences.isRestorePreviouslyUsedInputSource ? " (\("Default".i18n()))" : ""
        }
    }
}

import AppKit
import os

class ISPLogger {
    let category: String

    var disabled: Bool

    init(category: String, disabled: Bool = false) {
        self.category = category
        self.disabled = disabled
    }

    func debug(_ getString: () -> String) {
        if disabled { return }
        // TODO: - Add toggle
        #if DEBUG
            let str = getString()
            let formatter = DateFormatter()
            formatter.dateFormat = "H:mm:ss.SSSS"
            print(formatter.string(from: Date()), "[\(category)]", str)
        #endif
    }
}

import SwiftUI
import VisualEffects

struct PreferencesView: View {
    @EnvironmentObject var navigationVM: NavigationVM
    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var indicatorVM: IndicatorVM

    @State private var asyncSelection: NavigationVM.Nav = .general

    var body: some View {
        return HStack(spacing: 0) {
            ZStack {
                VisualEffectBlur(material: .sidebar, blendingMode: .behindWindow, state: .followsWindowActiveState)

                HStack {
                    VStack {
                        Button(action: {}) {
                            // Trap default focus ring
                            Text("Input Source Pro")
                        }

                        Spacer()
                    }
                    Spacer()
                }
                .opacity(0)

                VStack(spacing: 12) {
                    ForEach(NavigationVM.Nav.grouped, id: \.id) { group in
                        VStack(spacing: 2) {
                            if !group.title.isEmpty {
                                HStack {
                                    Text(group.title.i18n())
                                        .font(.system(size: 10))
                                        .opacity(0.6)
                                    Spacer()
                                }
                                .padding(.leading, 20)
                                .padding(.bottom, 2)
                            }

                            ForEach(group.nav) { nav in
                                let onSelect = { navigationVM.selection = nav }

                                Button(action: onSelect) {
                                    Text(nav.displayName)
                                }
                                .buttonStyle(
                                    NavButtonStyle(
                                        icon: nav.icon,
                                        isActive: navigationVM.selection == nav
                                    )
                                )
                            }
                        }
                    }

                    Spacer()

                    Text(" \(preferencesVM.versionStr) (\(preferencesVM.buildStr))")
                        .opacity(0.5)
                        .font(.system(size: 12))
                }
                .padding(.top, 40)
                .padding(.vertical)
            }
            .frame(width: 200)

            HStack {
                VStack(spacing: 0) {
                    HStack {
                        SwiftUI.Image(systemName: asyncSelection.icon)
                            .font(.system(size: 18, weight: .medium))
                            .opacity(0.8)
                            .frame(width: 20)

                        VStack(alignment: .leading, spacing: 0) {
                            Text("Input Source Pro")
                                .font(.system(size: 12, weight: .semibold))
                                .opacity(0.8)

                            Text(asyncSelection.displayName)
                                .font(.system(size: 11))
                                .opacity(0.6)
                        }

                        Spacer()
                    }
                    .frame(height: 52)
                    .padding(.horizontal)
                    .border(width: 1, edges: [.bottom], color: NSColor.border.color)

                    asyncSelection.getView()
                    Spacer(minLength: 0)
                }

                Spacer(minLength: 0)
            }
            .border(width: 1, edges: [.leading], color: NSColor.border.color)
        }
        .frame(minWidth: 780, minHeight: 620)
        .background(NSColor.background.color)
        .environment(\.managedObjectContext, preferencesVM.container.viewContext)
        .onChange(of: navigationVM.selection) { _ in
            asyncSelection = navigationVM.selection
        }
        .onAppear {
            asyncSelection = navigationVM.selection
        }
        .edgesIgnoringSafeArea(.top)
        .modifier(FeedbackModal())
    }
}

extension NavigationVM.Nav {
    var icon: String {
        switch self {
        case .general:
            return "slider.horizontal.3"

        case .appRules:
            return "square.grid.2x2"

        case .browserRules:
            return "safari"

        case .position:
            return "arrow.up.and.down.and.arrow.left.and.right"

        case .appearance:
            return "paintbrush"

        case .inputSourcesColorScheme:
            return "paintpalette"

        case .keyboardShortcut:
            return "command"

        case .troubleshooting:
            return "ladybug"
        }
    }

    var displayName: String {
        switch self {
        case .inputSourcesColorScheme:
            return "Color Scheme".i18n()
        default:
            return rawValue.i18n()
        }
    }

    @ViewBuilder
    func getView() -> some View {
        switch self {
        case .general:
            GeneralSettingsView()

        case .appRules:
            RulesSettingsView()

        case .browserRules:
            BrowserRulesSettingsView()

        case .position:
            PositionSettingsView()

        case .appearance:
            AppearanceSettingsView()

        case .inputSourcesColorScheme:
            InputSourcesAppearanceSettingsView()

        case .keyboardShortcut:
            KeyboardsSettingsView()

        case .troubleshooting:
            TroubleshootingSettingsView()
        }
    }
}

struct NavButtonStyle: ButtonStyle {
    let icon: String

    let isActive: Bool

    func makeBody(configuration: Self.Configuration) -> some View {
        HStack {
            VStack {
                SwiftUI.Image(systemName: icon)
                    .font(.system(size: 15, weight: .medium))
                    .frame(width: 15, height: 15)
                    .opacity(0.9)
            }

            configuration.label
                .lineLimit(1)

            Spacer()
        }
        .padding(.leading, 10)
        .padding(.trailing, 5)
        .padding(.vertical, 8)
        .frame(maxWidth: .infinity)
        .background(isActive ? Color.gray.opacity(0.2) : Color.clear)
        .background(configuration.isPressed ? Color.gray.opacity(0.1) : Color.clear)
        .foregroundColor(Color.primary)
        .clipShape(RoundedRectangle(cornerRadius: 6))
        .contentShape(Rectangle())
        .padding(.horizontal, 10)
    }
}

import AppKit

extension NSMenuItem {
    convenience init(
        title string: String,
        target: AnyObject = NSMenuItem.self as AnyObject,
        action selector: Selector?,
        keyEquivalent charCode: String,
        modifier: NSEvent.ModifierFlags = .command
    ) {
        self.init(title: string, action: selector, keyEquivalent: charCode)
        keyEquivalentModifierMask = modifier
        self.target = target
    }

    convenience init(title string: String, submenuItems: [NSMenuItem]) {
        self.init(title: string, action: nil, keyEquivalent: "")
        submenu = NSMenu()
        submenu?.items = submenuItems
    }
}

import CommonCrypto
import Foundation

extension Data {
    public func sha256() -> String {
        return hexStringFromData(input: digest(input: self as NSData))
    }

    private func digest(input: NSData) -> NSData {
        let digestLength = Int(CC_SHA256_DIGEST_LENGTH)
        var hash = [UInt8](repeating: 0, count: digestLength)
        CC_SHA256(input.bytes, UInt32(input.length), &hash)
        return NSData(bytes: hash, length: digestLength)
    }

    private func hexStringFromData(input: NSData) -> String {
        var bytes = [UInt8](repeating: 0, count: input.length)
        input.getBytes(&bytes, length: input.length)

        var hexString = ""
        for byte in bytes {
            hexString += String(format: "%02x", UInt8(byte))
        }

        return hexString
    }
}

public extension String {
    func sha256() -> String {
        if let stringData = data(using: String.Encoding.utf8) {
            return stringData.sha256()
        }
        return ""
    }
}

import AppKit
import AXSwift
import Foundation

extension PreferencesVM {
    func addHotKeyGroup(
        inputSources: [InputSource]
    ) {
        let rule = HotKeyGroup(context: container.viewContext)

        rule.createdAt = Date()
        rule.id = UUID().uuidString
        rule.inputSources = inputSources

        saveContext()
    }

    func deleteHotKeyGroup(_ group: HotKeyGroup) {
        container.viewContext.delete(group)
        saveContext()
    }

    func updateHotKeyGroup(
        _ group: HotKeyGroup,
        inputSources: [InputSource]
    ) {
        saveContext {
            group.inputSources = inputSources
        }
    }

    func getHotKeyGroups() -> [HotKeyGroup] {
        let request = NSFetchRequest<HotKeyGroup>(entityName: "HotKeyGroup")

        do {
            return try container.viewContext.fetch(request)
        } catch {
            print("getHotKeyGroups() error: \(error.localizedDescription)")
            return []
        }
    }
}

import Cocoa
import SwiftUI

extension KeyboardConfig {
    var textColor: Color? {
        get {
            if let hex = textColorHex {
                return Color(hex: hex)
            } else {
                return nil
            }
        }

        set {
            textColorHex = newValue?.hexWithAlpha
        }
    }

    var bgColor: Color? {
        get {
            if let hex = bgColorHex {
                return Color(hex: hex)
            } else {
                return nil
            }
        }

        set {
            bgColorHex = newValue?.hexWithAlpha
        }
    }
}

import SwiftUI
import VisualEffects

struct IndicatorPositionEditor: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)

    @State var hoveredAlignment: IndicatorPosition.Alignment? = nil

    let height: CGFloat = 230
    let windowHPadding: CGFloat = 30
    let windowVPadding: CGFloat = 15

    var selectedAlignment: IndicatorPosition.Alignment {
        preferencesVM.preferences.indicatorPositionAlignment ?? .bottomRight
    }

    var position: IndicatorPosition {
        preferencesVM.preferences.indicatorPosition ?? .nearMouse
    }

    var alignmentHPadding: CGFloat {
        let offset = position == .windowCorner ? windowHPadding : 0
        let minLength = position == .windowCorner ? height - offset * 2 : height
        let spacing = preferencesVM.calcSpacing(minLength: minLength)

        return spacing + offset
    }

    var alignmentVPadding: CGFloat {
        let offset = position == .windowCorner ? windowVPadding : 0
        let minLength = position == .windowCorner ? height - offset * 2 : height
        let spacing = preferencesVM.calcSpacing(minLength: minLength)

        return spacing + offset
    }

    let items: [IndicatorPosition.Alignment] = [
        .topLeft, .topCenter, .topRight,
        .centerLeft, .center, .centerRight,
        .bottomLeft, .bottomCenter, .bottomRight,
    ]

    var body: some View {
        LazyVGrid(columns: columns, spacing: 0) {
            ForEach(items, id: \.self) { (alignment: IndicatorPosition.Alignment) in
                IndicatorAlignmentItem(
                    alignment: alignment,
                    position: position,
                    isSelected: selectedAlignment == alignment,
                    content: {
                        let offset = offset(alignment: alignment)

                        alignment.indicator()
                            .rotationEffect(alignment.rotate)
                            .offset(x: offset.x, y: offset.y)
                            .foregroundColor(Color.primary)
                    }
                )
                .frame(height: height / 3)
                .onTapGesture {
                    withAnimation {
                        preferencesVM.update {
                            $0.indicatorPositionAlignment = alignment
                        }
                    }
                }
            }
            .opacity(position == .nearMouse ? 0 : 1)
        }
        .background(
            IndicatorAlignmentView(
                alignment: position == .nearMouse ? .center : selectedAlignment
            ) {
                IndicatorView()
                    .fixedSize()
                    .padding(.horizontal, alignmentHPadding)
                    .padding(.vertical, alignmentVPadding)
            }
        )
        .background(
            windowIndicator()
        )
        .background(
            Image("FakeDesktop")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .scaledToFill()
        )
    }

    @ViewBuilder
    func windowIndicator() -> some View {
        MockWindowView()
            .padding(.horizontal, windowHPadding)
            .padding(.vertical, windowVPadding)
            .frame(height: height)
            .opacity(position == .windowCorner ? 1 : 0)
            .offset(x: 0, y: position == .windowCorner ? 0 : height)
    }

    func offset(alignment: IndicatorPosition.Alignment) -> (x: Double, y: Double) {
        switch alignment {
        case .center:
            return (0, 0)
        case .centerLeft:
            return (alignmentHPadding, 0)
        case .centerRight:
            return (-alignmentHPadding, 0)
        case .topLeft:
            return (alignmentHPadding, alignmentVPadding)
        case .topCenter:
            return (0, alignmentVPadding)
        case .topRight:
            return (-alignmentHPadding, alignmentVPadding)
        case .bottomLeft:
            return (alignmentHPadding, -alignmentVPadding)
        case .bottomCenter:
            return (0, -alignmentVPadding)
        case .bottomRight:
            return (-alignmentHPadding, -alignmentVPadding)
        }
    }
}

struct IndicatorAlignmentItem<Content: View>: View {
    let alignment: IndicatorPosition.Alignment

    let position: IndicatorPosition

    let isSelected: Bool

    let content: Content

    @State var isHovered: Bool = false

    init(alignment: IndicatorPosition.Alignment, position: IndicatorPosition, isSelected: Bool, @ViewBuilder content: () -> Content) {
        self.alignment = alignment
        self.position = position
        self.isSelected = isSelected
        self.content = content()
    }

    var body: some View {
        IndicatorAlignmentView(alignment: alignment) {
            content
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .opacity(isSelected ? 0 : 1)
        .opacity(isHovered ? 0.9 : 0.3)
        .animation(.default, value: isSelected)
        .animation(.default, value: isHovered)
        .onHover(perform: { isHovered = $0 })
    }
}

private extension IndicatorPosition.Alignment {
    var rotate: Angle {
        switch self {
        case .topLeft:
            return .degrees(-90)
        case .topRight:
            return .zero
        case .bottomLeft:
            return .degrees(180)
        case .bottomRight:
            return .degrees(90)
        case .center:
            return .degrees(45)
        case .centerLeft:
            return .zero
        case .centerRight:
            return .zero
        case .topCenter:
            return .zero
        case .bottomCenter:
            return .zero
        }
    }

    @ViewBuilder
    func indicator() -> some View {
        switch self {
        case .center:
            RoundedRectangle(cornerRadius: 3)
                .frame(width: 30, height: 30)
        case .topCenter, .bottomCenter:
            RoundedRectangle(cornerRadius: 2)
                .frame(width: 44, height: 8)
        case .centerLeft, .centerRight:
            RoundedRectangle(cornerRadius: 2)
                .frame(width: 8, height: 30)
        default:
            Image(nsImage: .triangle)
                .resizable()
                .scaledToFit()
                .frame(width: 30)
        }
    }
}

import AppKit
import Combine

class NSViewHoverable: NSView {
    private let hoverdSubject: PassthroughSubject<Bool, Never>

    let hoverdPublisher: AnyPublisher<Bool, Never>

    override init(frame frameRect: NSRect) {
        hoverdSubject = PassthroughSubject()
        hoverdPublisher = hoverdSubject.eraseToAnyPublisher()

        super.init(frame: frameRect)
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func updateTrackingAreas() {
        trackingAreas.forEach { removeTrackingArea($0) }

        addTrackingArea(
            NSTrackingArea(
                rect: bounds,
                options: [.activeAlways, .mouseMoved, .mouseEnteredAndExited],
                owner: self,
                userInfo: nil
            )
        )
    }

    override func mouseMoved(with _: NSEvent) {
        hoverdSubject.send(true)
    }

    override func mouseExited(with _: NSEvent) {
        hoverdSubject.send(false)
    }
}

import AppKit
import SwiftUI

enum NSToggleViewState {
    case on, off, mixed

    var controlState: NSControl.StateValue {
        switch self {
        case .on:
            return .on
        case .mixed:
            return .mixed
        case .off:
            return .off
        }
    }
}

struct NSToggleView: NSViewRepresentable {
    let label: String
    let state: NSToggleViewState
    let onStateUpdate: () -> NSControl.StateValue

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeNSView(context: Self.Context) -> NSButton {
        let button = NSButton()

        button.title = label
        button.allowsMixedState = true
        button.setButtonType(.switch)
        button.target = context.coordinator
        button.action = #selector(Coordinator.onClick(_:))

        return button
    }

    func updateNSView(_ button: NSButton, context _: Self.Context) {
        button.title = label
        button.state = state.controlState
    }
}

extension NSToggleView {
    final class Coordinator: NSObject {
        private let parent: NSToggleView

        init(parent: NSToggleView) {
            self.parent = parent
        }

        @IBAction
        func onClick(_ sender: NSButton) {
            sender.state = parent.onStateUpdate()
        }
    }
}

import SwiftUI

struct BrowserRulesSettingsView: View {
    @State var isPresented = false

    @State var permissionRequest: Browser? = nil

    @State var selectedRules = Set<BrowserRule>()

    @FetchRequest(sortDescriptors: [
        NSSortDescriptor(key: "createdAt", ascending: true),
    ])
    var browserRules: FetchedResults<BrowserRule>

    @EnvironmentObject var preferencesVM: PreferencesVM

    @EnvironmentObject var permissionsVM: PermissionsVM

    var inputSourceItems: [PickerItem] {
        [PickerItem.empty]
            + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) }
    }

    var body: some View {
        let isShowPermissionRequest = Binding(
            get: { permissionRequest != nil },
            set: {
                if $0 == false {
                    permissionRequest = nil
                }
            }
        )

        return VStack(spacing: 0) {
            VStack {
                HStack {
                    Text("Default Keyboard for Address Bar".i18n())

                    PopUpButtonPicker<PickerItem?>(
                        items: inputSourceItems,
                        width: 150,
                        isItemSelected: { $0?.id == preferencesVM.preferences.browserAddressDefaultKeyboardId },
                        getTitle: { $0?.title ?? "" },
                        getToolTip: { $0?.toolTip },
                        onSelect: handleBrowserAddressDefaultKeyboardSelect
                    )
                    .disabled(!preferencesVM.preferences.isEnhancedModeEnabled)

                    EnhancedModeRequiredBadge()

                    Spacer(minLength: 0)
                }
            }
            .padding()
            .border(width: 1, edges: [.bottom], color: NSColor.border.color)

            List(browserRules, id: \.self, selection: $selectedRules) { rule in
                BrowserRuleRow(isSelected: selectedRules.contains(rule), rule: rule)
            }

            HStack(spacing: 0) {
                HStack(spacing: 5) {
                    Button(action: addRule) {
                        SwiftUI.Image(systemName: "plus")
                    }

                    Button(action: removeRules) {
                        SwiftUI.Image(systemName: "minus")
                    }
                    .disabled(selectedRules.isEmpty)
                }

                Divider()
                    .frame(height: 16)
                    .padding(.horizontal, 8)

                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 10) {
                        ForEach(Browser.allCases.sorted { $0.rawValue < $1.rawValue }, id: \.rawValue) { browser in
                            if NSApplication.isBrowserInstalled(browser.rawValue) {
                                Toggle(isOn: isEnableFor(browser)) {
                                    Text("\(browser.displayName)")
                                }
                            }
                        }
                    }
                }
            }
            .padding(10)
            .border(width: 1, edges: [.top], color: NSColor.gridColor.color)
            .sheet(isPresented: $isPresented, content: {
                BrowserRuleEditView(isPresented: $isPresented, rule: nil)
            })
            .sheet(isPresented: isShowPermissionRequest) {
                BrowserPermissionRequestView(
                    isPresented: isShowPermissionRequest,
                    onSuccess: permissionRequestSuccess
                )
            }
        }
    }

    func addRule() {
        isPresented = true
    }

    func removeRules() {
        for selectedRule in selectedRules {
            preferencesVM.deleteBrowserRule(selectedRule)
        }
        selectedRules.removeAll()
    }

    func isEnableFor(_ browser: Browser) -> Binding<Bool> {
        Binding(
            get: { preferencesVM.isBrowserEnabled(browser) },
            set: { enableFor(browser, enable: $0) }
        )
    }

    func enableFor(_ browser: Browser, enable: Bool) {
        if enable {
            // TODO: replace `isAccessibilityEnabled` with enhance mode
            if permissionsVM.isAccessibilityEnabled {
                toggle(browser: browser, isEnable: true)
            } else {
                permissionRequest = browser
            }
        } else {
            toggle(browser: browser, isEnable: false)
        }
    }

    func permissionRequestSuccess() {
        guard let browser = permissionRequest
        else { return }

        toggle(browser: browser, isEnable: true)
    }

    func toggle(browser: Browser, isEnable: Bool) {
        preferencesVM.update {
            if isEnable {
                $0.isEnhancedModeEnabled = true
            }

            switch browser {
            case .Chrome:
                $0.isEnableURLSwitchForChrome = isEnable
            case .Chromium:
                $0.isEnableURLSwitchForChromium = isEnable
            case .Arc:
                $0.isEnableURLSwitchForArc = isEnable
            case .Safari:
                $0.isEnableURLSwitchForSafari = isEnable
            case .SafariTechnologyPreview:
                $0.isEnableURLSwitchForSafariTechnologyPreview = isEnable
            case .Edge:
                $0.isEnableURLSwitchForEdge = isEnable
            case .Brave:
                $0.isEnableURLSwitchForBrave = isEnable
            case .BraveBeta:
                $0.isEnableURLSwitchForBraveBeta = isEnable
            case .BraveNightly:
                $0.isEnableURLSwitchForBraveNightly = isEnable
            case .Vivaldi:
                $0.isEnableURLSwitchForVivaldi = isEnable
            case .Opera:
                $0.isEnableURLSwitchForOpera = isEnable
            case .Thorium:
                $0.isEnableURLSwitchForThorium = isEnable
            case .Firefox:
                $0.isEnableURLSwitchForFirefox = isEnable
            case .FirefoxDeveloperEdition:
                $0.isEnableURLSwitchForFirefoxDeveloperEdition = isEnable
            case .FirefoxNightly:
                $0.isEnableURLSwitchForFirefoxNightly = isEnable
            case .Zen:
                $0.isEnableURLSwitchForZen = isEnable
            case .Dia:
                $0.isEnableURLSwitchForDia = isEnable
            }
        }
    }

    func handleBrowserAddressDefaultKeyboardSelect(_ index: Int) {
        let browserAddressDefaultKeyboard = inputSourceItems[index]

        preferencesVM.update {
            $0.browserAddressDefaultKeyboardId = browserAddressDefaultKeyboard.id
        }
    }
}

import AppKit

extension Bundle {
    var shortVersion: String {
        infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
    }

    var buildVersion: Int {
        Int(infoDictionary?["CFBundleVersion"] as? String ?? "0") ?? 0
    }
}

import AppKit

class FloatWindowController: NSWindowController, NSWindowDelegate {
    init(canBecomeKey: Bool = false) {
        super.init(window: FloatWindow(
            canBecomeKey: canBecomeKey,
            contentRect: CGRect(origin: .zero, size: .zero),
            styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
            backing: .buffered,
            defer: false
        ))

        window?.delegate = self
        window?.ignoresMouseEvents = true
        window?.standardWindowButton(.zoomButton)?.isEnabled = false
        window?.standardWindowButton(.miniaturizeButton)?.isEnabled = false
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension FloatWindowController {
    func active() {
        window?.orderFront(nil)
    }

    func deactive() {
        window?.orderOut(nil)
    }

    func moveTo(point: CGPoint) {
        window?.setFrameOrigin(point)
    }
}

import AppKit
import Combine

extension Timer {
    static func delay(
        seconds: TimeInterval,
        tolerance _: TimeInterval? = nil,
        options _: RunLoop.SchedulerOptions? = nil
    ) -> AnyPublisher<Date, Never> {
        return Timer.interval(seconds: seconds)
            .first()
            .eraseToAnyPublisher()
    }

    static func interval(
        seconds: TimeInterval
    ) -> AnyPublisher<Date, Never> {
        return Timer.publish(every: seconds, on: .main, in: .common)
            .autoconnect()
            .ignoreFailure()
    }
}

import SwiftUI

struct PreferenceSection<Content: View>: View {
    let title: String

    let content: Content

    let hideDivider: Bool

    init(title: String, hideDivider: Bool = false, @ViewBuilder _ content: () -> Content) {
        self.title = title
        self.hideDivider = hideDivider
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(alignment: .firstTextBaseline) {
                VStack {
                    Text(title.isEmpty ? title : "\(title.i18n()):")
                        .fontWeight(.bold)
                        .tracking(0.2)
                        .frame(width: 100, alignment: .leading)
                }

                VStack(alignment: .leading) {
                    content
                }

                Spacer(minLength: 0)
            }

            if !hideDivider {
                Divider()
                    .padding(.vertical)
            }
        }
    }
}

import Cocoa

class AppMenu: NSMenu {
    private lazy var applicationName = ProcessInfo.processInfo.processName

    override init(title: String) {
        super.init(title: title)

        let mainMenu = NSMenuItem()
        mainMenu.submenu = NSMenu(title: "MainMenu")
        mainMenu.submenu?.items = [
            NSMenuItem(title: "About \(applicationName)", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""),
            NSMenuItem.separator(),
            NSMenuItem(title: "Preferences...", action: nil, keyEquivalent: ","),
            NSMenuItem.separator(),
            NSMenuItem(title: "Hide \(applicationName)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h"),
            NSMenuItem(title: "Hide Others", target: self, action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h", modifier: .init(arrayLiteral: [.command, .option])),
            NSMenuItem(title: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: ""),
            NSMenuItem.separator(),
            NSMenuItem(title: "Quit \(applicationName)", action: #selector(NSApplication.shared.terminate(_:)), keyEquivalent: "q"),
        ]

        let fileMenu = NSMenuItem()
        fileMenu.submenu = NSMenu(title: "File")
        fileMenu.submenu?.items = [
            NSMenuItem(title: "New", action: #selector(NSDocumentController.newDocument(_:)), keyEquivalent: "n"),
            NSMenuItem(title: "Open", action: #selector(NSDocumentController.openDocument(_:)), keyEquivalent: "o"),
            NSMenuItem.separator(),
            NSMenuItem(title: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w"),
            NSMenuItem.separator(),
            NSMenuItem(title: "Save", action: #selector(NSDocument.save(_:)), keyEquivalent: "s"),
            NSMenuItem(title: "Save As...", action: #selector(NSDocument.saveAs(_:)), keyEquivalent: "S"),
            NSMenuItem(title: "Revert to Saved...", action: #selector(NSDocument.revertToSaved(_:)), keyEquivalent: ""),
            NSMenuItem.separator(),
            NSMenuItem(title: "Export", action: nil, keyEquivalent: ""),
        ]

        let editMenu = NSMenuItem()
        editMenu.submenu = NSMenu(title: "Edit")
        editMenu.submenu?.items = [
            NSMenuItem(title: "Undo", action: #selector(UndoManager.undo), keyEquivalent: "z"),
            NSMenuItem(title: "Redo", action: #selector(UndoManager.redo), keyEquivalent: "Z"),
            NSMenuItem.separator(),
            NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"),
            NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"),
            NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"),
            NSMenuItem.separator(),
            NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"),
            NSMenuItem.separator(),
            NSMenuItem(title: "Delete", target: self, action: nil, keyEquivalent: "⌫", modifier: .init()),
            NSMenuItem(title: "Duplicate", action: #selector(NSApplication.copy), keyEquivalent: "d"),
        ]

        let windowMenu = NSMenuItem()
        windowMenu.submenu = NSMenu(title: "Window")
        windowMenu.submenu?.items = [
            NSMenuItem(title: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m"),
            NSMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""),
            NSMenuItem.separator(),
            NSMenuItem(title: "Show All", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "m"),
        ]

        let viewMenu = NSMenuItem()
        viewMenu.submenu = NSMenu(title: "View")
        viewMenu.submenu?.items = []

        let helpMenu = NSMenuItem()
        let helpMenuSearch = NSMenuItem()
        helpMenuSearch.view = NSTextField()
        helpMenu.submenu = NSMenu(title: "Help")
        helpMenu.submenu?.items = [
            helpMenuSearch,
        ]

        items = [mainMenu, fileMenu, editMenu, viewMenu, windowMenu, helpMenu]
    }

    @available(*, unavailable)
    required init(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

import Foundation

extension CFRange {
    init(range: NSRange) {
        self = CFRangeMake(range.location == NSNotFound ? kCFNotFound : range.location, range.length)
    }

    func cursorRange(offset: Int, value: String) -> NSRange {
        return NSRange(location: max(min(location + offset, value.count - 1), 0), length: 1)
    }
}

extension NSRange {
    init(range: CFRange) {
        self = NSMakeRange(range.location == kCFNotFound ? NSNotFound : range.location, range.length)
    }
}

import Foundation

extension NSRegularExpression {
    func matches(_ string: String) -> Bool {
        let range = NSRange(location: 0, length: string.utf16.count)
        return firstMatch(in: string, options: [], range: range) != nil
    }
}

import SwiftUI

struct CursorLagFixView: View {
    @State private var isCursorLagFixEnabled = false
    @State private var isRunningCommand = false
    @State private var isRebootRequired = false

    var body: some View {
        let toggleBinding = Binding(
            get: { isCursorLagFixEnabled },
            set: {
                isCursorLagFixEnabled = $0
                toggleRedesignedTextCursor(disable: $0)
            }
        )

        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Toggle("", isOn: toggleBinding)
                    .disabled(isRunningCommand)
                    .toggleStyle(.switch)
                    .labelsHidden()

                Text("Cursor Lag Fix".i18n())
                    .font(.headline)

                Spacer()

                RebootRequiredBadge(isRequired: isRebootRequired)
            }

            Text(.init("Cursor Lag Fix Description".i18n()))
                .font(.system(size: 12))
                .opacity(0.8)
        }
        .padding()
        .onAppear(perform: checkCursorSetting)
    }

    func toggleRedesignedTextCursor(disable: Bool) {
        guard !isRunningCommand else { return }

        let command = "sudo defaults write /Library/Preferences/FeatureFlags/Domain/UIKit.plist redesigned_text_cursor -dict-add Enabled -bool \(disable ? "NO" : "YES")"

        isRunningCommand = true

        command.runCommand(
            requireSudo: true,
            completion: { _, _, _ in
                isRunningCommand = false
                isRebootRequired = true
                checkCursorSetting()
            }
        )
    }

    func checkCursorSetting() {
        guard !isRunningCommand else { return }

        let command = "defaults read /Library/Preferences/FeatureFlags/Domain/UIKit.plist redesigned_text_cursor"

        isRunningCommand = true

        command.runCommand(
            requireSudo: false,
            completion: { output, _, _ in
                isRunningCommand = false
                isCursorLagFixEnabled = output.contains("Enabled = 0")
            }
        )
    }
}

import Cocoa

extension HotKeyGroup {
    private static let separator = "∆"

    private var ids: [String] {
        return inputSourceIds?.components(separatedBy: Self.separator) ?? []
    }

    @MainActor
    var inputSources: [InputSource] {
        get {
            return InputSource.sources
                .filter { ids.contains($0.id) }
        }
        set {
            inputSourceIds = newValue.map { $0.id }.joined(separator: Self.separator)
        }
    }
}

import Foundation

extension String {
    func i18n(
        comment: String = "",
        tableName: String = "Localizable"
    ) -> String {
        #if DEBUG
            let fallback = "**\(self)**"
        #else
            let fallback = self
        #endif

        return NSLocalizedString(
            self,
            tableName: tableName,
            value: fallback,
            comment: comment
        )
    }
}

import Foundation

extension IndicatorWindowController {
    func getAppSize() -> CGSize? {
        return indicatorVC.fittingSize
    }

    func updateIndicator(event _: IndicatorVM.ActivateEvent, inputSource: InputSource) {
        let preferences = preferencesVM.preferences

        indicatorVC.prepare(config: IndicatorViewConfig(
            inputSource: inputSource,
            kind: preferences.indicatorKind,
            size: preferences.indicatorSize ?? .medium,
            bgColor: preferencesVM.getBgNSColor(inputSource),
            textColor: preferencesVM.getTextNSColor(inputSource)
        ))

        if isActive {
            indicatorVC.refresh()
        }
    }

    func moveIndicator(position: PreferencesVM.IndicatorPositionInfo) {
        indicatorVC.refresh()
        moveTo(point: position.point)
    }
}

import SwiftUI

struct AddSwitchingGroupButton: View {
    @State var isActive = false
    @State var selections = Set<String>()

    let onSelect: ([InputSource]) -> Void

    var body: some View {
        Button("Add Switch Group".i18n(), systemImage: "plus.circle") {
            isActive = true
        }
        .sheet(isPresented: $isActive, content: {
            VStack(alignment: .leading) {
                Text("Select Input Sources".i18n())
                    .font(.title)
                    .padding(.bottom, 4)

                Text("Add a switch group to allow switching between multiple input sources using the same hotkey. Select at least two input sources.".i18n())
                    .lineLimit(nil)
                    .multilineTextAlignment(.leading)
                    .frame(width: 500)

                VStack(alignment: .leading) {
                    ForEach(InputSource.sources, id: \.id) { inputSource in
                        VStack(alignment: .leading) {
                            HStack(spacing: 6) {
                                if selections.contains(inputSource.id) {
                                    Image(systemName: "checkmark.circle.fill")
                                        .foregroundColor(Color.accentColor)
                                        .font(.system(size: 16))
                                } else {
                                    Image(systemName: "circle")
                                        .foregroundColor(.gray)
                                        .font(.system(size: 16))
                                }

                                Text(inputSource.name)
                            }
                            .frame(height: 12)

                            Divider()
                        }
                        .contentShape(Rectangle())
                        .onTapGesture {
                            if selections.contains(inputSource.id) {
                                selections.remove(inputSource.id)
                            } else {
                                selections.insert(inputSource.id)
                            }
                        }
                    }
                }
                .padding(.vertical)

                HStack {
                    Spacer()

                    Button("Cancel".i18n()) { hide() }
                        .keyboardShortcut(.cancelAction)
                    Button("Add".i18n()) { add() }
                        .keyboardShortcut(.defaultAction)
                        .disabled(selections.count < 2)
                }
            }
            .padding()
        })
    }

    func add() {
        onSelect(InputSource.sources.filter { selections.contains($0.id) })
        hide()
    }

    func hide() {
        isActive = false
        selections.removeAll()
    }
}

struct MultipleSelectionRow: View {
    var title: String
    var isSelected: Bool
    var action: () -> Void

    var body: some View {
        Button(action: action) {
            HStack {
                Text(self.title)
                if self.isSelected {
                    Spacer()
                    Image(systemName: "checkmark")
                }
            }
        }
    }
}

import SwiftUI

struct ApplicationPicker: View {
    @Binding var selectedApp: Set<AppRule>

    @EnvironmentObject var preferencesVM: PreferencesVM

    @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "bundleName", ascending: true)])
    var appCustomizations: FetchedResults<AppRule>

    let appIconSize: CGFloat = 18
    let keyboardIconSize: CGFloat = 16

    var body: some View {
        VStack(spacing: 0) {
            List(appCustomizations, id: \.self, selection: $selectedApp) { app in
                HStack {
                    if let image = app.image {
                        SwiftUI.Image(nsImage: image)
                            .resizable()
                            .frame(width: appIconSize, height: appIconSize)
                    } else {
                        SwiftUI.Image(systemName: "app.dashed")
                            .resizable()
                            .frame(width: appIconSize, height: appIconSize)
                    }

                    Text(app.bundleName ?? "(unknown)")
                        .lineLimit(1)

                    Spacer()

                    if app.hideIndicator {
                        SwiftUI.Image(systemName: "eye.slash.circle.fill")
                            .opacity(0.7)
                    }

                    if preferencesVM.needDisplayEnhancedModePrompt(bundleIdentifier: app.bundleId) {
                        SwiftUI.Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundColor(Color(red: 1.0, green: 0.84, blue: 0.0))
                    }

                    if preferencesVM.preferences.isRestorePreviouslyUsedInputSource {
                        if app.doNotRestoreKeyboard {
                            SwiftUI.Image(systemName: "d.circle.fill")
                                .foregroundColor(selectedApp.contains(app) ? .primary : .green)
                                .opacity(selectedApp.contains(app) ? 0.7 : 1.0)
                        }
                    } else {
                        if app.doRestoreKeyboard {
                            SwiftUI.Image(systemName: "arrow.uturn.left.circle.fill")
                                .foregroundColor(selectedApp.contains(app) ? .primary : .blue)
                                .opacity(selectedApp.contains(app) ? 0.7 : 1.0)
                        }
                    }

                    if let icon = app.forcedKeyboard?.icon {
                        SwiftUI.Image(nsImage: icon)
                            .resizable()
                            .scaledToFit()
                            .frame(width: keyboardIconSize, height: keyboardIconSize)
                            .opacity(0.7)
                    }
                }
                .help(app.bundleName ?? app.url?.path ?? "(unknown)")
            }
            .listRowInsets(EdgeInsets())
        }
        .onAppear {
            if let app = appCustomizations.first {
                selectedApp.update(with: app)
            }
        }
    }
}

import SwiftUI

enum KeyboardRestoreStrategy: String, CaseIterable, Identifiable {
    case UseDefaultKeyboardInstead
    case RestorePreviouslyUsedOne

    var name: String {
        switch self {
        case .UseDefaultKeyboardInstead:
            return "Use Default Keyboard Instead".i18n()
        case .RestorePreviouslyUsedOne:
            return "Restore Previously Used One".i18n()
        }
    }

    var systemImageName: String {
        switch self {
        case .UseDefaultKeyboardInstead:
            return "d.circle.fill"
        case .RestorePreviouslyUsedOne:
            return "arrow.uturn.left.circle.fill"
        }
    }

    var id: Self { self }
}

import Foundation

extension FileManager {
    func isDirectory(atPath path: String) -> Bool {
        let fileManager = FileManager.default
        var isDir: ObjCBool = false

        if fileManager.fileExists(atPath: path, isDirectory: &isDir) {
            return isDir.boolValue
        } else {
            return false
        }
    }
}

import SwiftUI

struct PromotionBadge: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Promotion".i18n())

            Spacer(minLength: 10)

            HStack {
                Spacer()

                if #available(macOS 13, *) {
                    ShareLink(item: .website) {
                        Label {
                            Text("Share with friends".i18n())
                        } icon: {
                            Image(systemName: "square.and.arrow.up.fill")
                                .foregroundColor(.blue)
                        }
                    }
                } else {
                    Button(action: {
                        URL.website.open()
                    }) {
                        Label {
                            Text("Share with friends".i18n())
                        } icon: {
                            Image(systemName: "square.and.arrow.up.fill")
                                .foregroundColor(.blue)
                        }
                    }
                }

                Button(action: {
                    URL(string: "https://github.com/runjuu/InputSourcePro")?.open()
                }) {
                    Label {
                        Text("Star on GitHub".i18n())
                    } icon: {
                        Image(systemName: "star.fill")
                            .foregroundColor(Color(red: 1.0, green: 0.84, blue: 0.0))
                    }
                }

                Button(action: {
                    URL(string: "https://github.com/sponsors/runjuu")?.open()
                }) {
                    Label {
                        Text("Sponsor".i18n())
                    } icon: {
                        Image(systemName: "heart.fill")
                            .foregroundColor(.pink)
                    }
                }
            }
        }
        .padding()
    }
}

// add support for Canvas Preview
struct PromotionBadge_Previews: PreviewProvider {
    static var previews: some View {
        PromotionBadge()
            .frame(width: 635, height: 95)
    }
}

import AppKit
import Combine

@MainActor
struct AppsDiff {
    let removed: Set<NSRunningApplication>
    let added: Set<NSRunningApplication>
    let all: Set<NSRunningApplication>

    func diff(_ runningApps: Set<NSRunningApplication>) -> AppsDiff {
        let removedApps = all.subtracting(runningApps)
        let addedApps = runningApps.subtracting(all)

        return AppsDiff(removed: removedApps, added: addedApps, all: runningApps)
    }
}

extension AppsDiff {
    static var empty = AppsDiff(removed: [], added: [], all: [])

    static func publisher(preferencesVM: PreferencesVM) -> AnyPublisher<AppsDiff, Never> {
        NSWorkspace.shared
            .publisher(for: \.runningApplications)
            .map { apps in Set(preferencesVM.filterApps(apps)) }
            .scan(.empty) { appsDiff, runningApps -> AppsDiff in
                appsDiff.diff(runningApps)
            }
            .replaceError(with: .empty)
            .eraseToAnyPublisher()
    }
}

import Combine
import SwiftUI

struct BrowserPermissionRequestView: View {
    @EnvironmentObject var permissionsVM: PermissionsVM
    @EnvironmentObject var preferencesVM: PreferencesVM

    @Binding var isPresented: Bool

    @State var isDisableTips: Bool = true

    var onSuccess: () -> Void

    let timer = Timer
        .interval(seconds: 1)
        .mapToVoid()
        .ignoreFailure()
        .eraseToAnyPublisher()

    let fakeTimer = Empty(outputType: Void.self, failureType: Never.self)
        .eraseToAnyPublisher()

    var body: some View {
        VStack {
            Text("Permission Required")
                .font(.title)
                .fontWeight(.medium)
                .padding(.vertical, 10)

            VStack(alignment: .leading) {
                Text("Browser Rules Accessibility Permission Description")

                HStack {
                    Spacer()

                    if permissionsVM.isAccessibilityEnabled {
                        SwiftUI.Image(systemName: "checkmark.circle.fill")
                            .resizable()
                            .frame(width: 16, height: 16)
                            .foregroundColor(.green)

                        Button("Authorized", action: openAccessibilityPreferences)
                            .disabled(true)
                    } else {
                        Button("Open Accessibility Preferences", action: openAccessibilityPreferences)
                            .keyboardShortcut(.defaultAction)
                    }
                }
                .padding(.top, 5)
            }

            Divider()
                .padding(.vertical, 10)

            HStack {
                Button(action: URL.howToEnableBrowserRule.open) {
                    SwiftUI.Image(systemName: "questionmark")
                }
                .font(.system(size: 10).weight(.bold))
                .frame(width: 18, height: 18)
                .clipShape(RoundedRectangle(cornerRadius: 99))
                .disabled(isDisableTips)

                Spacer()

                Button("Close", action: close)
                    .keyboardShortcut(.cancelAction)
            }
        }
        .lineLimit(nil)
        .fixedSize(horizontal: false, vertical: true)
        .padding()
        .frame(width: 400)
        .onAppear {
            DispatchQueue.main.async {
                isDisableTips = false
            }
        }
        .onChange(of: permissionsVM.isAccessibilityEnabled) { _ in
            onAuthorizeSuccess()
        }
    }

    func close() {
        isPresented = false
    }

    func openAccessibilityPreferences() {
        NSWorkspace.shared.openAccessibilityPreferences()
    }

    func openAutomationPreferences() {
        NSWorkspace.shared.openAutomationPreferences()
    }

    func onAuthorizeSuccess() {
        onSuccess()
        close()
    }
}

import AppKit
import SwiftUI

@MainActor
class PreferencesWindowController: NSWindowController {
    let navigationVM: NavigationVM
    let permissionsVM: PermissionsVM
    let preferencesVM: PreferencesVM
    let indicatorVM: IndicatorVM
    let feedbackVM: FeedbackVM
    let inputSourceVM: InputSourceVM

    init(
        navigationVM: NavigationVM,
        permissionsVM: PermissionsVM,
        preferencesVM: PreferencesVM,
        indicatorVM: IndicatorVM,
        feedbackVM: FeedbackVM,
        inputSourceVM: InputSourceVM
    ) {
        self.navigationVM = navigationVM
        self.permissionsVM = permissionsVM
        self.preferencesVM = preferencesVM
        self.indicatorVM = indicatorVM
        self.feedbackVM = feedbackVM
        self.inputSourceVM = inputSourceVM

        let window = NSWindow(
            contentViewController: NSHostingController(
                rootView: PreferencesView()
                    .environmentObject(navigationVM)
                    .environmentObject(preferencesVM)
                    .environmentObject(indicatorVM)
                    .environmentObject(permissionsVM)
                    .environmentObject(feedbackVM)
                    .environmentObject(inputSourceVM)
            )
        )

        super.init(window: window)

        window.delegate = self

        configureWindow()
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func configureWindow() {
        window?.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isEnabled = false
        window?.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isEnabled = false

        window?.title = "Input Source Pro"
        window?.titleVisibility = .hidden
        window?.titlebarAppearsTransparent = true
        window?.styleMask.insert(.fullSizeContentView)
    }
}

extension PreferencesWindowController: NSWindowDelegate {
    func windowWillClose(_: Notification) {
        NSApp.setActivationPolicy(.accessory)
    }
}

import SwiftUI

struct QuestionButton<Content: View, Popover: View>: View {
    @State var displayTips = false

    @Environment(\.colorScheme) var colorScheme: ColorScheme

    let content: () -> Content
    let popover: (ColorScheme) -> Popover

    init(
        @ViewBuilder content: @escaping () -> Content,
        @ViewBuilder popover: @escaping (ColorScheme) -> Popover
    ) {
        self.content = content
        self.popover = popover
    }

    var body: some View {
        Button(action: { displayTips.toggle() }) {
            content()
                .font(.body.bold())
        }
        .buttonStyle(QuestionButtonStyle())
        .popover(isPresented: $displayTips, arrowEdge: .top) {
            VStack(spacing: 0) {
                popover(colorScheme)
            }
            .frame(width: 280)
        }
    }
}

struct QuestionButtonStyle: ButtonStyle {
    @State var isHover = false

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .opacity(isHover ? 1 : 0.8)
            .background(Color.gray.opacity(configuration.isPressed ? 0.6 : isHover ? 0.3 : 0.15))
            .clipShape(Circle())
            .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
            .onHover { hover in
                withAnimation {
                    isHover = hover
                }
            }
    }
}

import AVKit
import SwiftUI

struct PositionSettingsView: View {
    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var navigationVM: NavigationVM

    @Environment(\.colorScheme) var colorScheme: ColorScheme

    @State var displayIndicatorNearCursorTips = false
    @State var displayAlwaysOnIndicatorTips = false
    @State private var width = CGFloat.zero

    var body: some View {
        let sliderBinding = Binding(
            get: {
                Double(preferencesVM.preferences.indicatorPositionSpacing?.rawValue ?? 3)
            },
            set: { newValue in
                withAnimation(.easeInOut(duration: 0.2)) {
                    preferencesVM.update {
                        $0.indicatorPositionSpacing = .fromSlide(value: newValue)
                    }
                }
            }
        )

        let positionBinding = Binding(
            get: {
                preferencesVM.preferences.indicatorPosition ?? .nearMouse
            },
            set: { newValue in
                withAnimation(.easeInOut(duration: 0.3)) {
                    preferencesVM.update {
                        $0.indicatorPosition = newValue
                    }
                }
            }
        )

        ScrollView {
            VStack(spacing: 18) {
                SettingsSection(title: "Position") {
                    VStack(spacing: 0) {
                        IndicatorPositionEditor()
                            .clipShape(RoundedRectangle(cornerRadius: 6))
                            .padding(.horizontal)
                            .padding(.top)

                        Picker("Position", selection: positionBinding) {
                            ForEach(IndicatorPosition.allCases) { item in
                                Text(item.name).tag(item)
                            }
                        }
                        .labelsHidden()
                        .pickerStyle(.segmented)
                        .padding()

                        if preferencesVM.preferences.indicatorPosition != .nearMouse {
                            HStack {
                                Text("Spacing".i18n() + ":")
                                    .alignedView(width: $width, alignment: .leading)

                                HStack {
                                    Slider(value: sliderBinding, in: 0 ... 5, step: 1)

                                    if let name = preferencesVM.preferences.indicatorPositionSpacing?.name {
                                        Text(name)
                                            .foregroundColor(.primary)
                                            .frame(width: 50, height: 25)
                                            .background(Color.primary.opacity(0.05))
                                            .clipShape(RoundedRectangle(cornerRadius: 6))
                                            .noAnimation()
                                    }
                                }
                            }
                            .padding()
                            .border(width: 1, edges: [.top, .bottom], color: NSColor.border2.color)

                            HStack {
                                Text("Alignment".i18n() + ":")
                                    .alignedView(width: $width, alignment: .leading)

                                HStack {
                                    PopUpButtonPicker<IndicatorPosition.Alignment>(
                                        items: IndicatorPosition.Alignment.allCases,
                                        isItemSelected: { $0 == preferencesVM.preferences.indicatorPositionAlignment },
                                        getTitle: { $0.name },
                                        getToolTip: { $0.name },
                                        onSelect: { index in
                                            withAnimation(.easeInOut(duration: 0.3)) {
                                                preferencesVM.update {
                                                    let value = IndicatorPosition.Alignment.allCases[index]
                                                    $0.indicatorPositionAlignment = value
                                                }
                                            }
                                        }
                                    )
                                }
                            }
                            .padding()
                        }
                    }
                }

                SettingsSection(title: "Advanced", tips: EnhancedModeRequiredBadge()) {
                    VStack(spacing: 0) {
                        HStack {
                            Toggle(isOn: $preferencesVM.preferences.tryToDisplayIndicatorNearCursor) {}
                                .toggleStyle(.switch)
                                .disabled(!preferencesVM.preferences.isEnhancedModeEnabled)

                            Text("tryToDisplayIndicatorNearCursor".i18n())

                            Spacer()

                            QuestionButton(
                                content: {
                                    SwiftUI.Image(systemName: "video")
                                        .font(.system(size: 11, weight: .bold))
                                        .padding(6)
                                },
                                popover: {
                                    PlayerView(url: Bundle.main.url(
                                        forResource: "Indicator-Near-Cursor-Demo-\($0 == .dark ? "Dark" : "Light")",
                                        withExtension: "mp4"
                                    )!)
                                        .frame(height: 118)

                                    Text("Enhanced Mode Required Tips".i18n())
                                        .font(.footnote)
                                        .opacity(0.6)
                                        .padding(.vertical, 10)
                                }
                            )
                        }
                        .padding(.horizontal)
                        .padding(.vertical, 10)
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        VStack {
                            let needDisableAlwaysOnIndicator = !preferencesVM.preferences.isEnhancedModeEnabled || !preferencesVM.preferences.tryToDisplayIndicatorNearCursor

                            HStack {
                                Toggle("", isOn: $preferencesVM.preferences.isEnableAlwaysOnIndicator)
                                    .disabled(needDisableAlwaysOnIndicator)
                                    .toggleStyle(.switch)
                                    .labelsHidden()

                                Text("isEnableAlwaysOnIndicator".i18n())

                                Spacer()

                                QuestionButton(
                                    content: {
                                        SwiftUI.Image(systemName: "video")
                                            .font(.system(size: 11, weight: .bold))
                                            .padding(6)
                                    },
                                    popover: {
                                        PlayerView(url: Bundle.main.url(
                                            forResource: "Always-On-Indicator-Demo-\($0 == .dark ? "Dark" : "Light")",
                                            withExtension: "mp4"
                                        )!)
                                            .frame(height: 118)

                                        Text("alwaysOnIndicatorTips".i18n())
                                            .font(.footnote)
                                            .padding(.vertical, 10)
                                            .opacity(0.6)
                                    }
                                )
                            }
                        }
                        .padding(.horizontal)
                        .padding(.vertical, 10)
                    }
                }
            }
            .padding()
            .padding(.bottom)
        }
        .onAppear(perform: updatePreviewModeOnAppear)
        .background(NSColor.background1.color)
    }

    func updatePreviewModeOnAppear() {
        if preferencesVM.preferences.isAutoAppearanceMode {
            preferencesVM.preferences.appearanceMode = colorScheme == .dark ? .dark : .light
        }
    }

    func resetColors() {
        if preferencesVM.preferences.appearanceMode == .light {
            preferencesVM.preferences.indicatorForgegroundColor = IndicatorColor.forgeground.light
            preferencesVM.preferences.indicatorBackgroundColor = IndicatorColor.background.light
        }

        if preferencesVM.preferences.appearanceMode == .dark {
            preferencesVM.preferences.indicatorForgegroundColor = IndicatorColor.forgeground.dark
            preferencesVM.preferences.indicatorBackgroundColor = IndicatorColor.background.dark
        }
    }
}

import AppKit
import Combine

extension NSEvent {
    static func watch(matching: NSEvent.EventTypeMask) -> AnyPublisher<NSEvent, Never> {
        AnyPublisher<NSEvent, Never>
            .create { observer in
                let monitor = self.addGlobalMonitorForEvents(
                    matching: matching,
                    handler: { observer.send($0) }
                )

                return AnyCancellable { NSEvent.removeMonitor(monitor!) }
            }
    }
}

import KeyboardShortcuts
import SwiftUI

struct KeyboardsSettingsView: View {
    @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: true)])
    var hotKeyGroups: FetchedResults<HotKeyGroup>

    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var indicatorVM: IndicatorVM

    let imgSize: CGFloat = 16

    var body: some View {
        ScrollView {
            VStack(spacing: 0) {
                normalSection
                groupSection
                AddSwitchingGroupButton(onSelect: preferencesVM.addHotKeyGroup)
            }
            .padding()
        }
        .background(NSColor.background1.color)
    }

    var normalSection: some View {
        ForEach(InputSource.sources, id: \.id) { inputSource in
            SettingsSection(title: "") {
                HStack {
                    CustomizedIndicatorView(inputSource: inputSource)
                        .help(inputSource.id)

                    Spacer()

                    KeyboardShortcuts.Recorder(for: .init(inputSource.id), onChange: { _ in
                        indicatorVM.refreshShortcut()
                    })
                }
                .padding()
            }
            .padding(.bottom)
        }
    }

    var groupSection: some View {
        ForEach(hotKeyGroups, id: \.self) { group in
            SettingsSection(title: "") {
                HStack(alignment: .top) {
                    VStack(alignment: .leading) {
                        ForEach(group.inputSources, id: \.id) { inputSource in
                            CustomizedIndicatorView(inputSource: inputSource)
                                .help(inputSource.id)
                        }
                    }

                    Spacer()

                    VStack(alignment: .trailing) {
                        KeyboardShortcuts.Recorder(for: .init(group.id!), onChange: { _ in
                            indicatorVM.refreshShortcut()
                        })

                        HStack {
                            Button("Delete".i18n()) {
                                deleteGroup(group: group)
                            }
                        }
                    }
                }
                .padding()
            }
            .padding(.bottom)
        }
    }

    func deleteGroup(group: HotKeyGroup) {
        KeyboardShortcuts.reset([.init(group.id!)])
        preferencesVM.deleteHotKeyGroup(group)
        indicatorVM.refreshShortcut()
    }
}

import AppKit

class FloatWindow: NSPanel {
    private let _canBecomeKey: Bool

    override var canBecomeKey: Bool {
        return _canBecomeKey
    }

    init(
        canBecomeKey: Bool,
        contentRect: CGRect,
        styleMask style: NSWindow.StyleMask,
        backing backingStoreType: NSWindow.BackingStoreType,
        defer flag: Bool
    ) {
        _canBecomeKey = canBecomeKey

        super.init(
            contentRect: contentRect,
            styleMask: style,
            backing: backingStoreType,
            defer: flag
        )

        level = .screenSaver

        hasShadow = false
        isOpaque = false
        backgroundColor = .clear
        isMovableByWindowBackground = true
        hidesOnDeactivate = false

        titleVisibility = .hidden
        titlebarAppearsTransparent = true

        styleMask.insert(.nonactivatingPanel)

        collectionBehavior = [
            .moveToActiveSpace,
            .fullScreenAuxiliary,
            .stationary,
            .ignoresCycle,
        ]
    }
}

import SwiftUI

struct IndicatorView: NSViewControllerRepresentable {
    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var indicatorVM: IndicatorVM
    @EnvironmentObject var inputSourceVM: InputSourceVM

    func makeNSViewController(context _: Context) -> IndicatorViewController {
        return IndicatorViewController()
    }

    func updateNSViewController(_ indicatorViewController: IndicatorViewController, context _: Context) {
        indicatorViewController.prepare(config: IndicatorViewConfig(
            inputSource: indicatorVM.state.inputSource,
            kind: preferencesVM.preferences.indicatorKind,
            size: preferencesVM.preferences.indicatorSize ?? .medium,
            bgColor: NSColor(preferencesVM.preferences.indicatorBackgroundColor),
            textColor: NSColor(preferencesVM.preferences.indicatorForgegroundColor)
        ))

        indicatorViewController.refresh()
    }
}

struct DumpIndicatorView: NSViewControllerRepresentable {
    let config: IndicatorViewConfig

    init(config: IndicatorViewConfig) {
        self.config = config
    }

    func makeNSViewController(context _: Context) -> IndicatorViewController {
        return IndicatorViewController()
    }

    func updateNSViewController(_ indicatorViewController: IndicatorViewController, context _: Context) {
        indicatorViewController.prepare(config: config)
        indicatorViewController.refresh()
    }
}

import SwiftUI
import VisualEffects

struct KeyboardCustomization: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    @State var textColor: Color = .clear
    @State var bgColor: Color = .clear

    let inputSource: InputSource

    let columns = [
        GridItem(.fixed(100), alignment: .trailing),
        GridItem(.flexible(minimum: 50, maximum: .infinity), alignment: .leading),
    ]

    let indicatorColumns = [
        GridItem(.flexible(minimum: 50, maximum: .infinity), alignment: .center),
        GridItem(.flexible(minimum: 50, maximum: .infinity), alignment: .center),
    ]

    var body: some View {
        let keyboardConfig = preferencesVM.getOrCreateKeyboardConfig(inputSource)

        VStack(alignment: .leading) {
            ZStack {
                LazyVGrid(columns: indicatorColumns) {
                    DumpIndicatorView(config: IndicatorViewConfig(
                        inputSource: inputSource,
                        kind: .alwaysOn,
                        size: preferencesVM.preferences.indicatorSize ?? .medium,
                        bgColor: NSColor(bgColor),
                        textColor: NSColor(textColor)
                    ))

                    DumpIndicatorView(config: IndicatorViewConfig(
                        inputSource: inputSource,
                        kind: preferencesVM.preferences.indicatorKind,
                        size: preferencesVM.preferences.indicatorSize ?? .medium,
                        bgColor: NSColor(bgColor),
                        textColor: NSColor(textColor)
                    ))

                    Text("Always-On Indicator Style")
                        .font(.caption)
                        .opacity(0.5)

                    Text("Keyboard Indicator Style")
                        .font(.caption)
                        .opacity(0.5)
                }
                .padding()
                .itemSectionStyle()

                VStack {
                    Spacer()

                    HStack {
                        Spacer()
                        Button(action: { reset(keyboardConfig) }) {
                            Text("Reset")
                        }
                        .buttonStyle(GhostButton(icon: Image(systemName: "arrow.clockwise")))
                    }
                }
                .padding(.trailing, 4)
                .padding(.bottom, 4)
            }

            VStack(alignment: .center) {
                ColorBlocks(onSelectColor: {
                    textColor = $0.a
                    bgColor = $0.b
                })
                .padding(.vertical, 8)

                HStack {
                    ColorPicker("Color", selection: $textColor)

                    Button(
                        action: {
                            let a = textColor
                            let b = bgColor

                            textColor = b
                            bgColor = a
                        },
                        label: {
                            Image(systemName: "repeat")
                        }
                    )

                    ColorPicker("Background", selection: $bgColor)
                }
                .labelsHidden()
            }
            .padding(.horizontal)
        }
        .padding()
        .onAppear {
            textColor = preferencesVM.getTextColor(inputSource)
            bgColor = preferencesVM.getBgColor(inputSource)
        }
        .onChange(of: bgColor, perform: { _ in save(keyboardConfig) })
        .onChange(of: textColor, perform: { _ in save(keyboardConfig) })
        .onDisappear {
            NSColorPanel.shared.close()
        }
    }

    func save(_ keyboardConfig: KeyboardConfig) {
        preferencesVM.update(keyboardConfig, textColor: textColor, bgColor: bgColor)
    }

    func reset(_: KeyboardConfig) {
        bgColor = preferencesVM.preferences.indicatorBackgroundColor
        textColor = preferencesVM.preferences.indicatorForgegroundColor
    }
}

import SwiftUI

struct CustomizedIndicatorView: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    let inputSource: InputSource

    init(inputSource: InputSource) {
        self.inputSource = inputSource
    }

    var body: some View {
        return Group {
            DumpIndicatorView(config: IndicatorViewConfig(
                inputSource: inputSource,
                kind: preferencesVM.preferences.indicatorKind,
                size: preferencesVM.preferences.indicatorSize ?? .medium,
                bgColor: NSColor(preferencesVM.getBgColor(inputSource)),
                textColor: NSColor(preferencesVM.getTextColor(inputSource))
            ))
        }
    }
}

import SwiftUI

struct TroubleshootingSettingsView: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    @State var isShowCJKVFixEnableFailedView = false

    var body: some View {
        let isCJKVFixEnabledBinding = Binding(
            get: { preferencesVM.preferences.isCJKVFixEnabled },
            set: { onToggleCJKVFix($0) }
        )

        ScrollView {
            VStack(spacing: 18) {
                SettingsSection(title: "") {
                    VStack(alignment: .leading) {
                        HStack {
                            Toggle("", isOn: isCJKVFixEnabledBinding)
                                .disabled(!preferencesVM.preferences.isEnhancedModeEnabled)
                                .toggleStyle(.switch)
                                .labelsHidden()

                            Text("Enabled CJKV Fix".i18n())
                                .font(.headline)

                            Spacer()

                            EnhancedModeRequiredBadge()
                        }

                        Text(.init("Enabled CJKV Fix Description".i18n()))
                            .font(.system(size: 12))
                            .opacity(0.8)
                    }
                    .padding()
                }

                SettingsSection(title: "") {
                    CursorLagFixView()
                }

                SettingsSection(title: "") {
                    FeedbackButton()
                }
            }
            .padding()
        }
        .background(NSColor.background1.color)
        .sheet(isPresented: $isShowCJKVFixEnableFailedView) {
            CJKVFixEnableFailedView(isPresented: $isShowCJKVFixEnableFailedView)
        }
    }

    func onToggleCJKVFix(_ isCJKVFixEnabled: Bool) {
        if isCJKVFixEnabled {
            if InputSource.getSelectPreviousShortcut() == nil {
                isShowCJKVFixEnableFailedView = true
            } else {
                preferencesVM.update {
                    $0.isCJKVFixEnabled = true
                }
            }
        } else {
            preferencesVM.update {
                $0.isCJKVFixEnabled = false
            }
        }
    }
}

enum CursorSettingStatus {
    case enabled
    case disabled
    case undefined
    case unknown
}

import SwiftUI

struct IndicatorAlignmentView<Content: View>: View {
    let content: Content

    let alignment: IndicatorPosition.Alignment

    init(
        alignment: IndicatorPosition.Alignment,
        @ViewBuilder content: () -> Content
    ) {
        self.content = content()
        self.alignment = alignment
    }

    var body: some View {
        VStack {
            if [.bottomLeft, .bottomRight, .bottomCenter].contains(alignment) {
                Spacer(minLength: 0)
            }

            HStack {
                if [.topRight, .bottomRight, .centerRight].contains(alignment) {
                    Spacer(minLength: 0)
                }

                content

                if [.topLeft, .bottomLeft, .centerLeft].contains(alignment) {
                    Spacer(minLength: 0)
                }
            }

            if [.topRight, .topLeft, .topCenter].contains(alignment) {
                Spacer(minLength: 0)
            }
        }
    }
}

import CryptoKit
import Foundation

extension Data {
    func md5() -> String {
        Insecure.MD5
            .hash(data: self)
            .map { String(format: "%02hhx", $0) }
            .joined()
    }
}

import SwiftUI

struct QuestionMark<Content: View>: View {
    @State var isPresented: Bool = false

    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        Button(action: { isPresented.toggle() }) {
            SwiftUI.Image(systemName: "questionmark")
        }
        .font(.system(size: 10).weight(.bold))
        .frame(width: 18, height: 18)
        .clipShape(RoundedRectangle(cornerRadius: 99))
        .popover(isPresented: $isPresented, arrowEdge: .top) {
            content
        }
    }
}

import SwiftUI

struct BrowserRuleRow: View {
    @State var showModal = false

    var isSelected: Bool = false

    @ObservedObject var rule: BrowserRule

    let imgSize: CGFloat = 16

    var body: some View {
        HStack {
            Text(rule.value ?? "")

            Spacer()

            if rule.hideIndicator == true {
                Image(systemName: "eye.slash.circle.fill")
                    .opacity(0.7)
                    .frame(width: imgSize, height: imgSize)
            }

            if let keyboardRestoreStrategy = rule.keyboardRestoreStrategy {
                let symbolName = keyboardRestoreStrategy.systemImageName
                let color: Color = {
                    switch symbolName {
                    case "d.circle.fill", "d.square.fill":
                        return isSelected ? .primary.opacity(0.7) : .green
                    case "arrow.uturn.left.circle.fill":
                        return isSelected ? .primary.opacity(0.7) : .blue
                    default:
                        return .primary
                    }
                }()
                Image(systemName: symbolName)
                    .foregroundColor(color)
                    .frame(width: imgSize, height: imgSize)
            }

            if let forcedKeyboard = rule.forcedKeyboard {
                SwiftUI.Image(nsImage: forcedKeyboard.icon ?? NSImage())
                    .resizable()
                    .scaledToFit()
                    .frame(width: imgSize, height: imgSize)
                    .opacity(0.7)
            }

            Text(rule.type.name)
                .font(.footnote)
                .padding(.horizontal, 4)
                .padding(.vertical, 2)
                .foregroundColor(.white)
                .background(Color.accentColor)
                .clipShape(RoundedRectangle(cornerRadius: 2))

            Button("Edit") {
                showModal = true
            }
        }
        .sheet(isPresented: $showModal, content: {
            BrowserRuleEditView(isPresented: $showModal, rule: rule)
        })
    }
}

import SwiftUI

struct ColorBlocks: View {
    typealias Scheme = (a: Color, b: Color)

    let onSelectColor: (Scheme) -> Void

    let colors: [Scheme] = [
        (.init(hex: "#FFF"), .init(hex: "#000")),
        (.init(hex: "#FFF"), .init(hex: "#ef233c")),
        (.init(hex: "#FFF"), .init(hex: "#f77f00")),
        (.init(hex: "#000"), .init(hex: "#F6CB56")),
        (.init(hex: "#FFF"), .init(hex: "#2c6e49")),
        (.init(hex: "#FFF"), .init(hex: "#0c7489")),
        (.init(hex: "#FFF"), .init(hex: "#023e8a")),
        (.init(hex: "#FFF"), .init(hex: "#7209b7")),
    ]

    var body: some View {
        HStack {
            ForEach(Array(zip(colors.indices, colors)), id: \.0) { _, scheme in
                Spacer()
                ColorBlock(colorA: scheme.a, colorB: scheme.b)
                    .onTapGesture {
                        onSelectColor(scheme)
                    }
                Spacer()
            }
        }
    }
}

struct ColorBlock: View {
    let colorA: Color

    let colorB: Color

    var body: some View {
        RoundedRectangle(cornerRadius: 4)
            .fill(colorB)
            .overlay(
                SwiftUI.Image(systemName: "textformat")
                    .foregroundColor(colorA)
                    .font(.system(size: 12, weight: .semibold))
            )
            .frame(width: 28, height: 20)
    }
}

import Carbon
import Cocoa
import Combine
import CoreGraphics

private let nameMap = [
    "com.apple.inputmethod.Korean.2SetKorean": "한",
    "com.apple.inputmethod.Korean.3SetKorean": "한",
    "com.apple.inputmethod.Korean.390Sebulshik": "한",
    "com.apple.inputmethod.Korean.GongjinCheongRomaja": "한",
    "com.apple.inputmethod.Korean.HNCRomaja": "한",

    "com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese": "あ",
    "com.apple.inputmethod.Kotoeri.KanaTyping.Japanese": "あ",

    "com.apple.keylayout.Irish": "IE",
    "com.apple.keylayout.Dvorak-Right": "DV",
    "com.apple.keylayout.DVORAK-QWERTYCMD": "DV",
    "com.apple.keylayout.Dvorak-Left": "DV",
    "com.apple.keylayout.Dvorak": "DV",
    "com.apple.keylayout.Colemak": "CO",
    "com.apple.keylayout.British-PC": "GB",
    "com.apple.keylayout.British": "GB",
    "com.apple.keylayout.Australian": "AU",
    "com.apple.keylayout.ABC-India": "IN",
    "com.apple.keylayout.USInternational-PC": "US",
    "com.apple.keylayout.US": "US",
    "com.apple.keylayout.USExtended": "A",
    "com.apple.keylayout.ABC": "A",
    "com.apple.keylayout.Canadian": "CA",

    "com.apple.inputmethod.TCIM.Cangjie": "倉",
    "com.apple.inputmethod.TCIM.Pinyin": "繁拼",
    "com.apple.inputmethod.TCIM.Shuangpin": "雙",
    "com.apple.inputmethod.TCIM.WBH": "畫",
    "com.apple.inputmethod.TCIM.Jianyi": "速",
    "com.apple.inputmethod.TCIM.Zhuyin": "注",
    "com.apple.inputmethod.TCIM.ZhuyinEten": "注",

    "com.apple.inputmethod.TYIM.Sucheng": "速",
    "com.apple.inputmethod.TYIM.Stroke": "畫",
    "com.apple.inputmethod.TYIM.Phonetic": "粤拼",
    "com.apple.inputmethod.TYIM.Cangjie": "倉",

    "com.apple.inputmethod.SCIM.WBX": "五",
    "com.apple.inputmethod.SCIM.WBH": "画",
    "com.apple.inputmethod.SCIM.Shuangpin": "双",
    "com.apple.inputmethod.SCIM.ITABC": "拼",
]

extension InputSource {
    func getSystemLabelName() -> String? {
        return nameMap[id]
    }
}

//
//  ISPTextEditor.swift
//  Input Source Pro
//
//  Created by runjuu on 2023-03-05.
//

import SwiftUI

struct ISPTextEditor: View {
    @Binding var text: String

    var placeholder: String

    let minHeight: CGFloat

    var body: some View {
        ZStack(alignment: .topLeading) {
            TextEditor(text: $text)
                .padding(.vertical, 7)
                .padding(.horizontal, 2)
                .frame(minHeight: minHeight, maxHeight: 500, alignment: .leading)
                .foregroundColor(Color(.labelColor))
                .multilineTextAlignment(.leading)
                .background(Color(NSColor.textBackgroundColor))
                .clipShape(RoundedRectangle(cornerRadius: 6))
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(Color.primary.opacity(0.3), lineWidth: 1)
                )

            Text(placeholder)
                .padding(.vertical, 7)
                .padding(.horizontal, 7)
                .foregroundColor(Color(.placeholderTextColor))
                .opacity(text.isEmpty ? 1 : 0)
                .allowsHitTesting(false)
        }
        .font(.body)
    }
}

import SwiftUI

struct AccessibilityPermissionRequestView: View {
    @EnvironmentObject var permissionsVM: PermissionsVM
    @EnvironmentObject var preferencesVM: PreferencesVM

    @Binding var isPresented: Bool

    @State var isDisableTips: Bool = true

    var body: some View {
        VStack(alignment: .leading) {
            Text(.init("Enhanced Mode Permission Description".i18n()))
                .lineSpacing(4)

            HStack {
                Button(action: URL.howToEnableAccessbility.open) {
                    SwiftUI.Image(systemName: "questionmark")
                }
                .font(.system(size: 10).weight(.bold))
                .frame(width: 18, height: 18)
                .clipShape(RoundedRectangle(cornerRadius: 99))
                .disabled(isDisableTips)

                Spacer()

                Button("Open Accessibility Preferences") {
                    NSWorkspace.shared.openAccessibilityPreferences()
                }
                .keyboardShortcut(.defaultAction)

                Button("Cancel", action: cancel)
                    .keyboardShortcut(.cancelAction)
            }
            .padding(.top)
        }
        .padding()
        .lineLimit(nil)
        .frame(width: 400)
        .onChange(of: permissionsVM.isAccessibilityEnabled, perform: whenAccessibilityEnabled)
        .onAppear {
            DispatchQueue.main.async {
                isDisableTips = false
            }
        }
    }

    func cancel() {
        isPresented = false
    }

    func whenAccessibilityEnabled(_ isAccessibilityEnabled: Bool) {
        if isAccessibilityEnabled {
            isPresented = false
            preferencesVM.update {
                $0.isEnhancedModeEnabled = true
            }
        }
    }
}

import SwiftUI

// https://stackoverflow.com/a/63997630

// The preference key used to advise parent views of a change in value.
struct AlignedWidthPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = .zero

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())
    }
}

extension View {
    func alignedView(width: Binding<CGFloat>, alignment: Alignment = .trailing) -> some View {
        modifier(AlignedWidthView(width: width, alignment: alignment))
    }
}

struct AlignedWidthView: ViewModifier {
    @Binding var width: CGFloat

    var alignment: Alignment

    func body(content: Content) -> some View {
        content
            .background(GeometryReader {
                Color.clear.preference(
                    key: AlignedWidthPreferenceKey.self,
                    value: $0.frame(in: .local).size.width
                )
            })
            .onPreferenceChange(AlignedWidthPreferenceKey.self) {
                if $0 > self.width {
                    self.width = $0
                }
            }
            .frame(minWidth: width, alignment: alignment)
    }
}

import Alamofire
import SwiftUI

@MainActor
final class FeedbackVM: ObservableObject {
    enum Status {
        case hided, editing, sending, sent
    }

    var isSent: Bool { status == .sent }

    var isSending: Bool { status == .sending }

    var isPresented: Bool { status != .hided }

    @Published private(set) var status = Status.hided

    @Published var message = ""

    @Published var email = ""

    func show() {
        if status == .hided {
            status = .editing
        }
    }

    func hide() {
        if status != .sending {
            status = .hided
        }
    }

    func sendFeedback() async {
        withAnimation {
            status = .sending
        }

        let _ = await AF
            .request(
                "https://inputsource.pro/api/feedback",
                method: .post,
                parameters: [
                    "message": message,
                    "email": email,
                    "version": "\(Bundle.main.shortVersion) \(Bundle.main.buildVersion)",
                    "osVersion": ProcessInfo.processInfo.operatingSystemVersionString,
                ],
                encoder: .json
            )
            .serializingDecodable(Alamofire.Empty.self, emptyResponseCodes: [200])
            .response

        message = ""
        email = ""

        withAnimation {
            status = .sent
        }
    }
}

import SwiftUI

private struct EdgeBorder: Shape {
    var width: CGFloat
    var edges: [Edge]

    func path(in rect: CGRect) -> Path {
        var path = Path()
        for edge in edges {
            var x: CGFloat {
                switch edge {
                case .top, .bottom, .leading: return rect.minX
                case .trailing: return rect.maxX - width
                }
            }

            var y: CGFloat {
                switch edge {
                case .top, .leading, .trailing: return rect.minY
                case .bottom: return rect.maxY - width
                }
            }

            var w: CGFloat {
                switch edge {
                case .top, .bottom: return rect.width
                case .leading, .trailing: return width
                }
            }

            var h: CGFloat {
                switch edge {
                case .top, .bottom: return width
                case .leading, .trailing: return rect.height
                }
            }
            path.addPath(Path(CGRect(x: x, y: y, width: w, height: h)))
        }
        return path
    }
}

extension View {
    func border(width: CGFloat, edges: [Edge], color: Color) -> some View {
        overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color))
    }
}

import SwiftUI

struct ItemSection<Content: View>: View {
    let content: Content

    init(@ViewBuilder _ content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        HStack {
            Spacer()
            VStack {
                Spacer()
                content
                Spacer()
            }
            Spacer()
        }
        .itemSectionStyle()
    }
}

struct ItemSectionStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .background(NSColor.background1.color)
            .clipShape(RoundedRectangle(cornerRadius: 6))
            .overlay(
                RoundedRectangle(cornerRadius: 6)
                    .stroke(NSColor.border2.color, lineWidth: 1)
            )
    }
}

extension View {
    func itemSectionStyle() -> some View {
        modifier(ItemSectionStyle())
    }
}

import SwiftUI

struct GhostButton<Icon: View>: ButtonStyle {
    @State var isHover = false

    let icon: Icon

    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: 4) {
            icon

            if isHover {
                configuration.label
                    .scaleEffect(0.9)
            }
        }
        .padding(.vertical, 4)
        .padding(.horizontal, 6)
        .opacity(isHover ? 1 : 0.6)
        .background(Color.gray.opacity(configuration.isPressed ? 0.6 : isHover ? 0.3 : 0))
        .clipShape(Capsule())
        .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
        .onHover { hover in
            withAnimation {
                isHover = hover
            }
        }
    }
}

struct SectionButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .padding()
            .frame(maxWidth: .infinity)
            .background(configuration.isPressed ? Color.gray.opacity(0.05) : Color.clear)
            .foregroundColor(Color.accentColor)
            .clipShape(RoundedRectangle(cornerRadius: 6))
            .contentShape(Rectangle())
    }
}

import AVKit
import SwiftUI

struct AppearanceSettingsView: View {
    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var navigationVM: NavigationVM

    @Environment(\.colorScheme) var colorScheme: ColorScheme

    @State private var width = CGFloat.zero

    var body: some View {
        let infoBinding = Binding(
            get: {
                preferencesVM.preferences.indicatorInfo ?? .iconAndTitle
            },
            set: { newValue in
                withAnimation(.easeInOut(duration: 0.3)) {
                    preferencesVM.update {
                        $0.indicatorInfo = newValue
                    }
                }
            }
        )

        let sizeBinding = Binding(
            get: {
                preferencesVM.preferences.indicatorSize ?? .medium
            },
            set: { newValue in
                withAnimation(.easeInOut(duration: 0.3)) {
                    preferencesVM.update {
                        $0.indicatorSize = newValue
                    }
                }
            }
        )

        ScrollView {
            VStack(alignment: .leading, spacing: 18) {
                SettingsSection(title: "Indicator Info") {
                    VStack(spacing: 0) {
                        ItemSection {
                            IndicatorView()
                        }
                        .frame(height: 100)
                        .padding(.horizontal)
                        .padding(.top)

                        Picker("Position", selection: infoBinding) {
                            ForEach(IndicatorInfo.allCases) { item in
                                Text(item.name).tag(item)
                            }
                        }
                        .labelsHidden()
                        .pickerStyle(.segmented)
                        .padding()
                    }
                }

                SettingsSection(title: "Indicator Size") {
                    Picker("Size", selection: sizeBinding) {
                        ForEach(IndicatorSize.allCases) { item in
                            Text(item.displayName).tag(item)
                        }
                    }
                    .labelsHidden()
                    .pickerStyle(.segmented)
                    .padding()
                }

                SettingsSection(title: "Color Scheme") {
                    VStack(spacing: 0) {
                        HStack {
                            Toggle("", isOn: $preferencesVM.preferences.isAutoAppearanceMode)
                                .toggleStyle(.switch)
                                .labelsHidden()

                            Text("Sync with OS".i18n())

                            Spacer()
                        }
                        .padding()
                        .border(width: 1, edges: [.bottom], color: NSColor.border2.color)

                        VStack(spacing: 16) {
                            Picker("", selection: $preferencesVM.preferences.appearanceMode) {
                                Text("In Light Mode".i18n()).tag(Optional(Preferences.AppearanceMode.light))
                                Text("In Dark Mode".i18n()).tag(Optional(Preferences.AppearanceMode.dark))
                            }
                            .labelsHidden()
                            .pickerStyle(.segmented)

                            ColorBlocks(
                                onSelectColor: { scheme in
                                    preferencesVM.update {
                                        $0.indicatorForgegroundColor = scheme.a
                                        $0.indicatorBackgroundColor = scheme.b
                                    }
                                }
                            )

                            ItemSection {
                                IndicatorView()

                                HStack {
                                    ColorPicker(
                                        "Color",
                                        selection: $preferencesVM.preferences.indicatorForgegroundColor
                                    )
                                    .labelsHidden()

                                    Button(
                                        action: {
                                            preferencesVM.update {
                                                let a = $0.indicatorForgegroundColor
                                                let b = $0.indicatorBackgroundColor

                                                $0.indicatorForgegroundColor = b
                                                $0.indicatorBackgroundColor = a
                                            }
                                        },
                                        label: {
                                            Image(systemName: "repeat")
                                        }
                                    )

                                    ColorPicker(
                                        "Background",
                                        selection: $preferencesVM.preferences.indicatorBackgroundColor
                                    )
                                    .labelsHidden()
                                }
                            }
                            .frame(height: 130)
                        }
                        .padding()
                    }
                }
            }
            .padding()
            .padding(.bottom)
        }
        .background(NSColor.background1.color)
        .onAppear(perform: updatePreviewModeOnAppear)
        .onDisappear {
            NSColorPanel.shared.close()
        }
    }

    func updatePreviewModeOnAppear() {
        if preferencesVM.preferences.isAutoAppearanceMode {
            preferencesVM.preferences.appearanceMode = colorScheme == .dark ? .dark : .light
        }
    }

    func resetColors() {
        if preferencesVM.preferences.appearanceMode == .light {
            preferencesVM.preferences.indicatorForgegroundColor = IndicatorColor.forgeground.light
            preferencesVM.preferences.indicatorBackgroundColor = IndicatorColor.background.light
        }

        if preferencesVM.preferences.appearanceMode == .dark {
            preferencesVM.preferences.indicatorForgegroundColor = IndicatorColor.forgeground.dark
            preferencesVM.preferences.indicatorBackgroundColor = IndicatorColor.background.dark
        }
    }
}

import SwiftUI

struct EnhancedModeToggle: View {
    @EnvironmentObject var preferencesVM: PreferencesVM
    @EnvironmentObject var permissionsVM: PermissionsVM

    @State var isShowAccessibilityRequest = false

    var body: some View {
        let isDetectSpotlightLikeAppBinding = Binding(
            get: { preferencesVM.preferences.isEnhancedModeEnabled },
            set: { onNeedDetectSpotlightLikeApp($0) }
        )

        VStack(alignment: .leading) {
            HStack {
                Toggle("", isOn: isDetectSpotlightLikeAppBinding)
                    .sheet(isPresented: $isShowAccessibilityRequest) {
                        AccessibilityPermissionRequestView(isPresented: $isShowAccessibilityRequest)
                    }

                Text("Enhanced Mode".i18n())

                Spacer()
            }

            Text(.init("Enhanced Mode Description".i18n()))
                .font(.system(size: 12))
                .opacity(0.8)
        }
        .padding()
    }

    func onNeedDetectSpotlightLikeApp(_ isDetectSpotlightLikeApp: Bool) {
        preferencesVM.update {
            if isDetectSpotlightLikeApp {
                if permissionsVM.isAccessibilityEnabled {
                    $0.isEnhancedModeEnabled = true
                } else {
                    isShowAccessibilityRequest = true
                }
            } else {
                $0.isEnhancedModeEnabled = false
            }
        }
    }
}

import AppKit
import RxRelay

class FocusAwareTextField: NSTextField {
    let focus$ = PublishRelay<Bool>()

    override func becomeFirstResponder() -> Bool {
        focus$.accept(true)
        return super.becomeFirstResponder()
    }
}

import AppKit
import Foundation

extension CGRect {
    func relativeTo(screen: NSScreen) -> CGRect {
        let point = CGPoint(x: minX, y: screen.frame.maxY - maxY)

        return CGRect(origin: point, size: size)
    }
}

import SwiftUI

struct InputSourcesAppearanceSettingsView: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    let imgSize: CGFloat = 16

    var body: some View {
        ScrollView {
            VStack(spacing: 18) {
                ForEach(InputSource.sources, id: \.id) { inputSource in
                    SettingsSection(title: inputSource.name, noI18N: true) {
                        KeyboardCustomization(inputSource: inputSource)
                    }
                }
            }
            .padding()
            .padding(.bottom)
        }
        .background(NSColor.background1.color)
    }
}

import SwiftUI

struct CJKVFixEnableFailedView: View {
    @Binding var isPresented: Bool

    @State var isOpened = false

    var body: some View {
        VStack(spacing: 0) {
            Text("Enabled CJKV Fix Failed Desc")

            Image("Enabled CJKV Fix Shortcut Img".i18n())
                .resizable()
                .aspectRatio(contentMode: .fit)
                .shadow(color: .black.opacity(0.26), radius: 8)
                .padding(20)

            HStack {
                Spacer()

                if isOpened {
                    Button("Close", action: { isPresented = false })
                        .keyboardShortcut(.defaultAction)
                } else {
                    Button("Cancel", action: { isPresented = false })
                        .keyboardShortcut(.cancelAction)

                    Button("Open Keyboard Settings", action: {
                        NSWorkspace.shared.openKeyboardPreferences()
                        isOpened = true
                    })
                    .keyboardShortcut(.defaultAction)
                }
            }
        }
        .padding()
        .frame(width: 480)
    }
}

import SwiftUI

struct LoadingView<Content>: View where Content: View {
    var isShowing: Bool

    var content: () -> Content

    var body: some View {
        ZStack(alignment: .center) {
            self.content()
                .disabled(isShowing)
                .opacity(isShowing ? 0.5 : 1)

            VStack {
                ProgressView()
                    .frame(width: 30, height: 30)
            }
            .opacity(isShowing ? 1 : 0)
            .animation(.easeOut, value: isShowing)
        }
    }
}

import SwiftUI

struct NoAnimation: ViewModifier {
    func body(content: Content) -> some View {
        content
            .transaction { transaction in
                transaction.animation = nil
            }
    }
}

extension View {
    func noAnimation() -> some View {
        modifier(NoAnimation())
    }
}

import SwiftUI

struct FeedbackButton: View {
    @EnvironmentObject var feedbackVM: FeedbackVM

    var body: some View {
        Button(
            action: feedbackVM.show,
            label: {
                HStack {
                    Text("Send Feedback".i18n() + "...")
                    Spacer()
                }
            }
        )
        .buttonStyle(SectionButtonStyle())
    }
}

struct FeedbackModal: ViewModifier {
    @EnvironmentObject var feedbackVM: FeedbackVM

    func body(content: Content) -> some View {
        content
            .sheet(isPresented: .constant(feedbackVM.isPresented), content: {
                LoadingView(isShowing: feedbackVM.isSending) {
                    VStack(alignment: .leading) {
                        if feedbackVM.isSent {
                            HStack {
                                Spacer()

                                VStack {
                                    Text("FeedbackSuccessTips".i18n())
                                        .multilineTextAlignment(.center)
                                        .font(.system(size: 16))
                                        .padding(.horizontal)
                                        .padding(.vertical, 30)

                                    Button("Close".i18n()) {
                                        feedbackVM.hide()
                                    }
                                    .keyboardShortcut(.defaultAction)
                                    .padding(.bottom)
                                }

                                Spacer()
                            }
                        } else {
                            Text("FeedbackTips".i18n())
                                .foregroundColor(.primary.opacity(0.8))
                                .padding(.vertical, 8)

                            Text("FeedbackMessageTitle".i18n() + ":")

                            ISPTextEditor(text: $feedbackVM.message, placeholder: "FeedbackMessagePlaceholder".i18n(), minHeight: 80)

                            Text("FeedbackEmailTitle".i18n() + ":")
                                .padding(.top, 8)

                            ISPTextEditor(text: $feedbackVM.email, placeholder: "support@inputsource.pro", minHeight: 0)

                            HStack {
                                Spacer()

                                Button("Cancel".i18n()) {
                                    feedbackVM.hide()
                                }
                                .keyboardShortcut(.cancelAction)

                                Button("Send Feedback".i18n()) {
                                    Task {
                                        await feedbackVM.sendFeedback()
                                    }
                                }
                                .keyboardShortcut(.defaultAction)
                                .disabled(feedbackVM.message.isEmpty)
                            }
                            .padding(.top)
                        }
                    }
                    .lineLimit(nil)
                    .padding()
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(width: 450)
                    .disabled(feedbackVM.isSending)
                }
                .background(NSColor.background.color)
            })
    }
}

import SwiftUI

struct RebootRequiredBadge: View {
    let isRequired: Bool

    var body: some View {
        Button(action: {}) {
            Text("System Reboot Required".i18n())
        }
        .buttonStyle(RebootRequiredButtonStyle())
        .opacity(isRequired ? 1 : 0)
        .animation(.easeInOut, value: isRequired)
    }
}

struct RebootRequiredButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .font(.system(size: 10))
            .padding(.horizontal, 4)
            .padding(.vertical, 3)
            .background(Color.gray)
            .foregroundColor(Color.white)
            .clipShape(RoundedRectangle(cornerRadius: 4))
            .contentShape(Rectangle())
    }
}

import Cocoa

let delegate = AppDelegate()
let menu = AppMenu()

NSApplication.shared.delegate = delegate
NSApplication.shared.mainMenu = menu

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

import AppKit

extension NSToolbar.Identifier {
    static let preferencesWindowToolbarIdentifier = NSToolbar.Identifier("PreferencesWindowToolbar")
}

extension NSToolbarItem.Identifier {
    static let toolbarShareButtonItem = NSToolbarItem.Identifier(rawValue: "ToolbarShareButtonItem")
}

import SwiftUI
import VisualEffects

struct MockWindowView: View {
    var body: some View {
        ZStack {
            VisualEffectBlur(
                material: .windowBackground,
                blendingMode: .withinWindow,
                state: .active
            )

            VStack {
                HStack {
                    Circle()
                        .frame(width: 10, height: 10)
                        .foregroundColor(Color(NSColor.close))

                    Circle()
                        .frame(width: 10, height: 10)
                        .foregroundColor(Color(NSColor.minimise))

                    Circle()
                        .frame(width: 10, height: 10)
                        .foregroundColor(Color(NSColor.maximise))
                    Spacer()
                }
                Spacer()
            }
            .padding(10)
        }
        .clipShape(RoundedRectangle(cornerRadius: 9))
        .shadow(color: Color.black.opacity(0.2), radius: 5, x: 3, y: 3)
    }
}

import SwiftUI

struct EnhancedModeRequiredBadge: View {
    @EnvironmentObject var preferencesVM: PreferencesVM

    var body: some View {
        Button(action: {}) {
            Text("Enhanced Mode Required".i18n())
        }
        .buttonStyle(EnhanceModeRequiredButtonStyle())
        .opacity(preferencesVM.preferences.isEnhancedModeEnabled ? 0 : 1)
        .animation(.easeInOut, value: preferencesVM.preferences.isEnhancedModeEnabled)
    }
}

struct EnhanceModeRequiredButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .font(.system(size: 10))
            .padding(.horizontal, 4)
            .padding(.vertical, 3)
            .background(Color.yellow)
            .foregroundColor(Color.black)
            .clipShape(RoundedRectangle(cornerRadius: 4))
            .contentShape(Rectangle())
    }
}

import AppKit

// Removes background from List in SwiftUI
extension NSTableView {
    override open func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()

        backgroundColor = NSColor.clear
        if let esv = enclosingScrollView {
            esv.drawsBackground = false
        }
    }
}

import Combine

final class CancelBag {
    fileprivate(set) var subscriptions = Set<AnyCancellable>()

    func cancel() {
        subscriptions.removeAll()
    }
}

extension AnyCancellable {
    func store(in cancelBag: CancelBag) {
        cancelBag.subscriptions.insert(self)
    }
}

import AppKit
import Foundation

extension NSRunningApplication {
    // FIXME: check window info
    var isFloatingApp: Bool {
        NSApplication.isFloatingApp(bundleIdentifier)
    }
}

import AppKit
import SwiftUI

extension NSColor {
    var color: Color {
        Color(self)
    }
}

This code segment functions to: Encapsulate accessibility properties of a UI component by extracting and validating specific attributes during object initialization, ensuring required metadata is present before instance creation.

```swift
[TASK_BEGIN]
class Element {
    private let uiElement: UIElement
    private(set) var role: String?
    private(set) var title: String?
    private(set) var position: CGRect?
    private(set) var size: CGRect?

    weak var parent: Element?
    var children: [Element] = []

    init(uiElement: UIElement, parent: Element? = nil) {
        self.uiElement = uiElement
        self.parent = parent

        // Get basic properties
        role = try? uiElement.attribute(.role)
        title = try? uiElement.attribute(.title)
        position = try? uiElement.attribute(.position)
        size = try? uiElement.attribute(.size)

        // Get children
        if let childElements: [UIElement] = try? uiElement.attribute(.children) {
            children = childElements.map { Element(uiElement: $0, parent: self) }
        }
    }

    var isEmpty: Bool {
        children.isEmpty && (title?.isEmpty ?? true)
    }

    var isMenuBar: Bool {
        role == "AXMenuBar"
    }

    var isMenuBarItem: Bool {
        role == "AXMenuBarItem"
    }

    var isWindow: Bool {
        role == "AXWindow"
    }

    func click() {
        _ = try? uiElement.performAction(.press)
    }
}
[TASK_END]
```

class Element {
    let rawElement: AXUIElement
    let role: String
    let url: URL?
    var clippedFrame: NSRect?
    static func initialize(rawElement: AXUIElement) -> Element? {
        let uiElement = UIElement(rawElement)
        let valuesOptional = try? uiElement.getMultipleAttributes([.role, .url])
        guard let values = valuesOptional else { return nil }
        guard let role: String = values[Attribute.role] as! String? else { return nil }
        return Element(rawElement: rawElement, role: role, url: values[Attribute.url] as? URL)
    }
    init(rawElement: AXUIElement, role: String, url: URL?) {
        self.rawElement = rawElement
        self.role = role
        self.url = url
    }
}

class Element {
    private let uiElement: UIElement
    private(set) var role: String?
    private(set) var title: String?
    private(set) var position: CGRect?
    private(set) var size: CGRect?
    weak var parent: Element?
    var children: [Element] = []
    init(uiElement: UIElement, parent: Element? = nil) {
        self.uiElement = uiElement
        self.parent = parent
        role = try? uiElement.attribute(.role)
        title = try? uiElement.attribute(.title)
        position = try? uiElement.attribute(.position)
        size = try? uiElement.attribute(.size)
        if let childElements: [UIElement] = try? uiElement.attribute(.children) {
            children = childElements.map { Element(uiElement: $0, parent: self) }
        }
    }
    var isEmpty: Bool {
        children.isEmpty && (title?.isEmpty ?? true)
    }
    var isMenuBar: Bool {
        role == "AXMenuBar"
    }
    var isMenuBarItem: Bool {
        role == "AXMenuBarItem"
    }
    var isWindow: Bool {
        role == "AXWindow"
    }
    func click() {
        _ = try? uiElement.performAction(.press)
    }
}