mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2762 from ghostty-org/config-update
apprt config change notification, macOS transparent titlebar works with theme change
This commit is contained in:
@ -532,6 +532,11 @@ typedef struct {
|
||||
uint8_t b;
|
||||
} ghostty_action_color_change_s;
|
||||
|
||||
// apprt.action.ConfigChange
|
||||
typedef struct {
|
||||
ghostty_config_t config;
|
||||
} ghostty_action_config_change_s;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
@ -568,6 +573,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_KEY_SEQUENCE,
|
||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
GHOSTTY_ACTION_CONFIG_CHANGE_CONDITIONAL_STATE,
|
||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
@ -592,6 +598,7 @@ typedef union {
|
||||
ghostty_action_secure_input_e secure_input;
|
||||
ghostty_action_key_sequence_s key_sequence;
|
||||
ghostty_action_color_change_s color_change;
|
||||
ghostty_action_config_change_s config_change;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
@ -639,6 +646,7 @@ ghostty_info_s ghostty_info(void);
|
||||
|
||||
ghostty_config_t ghostty_config_new();
|
||||
void ghostty_config_free(ghostty_config_t);
|
||||
ghostty_config_t ghostty_config_clone(ghostty_config_t);
|
||||
void ghostty_config_load_cli_args(ghostty_config_t);
|
||||
void ghostty_config_load_default_files(ghostty_config_t);
|
||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||
|
@ -69,6 +69,9 @@ class AppDelegate: NSObject,
|
||||
/// seconds since the process was launched.
|
||||
private var applicationLaunchTime: TimeInterval = 0
|
||||
|
||||
/// This is the current configuration from the Ghostty configuration that we need.
|
||||
private var derivedConfig: DerivedConfig = DerivedConfig()
|
||||
|
||||
/// The ghostty global state. Only one per process.
|
||||
let ghostty: Ghostty.App = Ghostty.App()
|
||||
|
||||
@ -138,7 +141,7 @@ class AppDelegate: NSObject,
|
||||
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
||||
|
||||
// Initial config loading
|
||||
configDidReload(ghostty)
|
||||
ghosttyConfigDidChange(config: ghostty.config)
|
||||
|
||||
// Start our update checker.
|
||||
updaterController.startUpdater()
|
||||
@ -162,6 +165,12 @@ class AppDelegate: NSObject,
|
||||
name: .quickTerminalDidChangeVisibility,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Configure user notifications
|
||||
let actions = [
|
||||
@ -188,13 +197,13 @@ class AppDelegate: NSObject,
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && ghostty.config.initialWindow {
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
}
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return ghostty.config.shouldQuitAfterLastWindowClosed
|
||||
return derivedConfig.shouldQuitAfterLastWindowClosed
|
||||
}
|
||||
|
||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
@ -300,52 +309,52 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
|
||||
private func syncMenuShortcuts() {
|
||||
private func syncMenuShortcuts(_ config: Ghostty.Config) {
|
||||
guard ghostty.readiness == .ready else { return }
|
||||
|
||||
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
|
||||
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
|
||||
syncMenuShortcut(action: "quit", menuItem: self.menuQuit)
|
||||
syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig)
|
||||
syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig)
|
||||
syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit)
|
||||
|
||||
syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitRight)
|
||||
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitDown)
|
||||
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
||||
syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
|
||||
|
||||
syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(action: "select_all", menuItem: self.menuSelectAll)
|
||||
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
|
||||
|
||||
syncMenuShortcut(action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
syncMenuShortcut(action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||
syncMenuShortcut(action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
||||
syncMenuShortcut(action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
||||
syncMenuShortcut(action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
||||
syncMenuShortcut(action: "equalize_splits", menuItem: self.menuEqualizeSplits)
|
||||
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(config, action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||
syncMenuShortcut(config, action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
||||
syncMenuShortcut(config, action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
||||
syncMenuShortcut(config, action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
||||
syncMenuShortcut(config, action: "equalize_splits", menuItem: self.menuEqualizeSplits)
|
||||
|
||||
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
syncMenuShortcut(action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||
syncMenuShortcut(action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
||||
syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||
syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
||||
|
||||
syncMenuShortcut(action: "toggle_secure_input", menuItem: self.menuSecureInput)
|
||||
syncMenuShortcut(config, action: "toggle_secure_input", menuItem: self.menuSecureInput)
|
||||
|
||||
// This menu item is NOT synced with the configuration because it disables macOS
|
||||
// global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue
|
||||
// to work but it won't be reflected in the menu item.
|
||||
//
|
||||
// syncMenuShortcut(action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen)
|
||||
// syncMenuShortcut(config, action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen)
|
||||
|
||||
// Dock menu
|
||||
reloadDockMenu()
|
||||
@ -353,9 +362,9 @@ 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?) {
|
||||
private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) {
|
||||
guard let menu = menuItem else { return }
|
||||
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
|
||||
guard let equiv = config.keyEquivalent(for: action) else {
|
||||
// No shortcut, clear the menu item
|
||||
menu.keyEquivalent = ""
|
||||
menu.keyEquivalentModifierMask = []
|
||||
@ -422,6 +431,98 @@ class AppDelegate: NSObject,
|
||||
self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off }
|
||||
}
|
||||
|
||||
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||
// We only care if the configuration is a global configuration, not a surface one.
|
||||
guard notification.object == nil else { return }
|
||||
|
||||
// Get our managed configuration object out
|
||||
guard let config = notification.userInfo?[
|
||||
Notification.Name.GhosttyConfigChangeKey
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
ghosttyConfigDidChange(config: config)
|
||||
}
|
||||
|
||||
private func ghosttyConfigDidChange(config: Ghostty.Config) {
|
||||
// Update the config we need to store
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
||||
// 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 (config.windowSaveState) {
|
||||
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
|
||||
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
|
||||
case "default": fallthrough
|
||||
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
|
||||
}
|
||||
|
||||
// Sync our auto-update settings
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
config.autoUpdate == .check || config.autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
config.autoUpdate == .download
|
||||
|
||||
// Config could change keybindings, so update everything that depends on that
|
||||
syncMenuShortcuts(config)
|
||||
terminalManager.relabelAllTabs()
|
||||
|
||||
// Config could change window appearance. We wrap this in an async queue because when
|
||||
// this is called as part of application launch it can deadlock with an internal
|
||||
// AppKit mutex on the appearance.
|
||||
DispatchQueue.main.async { self.syncAppearance(config: config) }
|
||||
|
||||
// If we have configuration errors, we need to show them.
|
||||
let c = ConfigurationErrorsController.sharedInstance
|
||||
c.errors = config.errors
|
||||
if (c.errors.count > 0) {
|
||||
if (c.window == nil || !c.window!.isVisible) {
|
||||
c.showWindow(self)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to handle our global event tap depending on if there are global
|
||||
// events that we care about in Ghostty.
|
||||
if (ghostty_app_has_global_keybinds(ghostty.app!)) {
|
||||
if (timeSinceLaunch > 5) {
|
||||
// If the process has been running for awhile we enable right away
|
||||
// because no windows are likely to pop up.
|
||||
GlobalEventTap.shared.enable()
|
||||
} else {
|
||||
// If the process just started, we wait a couple seconds to allow
|
||||
// the initial windows and so on to load so our permissions dialog
|
||||
// doesn't get buried.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
|
||||
GlobalEventTap.shared.enable()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GlobalEventTap.shared.disable()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
private func syncAppearance(config: Ghostty.Config) {
|
||||
guard let theme = config.windowTheme else { return }
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
let appearance = NSAppearance(named: .darkAqua)
|
||||
NSApplication.shared.appearance = appearance
|
||||
|
||||
case "light":
|
||||
let appearance = NSAppearance(named: .aqua)
|
||||
NSApplication.shared.appearance = appearance
|
||||
|
||||
case "auto":
|
||||
let color = OSColor(config.backgroundColor)
|
||||
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
|
||||
NSApplication.shared.appearance = appearance
|
||||
|
||||
default:
|
||||
NSApplication.shared.appearance = nil
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Restorable State
|
||||
|
||||
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
|
||||
@ -470,88 +571,6 @@ class AppDelegate: NSObject,
|
||||
return nil
|
||||
}
|
||||
|
||||
func configDidReload(_ state: Ghostty.App) {
|
||||
// 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.config.windowSaveState) {
|
||||
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
|
||||
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
|
||||
case "default": fallthrough
|
||||
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
|
||||
}
|
||||
|
||||
// Sync our auto-update settings
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
ghostty.config.autoUpdate == .check || ghostty.config.autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
ghostty.config.autoUpdate == .download
|
||||
|
||||
// Config could change keybindings, so update everything that depends on that
|
||||
syncMenuShortcuts()
|
||||
terminalManager.relabelAllTabs()
|
||||
|
||||
// Config could change window appearance. We wrap this in an async queue because when
|
||||
// this is called as part of application launch it can deadlock with an internal
|
||||
// AppKit mutex on the appearance.
|
||||
DispatchQueue.main.async { self.syncAppearance() }
|
||||
|
||||
// Update all of our windows
|
||||
terminalManager.windows.forEach { window in
|
||||
window.controller.configDidReload()
|
||||
}
|
||||
|
||||
// If we have configuration errors, we need to show them.
|
||||
let c = ConfigurationErrorsController.sharedInstance
|
||||
c.errors = state.config.errors
|
||||
if (c.errors.count > 0) {
|
||||
if (c.window == nil || !c.window!.isVisible) {
|
||||
c.showWindow(self)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to handle our global event tap depending on if there are global
|
||||
// events that we care about in Ghostty.
|
||||
if (ghostty_app_has_global_keybinds(ghostty.app!)) {
|
||||
if (timeSinceLaunch > 5) {
|
||||
// If the process has been running for awhile we enable right away
|
||||
// because no windows are likely to pop up.
|
||||
GlobalEventTap.shared.enable()
|
||||
} else {
|
||||
// If the process just started, we wait a couple seconds to allow
|
||||
// the initial windows and so on to load so our permissions dialog
|
||||
// doesn't get buried.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
|
||||
GlobalEventTap.shared.enable()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GlobalEventTap.shared.disable()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
private func syncAppearance() {
|
||||
guard let theme = ghostty.config.windowTheme else { return }
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
let appearance = NSAppearance(named: .darkAqua)
|
||||
NSApplication.shared.appearance = appearance
|
||||
|
||||
case "light":
|
||||
let appearance = NSAppearance(named: .aqua)
|
||||
NSApplication.shared.appearance = appearance
|
||||
|
||||
case "auto":
|
||||
let color = OSColor(ghostty.config.backgroundColor)
|
||||
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
|
||||
NSApplication.shared.appearance = appearance
|
||||
|
||||
default:
|
||||
NSApplication.shared.appearance = nil
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Dock Menu
|
||||
|
||||
private func reloadDockMenu() {
|
||||
@ -629,7 +648,7 @@ class AppDelegate: NSObject,
|
||||
if quickController == nil {
|
||||
quickController = QuickTerminalController(
|
||||
ghostty,
|
||||
position: ghostty.config.quickTerminalPosition
|
||||
position: derivedConfig.quickTerminalPosition
|
||||
)
|
||||
}
|
||||
|
||||
@ -655,4 +674,22 @@ class AppDelegate: NSObject,
|
||||
|
||||
isVisible.toggle()
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let initialWindow: Bool
|
||||
let shouldQuitAfterLastWindowClosed: Bool
|
||||
let quickTerminalPosition: QuickTerminalPosition
|
||||
|
||||
init() {
|
||||
self.initialWindow = true
|
||||
self.shouldQuitAfterLastWindowClosed = false
|
||||
self.quickTerminalPosition = .top
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.initialWindow = config.initialWindow
|
||||
self.shouldQuitAfterLastWindowClosed = config.shouldQuitAfterLastWindowClosed
|
||||
self.quickTerminalPosition = config.quickTerminalPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,12 +18,16 @@ class QuickTerminalController: BaseTerminalController {
|
||||
/// application to the front.
|
||||
private var previousApp: NSRunningApplication? = nil
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
position: QuickTerminalPosition = .top,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.position = position
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
@ -35,8 +39,8 @@ class QuickTerminalController: BaseTerminalController {
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidReloadConfig),
|
||||
name: Ghostty.Notification.ghosttyDidReloadConfig,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@ -64,7 +68,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
window.isRestorable = false
|
||||
|
||||
// Setup our configured appearance that we support.
|
||||
syncAppearance()
|
||||
syncAppearance(ghostty.config)
|
||||
|
||||
// Setup our initial size based on our configured position
|
||||
position.setLoaded(window)
|
||||
@ -186,7 +190,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = ghostty.config.quickTerminalScreen.screen else { return }
|
||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||
|
||||
// Move our window off screen to the top
|
||||
position.setInitial(in: window, on: screen)
|
||||
@ -197,7 +201,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Run the animation that moves our window into the proper place and makes
|
||||
// it visible.
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setFinal(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
@ -287,7 +291,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setInitial(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
@ -297,7 +301,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
})
|
||||
}
|
||||
|
||||
private func syncAppearance() {
|
||||
private func syncAppearance(_ config: Ghostty.Config) {
|
||||
guard let window else { return }
|
||||
|
||||
// If our window is not visible, then delay this. This is possible specifically
|
||||
@ -306,7 +310,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// APIs such as window blur have no effect unless the window is visible.
|
||||
guard window.isVisible else {
|
||||
// Weak window so that if the window changes or is destroyed we aren't holding a ref
|
||||
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
|
||||
DispatchQueue.main.async { [weak self] in self?.syncAppearance(config) }
|
||||
return
|
||||
}
|
||||
|
||||
@ -314,7 +318,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (ghostty.config.windowColorspace) {
|
||||
switch (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
@ -324,7 +328,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
if (ghostty.config.backgroundOpacity < 1) {
|
||||
if (config.backgroundOpacity < 1) {
|
||||
window.isOpaque = false
|
||||
|
||||
// This is weird, but we don't use ".clear" because this creates a look that
|
||||
@ -371,8 +375,35 @@ class QuickTerminalController: BaseTerminalController {
|
||||
toggleFullscreen(mode: .nonNative)
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) {
|
||||
syncAppearance()
|
||||
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||
// We only care if the configuration is a global configuration, not a
|
||||
// surface-specific one.
|
||||
guard notification.object == nil else { return }
|
||||
|
||||
// Get our managed configuration object out
|
||||
guard let config = notification.userInfo?[
|
||||
Notification.Name.GhosttyConfigChangeKey
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
||||
syncAppearance(config)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let quickTerminalScreen: QuickTerminalScreen
|
||||
let quickTerminalAnimationDuration: Double
|
||||
|
||||
init() {
|
||||
self.quickTerminalScreen = .main
|
||||
self.quickTerminalAnimationDuration = 0.2
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.quickTerminalScreen = config.quickTerminalScreen
|
||||
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,9 @@ class BaseTerminalController: NSWindowController,
|
||||
/// The previous frame information from the window
|
||||
private var savedFrame: SavedFrame? = nil
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
struct SavedFrame {
|
||||
let window: NSRect
|
||||
let screen: NSRect
|
||||
@ -74,6 +77,7 @@ class BaseTerminalController: NSWindowController,
|
||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.ghostty = ghostty
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
super.init(window: nil)
|
||||
|
||||
@ -93,6 +97,11 @@ class BaseTerminalController: NSWindowController,
|
||||
selector: #selector(didChangeScreenParametersNotification),
|
||||
name: NSApplication.didChangeScreenParametersNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChangeBase(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil)
|
||||
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
@ -191,6 +200,20 @@ class BaseTerminalController: NSWindowController,
|
||||
window.setFrame(newFrame, display: true)
|
||||
}
|
||||
|
||||
@objc private func ghosttyConfigDidChangeBase(_ notification: Notification) {
|
||||
// We only care if the configuration is a global configuration, not a
|
||||
// surface-specific one.
|
||||
guard notification.object == nil else { return }
|
||||
|
||||
// Get our managed configuration object out
|
||||
guard let config = notification.userInfo?[
|
||||
Notification.Name.GhosttyConfigChangeKey
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
}
|
||||
|
||||
// MARK: Local Events
|
||||
|
||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||
@ -245,7 +268,7 @@ class BaseTerminalController: NSWindowController,
|
||||
func pwdDidChange(to: URL?) {
|
||||
guard let window else { return }
|
||||
|
||||
if ghostty.config.macosTitlebarProxyIcon == .visible {
|
||||
if derivedConfig.macosTitlebarProxyIcon == .visible {
|
||||
// Use the 'to' URL directly
|
||||
window.representedURL = to
|
||||
} else {
|
||||
@ -255,7 +278,7 @@ class BaseTerminalController: NSWindowController,
|
||||
|
||||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
guard ghostty.config.windowStepResize else { return }
|
||||
guard derivedConfig.windowStepResize else { return }
|
||||
self.window?.contentResizeIncrements = to
|
||||
}
|
||||
|
||||
@ -563,4 +586,19 @@ class BaseTerminalController: NSWindowController,
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.resetTerminal(surface: surface)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
|
||||
init() {
|
||||
self.macosTitlebarProxyIcon = .visible
|
||||
self.windowStepResize = false
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
|
||||
self.windowStepResize = config.windowStepResize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import GhosttyKit
|
||||
|
||||
/// A classic, tabbed terminal experience.
|
||||
@ -20,6 +21,12 @@ class TerminalController: BaseTerminalController {
|
||||
/// For example, terminals executing custom scripts are not restorable.
|
||||
private var restorable: Bool = true
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
/// The notification cancellable for focused surface property changes.
|
||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||
@ -31,6 +38,9 @@ class TerminalController: BaseTerminalController {
|
||||
// restoration.
|
||||
self.restorable = (base?.command ?? "") == ""
|
||||
|
||||
// Setup our initial derived config based on the current app config
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
@ -50,6 +60,12 @@ class TerminalController: BaseTerminalController {
|
||||
selector: #selector(onGotoTab),
|
||||
name: Ghostty.Notification.ghosttyGotoTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onFrameDidChange),
|
||||
@ -80,10 +96,38 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
//MARK: - Methods
|
||||
|
||||
func configDidReload() {
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
|
||||
syncAppearance()
|
||||
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||
// Get our managed configuration object out
|
||||
guard let config = notification.userInfo?[
|
||||
Notification.Name.GhosttyConfigChangeKey
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
// If this is an app-level config update then we update some things.
|
||||
if (notification.object == nil) {
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
window.focusFollowsMouse = config.focusFollowsMouse
|
||||
|
||||
// If we have no surfaces in our window (is that possible?) then we update
|
||||
// our window appearance based on the root config. If we have surfaces, we
|
||||
// don't call this because the TODO
|
||||
if surfaceTree == nil {
|
||||
syncAppearance(.init(config))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// This is a surface-level config update. If we have the surface, we
|
||||
// update our appearance based on it.
|
||||
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree?.contains(view: surfaceView) ?? false else { return }
|
||||
|
||||
// We can't use surfaceView.derivedConfig because it may not be updated
|
||||
// yet since it also responds to notifications.
|
||||
syncAppearance(.init(config))
|
||||
}
|
||||
|
||||
/// Update the accessory view of each tab according to the keyboard
|
||||
@ -144,28 +188,23 @@ class TerminalController: BaseTerminalController {
|
||||
self.relabelTabs()
|
||||
}
|
||||
|
||||
private func syncAppearance() {
|
||||
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
guard let window = self.window as? TerminalWindow else { return }
|
||||
|
||||
// If our window is not visible, then delay this. This is possible specifically
|
||||
// during state restoration but probably in other scenarios as well. To delay,
|
||||
// we just loop directly on the dispatch queue. We have to delay because some
|
||||
// APIs such as window blur have no effect unless the window is visible.
|
||||
guard window.isVisible else {
|
||||
// Weak window so that if the window changes or is destroyed we aren't holding a ref
|
||||
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
|
||||
return
|
||||
}
|
||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
guard window.isVisible else { return }
|
||||
|
||||
// Set the font for the window and tab titles.
|
||||
if let titleFontName = ghostty.config.windowTitleFontFamily {
|
||||
if let titleFontName = surfaceConfig.windowTitleFontFamily {
|
||||
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
|
||||
} else {
|
||||
window.titlebarFont = nil
|
||||
}
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
if (ghostty.config.backgroundOpacity < 1) {
|
||||
if (surfaceConfig.backgroundOpacity < 1) {
|
||||
window.isOpaque = false
|
||||
|
||||
// This is weird, but we don't use ".clear" because this creates a look that
|
||||
@ -179,14 +218,26 @@ class TerminalController: BaseTerminalController {
|
||||
window.backgroundColor = .windowBackgroundColor
|
||||
}
|
||||
|
||||
window.hasShadow = ghostty.config.macosWindowShadow
|
||||
window.hasShadow = surfaceConfig.macosWindowShadow
|
||||
|
||||
guard window.hasStyledTabs else { return }
|
||||
|
||||
// The titlebar is always updated. We don't need to worry about opacity
|
||||
// because we handle it here.
|
||||
let backgroundColor = OSColor(ghostty.config.backgroundColor)
|
||||
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
|
||||
// Our background color depends on if our focused surface borders the top or not.
|
||||
// If it does, we match the focused surface. If it doesn't, we use the app
|
||||
// configuration.
|
||||
let backgroundColor: OSColor
|
||||
if let surfaceTree {
|
||||
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor)
|
||||
} else {
|
||||
// We don't have a focused surface or our surface doesn't border the
|
||||
// top. We choose to match the color of the top-left most surface.
|
||||
backgroundColor = OSColor(surfaceTree.topLeft().backgroundColor ?? derivedConfig.backgroundColor)
|
||||
}
|
||||
} else {
|
||||
backgroundColor = OSColor(self.derivedConfig.backgroundColor)
|
||||
}
|
||||
window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity)
|
||||
|
||||
if (window.isOpaque) {
|
||||
// Bg color is only synced if we have no transparency. This is because
|
||||
@ -210,6 +261,12 @@ class TerminalController: BaseTerminalController {
|
||||
override func windowDidLoad() {
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
|
||||
// I copy this because we may change the source in the future but also because
|
||||
// I regularly audit our codebase for "ghostty.config" access because generally
|
||||
// you shouldn't use it. Its safe in this case because for a new window we should
|
||||
// use whatever the latest app-level config is.
|
||||
let config = ghostty.config
|
||||
|
||||
// Setting all three of these is required for restoration to work.
|
||||
window.isRestorable = restorable
|
||||
if (restorable) {
|
||||
@ -218,13 +275,13 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
// If window decorations are disabled, remove our title
|
||||
if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) }
|
||||
if (!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 this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (ghostty.config.windowColorspace) {
|
||||
switch (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
@ -256,30 +313,30 @@ class TerminalController: BaseTerminalController {
|
||||
window.center()
|
||||
|
||||
// Make sure our theme is set on the window so styling is correct.
|
||||
if let windowTheme = ghostty.config.windowTheme {
|
||||
if let windowTheme = config.windowTheme {
|
||||
window.windowTheme = .init(rawValue: windowTheme)
|
||||
}
|
||||
|
||||
// Handle titlebar tabs config option. Something about what we do while setting up the
|
||||
// titlebar tabs interferes with the window restore process unless window.tabbingMode
|
||||
// is set to .preferred, so we set it, and switch back to automatic as soon as we can.
|
||||
if (ghostty.config.macosTitlebarStyle == "tabs") {
|
||||
if (config.macosTitlebarStyle == "tabs") {
|
||||
window.tabbingMode = .preferred
|
||||
window.titlebarTabs = true
|
||||
DispatchQueue.main.async {
|
||||
window.tabbingMode = .automatic
|
||||
}
|
||||
} else if (ghostty.config.macosTitlebarStyle == "transparent") {
|
||||
} else if (config.macosTitlebarStyle == "transparent") {
|
||||
window.transparentTabs = true
|
||||
}
|
||||
|
||||
if window.hasStyledTabs {
|
||||
// Set the background color of the window
|
||||
let backgroundColor = NSColor(ghostty.config.backgroundColor)
|
||||
let backgroundColor = NSColor(config.backgroundColor)
|
||||
window.backgroundColor = backgroundColor
|
||||
|
||||
// This makes sure our titlebar renders correctly when there is a transparent background
|
||||
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
|
||||
window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity)
|
||||
}
|
||||
|
||||
// Initialize our content view to the SwiftUI root
|
||||
@ -290,7 +347,7 @@ class TerminalController: BaseTerminalController {
|
||||
))
|
||||
|
||||
// If our titlebar style is "hidden" we adjust the style appropriately
|
||||
if (ghostty.config.macosTitlebarStyle == "hidden") {
|
||||
if (config.macosTitlebarStyle == "hidden") {
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
@ -345,10 +402,12 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
|
||||
window.focusFollowsMouse = config.focusFollowsMouse
|
||||
|
||||
// Apply any additional appearance-related properties to the new window.
|
||||
syncAppearance()
|
||||
// Apply any additional appearance-related properties to the new window. We
|
||||
// apply this based on the root config but change it later based on surface
|
||||
// config (see focused surface change callback).
|
||||
syncAppearance(.init(config))
|
||||
}
|
||||
|
||||
// Shows the "+" button in the tab bar, responds to that click.
|
||||
@ -464,7 +523,7 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
// Custom toolbar-based title used when titlebar tabs are enabled.
|
||||
if let toolbar = window.toolbar as? TerminalToolbar {
|
||||
if (window.titlebarTabs || ghostty.config.macosTitlebarStyle == "hidden") {
|
||||
if (window.titlebarTabs || derivedConfig.macosTitlebarStyle == "hidden") {
|
||||
// Updating the title text as above automatically reveals the
|
||||
// native title view in macOS 15.0 and above. Since we're using
|
||||
// a custom view instead, we need to re-hide it.
|
||||
@ -485,6 +544,36 @@ class TerminalController: BaseTerminalController {
|
||||
window.surfaceIsZoomed = to
|
||||
}
|
||||
|
||||
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||
super.focusedSurfaceDidChange(to: to)
|
||||
|
||||
// We always cancel our event listener
|
||||
surfaceAppearanceCancellables.removeAll()
|
||||
|
||||
// When our focus changes, we update our window appearance based on the
|
||||
// currently focused surface.
|
||||
guard let focusedSurface else { return }
|
||||
syncAppearance(focusedSurface.derivedConfig)
|
||||
|
||||
// We also want to get notified of certain changes to update our appearance.
|
||||
focusedSurface.$derivedConfig
|
||||
.sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) }
|
||||
.store(in: &surfaceAppearanceCancellables)
|
||||
focusedSurface.$backgroundColor
|
||||
.sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) }
|
||||
.store(in: &surfaceAppearanceCancellables)
|
||||
}
|
||||
|
||||
private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) {
|
||||
guard let surface else { return }
|
||||
DispatchQueue.main.async { [weak self, weak surface] in
|
||||
guard let surface else { return }
|
||||
guard let self else { return }
|
||||
guard self.focusedSurface == surface else { return }
|
||||
self.syncAppearance(surface.derivedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notifications
|
||||
|
||||
@objc private func onMoveTab(notification: SwiftUI.Notification) {
|
||||
@ -593,4 +682,19 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
toggleFullscreen(mode: fullscreenMode)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let backgroundColor: Color
|
||||
let macosTitlebarStyle: String
|
||||
|
||||
init() {
|
||||
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
||||
self.macosTitlebarStyle = "system"
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.backgroundColor = config.backgroundColor
|
||||
self.macosTitlebarStyle = config.macosTitlebarStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,12 @@ class TerminalManager {
|
||||
return windows.last
|
||||
}
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
init(_ ghostty: Ghostty.App) {
|
||||
self.ghostty = ghostty
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
@ -51,6 +55,11 @@ class TerminalManager {
|
||||
selector: #selector(onNewWindow),
|
||||
name: Ghostty.Notification.ghosttyNewWindow,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -70,8 +79,8 @@ class TerminalManager {
|
||||
if let parent = focusedSurface?.window,
|
||||
parent.styleMask.contains(.fullScreen) {
|
||||
window.toggleFullScreen(nil)
|
||||
} else if ghostty.config.windowFullscreen {
|
||||
switch (ghostty.config.windowFullscreenMode) {
|
||||
} else if derivedConfig.windowFullscreen {
|
||||
switch (derivedConfig.windowFullscreenMode) {
|
||||
case .native:
|
||||
// Native has to be done immediately so that our stylemask contains
|
||||
// fullscreen for the logic later in this method.
|
||||
@ -81,7 +90,7 @@ class TerminalManager {
|
||||
// If we're non-native then we have to do it on a later loop
|
||||
// so that the content view is setup.
|
||||
DispatchQueue.main.async {
|
||||
c.toggleFullscreen(mode: self.ghostty.config.windowFullscreenMode)
|
||||
c.toggleFullscreen(mode: self.derivedConfig.windowFullscreenMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,9 +168,9 @@ class TerminalManager {
|
||||
|
||||
// If we have the "hidden" titlebar style we want to create new
|
||||
// tabs as windows instead, so just skip adding it to the parent.
|
||||
if (ghostty.config.macosTitlebarStyle != "hidden") {
|
||||
if (derivedConfig.macosTitlebarStyle != "hidden") {
|
||||
// Add the window to the tab group and show it.
|
||||
switch ghostty.config.windowNewTabPosition {
|
||||
switch derivedConfig.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.
|
||||
@ -325,4 +334,39 @@ class TerminalManager {
|
||||
|
||||
self.newTab(to: window, withBaseConfig: config)
|
||||
}
|
||||
|
||||
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||
// We only care if the configuration is a global configuration, not a
|
||||
// surface-specific one.
|
||||
guard notification.object == nil else { return }
|
||||
|
||||
// Get our managed configuration object out
|
||||
guard let config = notification.userInfo?[
|
||||
Notification.Name.GhosttyConfigChangeKey
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let windowFullscreen: Bool
|
||||
let windowFullscreenMode: FullscreenMode
|
||||
let macosTitlebarStyle: String
|
||||
let windowNewTabPosition: String
|
||||
|
||||
init() {
|
||||
self.windowFullscreen = false
|
||||
self.windowFullscreenMode = .native
|
||||
self.macosTitlebarStyle = "transparent"
|
||||
self.windowNewTabPosition = ""
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.windowFullscreen = config.windowFullscreen
|
||||
self.windowFullscreenMode = config.windowFullscreenMode
|
||||
self.macosTitlebarStyle = config.macosTitlebarStyle
|
||||
self.windowNewTabPosition = config.windowNewTabPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||
}
|
||||
|
||||
// If our configuration is "never" then we never restore the state
|
||||
// no matter what.
|
||||
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
|
||||
// no matter what. Note its safe to use "ghostty.config" directly here
|
||||
// because window restoration is only ever invoked on app start so we
|
||||
// don't have to deal with config reloads.
|
||||
if (appDelegate.ghostty.config.windowSaveState == "never") {
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
@ -5,6 +6,33 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
extension Ghostty.Action {
|
||||
struct ColorChange {
|
||||
let kind: Kind
|
||||
let color: Color
|
||||
|
||||
enum Kind {
|
||||
case foreground
|
||||
case background
|
||||
case cursor
|
||||
case palette(index: UInt8)
|
||||
}
|
||||
|
||||
init(c: ghostty_action_color_change_s) {
|
||||
switch (c.kind) {
|
||||
case GHOSTTY_ACTION_COLOR_KIND_FOREGROUND:
|
||||
self.kind = .foreground
|
||||
case GHOSTTY_ACTION_COLOR_KIND_BACKGROUND:
|
||||
self.kind = .background
|
||||
case GHOSTTY_ACTION_COLOR_KIND_CURSOR:
|
||||
self.kind = .cursor
|
||||
default:
|
||||
self.kind = .palette(index: UInt8(c.kind.rawValue))
|
||||
}
|
||||
|
||||
self.color = Color(red: Double(c.r) / 255, green: Double(c.g) / 255, blue: Double(c.b) / 255)
|
||||
}
|
||||
}
|
||||
|
||||
struct MoveTab {
|
||||
let amount: Int
|
||||
|
||||
|
@ -3,9 +3,6 @@ import UserNotifications
|
||||
import GhosttyKit
|
||||
|
||||
protocol GhosttyAppDelegate: AnyObject {
|
||||
/// Called when the configuration did finish reloading.
|
||||
func configDidReload(_ app: Ghostty.App)
|
||||
|
||||
#if os(macOS)
|
||||
/// Called when a callback needs access to a specific surface. This should return nil
|
||||
/// when the surface is no longer valid.
|
||||
@ -380,16 +377,6 @@ extension Ghostty {
|
||||
let state = Unmanaged<Self>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
state.config = newConfig
|
||||
|
||||
// If we have a delegate, notify.
|
||||
if let delegate = state.delegate {
|
||||
delegate.configDidReload(state)
|
||||
}
|
||||
|
||||
// Send an event out
|
||||
NotificationCenter.default.post(
|
||||
name: Ghostty.Notification.ghosttyDidReloadConfig,
|
||||
object: nil)
|
||||
|
||||
return newConfig.config
|
||||
}
|
||||
|
||||
@ -524,8 +511,12 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||
keySequence(app, target: target, v: action.action.key_sequence)
|
||||
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||
configChange(app, target: target, v: action.action.config_change)
|
||||
|
||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||
fallthrough
|
||||
colorChange(app, target: target, change: action.action.color_change)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||
@ -1159,6 +1150,71 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func configChange(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_config_change_s) {
|
||||
logger.info("config change notification")
|
||||
|
||||
// Clone the config so we own the memory. It'd be nicer to not have to do
|
||||
// this but since we async send the config out below we have to own the lifetime.
|
||||
// A future improvement might be to add reference counting to config or
|
||||
// something so apprt's do not have to do this.
|
||||
let config = Config(clone: v.config)
|
||||
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func colorChange(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
change: ghostty_action_color_change_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("color change does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyColorDidChange,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.GhosttyColorChangeKey: Action.ColorChange(c: change)
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: User Notifications
|
||||
|
||||
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
||||
|
@ -39,6 +39,10 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
init(clone config: ghostty_config_t) {
|
||||
self.config = ghostty_config_clone(config)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.config = nil
|
||||
}
|
||||
|
@ -38,6 +38,16 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func topLeft() -> SurfaceView {
|
||||
switch (self) {
|
||||
case .leaf(let leaf):
|
||||
return leaf.surface
|
||||
|
||||
case .split(let container):
|
||||
return container.topLeft.topLeft()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the view that would prefer receiving focus in this tree. This is always the
|
||||
/// top-left-most view. This is used when creating a split or closing a split to find the
|
||||
/// next view to send focus to.
|
||||
@ -136,6 +146,24 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the surface borders the top. Assumes the view is in the tree.
|
||||
func doesBorderTop(view: SurfaceView) -> Bool {
|
||||
switch (self) {
|
||||
case .leaf(let leaf):
|
||||
return leaf.surface == view
|
||||
|
||||
case .split(let container):
|
||||
switch (container.direction) {
|
||||
case .vertical:
|
||||
return container.topLeft.doesBorderTop(view: view)
|
||||
|
||||
case .horizontal:
|
||||
return container.topLeft.doesBorderTop(view: view) ||
|
||||
container.bottomRight.doesBorderTop(view: view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sequence
|
||||
|
||||
func makeIterator() -> IndexingIterator<[Leaf]> {
|
||||
|
@ -206,6 +206,14 @@ extension Ghostty {
|
||||
// MARK: Surface Notification
|
||||
|
||||
extension Notification.Name {
|
||||
/// Configuration change. If the object is nil then it is app-wide. Otherwise its surface-specific.
|
||||
static let ghosttyConfigDidChange = Notification.Name("com.mitchellh.ghostty.configDidChange")
|
||||
static let GhosttyConfigChangeKey = ghosttyConfigDidChange.rawValue
|
||||
|
||||
/// Color change. Object is the surface changing.
|
||||
static let ghosttyColorDidChange = Notification.Name("com.mitchellh.ghostty.ghosttyColorDidChange")
|
||||
static let GhosttyColorChangeKey = ghosttyColorDidChange.rawValue
|
||||
|
||||
/// Goto tab. Has tab index in the userinfo.
|
||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
||||
@ -217,9 +225,6 @@ extension Ghostty.Notification {
|
||||
/// Used to pass a configuration along when creating a new tab/window/split.
|
||||
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
|
||||
|
||||
/// Posted when the application configuration is reloaded.
|
||||
static let ghosttyDidReloadConfig = Notification.Name("com.mitchellh.ghostty.didReloadConfig")
|
||||
|
||||
/// Posted when a new split is requested. The sending object will be the surface that had focus. The
|
||||
/// userdata has one key "direction" with the direction to split to.
|
||||
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")
|
||||
|
@ -48,6 +48,13 @@ extension Ghostty {
|
||||
// Whether the pointer should be visible or not
|
||||
@Published private(set) var pointerStyle: BackportPointerStyle = .default
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
@Published private(set) var derivedConfig: DerivedConfig
|
||||
|
||||
/// The background color within the color palette of the surface. This is only set if it is
|
||||
/// dynamically updated. Otherwise, the background color is the default background color.
|
||||
@Published private(set) var backgroundColor: Color? = nil
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
@ -114,6 +121,13 @@ extension Ghostty {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
self.uuid = uuid ?? .init()
|
||||
|
||||
// Our initial config always is our application wide config.
|
||||
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
|
||||
self.derivedConfig = DerivedConfig(appDelegate.ghostty.config)
|
||||
} else {
|
||||
self.derivedConfig = DerivedConfig()
|
||||
}
|
||||
|
||||
// Initialize with some default frame size. The important thing is that this
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
// can do SOMETHING.
|
||||
@ -137,6 +151,16 @@ extension Ghostty {
|
||||
selector: #selector(ghosttyDidEndKeySequence),
|
||||
name: Ghostty.Notification.didEndKeySequence,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyColorDidChange(_:)),
|
||||
name: .ghosttyColorDidChange,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
@ -333,6 +357,31 @@ extension Ghostty {
|
||||
keySequence = []
|
||||
}
|
||||
|
||||
@objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) {
|
||||
// Get our managed configuration object out
|
||||
guard let config = notification.userInfo?[
|
||||
SwiftUI.Notification.Name.GhosttyConfigChangeKey
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
}
|
||||
|
||||
@objc private func ghosttyColorDidChange(_ notification: SwiftUI.Notification) {
|
||||
guard let change = notification.userInfo?[
|
||||
SwiftUI.Notification.Name.GhosttyColorChangeKey
|
||||
] as? Ghostty.Action.ColorChange else { return }
|
||||
|
||||
switch (change.kind) {
|
||||
case .background:
|
||||
self.backgroundColor = change.color
|
||||
|
||||
default:
|
||||
// We don't do anything for the other colors yet.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||
guard let window = self.window else { return }
|
||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||
@ -1025,6 +1074,27 @@ extension Ghostty {
|
||||
Ghostty.moveFocus(to: self)
|
||||
}
|
||||
}
|
||||
|
||||
struct DerivedConfig {
|
||||
let backgroundColor: Color
|
||||
let backgroundOpacity: Double
|
||||
let macosWindowShadow: Bool
|
||||
let windowTitleFontFamily: String?
|
||||
|
||||
init() {
|
||||
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
||||
self.backgroundOpacity = 1
|
||||
self.macosWindowShadow = true
|
||||
self.windowTitleFontFamily = nil
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.backgroundColor = config.backgroundColor
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
self.macosWindowShadow = config.macosWindowShadow
|
||||
self.windowTitleFontFamily = config.windowTitleFontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,14 @@ extension OSColor {
|
||||
return (0.299 * r) + (0.587 * g) + (0.114 * b)
|
||||
}
|
||||
|
||||
var hexString: String? {
|
||||
guard let rgb = usingColorSpace(.deviceRGB) else { return nil }
|
||||
let red = Int(rgb.redComponent * 255)
|
||||
let green = Int(rgb.greenComponent * 255)
|
||||
let blue = Int(rgb.blueComponent * 255)
|
||||
return String(format: "#%02X%02X%02X", red, green, blue)
|
||||
}
|
||||
|
||||
func darken(by amount: CGFloat) -> OSColor {
|
||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
|
||||
|
11
src/App.zig
11
src/App.zig
@ -147,11 +147,18 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
/// Update the configuration associated with the app. This can only be
|
||||
/// called from the main thread. The caller owns the config memory. The
|
||||
/// memory can be freed immediately when this returns.
|
||||
pub fn updateConfig(self: *App, config: *const Config) !void {
|
||||
pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void {
|
||||
// Go through and update all of the surface configurations.
|
||||
for (self.surfaces.items) |surface| {
|
||||
try surface.core_surface.handleMessage(.{ .change_config = config });
|
||||
}
|
||||
|
||||
// Notify the apprt that the app has changed configuration.
|
||||
try rt_app.performAction(
|
||||
.app,
|
||||
.config_change,
|
||||
.{ .config = config },
|
||||
);
|
||||
}
|
||||
|
||||
/// Add an initialized surface. This is really only for the runtime
|
||||
@ -257,7 +264,7 @@ pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
|
||||
log.debug("reloading configuration", .{});
|
||||
if (try rt_app.reloadConfig()) |new| {
|
||||
log.debug("new configuration received, applying", .{});
|
||||
try self.updateConfig(new);
|
||||
try self.updateConfig(rt_app, new);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1127,6 +1127,13 @@ pub fn updateConfig(
|
||||
self.queueRender() catch |err| {
|
||||
log.warn("failed to notify renderer of config change err={}", .{err});
|
||||
};
|
||||
|
||||
// Notify the window
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.config_change,
|
||||
.{ .config = config },
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns true if the terminal has a selection.
|
||||
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
@ -200,6 +201,20 @@ pub const Action = union(Key) {
|
||||
/// on the app or surface.
|
||||
config_change_conditional_state,
|
||||
|
||||
/// The configuration has changed. The value is a pointer to the new
|
||||
/// configuration. The pointer is only valid for the duration of the
|
||||
/// action and should not be stored.
|
||||
///
|
||||
/// This should be used by apprts to update any internal state that
|
||||
/// depends on configuration for the given target (i.e. headerbar colors).
|
||||
/// The apprt should copy any data it needs since the memory lifetime
|
||||
/// is only valid for the duration of the action.
|
||||
///
|
||||
/// This allows an apprt to have config-dependent state reactively
|
||||
/// change without having to store the entire configuration or poll
|
||||
/// for changes.
|
||||
config_change: ConfigChange,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
new_window,
|
||||
@ -236,6 +251,7 @@ pub const Action = union(Key) {
|
||||
key_sequence,
|
||||
color_change,
|
||||
config_change_conditional_state,
|
||||
config_change,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
@ -497,3 +513,18 @@ pub const ColorKind = enum(c_int) {
|
||||
// 0+ values indicate a palette index
|
||||
_,
|
||||
};
|
||||
|
||||
pub const ConfigChange = struct {
|
||||
config: *const configpkg.Config,
|
||||
|
||||
// Sync with: ghostty_action_config_change_s
|
||||
pub const C = extern struct {
|
||||
config: *const configpkg.Config,
|
||||
};
|
||||
|
||||
pub fn cval(self: ConfigChange) C {
|
||||
return .{
|
||||
.config = self.config,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -226,6 +226,7 @@ pub const App = struct {
|
||||
.color_change,
|
||||
.pwd,
|
||||
.config_change_conditional_state,
|
||||
.config_change,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
@ -488,6 +488,7 @@ pub fn performAction(
|
||||
.render_inspector,
|
||||
.renderer_health,
|
||||
.color_change,
|
||||
.config_change,
|
||||
=> log.warn("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export fn ghostty_config_new() ?*Config {
|
||||
|
||||
result.* = Config.default(global.alloc) catch |err| {
|
||||
log.err("error creating config err={}", .{err});
|
||||
global.alloc.destroy(result);
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -32,6 +33,22 @@ export fn ghostty_config_free(ptr: ?*Config) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deep clone the configuration.
|
||||
export fn ghostty_config_clone(self: *Config) ?*Config {
|
||||
const result = global.alloc.create(Config) catch |err| {
|
||||
log.err("error allocating config err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
result.* = self.clone(global.alloc) catch |err| {
|
||||
log.err("error cloning config err={}", .{err});
|
||||
global.alloc.destroy(result);
|
||||
return null;
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Load the configuration from the CLI args.
|
||||
export fn ghostty_config_load_cli_args(self: *Config) void {
|
||||
self.loadCliArgs(global.alloc) catch |err| {
|
||||
|
@ -365,7 +365,6 @@ const c = @cImport({
|
||||
/// be fixed in a future update:
|
||||
///
|
||||
/// - macOS: titlebar tabs style is not updated when switching themes.
|
||||
/// - macOS: native titlebar style is not supported.
|
||||
///
|
||||
theme: ?Theme = null,
|
||||
|
||||
@ -1519,6 +1518,13 @@ keybind: Keybinds = .{},
|
||||
/// This makes a more seamless window appearance but looks a little less
|
||||
/// typical for a macOS application and may not work well with all themes.
|
||||
///
|
||||
/// The "transparent" style will also update in real-time to dynamic
|
||||
/// changes to the window background color, i.e. via OSC 11. To make this
|
||||
/// more aesthetically pleasing, this only happens if the terminal is
|
||||
/// a window, tab, or split that borders the top of the window. This
|
||||
/// avoids a disjointed appearance where the titlebar color changes
|
||||
/// but all the topmost terminals don't match.
|
||||
///
|
||||
/// The "tabs" style is a completely custom titlebar that integrates the
|
||||
/// tab bar into the titlebar. This titlebar always matches the background
|
||||
/// color of the terminal. There are some limitations to this style:
|
||||
@ -1537,11 +1543,6 @@ keybind: Keybinds = .{},
|
||||
/// but its one I think is the most aesthetically pleasing and works in
|
||||
/// most cases.
|
||||
///
|
||||
/// BUG: If a separate light/dark mode theme is configured with "theme",
|
||||
/// then `macos-titlebar-style = transparent` will not work correctly. To
|
||||
/// avoid ugly titlebars, `macos-titlebar-style` will become `native` if
|
||||
/// a separate light/dark theme is configured.
|
||||
///
|
||||
/// Changing this option at runtime only applies to new windows.
|
||||
@"macos-titlebar-style": MacTitlebarStyle = .transparent,
|
||||
|
||||
@ -2756,12 +2757,6 @@ pub fn finalize(self: *Config) !void {
|
||||
// This setting doesn't make sense with different light/dark themes
|
||||
// because it'll force the theme based on the Ghostty theme.
|
||||
if (self.@"window-theme" == .auto) self.@"window-theme" = .system;
|
||||
|
||||
// This is buggy with different light/dark themes and is noted
|
||||
// in the documentation.
|
||||
if (self.@"macos-titlebar-style" == .transparent) {
|
||||
self.@"macos-titlebar-style" = .native;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user