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;
|
uint8_t b;
|
||||||
} ghostty_action_color_change_s;
|
} ghostty_action_color_change_s;
|
||||||
|
|
||||||
|
// apprt.action.ConfigChange
|
||||||
|
typedef struct {
|
||||||
|
ghostty_config_t config;
|
||||||
|
} ghostty_action_config_change_s;
|
||||||
|
|
||||||
// apprt.Action.Key
|
// apprt.Action.Key
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_ACTION_NEW_WINDOW,
|
GHOSTTY_ACTION_NEW_WINDOW,
|
||||||
@ -568,6 +573,7 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_KEY_SEQUENCE,
|
GHOSTTY_ACTION_KEY_SEQUENCE,
|
||||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||||
GHOSTTY_ACTION_CONFIG_CHANGE_CONDITIONAL_STATE,
|
GHOSTTY_ACTION_CONFIG_CHANGE_CONDITIONAL_STATE,
|
||||||
|
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@ -592,6 +598,7 @@ typedef union {
|
|||||||
ghostty_action_secure_input_e secure_input;
|
ghostty_action_secure_input_e secure_input;
|
||||||
ghostty_action_key_sequence_s key_sequence;
|
ghostty_action_key_sequence_s key_sequence;
|
||||||
ghostty_action_color_change_s color_change;
|
ghostty_action_color_change_s color_change;
|
||||||
|
ghostty_action_config_change_s config_change;
|
||||||
} ghostty_action_u;
|
} ghostty_action_u;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@ -639,6 +646,7 @@ ghostty_info_s ghostty_info(void);
|
|||||||
|
|
||||||
ghostty_config_t ghostty_config_new();
|
ghostty_config_t ghostty_config_new();
|
||||||
void ghostty_config_free(ghostty_config_t);
|
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_cli_args(ghostty_config_t);
|
||||||
void ghostty_config_load_default_files(ghostty_config_t);
|
void ghostty_config_load_default_files(ghostty_config_t);
|
||||||
void ghostty_config_load_recursive_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.
|
/// seconds since the process was launched.
|
||||||
private var applicationLaunchTime: TimeInterval = 0
|
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.
|
/// The ghostty global state. Only one per process.
|
||||||
let ghostty: Ghostty.App = Ghostty.App()
|
let ghostty: Ghostty.App = Ghostty.App()
|
||||||
|
|
||||||
@ -138,7 +141,7 @@ class AppDelegate: NSObject,
|
|||||||
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
||||||
|
|
||||||
// Initial config loading
|
// Initial config loading
|
||||||
configDidReload(ghostty)
|
ghosttyConfigDidChange(config: ghostty.config)
|
||||||
|
|
||||||
// Start our update checker.
|
// Start our update checker.
|
||||||
updaterController.startUpdater()
|
updaterController.startUpdater()
|
||||||
@ -162,6 +165,12 @@ class AppDelegate: NSObject,
|
|||||||
name: .quickTerminalDidChangeVisibility,
|
name: .quickTerminalDidChangeVisibility,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
|
name: .ghosttyConfigDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
// Configure user notifications
|
// Configure user notifications
|
||||||
let actions = [
|
let actions = [
|
||||||
@ -188,13 +197,13 @@ class AppDelegate: NSObject,
|
|||||||
// is possible to have other windows in a few scenarios:
|
// 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 opening a URL since `application(_:openFile:)` is called before this.
|
||||||
// - if we're restoring from persisted state
|
// - if we're restoring from persisted state
|
||||||
if terminalManager.windows.count == 0 && ghostty.config.initialWindow {
|
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||||
terminalManager.newWindow()
|
terminalManager.newWindow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return ghostty.config.shouldQuitAfterLastWindowClosed
|
return derivedConfig.shouldQuitAfterLastWindowClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
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.
|
/// 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 }
|
guard ghostty.readiness == .ready else { return }
|
||||||
|
|
||||||
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
|
syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig)
|
||||||
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
|
syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig)
|
||||||
syncMenuShortcut(action: "quit", menuItem: self.menuQuit)
|
syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit)
|
||||||
|
|
||||||
syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow)
|
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||||
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab)
|
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||||
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose)
|
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||||
syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow)
|
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||||
syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||||
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitRight)
|
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
||||||
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitDown)
|
syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
|
||||||
|
|
||||||
syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy)
|
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||||
syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste)
|
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||||
syncMenuShortcut(action: "select_all", menuItem: self.menuSelectAll)
|
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
|
||||||
|
|
||||||
syncMenuShortcut(action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||||
syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||||
syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit)
|
syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||||
syncMenuShortcut(action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
syncMenuShortcut(config, action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||||
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
syncMenuShortcut(config, action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||||
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||||
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||||
syncMenuShortcut(action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||||
syncMenuShortcut(action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
syncMenuShortcut(config, action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
||||||
syncMenuShortcut(action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
syncMenuShortcut(config, action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
||||||
syncMenuShortcut(action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
syncMenuShortcut(config, action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
||||||
syncMenuShortcut(action: "equalize_splits", menuItem: self.menuEqualizeSplits)
|
syncMenuShortcut(config, action: "equalize_splits", menuItem: self.menuEqualizeSplits)
|
||||||
|
|
||||||
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||||
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||||
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
|
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||||
syncMenuShortcut(action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||||
syncMenuShortcut(action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||||
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
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
|
// 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
|
// global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue
|
||||||
// to work but it won't be reflected in the menu item.
|
// 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
|
// Dock menu
|
||||||
reloadDockMenu()
|
reloadDockMenu()
|
||||||
@ -353,9 +362,9 @@ class AppDelegate: NSObject,
|
|||||||
|
|
||||||
/// Syncs a single menu shortcut for the given action. The action string is the same
|
/// Syncs a single menu shortcut for the given action. The action string is the same
|
||||||
/// action string used for the Ghostty configuration.
|
/// action string used for the Ghostty configuration.
|
||||||
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) {
|
private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) {
|
||||||
guard let menu = menuItem else { return }
|
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
|
// No shortcut, clear the menu item
|
||||||
menu.keyEquivalent = ""
|
menu.keyEquivalent = ""
|
||||||
menu.keyEquivalentModifierMask = []
|
menu.keyEquivalentModifierMask = []
|
||||||
@ -422,6 +431,98 @@ class AppDelegate: NSObject,
|
|||||||
self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off }
|
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
|
//MARK: - Restorable State
|
||||||
|
|
||||||
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
|
/// 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
|
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
|
//MARK: - Dock Menu
|
||||||
|
|
||||||
private func reloadDockMenu() {
|
private func reloadDockMenu() {
|
||||||
@ -629,7 +648,7 @@ class AppDelegate: NSObject,
|
|||||||
if quickController == nil {
|
if quickController == nil {
|
||||||
quickController = QuickTerminalController(
|
quickController = QuickTerminalController(
|
||||||
ghostty,
|
ghostty,
|
||||||
position: ghostty.config.quickTerminalPosition
|
position: derivedConfig.quickTerminalPosition
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,4 +674,22 @@ class AppDelegate: NSObject,
|
|||||||
|
|
||||||
isVisible.toggle()
|
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.
|
/// application to the front.
|
||||||
private var previousApp: NSRunningApplication? = nil
|
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,
|
init(_ ghostty: Ghostty.App,
|
||||||
position: QuickTerminalPosition = .top,
|
position: QuickTerminalPosition = .top,
|
||||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
) {
|
) {
|
||||||
self.position = position
|
self.position = position
|
||||||
|
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||||
|
|
||||||
// Setup our notifications for behaviors
|
// Setup our notifications for behaviors
|
||||||
@ -35,8 +39,8 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
object: nil)
|
object: nil)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(ghosttyDidReloadConfig),
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
name: Ghostty.Notification.ghosttyDidReloadConfig,
|
name: .ghosttyConfigDidChange,
|
||||||
object: nil)
|
object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +68,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
window.isRestorable = false
|
window.isRestorable = false
|
||||||
|
|
||||||
// Setup our configured appearance that we support.
|
// Setup our configured appearance that we support.
|
||||||
syncAppearance()
|
syncAppearance(ghostty.config)
|
||||||
|
|
||||||
// Setup our initial size based on our configured position
|
// Setup our initial size based on our configured position
|
||||||
position.setLoaded(window)
|
position.setLoaded(window)
|
||||||
@ -186,7 +190,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
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
|
// Move our window off screen to the top
|
||||||
position.setInitial(in: window, on: screen)
|
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
|
// Run the animation that moves our window into the proper place and makes
|
||||||
// it visible.
|
// it visible.
|
||||||
NSAnimationContext.runAnimationGroup({ context in
|
NSAnimationContext.runAnimationGroup({ context in
|
||||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||||
context.timingFunction = .init(name: .easeIn)
|
context.timingFunction = .init(name: .easeIn)
|
||||||
position.setFinal(in: window.animator(), on: screen)
|
position.setFinal(in: window.animator(), on: screen)
|
||||||
}, completionHandler: {
|
}, completionHandler: {
|
||||||
@ -287,7 +291,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSAnimationContext.runAnimationGroup({ context in
|
NSAnimationContext.runAnimationGroup({ context in
|
||||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||||
context.timingFunction = .init(name: .easeIn)
|
context.timingFunction = .init(name: .easeIn)
|
||||||
position.setInitial(in: window.animator(), on: screen)
|
position.setInitial(in: window.animator(), on: screen)
|
||||||
}, completionHandler: {
|
}, completionHandler: {
|
||||||
@ -297,7 +301,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncAppearance() {
|
private func syncAppearance(_ config: Ghostty.Config) {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
|
||||||
// If our window is not visible, then delay this. This is possible specifically
|
// 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.
|
// APIs such as window blur have no effect unless the window is visible.
|
||||||
guard window.isVisible else {
|
guard window.isVisible else {
|
||||||
// Weak window so that if the window changes or is destroyed we aren't holding a ref
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +318,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
// to "native" which is typically P3. There is a lot more resources
|
// to "native" which is typically P3. There is a lot more resources
|
||||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||||
// Ghostty defaults to sRGB but this can be overridden.
|
// Ghostty defaults to sRGB but this can be overridden.
|
||||||
switch (ghostty.config.windowColorspace) {
|
switch (config.windowColorspace) {
|
||||||
case "display-p3":
|
case "display-p3":
|
||||||
window.colorSpace = .displayP3
|
window.colorSpace = .displayP3
|
||||||
case "srgb":
|
case "srgb":
|
||||||
@ -324,7 +328,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
// 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
|
window.isOpaque = false
|
||||||
|
|
||||||
// This is weird, but we don't use ".clear" because this creates a look that
|
// 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)
|
toggleFullscreen(mode: .nonNative)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) {
|
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||||
syncAppearance()
|
// 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
|
/// The previous frame information from the window
|
||||||
private var savedFrame: SavedFrame? = nil
|
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 {
|
struct SavedFrame {
|
||||||
let window: NSRect
|
let window: NSRect
|
||||||
let screen: NSRect
|
let screen: NSRect
|
||||||
@ -74,6 +77,7 @@ class BaseTerminalController: NSWindowController,
|
|||||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
) {
|
) {
|
||||||
self.ghostty = ghostty
|
self.ghostty = ghostty
|
||||||
|
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||||
|
|
||||||
super.init(window: nil)
|
super.init(window: nil)
|
||||||
|
|
||||||
@ -93,6 +97,11 @@ class BaseTerminalController: NSWindowController,
|
|||||||
selector: #selector(didChangeScreenParametersNotification),
|
selector: #selector(didChangeScreenParametersNotification),
|
||||||
name: NSApplication.didChangeScreenParametersNotification,
|
name: NSApplication.didChangeScreenParametersNotification,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyConfigDidChangeBase(_:)),
|
||||||
|
name: .ghosttyConfigDidChange,
|
||||||
|
object: nil)
|
||||||
|
|
||||||
// Listen for local events that we need to know of outside of
|
// Listen for local events that we need to know of outside of
|
||||||
// single surface handlers.
|
// single surface handlers.
|
||||||
@ -191,6 +200,20 @@ class BaseTerminalController: NSWindowController,
|
|||||||
window.setFrame(newFrame, display: true)
|
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
|
// MARK: Local Events
|
||||||
|
|
||||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||||
@ -245,7 +268,7 @@ class BaseTerminalController: NSWindowController,
|
|||||||
func pwdDidChange(to: URL?) {
|
func pwdDidChange(to: URL?) {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
|
||||||
if ghostty.config.macosTitlebarProxyIcon == .visible {
|
if derivedConfig.macosTitlebarProxyIcon == .visible {
|
||||||
// Use the 'to' URL directly
|
// Use the 'to' URL directly
|
||||||
window.representedURL = to
|
window.representedURL = to
|
||||||
} else {
|
} else {
|
||||||
@ -255,7 +278,7 @@ class BaseTerminalController: NSWindowController,
|
|||||||
|
|
||||||
|
|
||||||
func cellSizeDidChange(to: NSSize) {
|
func cellSizeDidChange(to: NSSize) {
|
||||||
guard ghostty.config.windowStepResize else { return }
|
guard derivedConfig.windowStepResize else { return }
|
||||||
self.window?.contentResizeIncrements = to
|
self.window?.contentResizeIncrements = to
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,4 +586,19 @@ class BaseTerminalController: NSWindowController,
|
|||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.resetTerminal(surface: surface)
|
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 Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
/// A classic, tabbed terminal experience.
|
/// A classic, tabbed terminal experience.
|
||||||
@ -20,6 +21,12 @@ class TerminalController: BaseTerminalController {
|
|||||||
/// For example, terminals executing custom scripts are not restorable.
|
/// For example, terminals executing custom scripts are not restorable.
|
||||||
private var restorable: Bool = true
|
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,
|
init(_ ghostty: Ghostty.App,
|
||||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
@ -31,6 +38,9 @@ class TerminalController: BaseTerminalController {
|
|||||||
// restoration.
|
// restoration.
|
||||||
self.restorable = (base?.command ?? "") == ""
|
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)
|
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||||
|
|
||||||
// Setup our notifications for behaviors
|
// Setup our notifications for behaviors
|
||||||
@ -50,6 +60,12 @@ class TerminalController: BaseTerminalController {
|
|||||||
selector: #selector(onGotoTab),
|
selector: #selector(onGotoTab),
|
||||||
name: Ghostty.Notification.ghosttyGotoTab,
|
name: Ghostty.Notification.ghosttyGotoTab,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
|
name: .ghosttyConfigDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(onFrameDidChange),
|
selector: #selector(onFrameDidChange),
|
||||||
@ -80,10 +96,38 @@ class TerminalController: BaseTerminalController {
|
|||||||
|
|
||||||
//MARK: - Methods
|
//MARK: - Methods
|
||||||
|
|
||||||
func configDidReload() {
|
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
|
||||||
guard let window = window as? TerminalWindow else { return }
|
// Get our managed configuration object out
|
||||||
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
|
guard let config = notification.userInfo?[
|
||||||
syncAppearance()
|
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
|
/// Update the accessory view of each tab according to the keyboard
|
||||||
@ -144,28 +188,23 @@ class TerminalController: BaseTerminalController {
|
|||||||
self.relabelTabs()
|
self.relabelTabs()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncAppearance() {
|
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||||
guard let window = self.window as? TerminalWindow else { return }
|
guard let window = self.window as? TerminalWindow else { return }
|
||||||
|
|
||||||
// If our window is not visible, then delay this. This is possible specifically
|
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||||
// during state restoration but probably in other scenarios as well. To delay,
|
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||||
// we just loop directly on the dispatch queue. We have to delay because some
|
// at some point when a surface becomes focused.
|
||||||
// APIs such as window blur have no effect unless the window is visible.
|
guard window.isVisible else { return }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the font for the window and tab titles.
|
// 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)
|
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
|
||||||
} else {
|
} else {
|
||||||
window.titlebarFont = nil
|
window.titlebarFont = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
// 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
|
window.isOpaque = false
|
||||||
|
|
||||||
// This is weird, but we don't use ".clear" because this creates a look that
|
// 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.backgroundColor = .windowBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
window.hasShadow = ghostty.config.macosWindowShadow
|
window.hasShadow = surfaceConfig.macosWindowShadow
|
||||||
|
|
||||||
guard window.hasStyledTabs else { return }
|
guard window.hasStyledTabs else { return }
|
||||||
|
|
||||||
// The titlebar is always updated. We don't need to worry about opacity
|
// Our background color depends on if our focused surface borders the top or not.
|
||||||
// because we handle it here.
|
// If it does, we match the focused surface. If it doesn't, we use the app
|
||||||
let backgroundColor = OSColor(ghostty.config.backgroundColor)
|
// configuration.
|
||||||
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
|
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) {
|
if (window.isOpaque) {
|
||||||
// Bg color is only synced if we have no transparency. This is because
|
// Bg color is only synced if we have no transparency. This is because
|
||||||
@ -210,6 +261,12 @@ class TerminalController: BaseTerminalController {
|
|||||||
override func windowDidLoad() {
|
override func windowDidLoad() {
|
||||||
guard let window = window as? TerminalWindow else { return }
|
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.
|
// Setting all three of these is required for restoration to work.
|
||||||
window.isRestorable = restorable
|
window.isRestorable = restorable
|
||||||
if (restorable) {
|
if (restorable) {
|
||||||
@ -218,13 +275,13 @@ class TerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If window decorations are disabled, remove our title
|
// 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
|
// Terminals typically operate in sRGB color space and macOS defaults
|
||||||
// to "native" which is typically P3. There is a lot more resources
|
// to "native" which is typically P3. There is a lot more resources
|
||||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||||
// Ghostty defaults to sRGB but this can be overridden.
|
// Ghostty defaults to sRGB but this can be overridden.
|
||||||
switch (ghostty.config.windowColorspace) {
|
switch (config.windowColorspace) {
|
||||||
case "display-p3":
|
case "display-p3":
|
||||||
window.colorSpace = .displayP3
|
window.colorSpace = .displayP3
|
||||||
case "srgb":
|
case "srgb":
|
||||||
@ -256,30 +313,30 @@ class TerminalController: BaseTerminalController {
|
|||||||
window.center()
|
window.center()
|
||||||
|
|
||||||
// Make sure our theme is set on the window so styling is correct.
|
// 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)
|
window.windowTheme = .init(rawValue: windowTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle titlebar tabs config option. Something about what we do while setting up the
|
// 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
|
// 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.
|
// 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.tabbingMode = .preferred
|
||||||
window.titlebarTabs = true
|
window.titlebarTabs = true
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
window.tabbingMode = .automatic
|
window.tabbingMode = .automatic
|
||||||
}
|
}
|
||||||
} else if (ghostty.config.macosTitlebarStyle == "transparent") {
|
} else if (config.macosTitlebarStyle == "transparent") {
|
||||||
window.transparentTabs = true
|
window.transparentTabs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if window.hasStyledTabs {
|
if window.hasStyledTabs {
|
||||||
// Set the background color of the window
|
// Set the background color of the window
|
||||||
let backgroundColor = NSColor(ghostty.config.backgroundColor)
|
let backgroundColor = NSColor(config.backgroundColor)
|
||||||
window.backgroundColor = backgroundColor
|
window.backgroundColor = backgroundColor
|
||||||
|
|
||||||
// This makes sure our titlebar renders correctly when there is a transparent background
|
// 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
|
// 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 our titlebar style is "hidden" we adjust the style appropriately
|
||||||
if (ghostty.config.macosTitlebarStyle == "hidden") {
|
if (config.macosTitlebarStyle == "hidden") {
|
||||||
window.styleMask = [
|
window.styleMask = [
|
||||||
// We need `titled` in the mask to get the normal window frame
|
// We need `titled` in the mask to get the normal window frame
|
||||||
.titled,
|
.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.
|
// Apply any additional appearance-related properties to the new window. We
|
||||||
syncAppearance()
|
// 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.
|
// 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.
|
// Custom toolbar-based title used when titlebar tabs are enabled.
|
||||||
if let toolbar = window.toolbar as? TerminalToolbar {
|
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
|
// Updating the title text as above automatically reveals the
|
||||||
// native title view in macOS 15.0 and above. Since we're using
|
// native title view in macOS 15.0 and above. Since we're using
|
||||||
// a custom view instead, we need to re-hide it.
|
// a custom view instead, we need to re-hide it.
|
||||||
@ -485,6 +544,36 @@ class TerminalController: BaseTerminalController {
|
|||||||
window.surfaceIsZoomed = to
|
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
|
//MARK: - Notifications
|
||||||
|
|
||||||
@objc private func onMoveTab(notification: SwiftUI.Notification) {
|
@objc private func onMoveTab(notification: SwiftUI.Notification) {
|
||||||
@ -593,4 +682,19 @@ class TerminalController: BaseTerminalController {
|
|||||||
|
|
||||||
toggleFullscreen(mode: fullscreenMode)
|
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
|
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) {
|
init(_ ghostty: Ghostty.App) {
|
||||||
self.ghostty = ghostty
|
self.ghostty = ghostty
|
||||||
|
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||||
|
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
@ -51,6 +55,11 @@ class TerminalManager {
|
|||||||
selector: #selector(onNewWindow),
|
selector: #selector(onNewWindow),
|
||||||
name: Ghostty.Notification.ghosttyNewWindow,
|
name: Ghostty.Notification.ghosttyNewWindow,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
|
name: .ghosttyConfigDidChange,
|
||||||
|
object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -70,8 +79,8 @@ class TerminalManager {
|
|||||||
if let parent = focusedSurface?.window,
|
if let parent = focusedSurface?.window,
|
||||||
parent.styleMask.contains(.fullScreen) {
|
parent.styleMask.contains(.fullScreen) {
|
||||||
window.toggleFullScreen(nil)
|
window.toggleFullScreen(nil)
|
||||||
} else if ghostty.config.windowFullscreen {
|
} else if derivedConfig.windowFullscreen {
|
||||||
switch (ghostty.config.windowFullscreenMode) {
|
switch (derivedConfig.windowFullscreenMode) {
|
||||||
case .native:
|
case .native:
|
||||||
// Native has to be done immediately so that our stylemask contains
|
// Native has to be done immediately so that our stylemask contains
|
||||||
// fullscreen for the logic later in this method.
|
// 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
|
// If we're non-native then we have to do it on a later loop
|
||||||
// so that the content view is setup.
|
// so that the content view is setup.
|
||||||
DispatchQueue.main.async {
|
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
|
// If we have the "hidden" titlebar style we want to create new
|
||||||
// tabs as windows instead, so just skip adding it to the parent.
|
// 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.
|
// Add the window to the tab group and show it.
|
||||||
switch ghostty.config.windowNewTabPosition {
|
switch derivedConfig.windowNewTabPosition {
|
||||||
case "end":
|
case "end":
|
||||||
// If we already have a tab group and we want the new tab to open at the end,
|
// If we already have a tab group and we want the new tab to open at the end,
|
||||||
// then we use the last window in the tab group as the parent.
|
// then we use the last window in the tab group as the parent.
|
||||||
@ -325,4 +334,39 @@ class TerminalManager {
|
|||||||
|
|
||||||
self.newTab(to: window, withBaseConfig: config)
|
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
|
// If our configuration is "never" then we never restore the state
|
||||||
// no matter what.
|
// no matter what. Note its safe to use "ghostty.config" directly here
|
||||||
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
|
// 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)
|
completionHandler(nil, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import SwiftUI
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
extension Ghostty {
|
extension Ghostty {
|
||||||
@ -5,6 +6,33 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Ghostty.Action {
|
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 {
|
struct MoveTab {
|
||||||
let amount: Int
|
let amount: Int
|
||||||
|
|
||||||
|
@ -3,9 +3,6 @@ import UserNotifications
|
|||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
protocol GhosttyAppDelegate: AnyObject {
|
protocol GhosttyAppDelegate: AnyObject {
|
||||||
/// Called when the configuration did finish reloading.
|
|
||||||
func configDidReload(_ app: Ghostty.App)
|
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
/// Called when a callback needs access to a specific surface. This should return nil
|
/// Called when a callback needs access to a specific surface. This should return nil
|
||||||
/// when the surface is no longer valid.
|
/// when the surface is no longer valid.
|
||||||
@ -380,16 +377,6 @@ extension Ghostty {
|
|||||||
let state = Unmanaged<Self>.fromOpaque(userdata!).takeUnretainedValue()
|
let state = Unmanaged<Self>.fromOpaque(userdata!).takeUnretainedValue()
|
||||||
state.config = newConfig
|
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
|
return newConfig.config
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,8 +511,12 @@ extension Ghostty {
|
|||||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||||
keySequence(app, target: target, v: action.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:
|
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||||
fallthrough
|
colorChange(app, target: target, change: action.action.color_change)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||||
fallthrough
|
fallthrough
|
||||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
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
|
// MARK: User Notifications
|
||||||
|
|
||||||
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
/// 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 {
|
deinit {
|
||||||
self.config = nil
|
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
|
/// 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
|
/// top-left-most view. This is used when creating a split or closing a split to find the
|
||||||
/// next view to send focus to.
|
/// 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
|
// MARK: - Sequence
|
||||||
|
|
||||||
func makeIterator() -> IndexingIterator<[Leaf]> {
|
func makeIterator() -> IndexingIterator<[Leaf]> {
|
||||||
|
@ -206,6 +206,14 @@ extension Ghostty {
|
|||||||
// MARK: Surface Notification
|
// MARK: Surface Notification
|
||||||
|
|
||||||
extension Notification.Name {
|
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.
|
/// Goto tab. Has tab index in the userinfo.
|
||||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||||
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
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.
|
/// Used to pass a configuration along when creating a new tab/window/split.
|
||||||
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
|
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
|
/// 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.
|
/// userdata has one key "direction" with the direction to split to.
|
||||||
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")
|
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")
|
||||||
|
@ -48,6 +48,13 @@ extension Ghostty {
|
|||||||
// Whether the pointer should be visible or not
|
// Whether the pointer should be visible or not
|
||||||
@Published private(set) var pointerStyle: BackportPointerStyle = .default
|
@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
|
// An initial size to request for a window. This will only affect
|
||||||
// then the view is moved to a new window.
|
// then the view is moved to a new window.
|
||||||
var initialSize: NSSize? = nil
|
var initialSize: NSSize? = nil
|
||||||
@ -114,6 +121,13 @@ extension Ghostty {
|
|||||||
self.markedText = NSMutableAttributedString()
|
self.markedText = NSMutableAttributedString()
|
||||||
self.uuid = uuid ?? .init()
|
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
|
// 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
|
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||||
// can do SOMETHING.
|
// can do SOMETHING.
|
||||||
@ -137,6 +151,16 @@ extension Ghostty {
|
|||||||
selector: #selector(ghosttyDidEndKeySequence),
|
selector: #selector(ghosttyDidEndKeySequence),
|
||||||
name: Ghostty.Notification.didEndKeySequence,
|
name: Ghostty.Notification.didEndKeySequence,
|
||||||
object: self)
|
object: self)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
|
name: .ghosttyConfigDidChange,
|
||||||
|
object: self)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyColorDidChange(_:)),
|
||||||
|
name: .ghosttyColorDidChange,
|
||||||
|
object: self)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(windowDidChangeScreen),
|
selector: #selector(windowDidChangeScreen),
|
||||||
@ -333,6 +357,31 @@ extension Ghostty {
|
|||||||
keySequence = []
|
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) {
|
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||||
@ -1025,6 +1074,27 @@ extension Ghostty {
|
|||||||
Ghostty.moveFocus(to: self)
|
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)
|
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 {
|
func darken(by amount: CGFloat) -> OSColor {
|
||||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||||
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
|
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
|
/// Update the configuration associated with the app. This can only be
|
||||||
/// called from the main thread. The caller owns the config memory. The
|
/// called from the main thread. The caller owns the config memory. The
|
||||||
/// memory can be freed immediately when this returns.
|
/// 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.
|
// Go through and update all of the surface configurations.
|
||||||
for (self.surfaces.items) |surface| {
|
for (self.surfaces.items) |surface| {
|
||||||
try surface.core_surface.handleMessage(.{ .change_config = config });
|
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
|
/// 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", .{});
|
log.debug("reloading configuration", .{});
|
||||||
if (try rt_app.reloadConfig()) |new| {
|
if (try rt_app.reloadConfig()) |new| {
|
||||||
log.debug("new configuration received, applying", .{});
|
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| {
|
self.queueRender() catch |err| {
|
||||||
log.warn("failed to notify renderer of config change err={}", .{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.
|
/// Returns true if the terminal has a selection.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
|
const configpkg = @import("../config.zig");
|
||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
@ -200,6 +201,20 @@ pub const Action = union(Key) {
|
|||||||
/// on the app or surface.
|
/// on the app or surface.
|
||||||
config_change_conditional_state,
|
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
|
/// Sync with: ghostty_action_tag_e
|
||||||
pub const Key = enum(c_int) {
|
pub const Key = enum(c_int) {
|
||||||
new_window,
|
new_window,
|
||||||
@ -236,6 +251,7 @@ pub const Action = union(Key) {
|
|||||||
key_sequence,
|
key_sequence,
|
||||||
color_change,
|
color_change,
|
||||||
config_change_conditional_state,
|
config_change_conditional_state,
|
||||||
|
config_change,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sync with: ghostty_action_u
|
/// Sync with: ghostty_action_u
|
||||||
@ -497,3 +513,18 @@ pub const ColorKind = enum(c_int) {
|
|||||||
// 0+ values indicate a palette index
|
// 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,
|
.color_change,
|
||||||
.pwd,
|
.pwd,
|
||||||
.config_change_conditional_state,
|
.config_change_conditional_state,
|
||||||
|
.config_change,
|
||||||
=> log.info("unimplemented action={}", .{action}),
|
=> log.info("unimplemented action={}", .{action}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -488,6 +488,7 @@ pub fn performAction(
|
|||||||
.render_inspector,
|
.render_inspector,
|
||||||
.renderer_health,
|
.renderer_health,
|
||||||
.color_change,
|
.color_change,
|
||||||
|
.config_change,
|
||||||
=> log.warn("unimplemented action={}", .{action}),
|
=> log.warn("unimplemented action={}", .{action}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ export fn ghostty_config_new() ?*Config {
|
|||||||
|
|
||||||
result.* = Config.default(global.alloc) catch |err| {
|
result.* = Config.default(global.alloc) catch |err| {
|
||||||
log.err("error creating config err={}", .{err});
|
log.err("error creating config err={}", .{err});
|
||||||
|
global.alloc.destroy(result);
|
||||||
return null;
|
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.
|
/// Load the configuration from the CLI args.
|
||||||
export fn ghostty_config_load_cli_args(self: *Config) void {
|
export fn ghostty_config_load_cli_args(self: *Config) void {
|
||||||
self.loadCliArgs(global.alloc) catch |err| {
|
self.loadCliArgs(global.alloc) catch |err| {
|
||||||
|
@ -365,7 +365,6 @@ const c = @cImport({
|
|||||||
/// be fixed in a future update:
|
/// be fixed in a future update:
|
||||||
///
|
///
|
||||||
/// - macOS: titlebar tabs style is not updated when switching themes.
|
/// - macOS: titlebar tabs style is not updated when switching themes.
|
||||||
/// - macOS: native titlebar style is not supported.
|
|
||||||
///
|
///
|
||||||
theme: ?Theme = null,
|
theme: ?Theme = null,
|
||||||
|
|
||||||
@ -1519,6 +1518,13 @@ keybind: Keybinds = .{},
|
|||||||
/// This makes a more seamless window appearance but looks a little less
|
/// 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.
|
/// 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
|
/// The "tabs" style is a completely custom titlebar that integrates the
|
||||||
/// tab bar into the titlebar. This titlebar always matches the background
|
/// tab bar into the titlebar. This titlebar always matches the background
|
||||||
/// color of the terminal. There are some limitations to this style:
|
/// 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
|
/// but its one I think is the most aesthetically pleasing and works in
|
||||||
/// most cases.
|
/// 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.
|
/// Changing this option at runtime only applies to new windows.
|
||||||
@"macos-titlebar-style": MacTitlebarStyle = .transparent,
|
@"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
|
// This setting doesn't make sense with different light/dark themes
|
||||||
// because it'll force the theme based on the Ghostty theme.
|
// because it'll force the theme based on the Ghostty theme.
|
||||||
if (self.@"window-theme" == .auto) self.@"window-theme" = .system;
|
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