macos: Ghostty.Config to store all config-related operations

This commit is contained in:
Mitchell Hashimoto
2024-01-14 15:52:23 -08:00
parent 33b93799b9
commit eba3d5414d
9 changed files with 274 additions and 222 deletions

View File

@ -11,6 +11,8 @@
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; }; 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; };
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; }; A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; };
A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; };
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; };
@ -65,6 +67,7 @@
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = "<group>"; }; 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = "<group>"; };
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; }; A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
@ -237,6 +240,7 @@
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
@ -443,6 +447,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
@ -483,6 +488,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
); );

View File

@ -143,7 +143,7 @@ class AppDelegate: NSObject,
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return ghostty.shouldQuitAfterLastWindowClosed return ghostty.config.shouldQuitAfterLastWindowClosed
} }
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
@ -242,7 +242,7 @@ class AppDelegate: NSObject,
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
private func syncMenuShortcuts() { private func syncMenuShortcuts() {
guard ghostty.config != nil else { return } guard ghostty.readiness == .ready else { return }
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig) syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig) syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
@ -286,19 +286,16 @@ class AppDelegate: NSObject,
/// Syncs a single menu shortcut for the given action. The action string is the same /// Syncs a single menu shortcut for the given action. The action string is the same
/// action string used for the Ghostty configuration. /// action string used for the Ghostty configuration.
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) { private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) {
guard let cfg = ghostty.config else { return }
guard let menu = menuItem else { return } guard let menu = menuItem else { return }
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else {
// No shortcut, clear the menu item // No shortcut, clear the menu item
menu.keyEquivalent = "" menu.keyEquivalent = ""
menu.keyEquivalentModifierMask = [] menu.keyEquivalentModifierMask = []
return return
} }
menu.keyEquivalent = equiv menu.keyEquivalent = equiv.key
menu.keyEquivalentModifierMask = Ghostty.eventModifierFlags(mods: trigger.mods) menu.keyEquivalentModifierMask = equiv.modifiers
} }
private func focusedSurface() -> ghostty_surface_t? { private func focusedSurface() -> ghostty_surface_t? {
@ -357,7 +354,7 @@ class AppDelegate: NSObject,
// Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows
// configuration. This is the only way to carefully control whether macOS invokes the // configuration. This is the only way to carefully control whether macOS invokes the
// state restoration system. // state restoration system.
switch (ghostty.windowSaveState) { switch (ghostty.config.windowSaveState) {
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
case "default": fallthrough case "default": fallthrough
@ -373,7 +370,7 @@ class AppDelegate: NSObject,
// If we have configuration errors, we need to show them. // If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance let c = ConfigurationErrorsController.sharedInstance
c.errors = state.configErrors() c.errors = state.config.errors
if (c.errors.count > 0) { if (c.errors.count > 0) {
if (c.window == nil || !c.window!.isVisible) { if (c.window == nil || !c.window!.isVisible) {
c.showWindow(self) c.showWindow(self)
@ -383,7 +380,7 @@ class AppDelegate: NSObject,
/// Sync the appearance of our app with the theme specified in the config. /// Sync the appearance of our app with the theme specified in the config.
private func syncAppearance() { private func syncAppearance() {
guard let theme = ghostty.windowTheme else { return } guard let theme = ghostty.config.windowTheme else { return }
switch (theme) { switch (theme) {
case "dark": case "dark":
let appearance = NSAppearance(named: .darkAqua) let appearance = NSAppearance(named: .darkAqua)

View File

@ -101,7 +101,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
tabListenForFrame = false tabListenForFrame = false
guard let windows = self.window?.tabbedWindows else { return } guard let windows = self.window?.tabbedWindows else { return }
guard let cfg = ghostty.config else { return }
// We only listen for frame changes if we have more than 1 window, // We only listen for frame changes if we have more than 1 window,
// otherwise the accessory view doesn't matter. // otherwise the accessory view doesn't matter.
@ -109,8 +108,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
for (index, window) in windows.enumerated().prefix(9) { for (index, window) in windows.enumerated().prefix(9) {
let action = "goto_tab:\(index + 1)" let action = "goto_tab:\(index + 1)"
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) guard let equiv = ghostty.config.keyEquivalent(for: action) else {
guard let equiv = Ghostty.keyEquivalentLabel(key: trigger.key, mods: trigger.mods) else {
continue continue
} }
@ -157,13 +155,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
window.identifier = .init(String(describing: TerminalWindowRestoration.self)) window.identifier = .init(String(describing: TerminalWindowRestoration.self))
// If window decorations are disabled, remove our title // If window decorations are disabled, remove our title
if (!ghostty.windowDecorations) { window.styleMask.remove(.titled) } if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) }
// Terminals typically operate in sRGB color space and macOS defaults // Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources // to "native" which is typically P3. There is a lot more resources
// covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376 // covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden. // Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.windowColorspace) { switch (ghostty.config.windowColorspace) {
case "display-p3": case "display-p3":
window.colorSpace = .displayP3 window.colorSpace = .displayP3
case "srgb": case "srgb":
@ -462,7 +460,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} }
func cellSizeDidChange(to: NSSize) { func cellSizeDidChange(to: NSSize) {
guard ghostty.windowStepResize else { return } guard ghostty.config.windowStepResize else { return }
self.window?.contentResizeIncrements = to self.window?.contentResizeIncrements = to
} }

View File

@ -66,7 +66,7 @@ class TerminalManager {
let window = c.window! let window = c.window!
// We want to go fullscreen if we're configured for new windows to go fullscreen // We want to go fullscreen if we're configured for new windows to go fullscreen
var toggleFullScreen = ghostty.windowFullscreen var toggleFullScreen = ghostty.config.windowFullscreen
// If the previous focused window prior to creating this window is fullscreen, // If the previous focused window prior to creating this window is fullscreen,
// then this window also becomes fullscreen. // then this window also becomes fullscreen.
@ -130,7 +130,7 @@ class TerminalManager {
controller.showWindow(self) controller.showWindow(self)
// Add the window to the tab group and show it. // Add the window to the tab group and show it.
switch ghostty.windowNewTabPosition { switch ghostty.config.windowNewTabPosition {
case "end": case "end":
// If we already have a tab group and we want the new tab to open at the end, // If we already have a tab group and we want the new tab to open at the end,
// then we use the last window in the tab group as the parent. // then we use the last window in the tab group as the parent.

View File

@ -66,7 +66,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
// If our configuration is "never" then we never restore the state // If our configuration is "never" then we never restore the state
// no matter what. // no matter what.
if (appDelegate.terminalManager.ghostty.windowSaveState == "never") { if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
completionHandler(nil, nil) completionHandler(nil, nil)
return return
} }

View File

@ -36,16 +36,10 @@ extension Ghostty {
/// Optional delegate /// Optional delegate
weak var delegate: GhosttyAppStateDelegate? weak var delegate: GhosttyAppStateDelegate?
/// The ghostty global configuration. This should only be changed when it is definitely /// The global app configuration. This defines the app level configuration plus any behavior
/// safe to change. It is definite safe to change only when the embedded app runtime /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
/// in Ghostty says so (usually, only in a reload configuration callback). /// configuration (i.e. font size) from the previously focused window. This would override this.
@Published var config: ghostty_config_t? = nil { private(set) var config: Config
didSet {
// Free the old value whenever we change
guard let old = oldValue else { return }
ghostty_config_free(old)
}
}
/// The ghostty app instance. We only have one of these for the entire app, although I guess /// The ghostty app instance. We only have one of these for the entire app, although I guess
/// in theory you can have multiple... I don't know why you would... /// in theory you can have multiple... I don't know why you would...
@ -55,45 +49,6 @@ extension Ghostty {
ghostty_app_free(old) ghostty_app_free(old)
} }
} }
/// True if we should quit when the last window is closed.
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.count))
return v
}
/// window-colorspace
var windowColorspace: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
/// window-save-state
var windowSaveState: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-save-state"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
/// window-new-tab-position
var windowNewTabPosition: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-new-tab-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
/// True if we need to confirm before quitting. /// True if we need to confirm before quitting.
var needsConfirmQuit: Bool { var needsConfirmQuit: Bool {
@ -113,66 +68,19 @@ extension Ghostty {
return Info(mode: raw.build_mode, version: String(version)) return Info(mode: raw.build_mode, version: String(version))
} }
/// True if we want to render window decorations
var windowDecorations: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "window-decoration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
/// The window theme as a string.
var windowTheme: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "window-theme"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard let ptr = v else { return nil }
return String(cString: ptr)
}
/// Whether to resize windows in discrete steps or use "fluid" resizing
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.count))
return v
}
/// Whether to open new windows in fullscreen.
var windowFullscreen: Bool {
guard let config = self.config else { return true }
var v = false
let key = "fullscreen"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
/// The background opacity.
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.count))
return v;
}
init() { init() {
// Initialize ghostty global state. This happens once per process. // Initialize ghostty global state. This happens once per process.
guard ghostty_init() == GHOSTTY_SUCCESS else { if ghostty_init() != GHOSTTY_SUCCESS {
AppDelegate.logger.critical("ghostty_init failed") AppDelegate.logger.critical("ghostty_init failed, weird things may happen")
readiness = .error readiness = .error
return
} }
// Initialize the global configuration. // Initialize the global configuration.
guard let cfg = Self.loadConfig() else { self.config = Config()
if self.config.config == nil {
readiness = .error readiness = .error
return return
} }
self.config = cfg;
// Create our "runtime" config. The "runtime" is the configuration that ghostty // Create our "runtime" config. The "runtime" is the configuration that ghostty
// uses to interface with the application runtime environment. // uses to interface with the application runtime environment.
@ -210,7 +118,7 @@ extension Ghostty {
) )
// Create the ghostty app. // Create the ghostty app.
guard let app = ghostty_app_new(&runtime_cfg, cfg) else { guard let app = ghostty_app_new(&runtime_cfg, config.config) else {
AppDelegate.logger.critical("ghostty_app_new failed") AppDelegate.logger.critical("ghostty_app_new failed")
readiness = .error readiness = .error
return return
@ -230,7 +138,6 @@ extension Ghostty {
deinit { deinit {
// This will force the didSet callbacks to run which free. // This will force the didSet callbacks to run which free.
self.app = nil self.app = nil
self.config = nil
// Remove our observer // Remove our observer
NotificationCenter.default.removeObserver( NotificationCenter.default.removeObserver(
@ -239,58 +146,6 @@ extension Ghostty {
object: nil) object: nil)
} }
/// Initializes a new configuration and loads all the values.
static func loadConfig() -> ghostty_config_t? {
// Initialize the global configuration.
guard let cfg = ghostty_config_new() else {
AppDelegate.logger.critical("ghostty_config_new failed")
return nil
}
// Load our configuration files from the home directory.
ghostty_config_load_default_files(cfg);
ghostty_config_load_cli_args(cfg);
ghostty_config_load_recursive_files(cfg);
// 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.
// 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 errCount = ghostty_config_errors_count(cfg)
if errCount > 0 {
AppDelegate.logger.warning("config error: \(errCount) configuration errors on reload")
var errors: [String] = [];
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
AppDelegate.logger.warning("config error: \(message)")
}
}
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..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
}
return errors
}
func appTick() { func appTick() {
guard let app = self.app else { return } guard let app = self.app else { return }
@ -534,7 +389,8 @@ extension Ghostty {
} }
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
guard let newConfig = Self.loadConfig() else { let newConfig = Config()
guard newConfig.loaded else {
AppDelegate.logger.warning("failed to reload configuration") AppDelegate.logger.warning("failed to reload configuration")
return nil return nil
} }
@ -549,7 +405,7 @@ extension Ghostty {
delegate.configDidReload(state) delegate.configDidReload(state)
} }
return newConfig return newConfig.config
} }
static func wakeup(_ userdata: UnsafeMutableRawPointer?) { static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
@ -662,7 +518,7 @@ extension Ghostty {
let surface = self.surfaceUserdata(from: userdata) let surface = self.surfaceUserdata(from: userdata)
guard let appState = self.appState(fromView: surface) else { return } guard let appState = self.appState(fromView: surface) else { return }
guard appState.windowDecorations else { guard appState.config.windowDecorations else {
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Tabs are disabled" alert.messageText = "Tabs are disabled"
alert.informativeText = "Enable window decorations to use tabs" alert.informativeText = "Enable window decorations to use tabs"

View File

@ -0,0 +1,238 @@
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? = nil {
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 errors: [String] = [];
let errCount = ghostty_config_errors_count(cfg)
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
}
return errors
}
init() {
if let cfg = Self.loadConfig() {
self.config = cfg
}
}
deinit {
self.config = nil
}
/// Initializes a new configuration and loads all the values.
static private func loadConfig() -> 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)
ghostty_config_load_default_files(cfg);
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.
// 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 errCount = ghostty_config_errors_count(cfg)
if errCount > 0 {
logger.warning("config error: \(errCount) configuration errors on reload")
var errors: [String] = [];
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
logger.warning("config error: \(message)")
}
}
return cfg
}
#if os(macOS)
// MARK: - Keybindings
/// A convenience struct that has the key + modifiers for some keybinding.
struct KeyEquivalent: CustomStringConvertible {
let key: String
let modifiers: NSEvent.ModifierFlags
var description: String {
var key = self.key
// Note: the order below matters; it matches the ordering modifiers
// shown for macOS menu shortcut labels.
if modifiers.contains(.command) { key = "\(key)" }
if modifiers.contains(.shift) { key = "\(key)" }
if modifiers.contains(.option) { key = "\(key)" }
if modifiers.contains(.control) { key = "\(key)" }
return key
}
}
/// Return the key equivalent for the given action. The action is the name of the action
/// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty
/// configuration would be "quit" action.
///
/// Returns nil if there is no key equivalent for the given action.
func keyEquivalent(for action: String) -> KeyEquivalent? {
guard let cfg = self.config else { return nil }
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else { return nil }
return KeyEquivalent(
key: equiv,
modifiers: Ghostty.eventModifierFlags(mods: trigger.mods)
)
}
#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 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.count))
return v
}
var windowColorspace: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
var windowSaveState: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-save-state"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
var windowNewTabPosition: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-new-tab-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
var windowDecorations: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "window-decoration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
var windowTheme: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "window-theme"
guard ghostty_config_get(config, &v, key, UInt(key.count)) 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.count))
return v
}
var windowFullscreen: Bool {
guard let config = self.config else { return true }
var v = false
let key = "fullscreen"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
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.count))
return 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.count))
return 1 - opacity
}
var unfocusedSplitFill: Color {
guard let config = self.config else { return .white }
var rgb: UInt32 = 16777215 // white default
let key = "unfocused-split-fill"
if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) {
let bg_key = "background"
_ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count));
}
let red = Double(rgb & 0xff)
let green = Double((rgb >> 8) & 0xff)
let blue = Double((rgb >> 16) & 0xff)
return Color(
red: red / 255,
green: green / 255,
blue: blue / 255
)
}
}
}

