mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
565 lines
22 KiB
Swift
565 lines
22 KiB
Swift
import SwiftUI
|
|
import ObjectiveC.runtime
|
|
#if canImport(FoundationModels)
|
|
import FoundationModels
|
|
#endif
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
#if os(iOS)
|
|
import UIKit
|
|
#endif
|
|
|
|
/// MARK: - Types
|
|
|
|
private var runtimeLanguageBundleAssociationKey: UInt8 = 0
|
|
|
|
private final class RuntimeLanguageBundle: Bundle, @unchecked Sendable {
|
|
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
|
|
if let languageBundle = objc_getAssociatedObject(self, &runtimeLanguageBundleAssociationKey) as? Bundle {
|
|
return languageBundle.localizedString(forKey: key, value: value, table: tableName)
|
|
}
|
|
return super.localizedString(forKey: key, value: value, table: tableName)
|
|
}
|
|
}
|
|
|
|
private enum RuntimeLanguageOverride {
|
|
private static var didInstallBundleOverride = false
|
|
|
|
static func apply(languageCode: String) {
|
|
installBundleOverrideIfNeeded()
|
|
let bundle = languageBundle(for: languageCode)
|
|
objc_setAssociatedObject(
|
|
Bundle.main,
|
|
&runtimeLanguageBundleAssociationKey,
|
|
bundle,
|
|
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
)
|
|
}
|
|
|
|
private static func installBundleOverrideIfNeeded() {
|
|
guard !didInstallBundleOverride else { return }
|
|
object_setClass(Bundle.main, RuntimeLanguageBundle.self)
|
|
didInstallBundleOverride = true
|
|
}
|
|
|
|
private static func languageBundle(for languageCode: String) -> Bundle? {
|
|
guard languageCode != "system" else { return nil }
|
|
if let exact = Bundle.main.path(forResource: languageCode, ofType: "lproj").flatMap(Bundle.init(path:)) {
|
|
return exact
|
|
}
|
|
let fallbackCode = languageCode.split(separator: "-").first.map(String.init) ?? languageCode
|
|
return Bundle.main.path(forResource: fallbackCode, ofType: "lproj").flatMap(Bundle.init(path:))
|
|
}
|
|
}
|
|
|
|
#if os(macOS)
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
weak var viewModel: EditorViewModel? {
|
|
didSet {
|
|
guard let viewModel else { return }
|
|
Task { @MainActor in
|
|
self.flushPendingURLs(into: viewModel)
|
|
}
|
|
}
|
|
}
|
|
weak var appUpdateManager: AppUpdateManager?
|
|
private var pendingOpenURLs: [URL] = []
|
|
|
|
func application(_ application: NSApplication, open urls: [URL]) {
|
|
Task { @MainActor in
|
|
for url in urls {
|
|
if let existing = WindowViewModelRegistry.shared.viewModel(containing: url) {
|
|
_ = existing.viewModel.focusTabIfOpen(for: url)
|
|
if let window = NSApp.window(withWindowNumber: existing.windowNumber) {
|
|
window.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
continue
|
|
}
|
|
let target = WindowViewModelRegistry.shared.activeViewModel() ?? self.viewModel
|
|
if let target {
|
|
target.openFile(url: url)
|
|
} else {
|
|
self.pendingOpenURLs.append(url)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
|
|
return NSApp.windows.isEmpty && pendingOpenURLs.isEmpty
|
|
}
|
|
|
|
func applicationWillTerminate(_ notification: Notification) {
|
|
appUpdateManager?.applicationWillTerminate()
|
|
RuntimeReliabilityMonitor.shared.markGracefulTermination()
|
|
}
|
|
|
|
@MainActor
|
|
private func flushPendingURLs(into viewModel: EditorViewModel) {
|
|
guard !pendingOpenURLs.isEmpty else { return }
|
|
let urls = pendingOpenURLs
|
|
pendingOpenURLs.removeAll()
|
|
urls.forEach { viewModel.openFile(url: $0) }
|
|
}
|
|
}
|
|
|
|
private struct DetachedWindowContentView: View {
|
|
@State private var viewModel = EditorViewModel()
|
|
@ObservedObject var supportPurchaseManager: SupportPurchaseManager
|
|
@ObservedObject var appUpdateManager: AppUpdateManager
|
|
@Binding var showGrokError: Bool
|
|
@Binding var grokErrorMessage: String
|
|
|
|
var body: some View {
|
|
ContentView(startupBehavior: .forceBlankDocument)
|
|
.environment(viewModel)
|
|
.environmentObject(supportPurchaseManager)
|
|
.environmentObject(appUpdateManager)
|
|
.environment(\.showGrokError, $showGrokError)
|
|
.environment(\.grokErrorMessage, $grokErrorMessage)
|
|
.frame(minWidth: 600, minHeight: 400)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@main
|
|
struct NeonVisionEditorApp: App {
|
|
@State private var viewModel = EditorViewModel()
|
|
@StateObject private var supportPurchaseManager = SupportPurchaseManager()
|
|
@StateObject private var appUpdateManager = AppUpdateManager()
|
|
@AppStorage("SettingsAppearance") private var appearance: String = "system"
|
|
@AppStorage("SettingsAppLanguageCode") private var appLanguageCode: String = "system"
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
private let mainStartupBehavior: ContentView.StartupBehavior
|
|
private let startupSafeModeMessage: String?
|
|
@State private var didMarkLaunchCompleted: Bool = false
|
|
#if os(macOS)
|
|
@Environment(\.openWindow) private var openWindow
|
|
@State private var useAppleIntelligence: Bool = true
|
|
@State private var appleAIStatus: String = "Apple Intelligence: Checking…"
|
|
@State private var appleAIRoundTripMS: Double? = nil
|
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
#endif
|
|
@State private var showGrokError: Bool = false
|
|
@State private var grokErrorMessage: String = ""
|
|
|
|
private var preferredAppearance: ColorScheme? {
|
|
ReleaseRuntimePolicy.preferredColorScheme(for: appearance)
|
|
}
|
|
|
|
private var preferredLocale: Locale {
|
|
appLanguageCode == "system"
|
|
? .autoupdatingCurrent
|
|
: Locale(identifier: appLanguageCode)
|
|
}
|
|
|
|
private func applyRuntimeLanguageOverride() {
|
|
RuntimeLanguageOverride.apply(languageCode: appLanguageCode)
|
|
}
|
|
|
|
private func completeLaunchReliabilityTrackingIfNeeded() {
|
|
guard !didMarkLaunchCompleted else { return }
|
|
didMarkLaunchCompleted = true
|
|
RuntimeReliabilityMonitor.shared.markLaunchCompleted()
|
|
}
|
|
|
|
#if os(macOS)
|
|
private var appKitAppearance: NSAppearance? {
|
|
switch appearance {
|
|
case "light":
|
|
return NSAppearance(named: .aqua)
|
|
case "dark":
|
|
return NSAppearance(named: .darkAqua)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func applyGlobalAppearanceOverride() {
|
|
let override = appKitAppearance
|
|
NSApp.appearance = override
|
|
for window in NSApp.windows {
|
|
window.appearance = override
|
|
window.invalidateShadow()
|
|
window.displayIfNeeded()
|
|
}
|
|
}
|
|
|
|
private func applyMacWindowTabbingPolicy() {
|
|
// Use app-native file tab pills only; disable NSWindow tab bar to avoid duplicate tab systems.
|
|
NSWindow.allowsAutomaticWindowTabbing = false
|
|
for window in NSApp.windows {
|
|
window.tabbingMode = .disallowed
|
|
}
|
|
hideNativeTabBarMenuItems()
|
|
}
|
|
|
|
private func hideNativeTabBarMenuItems() {
|
|
guard let mainMenu = NSApp.mainMenu else { return }
|
|
let targets = ["Show Tab Bar", "Hide Tab Bar", "Move Tab to New Window", "Merge All Windows"]
|
|
|
|
func filter(menu: NSMenu) {
|
|
for item in menu.items {
|
|
if let submenu = item.submenu {
|
|
filter(menu: submenu)
|
|
}
|
|
}
|
|
menu.items.removeAll { item in
|
|
targets.contains(item.title)
|
|
}
|
|
}
|
|
|
|
filter(menu: mainMenu)
|
|
}
|
|
#endif
|
|
|
|
#if os(iOS)
|
|
private var userInterfaceStyle: UIUserInterfaceStyle {
|
|
switch appearance {
|
|
case "light":
|
|
return .light
|
|
case "dark":
|
|
return .dark
|
|
default:
|
|
return .unspecified
|
|
}
|
|
}
|
|
|
|
private func applyIOSAppearanceOverride() {
|
|
let style = userInterfaceStyle
|
|
UIApplication.shared.connectedScenes
|
|
.compactMap { $0 as? UIWindowScene }
|
|
.forEach { scene in
|
|
scene.windows.forEach { window in
|
|
if window.overrideUserInterfaceStyle != style {
|
|
window.overrideUserInterfaceStyle = style
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
init() {
|
|
let defaults = UserDefaults.standard
|
|
let launchCountKey = "AppLaunchCountV1"
|
|
defaults.set(defaults.integer(forKey: launchCountKey) + 1, forKey: launchCountKey)
|
|
// Default editor behavior:
|
|
// - keep line numbers on
|
|
// - keep style/space visualization toggles off unless user enables them in Settings
|
|
defaults.register(defaults: [
|
|
"SettingsShowLineNumbers": true,
|
|
"SettingsHighlightCurrentLine": false,
|
|
"SettingsHighlightMatchingBrackets": false,
|
|
"SettingsShowScopeGuides": false,
|
|
"SettingsHighlightScopeBackground": false,
|
|
"SettingsLineWrapEnabled": false,
|
|
"SettingsShowInvisibleCharacters": false,
|
|
"SettingsUseSystemFont": false,
|
|
"SettingsIndentStyle": "spaces",
|
|
"SettingsIndentWidth": 4,
|
|
"SettingsAutoIndent": true,
|
|
"SettingsAutoCloseBrackets": false,
|
|
"SettingsTrimTrailingWhitespace": false,
|
|
"SettingsTrimWhitespaceForSyntaxDetection": false,
|
|
"SettingsCompletionEnabled": false,
|
|
"SettingsCompletionFromDocument": false,
|
|
"SettingsCompletionFromSyntax": false,
|
|
"SettingsReopenLastSession": true,
|
|
"SettingsOpenWithBlankDocument": false,
|
|
"SettingsAppLanguageCode": "system",
|
|
"SettingsDefaultNewFileLanguage": "plain",
|
|
"SettingsConfirmCloseDirtyTab": true,
|
|
"SettingsConfirmClearEditor": true,
|
|
"SettingsRemoteSessionsEnabled": false,
|
|
"SettingsRemoteHost": "",
|
|
"SettingsRemoteUsername": "",
|
|
"SettingsRemotePort": 22,
|
|
"SettingsRemotePreparedTarget": "",
|
|
"SettingsAutoCheckForUpdates": true,
|
|
"SettingsUpdateCheckInterval": AppUpdateCheckInterval.daily.rawValue,
|
|
"SettingsAutoDownloadUpdates": false
|
|
])
|
|
let vimResetMigrationKey = "SettingsMigrationVimModeResetV1"
|
|
if !defaults.bool(forKey: vimResetMigrationKey) {
|
|
// One-time safety reset: avoid stale NORMAL-mode state making editor appear non-editable.
|
|
defaults.set(false, forKey: "EditorVimModeEnabled")
|
|
defaults.set(true, forKey: vimResetMigrationKey)
|
|
}
|
|
let whitespaceMigrationKey = "SettingsMigrationWhitespaceGlyphResetV1"
|
|
if !defaults.bool(forKey: whitespaceMigrationKey) {
|
|
defaults.set(false, forKey: "SettingsShowInvisibleCharacters")
|
|
defaults.set(false, forKey: "NSShowAllInvisibles")
|
|
defaults.set(false, forKey: "NSShowControlCharacters")
|
|
defaults.set(true, forKey: whitespaceMigrationKey)
|
|
}
|
|
RuntimeReliabilityMonitor.shared.markLaunch()
|
|
let safeModeDecision = RuntimeReliabilityMonitor.shared.consumeSafeModeLaunchDecision()
|
|
self.mainStartupBehavior = safeModeDecision.isEnabled ? .safeMode : .standard
|
|
self.startupSafeModeMessage = safeModeDecision.message
|
|
RuntimeReliabilityMonitor.shared.startMainThreadWatchdog()
|
|
EditorPerformanceMonitor.shared.markLaunchConfigured()
|
|
RuntimeLanguageOverride.apply(
|
|
languageCode: defaults.string(forKey: "SettingsAppLanguageCode") ?? "system"
|
|
)
|
|
}
|
|
|
|
#if os(macOS)
|
|
private var activeWindowNumber: Int? {
|
|
NSApp.keyWindow?.windowNumber ?? NSApp.mainWindow?.windowNumber
|
|
}
|
|
|
|
private var activeEditorViewModel: EditorViewModel {
|
|
WindowViewModelRegistry.shared.activeViewModel() ?? viewModel
|
|
}
|
|
|
|
private func postWindowCommand(_ name: Notification.Name, object: Any? = nil) {
|
|
var userInfo: [AnyHashable: Any] = [:]
|
|
if let activeWindowNumber {
|
|
userInfo[EditorCommandUserInfo.windowNumber] = activeWindowNumber
|
|
}
|
|
NotificationCenter.default.post(
|
|
name: name,
|
|
object: object,
|
|
userInfo: userInfo.isEmpty ? nil : userInfo
|
|
)
|
|
}
|
|
#endif
|
|
|
|
var body: some Scene {
|
|
#if os(macOS)
|
|
WindowGroup {
|
|
ContentView(
|
|
startupBehavior: mainStartupBehavior,
|
|
safeModeMessage: startupSafeModeMessage
|
|
)
|
|
.environment(viewModel)
|
|
.environmentObject(supportPurchaseManager)
|
|
.environmentObject(appUpdateManager)
|
|
.onAppear {
|
|
appDelegate.viewModel = viewModel
|
|
appDelegate.appUpdateManager = appUpdateManager
|
|
}
|
|
.onAppear { applyGlobalAppearanceOverride() }
|
|
.onAppear { applyMacWindowTabbingPolicy() }
|
|
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
|
.onAppear { applyRuntimeLanguageOverride() }
|
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
|
.environment(\.showGrokError, $showGrokError)
|
|
.environment(\.grokErrorMessage, $grokErrorMessage)
|
|
.environment(\.locale, preferredLocale)
|
|
.tint(.blue)
|
|
.preferredColorScheme(preferredAppearance)
|
|
.onChange(of: scenePhase) { _, newPhase in
|
|
guard newPhase == .active else { return }
|
|
completeLaunchReliabilityTrackingIfNeeded()
|
|
}
|
|
.frame(minWidth: 600, minHeight: 400)
|
|
.task {
|
|
completeLaunchReliabilityTrackingIfNeeded()
|
|
guard mainStartupBehavior != .safeMode else { return }
|
|
if ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution {
|
|
appUpdateManager.startAutomaticChecks()
|
|
}
|
|
#if USE_FOUNDATION_MODELS && canImport(FoundationModels)
|
|
do {
|
|
let start = Date()
|
|
_ = try await AppleFM.appleFMHealthCheck()
|
|
let end = Date()
|
|
appleAIStatus = "Apple Intelligence: Ready"
|
|
appleAIRoundTripMS = end.timeIntervalSince(start) * 1000.0
|
|
AIActivityLog.record(
|
|
"Startup AI health check succeeded (\(String(format: "%.1f", appleAIRoundTripMS ?? 0)) ms).",
|
|
source: "Startup"
|
|
)
|
|
} catch {
|
|
appleAIStatus = "Apple Intelligence: Error — \(error.localizedDescription)"
|
|
appleAIRoundTripMS = nil
|
|
AIActivityLog.record(
|
|
"Startup AI health check failed: \(error.localizedDescription)",
|
|
level: .error,
|
|
source: "Startup"
|
|
)
|
|
}
|
|
#else
|
|
appleAIStatus = "Apple Intelligence: Unavailable (build without USE_FOUNDATION_MODELS)"
|
|
AIActivityLog.record(
|
|
"Startup AI health check unavailable (built without USE_FOUNDATION_MODELS).",
|
|
level: .warning,
|
|
source: "Startup"
|
|
)
|
|
#endif
|
|
}
|
|
}
|
|
.defaultSize(width: 1000, height: 600)
|
|
.handlesExternalEvents(matching: ["*"])
|
|
|
|
WindowGroup("New Window", id: "blank-window") {
|
|
DetachedWindowContentView(
|
|
supportPurchaseManager: supportPurchaseManager,
|
|
appUpdateManager: appUpdateManager,
|
|
showGrokError: $showGrokError,
|
|
grokErrorMessage: $grokErrorMessage
|
|
)
|
|
.onAppear { applyGlobalAppearanceOverride() }
|
|
.onAppear { applyMacWindowTabbingPolicy() }
|
|
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
|
.onAppear { applyRuntimeLanguageOverride() }
|
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
|
.environment(\.locale, preferredLocale)
|
|
.tint(.blue)
|
|
.preferredColorScheme(preferredAppearance)
|
|
}
|
|
.defaultSize(width: 1000, height: 600)
|
|
.handlesExternalEvents(matching: [])
|
|
|
|
Settings {
|
|
ConfiguredSettingsView(
|
|
supportsOpenInTabs: false,
|
|
supportsTranslucency: true,
|
|
editorViewModel: activeEditorViewModel,
|
|
supportPurchaseManager: supportPurchaseManager,
|
|
appUpdateManager: appUpdateManager
|
|
)
|
|
.onAppear { applyGlobalAppearanceOverride() }
|
|
.onAppear { applyMacWindowTabbingPolicy() }
|
|
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
|
.onAppear { applyRuntimeLanguageOverride() }
|
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
|
.environment(\.locale, preferredLocale)
|
|
.tint(.blue)
|
|
.preferredColorScheme(preferredAppearance)
|
|
}
|
|
|
|
Window("AI Activity Log", id: "ai-logs") {
|
|
AIActivityLogView()
|
|
.frame(minWidth: 720, minHeight: 420)
|
|
.onAppear { applyRuntimeLanguageOverride() }
|
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
|
.environment(\.locale, preferredLocale)
|
|
.preferredColorScheme(preferredAppearance)
|
|
.tint(.blue)
|
|
}
|
|
.defaultSize(width: 860, height: 520)
|
|
.handlesExternalEvents(matching: [])
|
|
|
|
MenuBarExtra("Welcome Tour", systemImage: "sparkles.rectangle.stack") {
|
|
Button {
|
|
postWindowCommand(.showWelcomeTourRequested)
|
|
} label: {
|
|
Label("Show Welcome Tour", systemImage: "sparkles.rectangle.stack")
|
|
}
|
|
|
|
Button {
|
|
postWindowCommand(.showEditorHelpRequested)
|
|
} label: {
|
|
Label("Editor Help…", systemImage: "questionmark.circle")
|
|
}
|
|
|
|
Button {
|
|
postWindowCommand(.showSupportPromptRequested)
|
|
} label: {
|
|
Label("Support Neon Vision Editor…", systemImage: "heart.circle.fill")
|
|
}
|
|
|
|
Divider()
|
|
|
|
SettingsLink {
|
|
Label("Settings…", systemImage: "gearshape")
|
|
}
|
|
|
|
if ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution {
|
|
Button {
|
|
postWindowCommand(.showUpdaterRequested, object: true)
|
|
} label: {
|
|
Label("Check for Updates…", systemImage: "arrow.triangle.2.circlepath.circle")
|
|
}
|
|
}
|
|
}
|
|
|
|
.commands {
|
|
NeonVisionMacAppCommands(
|
|
activeEditorViewModel: { activeEditorViewModel },
|
|
hasActiveEditorWindow: { WindowViewModelRegistry.shared.activeViewModel() != nil },
|
|
openNewWindow: { openWindow(id: "blank-window") },
|
|
openAIDiagnosticsWindow: { openWindow(id: "ai-logs") },
|
|
postWindowCommand: { name, object in
|
|
postWindowCommand(name, object: object)
|
|
},
|
|
isUpdaterEnabled: ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution,
|
|
recentFilesProvider: { RecentFilesStore.items(limit: 10) },
|
|
clearRecentFiles: { RecentFilesStore.clearUnpinned() },
|
|
useAppleIntelligence: $useAppleIntelligence,
|
|
appleAIStatus: $appleAIStatus,
|
|
appleAIRoundTripMS: $appleAIRoundTripMS,
|
|
showGrokError: $showGrokError,
|
|
grokErrorMessage: $grokErrorMessage
|
|
)
|
|
}
|
|
#else
|
|
WindowGroup {
|
|
ContentView(
|
|
startupBehavior: mainStartupBehavior,
|
|
safeModeMessage: startupSafeModeMessage
|
|
)
|
|
.environment(viewModel)
|
|
.environmentObject(supportPurchaseManager)
|
|
.environmentObject(appUpdateManager)
|
|
.environment(\.showGrokError, $showGrokError)
|
|
.environment(\.grokErrorMessage, $grokErrorMessage)
|
|
.environment(\.locale, preferredLocale)
|
|
.onAppear { applyRuntimeLanguageOverride() }
|
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
|
.tint(.blue)
|
|
.onAppear { applyIOSAppearanceOverride() }
|
|
.onChange(of: scenePhase) { _, newPhase in
|
|
guard newPhase == .active else { return }
|
|
completeLaunchReliabilityTrackingIfNeeded()
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in
|
|
RuntimeReliabilityMonitor.shared.markGracefulTermination()
|
|
}
|
|
.onChange(of: appearance) { _, _ in applyIOSAppearanceOverride() }
|
|
.preferredColorScheme(preferredAppearance)
|
|
.task {
|
|
completeLaunchReliabilityTrackingIfNeeded()
|
|
}
|
|
}
|
|
.commands {
|
|
CommandGroup(replacing: .undoRedo) {
|
|
Button("Undo") {
|
|
UIApplication.shared.sendAction(Selector(("undo:")), to: nil, from: nil, for: nil)
|
|
}
|
|
.keyboardShortcut("z", modifiers: .command)
|
|
|
|
Button("Redo") {
|
|
UIApplication.shared.sendAction(Selector(("redo:")), to: nil, from: nil, for: nil)
|
|
}
|
|
.keyboardShortcut("z", modifiers: [.command, .shift])
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
struct ShowGrokErrorKey: EnvironmentKey {
|
|
static let defaultValue: Binding<Bool> = .constant(false)
|
|
}
|
|
|
|
struct GrokErrorMessageKey: EnvironmentKey {
|
|
static let defaultValue: Binding<String> = .constant("")
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var showGrokError: Binding<Bool> {
|
|
get { self[ShowGrokErrorKey.self] }
|
|
set { self[ShowGrokErrorKey.self] = newValue }
|
|
}
|
|
|
|
var grokErrorMessage: Binding<String> {
|
|
get { self[GrokErrorMessageKey.self] }
|
|
set { self[GrokErrorMessageKey.self] = newValue }
|
|
}
|
|
}
|