@@ -121,6 +121,8 @@ enum KittyGraphicsPayload {
121121
122122struct KittyGraphicsImage {
123123 let payload : KittyGraphicsPayload
124+ let byteSize : Int
125+ var lastAccessTick : UInt64
124126}
125127
126128struct KittyGraphicsPending {
@@ -135,11 +137,14 @@ final class KittyGraphicsState {
135137 var nextPlacementId : UInt32 = 1
136138 var pending : KittyGraphicsPending ?
137139 var placementsByKey : [ KittyPlacementKey : KittyPlacementRecord ] = [ : ]
140+ var totalImageBytes : Int = 0
141+ var nextImageAccessTick : UInt64 = 1
138142}
139143
140144extension Terminal {
141145 private static let kittyMaxImageBytes = 400 * 1024 * 1024
142146 private static let kittyMaxImageDimension = 10000
147+ private static let kittyMaxImageCacheBytes = 4 * 1024 * 1024 * 1024
143148
144149 func handleKittyGraphics( _ data: ArraySlice < UInt8 > ) {
145150 guard let ( control, payload) = parseKittyGraphicsControl ( data) else {
@@ -333,10 +338,7 @@ extension Terminal {
333338 }
334339
335340 if let id = resolved. imageId {
336- kittyGraphicsState. imagesById [ id] = KittyGraphicsImage ( payload: payload)
337- if let number = resolved. imageNumber {
338- kittyGraphicsState. imageNumbers [ number] = id
339- }
341+ storeKittyImage ( payload: payload, imageId: id, imageNumber: resolved. imageNumber)
340342 }
341343
342344 var displayed = true
@@ -440,10 +442,10 @@ extension Terminal {
440442 }
441443
442444 private func resolveKittyImageForDisplay( control: KittyGraphicsControl ) -> ( image: KittyGraphicsImage ? , imageId: UInt32 ? , imageNumber: UInt32 ? , shouldReply: Bool ) {
443- if let number = control. imageNumber, let imageId = kittyGraphicsState. imageNumbers [ number] , let image = kittyGraphicsState . imagesById [ imageId] {
445+ if let number = control. imageNumber, let imageId = kittyGraphicsState. imageNumbers [ number] , let image = updateKittyImageAccess ( imageId: imageId ) {
444446 return ( image, imageId, number, control. suppressResponses == 0 )
445447 }
446- if let imageId = control. imageId, let image = kittyGraphicsState . imagesById [ imageId] {
448+ if let imageId = control. imageId, let image = updateKittyImageAccess ( imageId: imageId ) {
447449 return ( image, imageId, nil , control. suppressResponses == 0 )
448450 }
449451 return ( nil , nil , nil , control. suppressResponses == 0 )
@@ -1468,9 +1470,23 @@ extension Terminal {
14681470 kittyGraphicsState. imagesById. removeAll ( )
14691471 kittyGraphicsState. imageNumbers. removeAll ( )
14701472 kittyGraphicsState. placementsByKey. removeAll ( )
1473+ kittyGraphicsState. totalImageBytes = 0
1474+ kittyGraphicsState. nextImageAccessTick = 1
14711475 updateRange ( startLine: buffer. scrollTop, endLine: buffer. scrollBottom)
14721476 }
14731477
1478+ func clearKittyImages( in buffer: Buffer , isAlternateBuffer: Bool ) {
1479+ let removedKeys = removeKittyPlacements ( in: buffer, lineRange: 0 ..< buffer. lines. count) { _ in true }
1480+ let recordKeys = removePlacementRecords { record in
1481+ record. isAlternateBuffer == isAlternateBuffer
1482+ }
1483+ let extraKeys = recordKeys. subtracting ( removedKeys)
1484+ if !extraKeys. isEmpty {
1485+ _ = removeKittyPlacementsByKey ( extraKeys)
1486+ }
1487+ cleanupUnusedKittyImages ( )
1488+ }
1489+
14741490 private func deletePlacementsVisibleOnScreen( ) {
14751491 let start = buffer. yBase
14761492 let end = min ( buffer. yBase + rows, buffer. lines. count)
@@ -1704,20 +1720,113 @@ extension Terminal {
17041720 }
17051721
17061722 private func cleanupUnusedKittyImages( ) {
1723+ let used = collectUsedKittyImageIds ( )
1724+ let unusedIds = kittyGraphicsState. imagesById. keys. filter { !used. contains ( $0) }
1725+ for id in unusedIds {
1726+ removeKittyImage ( imageId: id)
1727+ }
1728+ }
1729+
1730+ private func storeKittyImage( payload: KittyGraphicsPayload , imageId: UInt32 , imageNumber: UInt32 ? ) {
1731+ let byteSize = kittyPayloadByteSize ( payload)
1732+ let lastAccessTick = nextKittyImageAccessTick ( )
1733+ if let existing = kittyGraphicsState. imagesById [ imageId] {
1734+ kittyGraphicsState. totalImageBytes = max ( 0 , kittyGraphicsState. totalImageBytes - existing. byteSize)
1735+ }
1736+ kittyGraphicsState. imagesById [ imageId] = KittyGraphicsImage ( payload: payload,
1737+ byteSize: byteSize,
1738+ lastAccessTick: lastAccessTick)
1739+ kittyGraphicsState. totalImageBytes += byteSize
1740+ if let number = imageNumber {
1741+ kittyGraphicsState. imageNumbers [ number] = imageId
1742+ }
1743+ enforceKittyImageCacheLimit ( )
1744+ }
1745+
1746+ private func updateKittyImageAccess( imageId: UInt32 ) -> KittyGraphicsImage ? {
1747+ guard var image = kittyGraphicsState. imagesById [ imageId] else {
1748+ return nil
1749+ }
1750+ image. lastAccessTick = nextKittyImageAccessTick ( )
1751+ kittyGraphicsState. imagesById [ imageId] = image
1752+ return image
1753+ }
1754+
1755+ private func kittyPayloadByteSize( _ payload: KittyGraphicsPayload ) -> Int {
1756+ switch payload {
1757+ case . png( let data) :
1758+ return data. count
1759+ case . rgba( let bytes, _, _) :
1760+ return bytes. count
1761+ }
1762+ }
1763+
1764+ private func nextKittyImageAccessTick( ) -> UInt64 {
1765+ let tick = kittyGraphicsState. nextImageAccessTick
1766+ kittyGraphicsState. nextImageAccessTick &+= 1
1767+ return tick
1768+ }
1769+
1770+ private func enforceKittyImageCacheLimit( ) {
1771+ let limit = clampedKittyImageCacheLimitBytes ( )
1772+ guard kittyGraphicsState. totalImageBytes > limit else {
1773+ return
1774+ }
1775+
1776+ let used = collectUsedKittyImageIds ( )
1777+ let unusedIds = kittyGraphicsState. imagesById
1778+ . filter { !used. contains ( $0. key) }
1779+ . sorted { $0. value. lastAccessTick < $1. value. lastAccessTick }
1780+ . map { $0. key }
1781+ for id in unusedIds {
1782+ removeKittyImage ( imageId: id)
1783+ if kittyGraphicsState. totalImageBytes <= limit {
1784+ return
1785+ }
1786+ }
1787+
1788+ let oldestIds = kittyGraphicsState. imagesById
1789+ . sorted { $0. value. lastAccessTick < $1. value. lastAccessTick }
1790+ . map { $0. key }
1791+ for id in oldestIds {
1792+ removeKittyImage ( imageId: id)
1793+ if kittyGraphicsState. totalImageBytes <= limit {
1794+ return
1795+ }
1796+ }
1797+ }
1798+
1799+ private func clampedKittyImageCacheLimitBytes( ) -> Int {
1800+ let configured = options. kittyImageCacheLimitBytes
1801+ if configured <= 0 {
1802+ return 0
1803+ }
1804+ return min ( configured, Terminal . kittyMaxImageCacheBytes)
1805+ }
1806+
1807+ private func removeKittyImage( imageId: UInt32 ) {
1808+ guard let removed = kittyGraphicsState. imagesById. removeValue ( forKey: imageId) else {
1809+ return
1810+ }
1811+ kittyGraphicsState. totalImageBytes = max ( 0 , kittyGraphicsState. totalImageBytes - removed. byteSize)
1812+ removeKittyImageNumbers ( for: imageId)
1813+ }
1814+
1815+ private func removeKittyImageNumbers( for imageId: UInt32 ) {
1816+ let numbers = kittyGraphicsState. imageNumbers. filter { $0. value == imageId } . map { $0. key }
1817+ for number in numbers {
1818+ kittyGraphicsState. imageNumbers. removeValue ( forKey: number)
1819+ }
1820+ }
1821+
1822+ private func collectUsedKittyImageIds( ) -> Set < UInt32 > {
17071823 var used = Set < UInt32 > ( )
17081824 collectUsedKittyImageIds ( from: normalBuffer, into: & used)
17091825 collectUsedKittyImageIds ( from: altBuffer, into: & used)
17101826 for record in kittyGraphicsState. placementsByKey. values {
17111827 used. insert ( record. imageId)
17121828 }
1713- let unusedIds = kittyGraphicsState. imagesById. keys. filter { !used. contains ( $0) }
1714- for id in unusedIds {
1715- kittyGraphicsState. imagesById. removeValue ( forKey: id)
1716- }
1717- let unusedNumbers = kittyGraphicsState. imageNumbers. filter { !used. contains ( $0. value) } . map { $0. key }
1718- for number in unusedNumbers {
1719- kittyGraphicsState. imageNumbers. removeValue ( forKey: number)
1720- }
1829+ return used
17211830 }
17221831
17231832 private func collectUsedKittyImageIds( from buffer: Buffer , into set: inout Set < UInt32 > ) {
0 commit comments