mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: Ghostty.Config to store all config-related operations
This commit is contained in:
@ -11,6 +11,8 @@
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; };
|
||||
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 */; };
|
||||
A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -237,6 +240,7 @@
|
||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
|
||||
@ -443,6 +447,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
@ -483,6 +488,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
||||
);
|
||||
|
@ -143,7 +143,7 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return ghostty.shouldQuitAfterLastWindowClosed
|
||||
return ghostty.config.shouldQuitAfterLastWindowClosed
|
||||
}
|
||||
|
||||
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.
|
||||
private func syncMenuShortcuts() {
|
||||
guard ghostty.config != nil else { return }
|
||||
guard ghostty.readiness == .ready else { return }
|
||||
|
||||
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
|
||||
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
|
||||
/// action string used for the Ghostty configuration.
|
||||
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) {
|
||||
guard let cfg = ghostty.config else { return }
|
||||
guard let menu = menuItem else { return }
|
||||
|
||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
|
||||
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else {
|
||||
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
|
||||
// No shortcut, clear the menu item
|
||||
menu.keyEquivalent = ""
|
||||
menu.keyEquivalentModifierMask = []
|
||||
return
|
||||
}
|
||||
|
||||
menu.keyEquivalent = equiv
|
||||
menu.keyEquivalentModifierMask = Ghostty.eventModifierFlags(mods: trigger.mods)
|
||||
menu.keyEquivalent = equiv.key
|
||||
menu.keyEquivalentModifierMask = equiv.modifiers
|
||||
}
|
||||
|
||||
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
|
||||
// configuration. This is the only way to carefully control whether macOS invokes the
|
||||
// state restoration system.
|
||||
switch (ghostty.windowSaveState) {
|
||||
switch (ghostty.config.windowSaveState) {
|
||||
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
|
||||
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
|
||||
case "default": fallthrough
|
||||
@ -373,7 +370,7 @@ class AppDelegate: NSObject,
|
||||
|
||||
// If we have configuration errors, we need to show them.
|
||||
let c = ConfigurationErrorsController.sharedInstance
|
||||
c.errors = state.configErrors()
|
||||
c.errors = state.config.errors
|
||||
if (c.errors.count > 0) {
|
||||
if (c.window == nil || !c.window!.isVisible) {
|
||||
c.showWindow(self)
|
||||
@ -383,7 +380,7 @@ class AppDelegate: NSObject,
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
private func syncAppearance() {
|
||||
guard let theme = ghostty.windowTheme else { return }
|
||||
guard let theme = ghostty.config.windowTheme else { return }
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
let appearance = NSAppearance(named: .darkAqua)
|
||||
|
@ -101,7 +101,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
tabListenForFrame = false
|
||||
|
||||
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,
|
||||
// otherwise the accessory view doesn't matter.
|
||||
@ -109,8 +108,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
|
||||
for (index, window) in windows.enumerated().prefix(9) {
|
||||
let action = "goto_tab:\(index + 1)"
|
||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
|
||||
guard let equiv = Ghostty.keyEquivalentLabel(key: trigger.key, mods: trigger.mods) else {
|
||||
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -157,13 +155,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
window.identifier = .init(String(describing: TerminalWindowRestoration.self))
|
||||
|
||||
// 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
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (ghostty.windowColorspace) {
|
||||
switch (ghostty.config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
@ -462,7 +460,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
}
|
||||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
guard ghostty.windowStepResize else { return }
|
||||
guard ghostty.config.windowStepResize else { return }
|
||||
self.window?.contentResizeIncrements = to
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ class TerminalManager {
|
||||
let window = c.window!
|
||||
|
||||
// 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,
|
||||
// then this window also becomes fullscreen.
|
||||
@ -130,7 +130,7 @@ class TerminalManager {
|
||||
controller.showWindow(self)
|
||||
|
||||
// Add the window to the tab group and show it.
|
||||
switch ghostty.windowNewTabPosition {
|
||||
switch ghostty.config.windowNewTabPosition {
|
||||
case "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.
|
||||
|
@ -66,7 +66,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||
|
||||
// If our configuration is "never" then we never restore the state
|
||||
// no matter what.
|
||||
if (appDelegate.terminalManager.ghostty.windowSaveState == "never") {
|
||||
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
|
@ -36,16 +36,10 @@ extension Ghostty {
|
||||
/// Optional delegate
|
||||
weak var delegate: GhosttyAppStateDelegate?
|
||||
|
||||
/// The ghostty global configuration. This should only be changed when it is definitely
|
||||
/// safe to change. It is definite safe to change only when the embedded app runtime
|
||||
/// in Ghostty says so (usually, only in a reload configuration callback).
|
||||
@Published var config: ghostty_config_t? = nil {
|
||||
didSet {
|
||||
// Free the old value whenever we change
|
||||
guard let old = oldValue else { return }
|
||||
ghostty_config_free(old)
|
||||
}
|
||||
}
|
||||
/// The global app configuration. This defines the app level configuration plus any behavior
|
||||
/// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
|
||||
/// configuration (i.e. font size) from the previously focused window. This would override this.
|
||||
private(set) var config: Config
|
||||
|
||||
/// 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...
|
||||
@ -56,45 +50,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
var needsConfirmQuit: Bool {
|
||||
guard let app = app else { return false }
|
||||
@ -113,66 +68,19 @@ extension Ghostty {
|
||||
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() {
|
||||
// Initialize ghostty global state. This happens once per process.
|
||||
guard ghostty_init() == GHOSTTY_SUCCESS else {
|
||||
AppDelegate.logger.critical("ghostty_init failed")
|
||||
if ghostty_init() != GHOSTTY_SUCCESS {
|
||||
AppDelegate.logger.critical("ghostty_init failed, weird things may happen")
|
||||
readiness = .error
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the global configuration.
|
||||
guard let cfg = Self.loadConfig() else {
|
||||
self.config = Config()
|
||||
if self.config.config == nil {
|
||||
readiness = .error
|
||||
return
|
||||
}
|
||||
self.config = cfg;
|
||||
|
||||
// Create our "runtime" config. The "runtime" is the configuration that ghostty
|
||||
// uses to interface with the application runtime environment.
|
||||
@ -210,7 +118,7 @@ extension Ghostty {
|
||||
)
|
||||
|
||||
// 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")
|
||||
readiness = .error
|
||||
return
|
||||
@ -230,7 +138,6 @@ extension Ghostty {
|
||||
deinit {
|
||||
// This will force the didSet callbacks to run which free.
|
||||
self.app = nil
|
||||
self.config = nil
|
||||
|
||||
// Remove our observer
|
||||
NotificationCenter.default.removeObserver(
|
||||
@ -239,58 +146,6 @@ extension Ghostty {
|
||||
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() {
|
||||
guard let app = self.app else { return }
|
||||
|
||||
@ -534,7 +389,8 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
@ -549,7 +405,7 @@ extension Ghostty {
|
||||
delegate.configDidReload(state)
|
||||
}
|
||||
|
||||
return newConfig
|
||||
return newConfig.config
|
||||
}
|
||||
|
||||
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
|
||||
@ -662,7 +518,7 @@ extension Ghostty {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
guard let appState = self.appState(fromView: surface) else { return }
|
||||
guard appState.windowDecorations else {
|
||||
guard appState.config.windowDecorations else {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Tabs are disabled"
|
||||
alert.informativeText = "Enable window decorations to use tabs"
|
||||
|
238
macos/Sources/Ghostty/Ghostty.Config.swift
Normal file
238
macos/Sources/Ghostty/Ghostty.Config.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -7,21 +7,6 @@ extension Ghostty {
|
||||
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.
|
||||
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
|
||||
var flags = NSEvent.ModifierFlags(rawValue: 0);
|
||||
|
@ -55,34 +55,6 @@ extension Ghostty {
|
||||
// it is both individually focused and the containing window is key.
|
||||
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 {
|
||||
ZStack {
|
||||
// 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
|
||||
// focus.
|
||||
if (isSplit && !surfaceFocus) {
|
||||
let overlayOpacity = unfocusedOpacity;
|
||||
let overlayOpacity = ghostty.config.unfocusedSplitOpacity;
|
||||
if (overlayOpacity > 0) {
|
||||
Rectangle()
|
||||
.fill(unfocusedFill)
|
||||
.fill(ghostty.config.unfocusedSplitFill)
|
||||
.allowsHitTesting(false)
|
||||
.opacity(overlayOpacity)
|
||||
}
|
||||
|
Reference in New Issue
Block a user