From c51de4bc03eed21ea2cf209f46102a918a769026 Mon Sep 17 00:00:00 2001 From: phranck Date: Fri, 30 Jan 2026 15:46:10 +0100 Subject: [PATCH] docs: Add inline comments to complex logic in KeyEvent, FrameBuffer, ViewRenderer (E.2) - KeyEvent: Document CSI sequence format, explain final-byte role, add VT key identifier table to parseExtendedKey - FrameBuffer: Explain why ANSI stripping is needed in insertOverlay (escape sequences shift column positions) - ViewRenderer: Clarify spacer distribution algorithm in VStack/HStack (remaining space divided equally among spacers) --- Sources/TUIKit/Core/KeyEvent.swift | 35 +++++++++++---------- Sources/TUIKit/Rendering/FrameBuffer.swift | 13 ++++---- Sources/TUIKit/Rendering/ViewRenderer.swift | 9 +++--- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/Sources/TUIKit/Core/KeyEvent.swift b/Sources/TUIKit/Core/KeyEvent.swift index f75d972..d4cbe25 100644 --- a/Sources/TUIKit/Core/KeyEvent.swift +++ b/Sources/TUIKit/Core/KeyEvent.swift @@ -189,10 +189,15 @@ extension KeyEvent { } /// Parses a CSI (Control Sequence Introducer) sequence. + /// + /// CSI format: `ESC [ `. + /// The final byte identifies the key (e.g. `A` = up arrow). + /// Numeric parameters before the final byte encode extended keys + /// like Page Up/Down (`ESC [ 5 ~`). private static func parseCSISequence(_ params: [UInt8]) -> KeyEvent? { guard !params.isEmpty else { return nil } - // Arrow keys: A=up, B=down, C=right, D=left + // The last byte is the CSI function identifier switch params.last { case ASCIIByte.arrowUp: return KeyEvent(key: .up) @@ -213,9 +218,12 @@ extension KeyEvent { } } - /// Parses extended key sequences (ESC [ n ~). + /// Parses extended key sequences (`ESC [ n ~`). + /// + /// These are VT-style sequences where `n` is a numeric key identifier: + /// 1=Home, 2=Insert, 3=Delete, 4=End, 5=PageUp, 6=PageDown. private static func parseExtendedKey(_ params: [UInt8]) -> KeyEvent? { - // Parse the number before '~' + // Extract the numeric identifier before the '~' terminator let numberBytes = params.dropLast() guard let string = String(bytes: numberBytes, encoding: .ascii), let number = Int(string) @@ -224,20 +232,13 @@ extension KeyEvent { } switch number { - case 1: - return KeyEvent(key: .home) - case 2: - return nil // Insert - not commonly used - case 3: - return KeyEvent(key: .delete) - case 4: - return KeyEvent(key: .end) - case 5: - return KeyEvent(key: .pageUp) - case 6: - return KeyEvent(key: .pageDown) - default: - return nil + case 1: return KeyEvent(key: .home) + case 2: return nil // Insert — not commonly used in TUI apps + case 3: return KeyEvent(key: .delete) + case 4: return KeyEvent(key: .end) + case 5: return KeyEvent(key: .pageUp) + case 6: return KeyEvent(key: .pageDown) + default: return nil } } } diff --git a/Sources/TUIKit/Rendering/FrameBuffer.swift b/Sources/TUIKit/Rendering/FrameBuffer.swift index 235e770..ac66ed5 100644 --- a/Sources/TUIKit/Rendering/FrameBuffer.swift +++ b/Sources/TUIKit/Rendering/FrameBuffer.swift @@ -169,27 +169,28 @@ public struct FrameBuffer { /// - column: The column position (0-based). /// - Returns: The composited line. private func insertOverlay(base: String, overlay: String, atColumn column: Int) -> String { - // For ANSI-safe insertion, we work with visible characters + // Strip ANSI codes from the base to get accurate column positions. + // Without stripping, escape sequences would shift character offsets. + // The overlay keeps its ANSI codes intact so its styling is preserved. let baseChars = Array(base.stripped) let overlayStripped = overlay.stripped - // Build result: characters before overlay position + overlay + characters after + // Build: [base prefix] + [overlay with ANSI] + [base suffix] var result = "" - // Add characters before the overlay position + // Base characters before the overlay insertion point if column > 0 { let prefixEnd = min(column, baseChars.count) result += String(baseChars[0.. FrameBuffer { let infos = resolveChildInfos(from: content, context: context) - // Count spacers and measure fixed children + // Spacer distribution: divide remaining vertical space equally + // among all spacers after subtracting fixed children and inter-item spacing. let spacerCount = infos.filter(\.isSpacer).count let fixedHeight = infos.compactMap(\.buffer).reduce(0) { $0 + $1.height } let totalSpacing = max(0, infos.count - 1) * spacing @@ -128,7 +129,7 @@ extension VStack: Renderable { let availableForSpacers = max(0, context.availableHeight - fixedHeight - totalSpacing) let spacerHeight = spacerCount > 0 ? availableForSpacers / spacerCount : 0 - // Calculate max width for alignment + // Max width across all children determines alignment reference let maxWidth = infos.compactMap(\.buffer).map(\.width).max() ?? 0 var result = FrameBuffer() @@ -138,7 +139,6 @@ extension VStack: Renderable { let height = max(info.spacerMinLength ?? 0, spacerHeight) result.appendVertically(FrameBuffer(emptyWithHeight: height), spacing: spacingToApply) } else if let buffer = info.buffer { - // Apply horizontal alignment let alignedBuffer = alignBuffer(buffer, toWidth: maxWidth, alignment: alignment) result.appendVertically(alignedBuffer, spacing: spacingToApply) } @@ -181,7 +181,8 @@ extension HStack: Renderable { public func renderToBuffer(context: RenderContext) -> FrameBuffer { let infos = resolveChildInfos(from: content, context: context) - // Count spacers and measure fixed children + // Spacer distribution: divide remaining horizontal space equally + // among all spacers after subtracting fixed children and inter-item spacing. let spacerCount = infos.filter(\.isSpacer).count let fixedWidth = infos.compactMap(\.buffer).reduce(0) { $0 + $1.width } let totalSpacing = max(0, infos.count - 1) * spacing