@@ -17,6 +17,7 @@ final class LitheTerminalView: LocalProcessTerminalView {
1717 /// App theme light/dark preference while the view is not yet in a window; nil follows the system appearance.
1818 var preferredAppearanceIsDark : ( ( ) -> Bool ? ) ?
1919 private var lastReportedBackgroundPayload : String ?
20+ private var backgroundChangeNotifications = MacTerminalBackgroundSubscription ( )
2021
2122 override init ( frame frameRect : NSRect ) {
2223 super . init ( frame : frameRect )
@@ -158,19 +159,20 @@ final class LitheTerminalView: LocalProcessTerminalView {
158159
159160 override func viewDidChangeEffectiveAppearance ( ) {
160161 super . viewDidChangeEffectiveAppearance ( )
161- applyThemeColors ( )
162+ applyThemeColors ( theme : LitheTheme . activeTheme )
162163 }
163164
164- func applyThemeColors ( ) {
165- // Outside a window, effectiveAppearance follows the system; the app theme decides the reported background.
165+ func applyThemeColors ( theme : AppColorTheme , isDark requestedIsDark : Bool ? = nil ) {
166166 let isDark : Bool
167- if window == nil , let preferredAppearanceIsDark {
167+ if let requestedIsDark {
168+ isDark = requestedIsDark
169+ } else if window == nil , let preferredAppearanceIsDark {
168170 isDark = preferredAppearanceIsDark ( )
169171 ?? ( effectiveAppearance . bestMatch ( from : [ . aqua , . darkAqua ] ) == . darkAqua )
170172 } else {
171173 isDark = effectiveAppearance . bestMatch ( from : [ . aqua , . darkAqua ] ) == . darkAqua
172174 }
173- let editorColor = LitheTheme . nsColor ( . editor , isDark : isDark )
175+ let editorColor = LitheTheme . nsColor ( . editor , theme : theme , isDark : isDark )
174176 let palette = MacTerminalPalette (
175177 preferences : UserDefaults . standard . persistentDomain ( forName : " com.apple.Terminal " ) ,
176178 backgroundColor : editorColor
@@ -192,7 +194,7 @@ final class LitheTerminalView: LocalProcessTerminalView {
192194 // Terminal.app profiles may use a foreground color that assumes their
193195 // own opaque background. The terminal is composited over Lithe's
194196 // editor or wallpaper, so keep the default text tied to the app theme.
195- nativeForegroundColor = LitheTheme . nsColor ( . primaryText , isDark : isDark )
197+ nativeForegroundColor = LitheTheme . nsColor ( . primaryText , theme : theme , isDark : isDark )
196198 caretColor = isDark
197199 ? NSColor ( srgbRed : 0.35 , green : 0.67 , blue : 0.98 , alpha : 1 )
198200 : NSColor ( srgbRed : 0.18 , green : 0.43 , blue : 0.79 , alpha : 1 )
@@ -212,38 +214,51 @@ final class LitheTerminalView: LocalProcessTerminalView {
212214 reportBackgroundChangeIfNeeded ( )
213215 }
214216
215- /// On reported- background change, sends the OSC 11 answer plus kitty DSR 997; trackers re-query, others ignore .
217+ /// Reports background changes only to programs subscribed to kitty DEC mode 2031 .
216218 private func reportBackgroundChangeIfNeeded ( ) {
217219 let payload = MacTerminalBackgroundReporting . osc11Payload ( for : nativeBackgroundColor )
218220 guard payload != lastReportedBackgroundPayload else { return }
219221 lastReportedBackgroundPayload = payload
220- guard process . running else { return }
221- terminal . sendResponse ( text : MacTerminalBackgroundReporting . changeNotification ( for : nativeBackgroundColor ) )
222+ guard process . running ,
223+ let notification = MacTerminalBackgroundReporting . changeNotification (
224+ for : nativeBackgroundColor ,
225+ isSubscribed : backgroundChangeNotifications . isSubscribed
226+ ) else { return }
227+ terminal . sendResponse ( text : notification )
222228 }
223229
224- func setWorkbenchBackgroundVisible ( _ isVisible : Bool ) {
225- guard showsWorkbenchBackground != isVisible else { return }
230+ func updateWorkbenchSurface ( isVisible : Bool ) {
226231 showsWorkbenchBackground = isVisible
227- applyThemeColors ( )
232+ applyThemeColors ( theme : LitheTheme . activeTheme )
228233 }
229234
235+ func updateWorkbenchSurface ( isVisible : Bool , theme : AppColorTheme , isDark : Bool ) {
236+ showsWorkbenchBackground = isVisible
237+ applyThemeColors ( theme : theme , isDark : isDark )
238+ }
239+
240+
230241 override func requestOpenLink ( source : SwiftTerm . TerminalView , link : String , params : [ String : String ] ) {
231242 onOpenLink ? ( link , params )
232243 }
233244
234245 override func dataReceived ( slice : ArraySlice < UInt8 > ) {
246+ backgroundChangeNotifications . feed ( slice )
235247 onProcessOutput ? ( Data ( slice ) )
236248 super . dataReceived ( slice : slice )
237249 }
238250
251+ func resetBackgroundChangeNotifications ( ) {
252+ backgroundChangeNotifications . reset ( )
253+ }
254+
239255 deinit {
240256 NotificationCenter . default . removeObserver ( self )
241257 }
242258}
243259
244- extension LitheTerminalView : WorkbenchBackgroundRendering { }
245260
246- /// Report pushed when the background changes: SwiftTerm's own OSC 11 answer plus the kitty DSR 997 notification .
261+ /// Reports changed backgrounds to subscribers; kitty DSR 997 uses 1 for dark and 2 for light .
247262enum MacTerminalBackgroundReporting {
248263 static func osc11Payload ( for color : NSColor ) -> String {
249264 let srgb = color . usingColorSpace ( . sRGB ) ?? color
@@ -255,8 +270,99 @@ enum MacTerminalBackgroundReporting {
255270 return " rgb: \( channel ( red ) ) / \( channel ( green ) ) / \( channel ( blue ) ) "
256271 }
257272
258- static func changeNotification ( for color : NSColor ) -> String {
259- " \u{1b} ]11; \( osc11Payload ( for : color ) ) \u{1b} \\ \u{1b} [?997;2n "
273+ static func reportsDarkBackground ( for color : NSColor ) -> Bool {
274+ let srgb = color . usingColorSpace ( . sRGB ) ?? color
275+ var red : CGFloat = 0 , green : CGFloat = 0 , blue : CGFloat = 0 , alpha : CGFloat = 1
276+ srgb . getRed ( & red , green : & green , blue : & blue , alpha : & alpha )
277+ func linearize ( _ component : CGFloat ) -> Double {
278+ let value = min ( 1 , max ( 0 , Double ( component ) ) )
279+ return value <= 0.04045 ? value / 12.92 : pow ( ( value + 0.055 ) / 1.055 , 2.4 )
280+ }
281+ let luminance = 0.2126 * linearize ( red ) + 0.7152 * linearize ( green ) + 0.0722 * linearize ( blue )
282+ return luminance < 0.5
283+ }
284+
285+ static func changeNotification ( for color : NSColor , isSubscribed : Bool ) -> String ? {
286+ guard isSubscribed else { return nil }
287+ let backgroundState = reportsDarkBackground ( for : color ) ? 1 : 2
288+ return " \u{1b} ]11; \( osc11Payload ( for : color ) ) \u{1b} \\ \u{1b} [?997; \( backgroundState ) n "
289+ }
290+ }
291+
292+ /// Tracks kitty DEC mode 2031 so change notifications are sent only to programs that requested them.
293+ struct MacTerminalBackgroundSubscription {
294+ private( set) var isSubscribed = false
295+ private var pendingEscape : [ UInt8 ] = [ ]
296+
297+ private static let escape = UInt8 ( 0x1b )
298+ private static let mode2031 : [ UInt8 ] = [ 0x32 , 0x30 , 0x33 , 0x31 ]
299+ private static let altScreenModes : [ [ UInt8 ] ] = [
300+ [ 0x34 , 0x37 ] ,
301+ [ 0x31 , 0x30 , 0x34 , 0x37 ] ,
302+ [ 0x31 , 0x30 , 0x34 , 0x39 ] ,
303+ ]
304+ private static let maximumSequenceLength = 64
305+
306+ init ( ) {
307+ pendingEscape . reserveCapacity ( Self . maximumSequenceLength )
308+ }
309+
310+ mutating func feed < S : Sequence > ( _ bytes : S ) where S . Element == UInt8 {
311+ for byte in bytes {
312+ if pendingEscape . isEmpty {
313+ if byte == Self . escape { pendingEscape . append ( byte ) }
314+ continue
315+ }
316+ if byte == Self . escape {
317+ pendingEscape = [ Self . escape ]
318+ continue
319+ }
320+ pendingEscape . append ( byte )
321+ if pendingEscape == [ Self . escape , 0x63 ] {
322+ isSubscribed = false
323+ pendingEscape . removeAll ( keepingCapacity : true )
324+ continue
325+ }
326+ if pendingEscape . count == 2 {
327+ if byte != 0x5b { pendingEscape . removeAll ( keepingCapacity : true ) }
328+ continue
329+ }
330+ if pendingEscape . count == 3 {
331+ if byte != 0x3f { pendingEscape . removeAll ( keepingCapacity : true ) }
332+ continue
333+ }
334+ if ( 0x40 ... 0x7e ) . contains ( byte ) {
335+ if byte == 0x68 || byte == 0x6c ,
336+ contains ( Self . mode2031 , in : pendingEscape [ 3 ..< ( pendingEscape . count - 1 ) ] ) {
337+ isSubscribed = byte == 0x68
338+ } else if byte == 0x6c ,
339+ leavesAltScreen ( pendingEscape [ 3 ..< ( pendingEscape . count - 1 ) ] ) {
340+ // A subscribed TUI runs inside the alternate screen; when it leaves, the shell must not inherit pushes.
341+ isSubscribed = false
342+ }
343+ pendingEscape . removeAll ( keepingCapacity : true )
344+ } else if pendingEscape . count >= Self . maximumSequenceLength {
345+ pendingEscape . removeAll ( keepingCapacity : true )
346+ }
347+ }
348+ }
349+
350+ mutating func reset ( ) {
351+ isSubscribed = false
352+ pendingEscape . removeAll ( keepingCapacity : true )
353+ }
354+
355+ private func contains ( _ parameter : [ UInt8 ] , in parameters : ArraySlice < UInt8 > ) -> Bool {
356+ var start = parameters . startIndex
357+ for index in parameters . indices where parameters [ index ] == 0x3b {
358+ if parameters [ start ..< index ] . elementsEqual ( parameter ) { return true }
359+ start = parameters . index ( after : index )
360+ }
361+ return parameters [ start ..< parameters . endIndex ] . elementsEqual ( parameter )
362+ }
363+
364+ private func leavesAltScreen ( _ parameters : ArraySlice < UInt8 > ) -> Bool {
365+ Self . altScreenModes . contains { contains ( $0 , in : parameters ) }
260366 }
261367}
262368
@@ -428,6 +534,13 @@ final class MacTerminalTransport: NSObject, TerminalTransport, @preconcurrency L
428534
429535 var nativeView : AnyObject { view }
430536
537+ func configureInitialSurface ( showsWorkbenchBackground : Bool ) {
538+ view . updateWorkbenchSurface ( isVisible : showsWorkbenchBackground )
539+ }
540+
541+ // Keep the exported COLORFGBG in sync with the OSC 11 answer: wallpaper transparency reports black, so it must export dark.
542+ var reportsLightBackground : Bool ? { ! MacTerminalBackgroundReporting . reportsDarkBackground ( for : view . nativeBackgroundColor ) }
543+
431544 private let scrollbackLines : Int
432545
433546 init ( scrollbackLines : Int = 2_000 , preferredAppearanceIsDark : ( ( ) -> Bool ? ) ? = nil ) {
@@ -444,7 +557,7 @@ final class MacTerminalTransport: NSObject, TerminalTransport, @preconcurrency L
444557 }
445558 view . font = Self . preferredTerminalFont ( )
446559 view . preferredAppearanceIsDark = preferredAppearanceIsDark
447- view . applyThemeColors ( )
560+ view . applyThemeColors ( theme : LitheTheme . activeTheme )
448561 view . allowMouseReporting = true
449562 view . linkReporting = . implicit
450563 view . linkHighlightMode = . hoverWithModifier
@@ -526,6 +639,7 @@ final class MacTerminalTransport: NSObject, TerminalTransport, @preconcurrency L
526639 selectedShellPath = executablePath
527640
528641 view . terminal . resetToInitialState ( )
642+ view . resetBackgroundChangeNotifications ( )
529643 var options = view . terminal . options
530644 options . termName = environment [ " TERM " ] ?? " xterm-256color "
531645 view . terminal . options = options
0 commit comments