From ad57e10772c4d3e5335b8ecab6f3e83077c14999 Mon Sep 17 00:00:00 2001 From: Hiroki Kajiwara Date: Sun, 22 Mar 2026 14:45:53 +0900 Subject: [PATCH] fix: dispatch UI updates to main thread for macOS 26 compatibility Publishing changes from background threads is not allowed in macOS 26 (Tahoe). Ghostty action callbacks run on I/O threads but were modifying AppKit view properties and posting notifications without dispatching to the main thread. Fixes: - GHOSTTY_ACTION_SCROLLBAR: wrap in DispatchQueue.main.async - GHOSTTY_ACTION_CELL_SIZE: wrap in DispatchQueue.main.async - GHOSTTY_ACTION_COLOR_CHANGE: wrap background color updates in main async - GHOSTTY_ACTION_CONFIG_CHANGE: wrap background color clear in main async --- Sources/GhosttyTerminalView.swift | 61 +++++++++++++++++-------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 2ee395f0..d349a172 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2249,24 +2249,28 @@ class GhosttyApp { } case GHOSTTY_ACTION_SCROLLBAR: let scrollbar = GhosttyScrollbar(c: action.action.scrollbar) - surfaceView.scrollbar = scrollbar - NotificationCenter.default.post( - name: .ghosttyDidUpdateScrollbar, - object: surfaceView, - userInfo: [GhosttyNotificationKey.scrollbar: scrollbar] - ) + DispatchQueue.main.async { + surfaceView.scrollbar = scrollbar + NotificationCenter.default.post( + name: .ghosttyDidUpdateScrollbar, + object: surfaceView, + userInfo: [GhosttyNotificationKey.scrollbar: scrollbar] + ) + } return true case GHOSTTY_ACTION_CELL_SIZE: let cellSize = CGSize( width: CGFloat(action.action.cell_size.width), height: CGFloat(action.action.cell_size.height) ) - surfaceView.cellSize = cellSize - NotificationCenter.default.post( - name: .ghosttyDidUpdateCellSize, - object: surfaceView, - userInfo: [GhosttyNotificationKey.cellSize: cellSize] - ) + DispatchQueue.main.async { + surfaceView.cellSize = cellSize + NotificationCenter.default.post( + name: .ghosttyDidUpdateCellSize, + object: surfaceView, + userInfo: [GhosttyNotificationKey.cellSize: cellSize] + ) + } return true case GHOSTTY_ACTION_START_SEARCH: guard let terminalSurface = surfaceView.terminalSurface else { return true } @@ -2363,7 +2367,7 @@ class GhosttyApp { case GHOSTTY_ACTION_COLOR_CHANGE: if action.action.color_change.kind == GHOSTTY_ACTION_COLOR_KIND_BACKGROUND { let change = action.action.color_change - surfaceView.backgroundColor = NSColor( + let newColor = NSColor( red: CGFloat(change.r) / 255, green: CGFloat(change.g) / 255, blue: CGFloat(change.b) / 255, @@ -2371,28 +2375,29 @@ class GhosttyApp { ) if backgroundLogEnabled { logBackground( - "surface override set tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil") override=\(surfaceView.backgroundColor?.hexString() ?? "nil") default=\(defaultBackgroundColor.hexString()) source=action.color_change.surface" + "surface override set tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil") override=\(newColor.hexString()) default=\(defaultBackgroundColor.hexString()) source=action.color_change.surface" ) } - surfaceView.applySurfaceBackground() - if backgroundLogEnabled { - logBackground("OSC background change tab=\(surfaceView.tabId?.uuidString ?? "unknown") color=\(surfaceView.backgroundColor?.description ?? "nil")") - } - DispatchQueue.main.async { + DispatchQueue.main.async { [self] in + surfaceView.backgroundColor = newColor + surfaceView.applySurfaceBackground() + if backgroundLogEnabled { + logBackground("OSC background change tab=\(surfaceView.tabId?.uuidString ?? "unknown") color=\(surfaceView.backgroundColor?.description ?? "nil")") + } surfaceView.applyWindowBackgroundIfActive() } } return true case GHOSTTY_ACTION_CONFIG_CHANGE: - if let staleOverride = surfaceView.backgroundColor { - surfaceView.backgroundColor = nil - if backgroundLogEnabled { - logBackground( - "surface override cleared tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil") cleared=\(staleOverride.hexString()) source=action.config_change.surface" - ) - } - surfaceView.applySurfaceBackground() - DispatchQueue.main.async { + DispatchQueue.main.async { [self] in + if let staleOverride = surfaceView.backgroundColor { + surfaceView.backgroundColor = nil + if backgroundLogEnabled { + logBackground( + "surface override cleared tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil") cleared=\(staleOverride.hexString()) source=action.config_change.surface" + ) + } + surfaceView.applySurfaceBackground() surfaceView.applyWindowBackgroundIfActive() } }