diff --git a/Sources/SwiftTerm/Apple/AppleTerminalView.swift b/Sources/SwiftTerm/Apple/AppleTerminalView.swift index ed17f9a1..0ebdeee5 100644 --- a/Sources/SwiftTerm/Apple/AppleTerminalView.swift +++ b/Sources/SwiftTerm/Apple/AppleTerminalView.swift @@ -1284,6 +1284,16 @@ extension TerminalView { } var placeholderImageCache: [UInt32: TTImage] = [:] + #if os(macOS) + // Clear the invalidated region before painting. We fill only cells that carry + // an explicit background; default-background cells rely on transparent backing- + // store pixels showing the layer's background color. AppKit clears the backing + // store only on a full-view redraw, so a partial repaint (a restricted DECSTBM + // scroll region, line insert/delete) otherwise keeps stale glyphs/backgrounds. + // Clear to transparent — not fill — so a translucent background is preserved. + context.clear(dirtyRect) + #endif + for row in firstRow...lastRow { if row < 0 { continue @@ -1773,6 +1783,14 @@ extension TerminalView { let oh = region.height let oy = region.origin.y region = CGRect (x: 0, y: 0, width: frame.width, height: oh + oy) + } else { + // Region ends mid-screen (a restricted DECSTBM region): extend the + // invalidation down by one cell so the sub-cell remainder just below the + // band's bottom row (descenders / tall unicode) is cleared too. Previously + // only rowEnd == rows-1 got this, leaving a one-row ghost below the region. + let extra = cellDimension.height + let newY = max (0, region.origin.y - extra) + region = CGRect (x: 0, y: newY, width: frame.width, height: region.maxY - newY) } #if canImport(MetalKit) if metalView != nil { diff --git a/Sources/SwiftTerm/Terminal.swift b/Sources/SwiftTerm/Terminal.swift index 4e8532ea..c822d27c 100644 --- a/Sources/SwiftTerm/Terminal.swift +++ b/Sources/SwiftTerm/Terminal.swift @@ -2523,6 +2523,11 @@ open class Terminal { } // this.maxRange(); updateRange (startLine: buffer.y, endLine: buffer.scrollBottom) + // A restricted region leaves stale pixels / a bottom-edge ghost outside + // [y, scrollBottom] on the CG renderer (as in scroll()); the range above + // already covers full-screen, so widen to the whole viewport only when the + // region is restricted. + refreshScrolledRegion(top: buffer.scrollTop, bottom: buffer.scrollBottom, canBlit: true) } // @@ -4750,7 +4755,7 @@ open class Terminal { } } // this.maxRange(); - updateRange (startLine: buffer.scrollTop, endLine: buffer.scrollBottom) + refreshScrolledRegion(top: buffer.scrollTop, bottom: buffer.scrollBottom, canBlit: false) } // @@ -4786,7 +4791,7 @@ open class Terminal { } } // this.maxRange(); - updateRange (startLine: buffer.scrollTop, endLine: buffer.scrollBottom) + refreshScrolledRegion(top: buffer.scrollTop, bottom: buffer.scrollBottom, canBlit: false) } // @@ -4863,6 +4868,11 @@ open class Terminal { // this.maxRange(); updateRange (startLine: buffer.y, endLine: buffer.scrollBottom) + // A restricted region leaves stale pixels / a bottom-edge ghost outside + // [y, scrollBottom] on the CG renderer (as in scroll()); the range above + // already covers full-screen, so widen to the whole viewport only when the + // region is restricted. + refreshScrolledRegion(top: buffer.scrollTop, bottom: buffer.scrollBottom, canBlit: true) } // @@ -5260,6 +5270,16 @@ open class Terminal { var blankLine: BufferLine = BufferLine(cols: 0) + /// Flag the scrolled region dirty. The CoreGraphics renderer now clears any + /// dirtied region before painting (see AppleTerminalView), so flagging just + /// [top, bottom] fixes the stale rows / bottom ghost — no whole-viewport repaint + /// needed. Full-screen + scrollback (`canBlit`) keeps the cheap path. + private func refreshScrolledRegion(top: Int, bottom: Int, canBlit: Bool) { + if top != 0 || bottom != rows - 1 || !canBlit { + updateRange(startLine: top, endLine: bottom) + } + } + public func scroll (isWrapped: Bool = false) { let buffer = self.buffer @@ -5385,9 +5405,7 @@ open class Terminal { updateRange (scrollTop, scrolling: true) updateRange (scrollBottom, scrolling: true) - if !hasScrollback { - updateRange(startLine: scrollTop, endLine: scrollBottom) - } + refreshScrolledRegion(top: scrollTop, bottom: scrollBottom, canBlit: hasScrollback) if buffer.hasAnyImages { updateKittyRelativePlacementsForCurrentBuffer() @@ -5826,7 +5844,7 @@ open class Terminal { } buffer.lines [topRow] = buffer.getBlankLine (attribute: eraseAttr ()) } - updateRange (startLine: buffer.scrollTop, endLine: buffer.scrollBottom) + refreshScrolledRegion(top: buffer.scrollTop, bottom: buffer.scrollBottom, canBlit: false) } } else if buffer.y > 0 { buffer.y -= 1