View File

@ -7,21 +7,6 @@ extension Ghostty {
return Self.keyToEquivalent[key] return Self.keyToEquivalent[key]
} }
/// Returns the keyEquivalent label that includes the mods.
static func keyEquivalentLabel(key: ghostty_input_key_e, mods: ghostty_input_mods_e) -> String? {
guard var key = Self.keyEquivalent(key: key) else { return nil }
let flags = Self.eventModifierFlags(mods: mods)
// Note: the order below matters; it matches the ordering modifiers show for
// macOS menu shortcut labels.
if flags.contains(.command) { key = "\(key)" }
if flags.contains(.shift) { key = "\(key)" }
if flags.contains(.option) { key = "\(key)" }
if flags.contains(.control) { key = "\(key)" }
return key
}
/// Returns the event modifier flags set for the Ghostty mods enum. /// Returns the event modifier flags set for the Ghostty mods enum.
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
var flags = NSEvent.ModifierFlags(rawValue: 0); var flags = NSEvent.ModifierFlags(rawValue: 0);

View File

@ -55,34 +55,6 @@ extension Ghostty {
// it is both individually focused and the containing window is key. // it is both individually focused and the containing window is key.
private var hasFocus: Bool { surfaceFocus && windowFocus } private var hasFocus: Bool { surfaceFocus && windowFocus }
// The opacity of the rectangle when unfocused.
private var unfocusedOpacity: Double {
var opacity: Double = 0.85
let key = "unfocused-split-opacity"
_ = ghostty_config_get(ghostty.config, &opacity, key, UInt(key.count))
return 1 - opacity
}
// The color for the rectangle overlay when unfocused.
private var unfocusedFill: Color {
var rgb: UInt32 = 16777215 // white default
let key = "unfocused-split-fill"
if (!ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count))) {
let bg_key = "background"
_ = ghostty_config_get(ghostty.config, &rgb, bg_key, UInt(bg_key.count));
}
let red = Double(rgb & 0xff)
let green = Double((rgb >> 8) & 0xff)
let blue = Double((rgb >> 16) & 0xff)
return Color(
red: red / 255,
green: green / 255,
blue: blue / 255
)
}
var body: some View { var body: some View {
ZStack { ZStack {
// We use a GeometryReader to get the frame bounds so that our metal surface // We use a GeometryReader to get the frame bounds so that our metal surface
@ -175,10 +147,10 @@ extension Ghostty {
// because we want to keep our focused surface dark even if we don't have window // because we want to keep our focused surface dark even if we don't have window
// focus. // focus.
if (isSplit && !surfaceFocus) { if (isSplit && !surfaceFocus) {
let overlayOpacity = unfocusedOpacity; let overlayOpacity = ghostty.config.unfocusedSplitOpacity;
if (overlayOpacity > 0) { if (overlayOpacity > 0) {
Rectangle() Rectangle()
.fill(unfocusedFill) .fill(ghostty.config.unfocusedSplitFill)
.allowsHitTesting(false) .allowsHitTesting(false)
.opacity(overlayOpacity) .opacity(overlayOpacity)
} }