diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 1a9d21e83..bf290f623 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; }; + A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; }; + A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; }; A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; }; A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; }; A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; @@ -55,6 +58,9 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = ""; }; + A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = ""; }; + A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = ""; }; A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = ""; }; A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; @@ -112,6 +118,9 @@ isa = PBXGroup; children = ( A59444F629A2ED5200725BBA /* SettingsView.swift */, + A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */, + A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */, + A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */, ); path = Settings; sourceTree = ""; @@ -249,6 +258,7 @@ files = ( A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, + A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, ); @@ -265,6 +275,7 @@ 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, + A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */, @@ -272,6 +283,7 @@ A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */, A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, + A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index 4ff6dcb51..7a5b41b30 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -60,12 +60,12 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp "ApplePressAndHoldEnabled": false, ]) - // Sync our menu shortcuts with our Ghostty config - syncMenuShortcuts() - // Let's launch our first window. // TODO: we should detect if we restored windows and if so not launch a new window. windowManager.addInitialWindow() + + // Initial config loading + configDidReload(ghostty) } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { @@ -180,7 +180,13 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp //MARK: - GhosttyAppStateDelegate func configDidReload(_ state: Ghostty.AppState) { + // Config could change keybindings, so update our menu syncMenuShortcuts() + + // If we have configuration errors, we need to show them. + let c = ConfigurationErrorsController.sharedInstance + c.model.errors = state.configErrors() + if (c.model.errors.count > 0) { c.showWindow(self) } } //MARK: - Dock Menu diff --git a/macos/Sources/Features/Settings/ConfigurationErrors.xib b/macos/Sources/Features/Settings/ConfigurationErrors.xib new file mode 100644 index 000000000..d3f94b14d --- /dev/null +++ b/macos/Sources/Features/Settings/ConfigurationErrors.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift new file mode 100644 index 000000000..fba594dde --- /dev/null +++ b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift @@ -0,0 +1,49 @@ +import Foundation +import Cocoa +import SwiftUI +import Combine + +class ConfigurationErrorsController: NSWindowController, NSWindowDelegate { + /// Singleton for the errors view. + static let sharedInstance = ConfigurationErrorsController() + + override var windowNibName: NSNib.Name? { "ConfigurationErrors" } + + /// The data model for this view. Update this directly and the associated view will be updated, too. + let model = ConfigurationErrorsView.Model() + + private var cancellable: AnyCancellable? + + //MARK: - NSWindowController + + override func windowWillLoad() { + shouldCascadeWindows = false + + if let c = cancellable { c.cancel() } + cancellable = model.objectWillChange.sink { + if (self.model.errors.count == 0) { + self.window?.close() + } + } + } + + override func windowDidLoad() { + guard let window = window else { return } + window.center() + window.level = .popUpMenu + window.contentView = NSHostingView(rootView: ConfigurationErrorsView(model: model)) + window.makeKeyAndOrderFront(self) + } + + //MARK: - NSWindowDelegate + + func windowWillClose(_ notification: Notification) { + guard let window = window else { return } + window.contentView = nil + + if let cancellable = cancellable { + cancellable.cancel() + self.cancellable = nil + } + } +} diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsView.swift b/macos/Sources/Features/Settings/ConfigurationErrorsView.swift new file mode 100644 index 000000000..5f15e6a78 --- /dev/null +++ b/macos/Sources/Features/Settings/ConfigurationErrorsView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct ConfigurationErrorsView: View { + class Model: ObservableObject { + @Published var errors: [String] = [] + } + + var model: Model + + var body: some View { + VStack { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.yellow) + .font(.system(size: 52)) + .padding() + .frame(alignment: .center) + + Text(""" + ^[\(model.errors.count) error was](inflect: true) found while loading the configuration. \ + Please review the errors below and reload your configuration. + """) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + + GeometryReader { geo in + ScrollView { + VStack { + ForEach(model.errors, id: \.self) { error in + Text(error) + .lineLimit(nil) + .font(.system(size: 12).monospaced()) + .textSelection(.enabled) + .padding(.all) + .frame(maxWidth: .infinity, alignment: .topLeading) + } + Spacer() + } + .frame(height: geo.size.height) + .background(Color.white) + } + } + } + .frame(minWidth: 480, maxWidth: 960, minHeight: 270) + } +} diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 40225b2b1..9870fbcd6 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -145,6 +145,21 @@ extension Ghostty { return cfg } + /// Returns the configuration errors (if any). + func configErrors() -> [String] { + guard let cfg = self.config else { return [] } + + var errors: [String] = []; + let errCount = ghostty_config_errors_count(cfg) + for i in 0..