import SwiftUI import GhosttyKit extension Ghostty { /// Maps to a `ghostty_config_t` and the various operations on that. class Config: ObservableObject { // The underlying C pointer to the Ghostty config structure. This // should never be accessed directly. Any operations on this should // be called from the functions on this or another class. private(set) var config: ghostty_config_t? { didSet { // Free the old value whenever we change guard let old = oldValue else { return } ghostty_config_free(old) } } /// True if the configuration is loaded var loaded: Bool { config != nil } /// Return the errors found while loading the configuration. var errors: [String] { guard let cfg = self.config else { return [] } var diags: [String] = [] let diagsCount = ghostty_config_diagnostics_count(cfg) for i in 0.. ghostty_config_t? { // Initialize the global configuration. guard let cfg = ghostty_config_new() else { logger.critical("ghostty_config_new failed") return nil } // Load our configuration from files, CLI args, and then any referenced files. // We only do this on macOS because other Apple platforms do not have the // same filesystem concept. #if os(macOS) if let path { ghostty_config_load_file(cfg, path) } else { ghostty_config_load_default_files(cfg) } // We only load CLI args when not running in Xcode because in Xcode we // pass some special parameters to control the debugger. if !isRunningInXcode() { ghostty_config_load_cli_args(cfg) } ghostty_config_load_recursive_files(cfg) #endif // TODO: we'd probably do some config loading here... for now we'd // have to do this synchronously. When we support config updating we can do // this async and update later. if finalize { // Finalize will make our defaults available. ghostty_config_finalize(cfg) } // Log any configuration errors. These will be automatically shown in a // pop-up window too. let diagsCount = ghostty_config_diagnostics_count(cfg) if diagsCount > 0 { logger.warning("config error: \(diagsCount) configuration errors on reload") var diags: [String] = [] for i in 0.. KeyboardShortcut? { guard let cfg = self.config else { return nil } let trigger = ghostty_config_trigger(cfg, action, UInt(action.lengthOfBytes(using: .utf8))) return Ghostty.keyboardShortcut(for: trigger) } #endif // MARK: - Configuration Values /// For all of the configuration values below, see the associated Ghostty documentation for /// details on what each means. We only add documentation if there is a strange conversion /// due to the embedded library and Swift. var bellFeatures: BellFeatures { guard let config = self.config else { return .init() } var v: CUnsignedInt = 0 let key = "bell-features" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .init() } return .init(rawValue: v) } var notifyOnCommandFinish: NotifyOnCommandFinish { guard let config = self.config else { return .never } var v: UnsafePointer? let key = "notify-on-command-finish" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never } guard let ptr = v else { return .never } return NotifyOnCommandFinish(rawValue: String(cString: ptr)) ?? .never } var notifyOnCommandFinishAction: NotifyOnCommandFinishAction { let defaultValue = NotifyOnCommandFinishAction.bell guard let config = self.config else { return defaultValue } var v: CUnsignedInt = 0 let key = "notify-on-command-finish-action" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } return .init(rawValue: v) } var notifyOnCommandFinishAfter: Duration { guard let config = self.config else { return .seconds(5) } var v: UInt = 0 let key = "notify-on-command-finish-after" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return .milliseconds(v) } var splitPreserveZoom: SplitPreserveZoom { guard let config = self.config else { return .init() } var v: CUnsignedInt = 0 let key = "split-preserve-zoom" guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() } return .init(rawValue: v) } var initialWindow: Bool { guard let config = self.config else { return true } var v = true let key = "initial-window" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var shouldQuitAfterLastWindowClosed: Bool { guard let config = self.config else { return true } var v = false let key = "quit-after-last-window-closed" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var title: String? { guard let config = self.config else { return nil } var v: UnsafePointer? let key = "title" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } return String(cString: ptr) } var windowSaveState: String { guard let config = self.config else { return "" } var v: UnsafePointer? let key = "window-save-state" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } return String(cString: ptr) } var windowPositionX: Int16? { guard let config = self.config else { return nil } var v: Int16 = 0 let key = "window-position-x" return ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) ? v : nil } var windowPositionY: Int16? { guard let config = self.config else { return nil } var v: Int16 = 0 let key = "window-position-y" return ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) ? v : nil } var windowNewTabPosition: String { guard let config = self.config else { return "" } var v: UnsafePointer? let key = "window-new-tab-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } return String(cString: ptr) } var windowDecorations: Bool { let defaultValue = true guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "window-decoration" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue } var windowTheme: String? { guard let config = self.config else { return nil } var v: UnsafePointer? let key = "window-theme" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } return String(cString: ptr) } var windowStepResize: Bool { guard let config = self.config else { return true } var v = false let key = "window-step-resize" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } /// Returns the fullscreen mode if fullscreen is enabled, or nil if disabled. /// This parses the `fullscreen` enum config which supports both /// native and non-native fullscreen modes. #if canImport(AppKit) var windowFullscreen: FullscreenMode? { guard let config = self.config else { return nil } var v: UnsafePointer? let key = "fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } let str = String(cString: ptr) return switch str { case "false": nil case "true": .native case "non-native": .nonNative case "non-native-visible-menu": .nonNativeVisibleMenu case "non-native-padded-notch": .nonNativePaddedNotch default: nil } } #else var windowFullscreen: Bool { guard let config = self.config else { return false } var v: UnsafePointer? let key = "fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return false } guard let ptr = v else { return false } let str = String(cString: ptr) return str != "false" } #endif /// Returns the fullscreen mode for toggle actions (keybindings). /// This is controlled by `macos-non-native-fullscreen` config. #if canImport(AppKit) var windowFullscreenMode: FullscreenMode { let defaultValue: FullscreenMode = .native guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-non-native-fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return switch str { case "false": .native case "true": .nonNative case "visible-menu": .nonNativeVisibleMenu case "padded-notch": .nonNativePaddedNotch default: defaultValue } } #endif var windowTitleFontFamily: String? { guard let config = self.config else { return nil } var v: UnsafePointer? let key = "window-title-font-family" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } return String(cString: ptr) } var macosWindowButtons: MacOSWindowButtons { let defaultValue = MacOSWindowButtons.visible guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-window-buttons" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return MacOSWindowButtons(rawValue: str) ?? defaultValue } var macosTitlebarStyle: String { let defaultValue = "transparent" guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-titlebar-style" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } return String(cString: ptr) } var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon { let defaultValue = MacOSTitlebarProxyIcon.visible guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-titlebar-proxy-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return MacOSTitlebarProxyIcon(rawValue: str) ?? defaultValue } var macosDockDropBehavior: MacDockDropBehavior { let defaultValue = MacDockDropBehavior.new_tab guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-dock-drop-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return MacDockDropBehavior(rawValue: str) ?? defaultValue } var macosWindowShadow: Bool { guard let config = self.config else { return false } var v = false let key = "macos-window-shadow" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var macosIcon: MacOSIcon { let defaultValue = MacOSIcon.official guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return MacOSIcon(rawValue: str) ?? defaultValue } var macosCustomIcon: String { #if os(macOS) let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-custom-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } guard let path = NSString(utf8String: ptr) else { return defaultValue } return path.expandingTildeInPath #else return "" #endif } var macosIconFrame: MacOSIconFrame { let defaultValue = MacOSIconFrame.aluminum guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-icon-frame" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return MacOSIconFrame(rawValue: str) ?? defaultValue } var macosIconGhostColor: OSColor? { guard let config = self.config else { return nil } var v: ghostty_config_color_s = .init() let key = "macos-icon-ghost-color" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } return .init(ghostty: v) } var macosIconScreenColor: [OSColor]? { guard let config = self.config else { return nil } var v: ghostty_config_color_list_s = .init() let key = "macos-icon-screen-color" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard v.len > 0 else { return nil } let buffer = UnsafeBufferPointer(start: v.colors, count: v.len) return buffer.map { .init(ghostty: $0) } } var macosHidden: MacHidden { guard let config = self.config else { return .never } var v: UnsafePointer? let key = "macos-hidden" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never } guard let ptr = v else { return .never } let str = String(cString: ptr) return MacHidden(rawValue: str) ?? .never } var focusFollowsMouse: Bool { guard let config = self.config else { return false } var v = false let key = "focus-follows-mouse" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var backgroundColor: Color { var color: ghostty_config_color_s = .init() let bg_key = "background" if !ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) { #if os(macOS) return Color(NSColor.windowBackgroundColor) #elseif os(iOS) return Color(UIColor.systemBackground) #else #error("unsupported") #endif } return .init( red: Double(color.r) / 255, green: Double(color.g) / 255, blue: Double(color.b) / 255 ) } var backgroundOpacity: Double { guard let config = self.config else { return 1 } var v: Double = 1 let key = "background-opacity" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var backgroundBlur: BackgroundBlur { guard let config = self.config else { return .disabled } var v: Int16 = 0 let key = "background-blur" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return BackgroundBlur(fromCValue: v) } var unfocusedSplitOpacity: Double { guard let config = self.config else { return 1 } var opacity: Double = 0.85 let key = "unfocused-split-opacity" _ = ghostty_config_get(config, &opacity, key, UInt(key.lengthOfBytes(using: .utf8))) return 1 - opacity } var unfocusedSplitFill: Color { guard let config = self.config else { return .white } var color: ghostty_config_color_s = .init() let key = "unfocused-split-fill" if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { let bg_key = "background" _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) } return .init( red: Double(color.r) / 255, green: Double(color.g) / 255, blue: Double(color.b) / 255 ) } var splitDividerColor: Color { let backgroundColor = OSColor(backgroundColor) let isLightBackground = backgroundColor.isLightColor let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4) guard let config = self.config else { return Color(newColor) } var color: ghostty_config_color_s = .init() let key = "split-divider-color" if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { return Color(newColor) } return .init( red: Double(color.r) / 255, green: Double(color.g) / 255, blue: Double(color.b) / 255 ) } #if canImport(AppKit) var quickTerminalPosition: QuickTerminalPosition { guard let config = self.config else { return .top } var v: UnsafePointer? let key = "quick-terminal-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .top } guard let ptr = v else { return .top } let str = String(cString: ptr) return QuickTerminalPosition(rawValue: str) ?? .top } var quickTerminalScreen: QuickTerminalScreen { guard let config = self.config else { return .main } var v: UnsafePointer? let key = "quick-terminal-screen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .main } guard let ptr = v else { return .main } let str = String(cString: ptr) return QuickTerminalScreen(fromGhosttyConfig: str) ?? .main } var quickTerminalAnimationDuration: Double { guard let config = self.config else { return 0.2 } var v: Double = 0.2 let key = "quick-terminal-animation-duration" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var quickTerminalAutoHide: Bool { guard let config = self.config else { return true } var v = true let key = "quick-terminal-autohide" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior { guard let config = self.config else { return .move } var v: UnsafePointer? let key = "quick-terminal-space-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .move } guard let ptr = v else { return .move } let str = String(cString: ptr) return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move } var quickTerminalSize: QuickTerminalSize { guard let config = self.config else { return QuickTerminalSize() } var v = ghostty_config_quick_terminal_size_s() let key = "quick-terminal-size" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return QuickTerminalSize() } return QuickTerminalSize(from: v) } #endif var resizeOverlay: ResizeOverlay { guard let config = self.config else { return .after_first } var v: UnsafePointer? let key = "resize-overlay" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .after_first } guard let ptr = v else { return .after_first } let str = String(cString: ptr) return ResizeOverlay(rawValue: str) ?? .after_first } var resizeOverlayPosition: ResizeOverlayPosition { let defaultValue = ResizeOverlayPosition.center guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "resize-overlay-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return ResizeOverlayPosition(rawValue: str) ?? defaultValue } var resizeOverlayDuration: UInt { guard let config = self.config else { return 1000 } var v: UInt = 0 let key = "resize-overlay-duration" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var undoTimeout: Duration { guard let config = self.config else { return .seconds(5) } var v: UInt = 0 let key = "undo-timeout" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return .milliseconds(v) } var autoUpdate: AutoUpdate? { guard let config = self.config else { return nil } var v: UnsafePointer? let key = "auto-update" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } let str = String(cString: ptr) return AutoUpdate(rawValue: str) } var autoUpdateChannel: AutoUpdateChannel { let defaultValue = AutoUpdateChannel.stable guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "auto-update-channel" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return AutoUpdateChannel(rawValue: str) ?? defaultValue } var autoSecureInput: Bool { guard let config = self.config else { return true } var v = false let key = "macos-auto-secure-input" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var secureInputIndication: Bool { guard let config = self.config else { return true } var v = false let key = "macos-secure-input-indication" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var maximize: Bool { guard let config = self.config else { return true } var v = false let key = "maximize" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var macosShortcuts: MacShortcuts { let defaultValue = MacShortcuts.ask guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-shortcuts" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return MacShortcuts(rawValue: str) ?? defaultValue } var scrollbar: Scrollbar { let defaultValue = Scrollbar.system guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "scrollbar" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } let str = String(cString: ptr) return Scrollbar(rawValue: str) ?? defaultValue } var commandPaletteEntries: [Ghostty.Command] { guard let config = self.config else { return [] } var v: ghostty_config_command_list_s = .init() let key = "command-palette-entry" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return [] } guard v.len > 0 else { return [] } let buffer = UnsafeBufferPointer(start: v.commands, count: v.len) return buffer.map { Ghostty.Command(cValue: $0) } } } } // MARK: Configuration Enums extension Ghostty.Config { enum AutoUpdate: String { case off case check case download } /// Background blur configuration that maps from the C API values. /// Positive values represent blur radius, special negative values /// represent macOS-specific glass effects. enum BackgroundBlur: Equatable { case disabled case radius(Int) case macosGlassRegular case macosGlassClear init(fromCValue value: Int16) { switch value { case 0: self = .disabled case -1: if #available(macOS 26.0, *) { self = .macosGlassRegular } else { self = .disabled } case -2: if #available(macOS 26.0, *) { self = .macosGlassClear } else { self = .disabled } default: self = .radius(Int(value)) } } var isEnabled: Bool { switch self { case .disabled: return false default: return true } } /// Returns true if this is a macOS glass style (regular or clear). var isGlassStyle: Bool { switch self { case .macosGlassRegular, .macosGlassClear: return true default: return false } } /// Returns the blur radius if applicable, nil for glass effects. var radius: Int? { switch self { case .disabled: return nil case .radius(let r): return r case .macosGlassRegular, .macosGlassClear: return nil } } } struct BellFeatures: OptionSet { let rawValue: CUnsignedInt static let system = BellFeatures(rawValue: 1 << 0) static let audio = BellFeatures(rawValue: 1 << 1) static let attention = BellFeatures(rawValue: 1 << 2) static let title = BellFeatures(rawValue: 1 << 3) static let border = BellFeatures(rawValue: 1 << 4) } struct SplitPreserveZoom: OptionSet { let rawValue: CUnsignedInt static let navigation = SplitPreserveZoom(rawValue: 1 << 0) } enum MacDockDropBehavior: String { case new_tab = "new-tab" case new_window = "new-window" } enum MacHidden: String { case never case always } enum MacShortcuts: String { case allow case deny case ask } enum Scrollbar: String { case system case never } enum ResizeOverlay: String { case always case never case after_first = "after-first" } enum ResizeOverlayPosition: String { case center case top_left = "top-left" case top_center = "top-center" case top_right = "top-right" case bottom_left = "bottom-left" case bottom_center = "bottom-center" case bottom_right = "bottom-right" func top() -> Bool { switch self { case .top_left, .top_center, .top_right: return true default: return false } } func bottom() -> Bool { switch self { case .bottom_left, .bottom_center, .bottom_right: return true default: return false } } func left() -> Bool { switch self { case .top_left, .bottom_left: return true default: return false } } func right() -> Bool { switch self { case .top_right, .bottom_right: return true default: return false } } } enum WindowDecoration: String { case none case client case server case auto func enabled() -> Bool { switch self { case .client, .server, .auto: return true case .none: return false } } } enum NotifyOnCommandFinish: String { case never case unfocused case always } struct NotifyOnCommandFinishAction: OptionSet { let rawValue: CUnsignedInt static let bell = NotifyOnCommandFinishAction(rawValue: 1 << 0) static let notify = NotifyOnCommandFinishAction(rawValue: 1 << 1) } }