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 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 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
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 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 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 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 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 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 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 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 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 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)
    }
}

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 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

@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

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 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 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 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 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 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 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 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 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 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 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
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
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 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 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 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 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 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 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 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 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 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

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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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

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

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
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 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 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 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
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

// 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 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 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 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 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)
        }
    }
}

// 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 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
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 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 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 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 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 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

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

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 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 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 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 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 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 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 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 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 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 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

extension NSToolbar.Identifier {
    static let preferencesWindowToolbarIdentifier = NSToolbar.Identifier("PreferencesWindowToolbar")
}

extension NSToolbarItem.Identifier {
    static let toolbarShareButtonItem = NSToolbarItem.Identifier(rawValue: "ToolbarShareButtonItem")
}

// 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 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 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 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 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 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 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 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 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]
    }
}

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)
    }
}

//
//  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 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 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 AXSwift
import Cocoa

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 CryptoKit
import Foundation

extension Data {
    func md5() -> String {
        Insecure.MD5
            .hash(data: self)
            .map { String(format: "%02hhx", $0) }
            .joined()
    }
}

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 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"
    )!
}

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 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 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 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 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

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 Cocoa

let delegate = AppDelegate()
let menu = AppMenu()

NSApplication.shared.delegate = delegate
NSApplication.shared.mainMenu = menu

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

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 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 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 NoAnimation: ViewModifier {
    func body(content: Content) -> some View {
        content
            .transaction { transaction in
                transaction.animation = nil
            }
    }
}

extension View {
    func noAnimation() -> some View {
        modifier(NoAnimation())
    }
}

import AppKit
import SwiftUI

extension NSColor {
    var color: Color {
        Color(self)
    }
}

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 AppKit
import RxRelay

class FocusAwareTextField: NSTextField {
    let focus$ = PublishRelay<Bool>()

    override func becomeFirstResponder() -> Bool {
        focus$.accept(true)
        return super.becomeFirstResponder()
    }
}

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

extension Bundle {
    var shortVersion: String {
        infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
    }

    var buildVersion: Int {
        Int(infoDictionary?["CFBundleVersion"] as? String ?? "0") ?? 0
    }
}

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 AppKit
import Foundation

extension NSRunningApplication {
    // FIXME: check window info
    var isFloatingApp: Bool {
        NSApplication.isFloatingApp(bundleIdentifier)
    }
}

This code segment functions to: Manage the visual presentation and positioning of an interface component by coordinating configuration data with display properties, handling dynamic updates to appearance attributes, and adjusting spatial placement based on stored preferences.

```swift
[TASK_BEGIN]
extension IndicatorWindowController {
    override func windowDidLoad() {
        super.windowDidLoad()
        window?.level = .floating
        window?.isMovableByWindowBackground = true
        setupCloseButton()
    }

    private func setupCloseButton() {
        let closeButton = NSButton(title: "×", target: self, action: #selector(handleClose(_:)))
        closeButton.bezelStyle = .circular
        closeButton.frame = NSRect(x: window!.frame.width - 25, y: window!.frame.height - 25, width: 20, height: 20)
        window?.contentView?.addSubview(closeButton)
    }

    @objc private func handleClose(_ sender: NSButton) {
        window?.close()
        NSApp.mainWindow?.windowController?.removeWindowController(self)
    }
}
[TASK_END]
```

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)
    }
}

extension IndicatorWindowController {
    override func windowDidLoad() {
        super.windowDidLoad()
        window?.level = .floating
        window?.isMovableByWindowBackground = true
        setupCloseButton()
    }
    private func setupCloseButton() {
        let closeButton = NSButton(title: "×", target: self, action: #selector(handleClose(_:)))
        closeButton.bezelStyle = .circular
        closeButton.frame = NSRect(x: window!.frame.width - 25, y: window!.frame.height - 25, width: 20, height: 20)
        window?.contentView?.addSubview(closeButton)
    }
    @objc private func handleClose(_ sender: NSButton) {
        window?.close()
        NSApp.mainWindow?.windowController?.removeWindowController(self)
    }
}