├── .gitattributes ├── Sources ├── libunistring │ ├── libunistring.c │ └── include │ │ ├── module.modulemap │ │ └── libunistring.h ├── VirtualTerminal │ ├── Protocol │ │ ├── Operators.swift │ │ └── Encoding.swift │ ├── Platform │ │ ├── Event+POSIX.swift │ │ ├── Event+Windows.swift │ │ ├── POSIXTerminal.swift │ │ └── WindowsTerminal.swift │ ├── Extensions │ │ ├── Duration+Extensions.swift │ │ └── Task+Extensions.swift │ ├── Types │ │ ├── VTMode.swift │ │ └── VTColor.swift │ ├── Buffer │ │ ├── VTCell.swift │ │ ├── VTPosition.swift │ │ ├── TextMetrics.swift │ │ ├── VTStyle.swift │ │ └── VTBuffer.swift │ ├── Terminal │ │ └── VTTerminal.swift │ ├── Rendering │ │ ├── CursorMotionOptimizer.swift │ │ ├── DeltaCompression.swift │ │ ├── SGROptimizer.swift │ │ ├── VTDisplayLink.swift │ │ ├── VTBufferedTerminalStream.swift │ │ ├── VTProfiler.swift │ │ └── VTRenderer.swift │ └── Input │ │ ├── VTEventStream.swift │ │ ├── VTEvent.swift │ │ └── VTInputParser.swift ├── Geometry │ ├── Size.swift │ ├── Point.swift │ └── Rect.swift ├── Primitives │ └── RingBuffer.swift └── VTDemo │ └── VTDemo.swift ├── .gitignore ├── .github └── workflows │ └── build.yml ├── LICENSE ├── Package.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.swift text eol=lf 2 | *.md text eol=lf 3 | *.txt text eol=lf 4 | -------------------------------------------------------------------------------- /Sources/libunistring/libunistring.c: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Saleem Abdulrasool */ 2 | /* SPDX-License-Identifier: BSD-3-Clause */ 3 | 4 | extern unsigned char _; 5 | -------------------------------------------------------------------------------- /Sources/libunistring/include/module.modulemap: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Saleem Abdulrasool */ 2 | /* SPDX-License-Identifier: BSD-3-Clause */ 3 | 4 | module libunistring [system] { 5 | header "libunistring.h" 6 | 7 | link "unistring" 8 | } 9 | -------------------------------------------------------------------------------- /Sources/libunistring/include/libunistring.h: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Saleem Abdulrasool */ 2 | /* SPDX-License-Identifier: BSD-3-Clause */ 3 | 4 | #ifndef libunistring_libunistring_h 5 | #define libunistring_libunistring_h 6 | 7 | #include 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Protocol/Operators.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | precedencegroup StreamOperatorPrecedence { 5 | associativity: left 6 | 7 | higherThan: AssignmentPrecedence 8 | lowerThan: NilCoalescingPrecedence 9 | } 10 | 11 | infix operator <<<: StreamOperatorPrecedence 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | 6 | **/.DS_Store 7 | .*.sw[nop] 8 | *~ 9 | 10 | /build/ 11 | 12 | /package.resolved 13 | /Packages/ 14 | .build/ 15 | 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Platform/Event+POSIX.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if !os(Windows) 5 | 6 | extension KeyEvent { 7 | internal static func from(_ character: Character) -> KeyEvent { 8 | KeyEvent(character: character, keycode: .max, modifiers: [], type: .press) 9 | } 10 | } 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Extensions/Duration+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | extension Duration { 5 | package var seconds: Double { 6 | return Double(components.seconds) + Double(components.attoseconds) / 1e18 7 | } 8 | 9 | package var nanoseconds: Int64 { 10 | return Int64(components.seconds) * 1_000_000_000 + Int64(components.attoseconds) / 1_000_000_000 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - os: ubuntu-latest 14 | swift-version: swift-6.1.2-release 15 | swift-build: 6.1.2-RELEASE 16 | build-args: "--traits GNU" 17 | 18 | - os: macos-latest 19 | swift-version: swift-6.1.2-release 20 | swift-build: 6.1.2-RELEASE 21 | build-args: "" 22 | 23 | - os: windows-latest 24 | swift-version: swift-6.1.2-release 25 | swift-build: 6.1.2-RELEASE 26 | build-args: "" 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup Swift 35 | uses: compnerd/gha-setup-swift@main 36 | with: 37 | swift-version: ${{ matrix.swift-version }} 38 | swift-build: ${{ matrix.swift-build }} 39 | update-sdk-modules: true 40 | 41 | - if: matrix.os == 'ubuntu-latest' 42 | run: | 43 | sudo apt-get update 44 | sudo apt-get install -y libunistring-dev 45 | 46 | - name: Show Swift version 47 | run: swift --version 48 | 49 | - name: Resolve dependencies 50 | run: swift package resolve 51 | 52 | - name: Build 53 | run: swift build --configuration debug ${{ matrix.build-args }} 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Saleem Abdulrasool 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.1 2 | 3 | import PackageDescription 4 | 5 | let _: Package = 6 | .init(name: "VirtualTerminal", 7 | platforms: [ 8 | .macOS(.v15), 9 | ], 10 | products: [ 11 | .executable(name: "VTDemo", targets: ["VTDemo"]), 12 | .library(name: "VirtualTerminal", targets: ["VirtualTerminal"]), 13 | ], 14 | traits: [ 15 | .trait(name: "GNU", description: "GNU C Library") 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/compnerd/swift-platform-core.git", branch: "main", 19 | traits: [.trait(name: "GNU", condition: .when(traits: ["GNU"]))]), 20 | ], 21 | targets: [ 22 | .target(name: "libunistring"), 23 | .target(name: "Geometry"), 24 | .target(name: "Primitives"), 25 | .target(name: "VirtualTerminal", dependencies: [ 26 | .target(name: "Geometry"), 27 | .target(name: "Primitives"), 28 | .target(name: "libunistring", condition: .when(traits: ["GNU"])), 29 | .product(name: "POSIXCore", package: "swift-platform-core", condition: .when(platforms: [.macOS, .linux])), 30 | .product(name: "WindowsCore", package: "swift-platform-core", condition: .when(platforms: [.windows])), 31 | ]), 32 | .executableTarget(name: "VTDemo", dependencies: [ 33 | .target(name: "VirtualTerminal"), 34 | .product(name: "POSIXCore", package: "swift-platform-core", condition: .when(platforms: [.macOS, .linux])), 35 | ]), 36 | ]) 37 | -------------------------------------------------------------------------------- /Sources/Geometry/Size.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | @frozen 5 | public struct Size { 6 | public let width: Int 7 | public let height: Int 8 | 9 | public init(width: Int, height: Int) { 10 | self.width = width 11 | self.height = height 12 | } 13 | } 14 | 15 | extension Size: AdditiveArithmetic { 16 | public static var zero: Size { 17 | Size(width: 0, height: 0) 18 | } 19 | 20 | public static func + (_ lhs: Size, _ rhs: Size) -> Size { 21 | Size(width: lhs.width + rhs.width, height: lhs.height + rhs.height) 22 | } 23 | 24 | public static func - (_ lhs: Size, _ rhs: Size) -> Size { 25 | Size(width: lhs.width - rhs.width, height: lhs.height - rhs.height) 26 | } 27 | } 28 | 29 | extension Size: Codable { } 30 | 31 | extension Size: CustomStringConvertible { 32 | public var description: String { 33 | "\(width) × \(height)" 34 | } 35 | } 36 | 37 | extension Size: Equatable { } 38 | 39 | extension Size: Hashable { } 40 | 41 | extension Size: Sendable { } 42 | 43 | extension Size { 44 | public static func * (_ size: Size, _ scalar: Int) -> Size { 45 | Size(width: size.width * scalar, height: size.height * scalar) 46 | } 47 | } 48 | 49 | extension Size { 50 | public static func / (_ size: Size, _ scalar: Int) -> Size { 51 | return Size(width: size.width / scalar, height: size.height / scalar) 52 | } 53 | } 54 | 55 | extension Size { 56 | public var area: Int { 57 | width * height 58 | } 59 | } 60 | 61 | extension Size { 62 | public var aspectRatio: Double { 63 | guard height != 0 else { return 0.0 } 64 | return Double(width) / Double(height) 65 | } 66 | } 67 | 68 | extension Size { 69 | public var isEmpty: Bool { 70 | width <= 0 || height <= 0 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Geometry/Point.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | @frozen 5 | public struct Point { 6 | public let x: Int 7 | public let y: Int 8 | 9 | public init(x: Int, y: Int) { 10 | self.x = x 11 | self.y = y 12 | } 13 | } 14 | 15 | extension Point: AdditiveArithmetic { 16 | public static var zero: Point { 17 | Point(x: 0, y: 0) 18 | } 19 | 20 | public static func + (_ lhs: Point, _ rhs: Point) -> Point { 21 | Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 22 | } 23 | 24 | public static func - (_ lhs: Point, _ rhs: Point) -> Point { 25 | Point(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 26 | } 27 | } 28 | 29 | extension Point: Codable { } 30 | 31 | extension Point: CustomStringConvertible { 32 | public var description: String { 33 | "(\(x), \(y))" 34 | } 35 | } 36 | 37 | extension Point: Equatable { } 38 | 39 | extension Point: Hashable { } 40 | 41 | extension Point: Sendable { } 42 | 43 | extension Point { 44 | public static func * (_ point: Point, _ scalar: Int) -> Point { 45 | Point(x: point.x * scalar, y: point.y * scalar) 46 | } 47 | } 48 | 49 | extension Point { 50 | public static func / (_ point: Point, _ scalar: Int) -> Point { 51 | return Point(x: point.x / scalar, y: point.y / scalar) 52 | } 53 | } 54 | 55 | extension Point { 56 | public func distance(to point: Point) -> Double { 57 | let dx = self.x - point.x 58 | let dy = self.y - point.y 59 | return Double(dx * dx + dy * dy).squareRoot() 60 | } 61 | } 62 | 63 | extension Point { 64 | public var magnitude: Double { 65 | distance(to: .zero) 66 | } 67 | } 68 | 69 | extension Point { 70 | public func midpoint(to point: Point) -> Point { 71 | return Point(x: (self.x + point.x) / 2, y: (self.y + point.y) / 2) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Platform/Event+Windows.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if os(Windows) 5 | 6 | import Geometry 7 | import WindowsCore 8 | 9 | extension KeyModifiers { 10 | internal static func from(_ dwControlKeyState: DWORD) -> KeyModifiers { 11 | KeyModifiers([ 12 | dwControlKeyState & SHIFT_PRESSED == SHIFT_PRESSED ? .shift : nil, 13 | dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) == 0 ? nil : .alt, 14 | dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) == 0 ? nil : .ctrl, 15 | ].compactMap { $0 }) 16 | } 17 | } 18 | 19 | extension KeyEvent { 20 | internal static func from(_ record: KEY_EVENT_RECORD) -> KeyEvent { 21 | KeyEvent(scalar: UnicodeScalar(record.uChar.UnicodeChar), 22 | keycode: record.wVirtualKeyCode, 23 | modifiers: .from(record.dwControlKeyState), 24 | type: record.bKeyDown == true ? .press : .release) 25 | } 26 | } 27 | 28 | extension MouseEventType { 29 | internal static func from(_ record: MOUSE_EVENT_RECORD) -> MouseEventType { 30 | // TODO(compnerd) differentiate between button press/release 31 | return switch record.dwEventFlags { 32 | case MOUSE_MOVED: 33 | .move 34 | case MOUSE_WHEELED: 35 | .scroll(deltaX: 0, deltaY: Int(HIWORD(record.dwButtonState)) / 120) 36 | case MOUSE_HWHEELED: 37 | .scroll(deltaX: Int(HIWORD(record.dwButtonState)) / 120, deltaY: 0) 38 | default: 39 | .pressed(MouseButton([ 40 | record.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED == 0 ? nil : .left, 41 | record.dwButtonState & RIGHTMOST_BUTTON_PRESSED == 0 ? nil : .right, 42 | record.dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED == 0 ? nil : .middle, 43 | ].compactMap { $0 })) 44 | } 45 | } 46 | } 47 | 48 | extension MouseEvent { 49 | internal static func from(_ record: MOUSE_EVENT_RECORD) -> MouseEvent { 50 | MouseEvent(position: Point(x: Int(record.dwMousePosition.X), y: Int(record.dwMousePosition.Y)), 51 | type: .from(record)) 52 | } 53 | } 54 | 55 | extension ResizeEvent { 56 | internal static func from(_ record: WINDOW_BUFFER_SIZE_RECORD) -> ResizeEvent { 57 | ResizeEvent(size: Size(width: Int(record.dwSize.X), height: Int(record.dwSize.Y))) 58 | } 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Types/VTMode.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// Terminal input processing modes that control how user input is handled. 5 | /// 6 | /// Terminal modes determine whether input is processed by the system before 7 | /// being delivered to your application. This affects everything from line 8 | /// editing to signal handling and special key processing. 9 | /// 10 | /// ## Choosing the Right Mode 11 | /// 12 | /// - **Canonical (Cooked)**: Best for line-oriented applications like shells 13 | /// or utilities that read complete lines of input 14 | /// - **Raw**: Required for interactive applications, games, or full-screen 15 | /// programs that need immediate key press detection 16 | public enum VTMode: Sendable { 17 | /// Line-buffered input mode with system processing enabled. 18 | /// 19 | /// In canonical mode, the terminal system handles: 20 | /// - Line editing (backspace, delete, cursor movement) 21 | /// - Signal generation (Ctrl+C for SIGINT, Ctrl+Z for SIGTSTP) 22 | /// - Input buffering until newline is pressed 23 | /// - Echo of typed characters to the terminal 24 | /// 25 | /// This mode is ideal for command-line utilities, shells, and applications 26 | /// that process complete lines of text rather than individual keystrokes. 27 | case canonical 28 | 29 | /// Immediate input mode with minimal system processing. 30 | /// 31 | /// In raw mode, your application receives: 32 | /// - Individual keystrokes without buffering 33 | /// - Special keys (arrows, function keys, etc.) as escape sequences 34 | /// - Control characters without signal generation 35 | /// - Complete control over input processing and display 36 | /// 37 | /// This mode is essential for interactive applications, text editors, 38 | /// games, and any program that needs real-time input handling. 39 | case raw 40 | } 41 | 42 | extension VTMode { 43 | /// Alias for canonical mode using traditional terminal terminology. 44 | /// 45 | /// "Cooked" mode is the traditional Unix term for canonical input 46 | /// processing, where the system "cooks" (processes) input before 47 | /// delivering it to applications. This provides better semantic 48 | /// clarity for developers familiar with Unix terminal concepts. 49 | public static var cooked: VTMode { 50 | return .canonical 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Extensions/Task+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Dispatch 5 | 6 | extension Task where Failure == Error { 7 | @discardableResult 8 | package static func synchronously(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) throws(Failure) -> Success { 9 | var result: Result! 10 | 11 | let semaphore = DispatchSemaphore(value: 0) 12 | Task(priority: priority) { 13 | // This task will run the operation and capture the result. 14 | // It will signal the semaphore when done. 15 | defer { semaphore.signal() } 16 | 17 | do { 18 | result = try await .success(operation()) 19 | } catch { 20 | result = .failure(error) 21 | } 22 | } 23 | semaphore.wait() 24 | 25 | return try result.get() 26 | } 27 | } 28 | 29 | extension Task where Failure == Never { 30 | @discardableResult 31 | package static func synchronously(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success) -> Success { 32 | var result: Success! 33 | 34 | let semaphore = DispatchSemaphore(value: 0) 35 | Task(priority: priority) { 36 | defer { semaphore.signal() } 37 | result = await operation() 38 | } 39 | semaphore.wait() 40 | 41 | return result 42 | } 43 | } 44 | 45 | internal struct TimeoutError: Error, CustomStringConvertible { 46 | public var description: String { "The operation timed out." } 47 | } 48 | 49 | extension Task { 50 | internal static func withTimeout(timeout: Duration, 51 | priority: TaskPriority? = nil, 52 | operation: @escaping @Sendable () async throws -> Success) async throws -> Success { 53 | return try await withThrowingTaskGroup(of: Success.self) { group in 54 | defer { group.cancelAll() } 55 | 56 | group.addTask(priority: priority) { 57 | try await operation() 58 | } 59 | 60 | group.addTask { 61 | // Wait for the specified timeout 62 | try await Task.sleep(for: timeout) 63 | throw TimeoutError() 64 | } 65 | 66 | guard let result = try await group.next() else { 67 | throw TimeoutError() 68 | } 69 | 70 | return result 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Buffer/VTCell.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// A single character cell in the terminal buffer with associated styling. 5 | /// 6 | /// Terminal cells are the fundamental building blocks of terminal content. 7 | /// Each cell holds exactly one displayable character (which may be a 8 | /// multi-byte Unicode character) along with its visual styling information. 9 | /// 10 | /// For wide characters like emoji or CJK text that span multiple columns, 11 | /// only the first cell contains the actual character - subsequent cells 12 | /// use continuation markers (typically NUL characters). 13 | public struct VTCell: Sendable, Equatable { 14 | /// The character displayed in this terminal cell. 15 | /// 16 | /// This can be any Unicode character, including emoji, accented characters, 17 | /// and CJK text. For wide characters that don't fit in a single column, 18 | /// continuation cells will contain NUL characters. 19 | public let character: Character 20 | 21 | /// The visual styling applied to this character. 22 | public let style: VTStyle 23 | 24 | /// Creates a terminal cell with a character and styling. 25 | /// 26 | /// This is the primary way to create styled terminal content. The character 27 | /// can be any valid Unicode character, and the style determines colors and 28 | /// text attributes. 29 | /// 30 | /// - Parameters: 31 | /// - character: The character to display in this cell. 32 | /// - style: The visual styling to apply. 33 | public init(character: Character, style: VTStyle) { 34 | self.character = character 35 | self.style = style 36 | } 37 | 38 | /// Creates a terminal cell from an ASCII byte value. 39 | /// 40 | /// This convenience initializer is useful when working with ASCII text 41 | /// or control characters where you have the raw byte value. 42 | /// 43 | /// - Parameters: 44 | /// - ascii: The ASCII byte value (0-127) to convert to a character. 45 | /// - style: The visual styling to apply. 46 | /// 47 | /// - Precondition: The ascii value must be a valid ASCII character (0-127). 48 | public init(ascii: UInt8, style: VTStyle) { 49 | self.character = Character(UnicodeScalar(ascii)) 50 | self.style = style 51 | } 52 | } 53 | 54 | extension VTCell { 55 | /// A cell containing a space character with default styling. 56 | /// 57 | /// This is commonly used to represent empty or cleared areas of the 58 | /// terminal. It's more efficient than creating new space cells repeatedly 59 | /// since this is a static value that can be reused. 60 | public static var blank: VTCell { 61 | VTCell(character: " ", style: .default) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Buffer/VTPosition.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Geometry 5 | 6 | /// Represents a position in terminal coordinate space using 1-based indexing. 7 | /// 8 | /// Terminal positions follow the traditional terminal convention where the 9 | /// top-left corner is at row 1, column 1 (not 0, 0 as in many programming 10 | /// contexts). This matches VT100 terminal behavior and ANSI escape sequence 11 | /// expectations. 12 | @frozen 13 | public struct VTPosition: Sendable, Equatable { 14 | /// The row coordinate, with 1 representing the topmost row. 15 | public let row: Int 16 | 17 | /// The column coordinate, with 1 representing the leftmost column. 18 | public let column: Int 19 | 20 | /// Creates a terminal position with explicit row and column coordinates. 21 | /// 22 | /// - Parameters: 23 | /// - row: The 1-based row coordinate (vertical position). 24 | /// - column: The 1-based column coordinate (horizontal position). 25 | public init(row: Int, column: Int) { 26 | self.row = row 27 | self.column = column 28 | } 29 | 30 | /// Converts a geometric point to terminal coordinates. 31 | /// 32 | /// This convenience initializer translates from 0-based geometric 33 | /// coordinates to 1-based terminal coordinates by adding 1 to both x and y 34 | /// components. 35 | /// 36 | /// - Parameter point: A geometric point with 0-based coordinates. 37 | public init(point: Point) { 38 | self.row = Int(point.y) + 1 39 | self.column = Int(point.x) + 1 40 | } 41 | } 42 | 43 | extension VTPosition { 44 | /// Tests whether this position lies within the bounds of a terminal buffer. 45 | /// 46 | /// Terminal positions are valid when both coordinates fall within the 47 | /// 1-based ranges: row ∈ [1, height] and column ∈ [1, width]. 48 | /// 49 | /// - Parameter size: The dimensions of the terminal buffer to test against. 50 | /// - Returns: `true` if the position is within bounds, `false` otherwise. 51 | @inlinable 52 | internal func valid(in size: Size) -> Bool { 53 | return 1 ... size.height ~= row && 1 ... size.width ~= column 54 | } 55 | 56 | /// Converts this 1-based position to a linear buffer offset. 57 | /// 58 | /// Terminal buffers store cells in row-major order as a flat array. This 59 | /// method transforms 2D terminal coordinates into the corresponding 1D 60 | /// array index, accounting for the coordinate system difference (1-based 61 | /// vs 0-based). 62 | /// 63 | /// - Parameter size: The buffer dimensions used for offset calculation. 64 | /// - Returns: The zero-based linear offset into the buffer array. 65 | /// 66 | /// - Precondition: The position must be valid within the given size. 67 | @inlinable 68 | internal func offset(in size: Size) -> Int { 69 | assert(valid(in: size), "Invalid position '\(self)' for size '\(size)'") 70 | return (row - 1) &* size.width &+ (column - 1) 71 | } 72 | } 73 | 74 | extension VTPosition { 75 | /// The origin position at the top-left corner of the terminal. 76 | /// 77 | /// This represents the traditional terminal origin point at row 1, column 1, 78 | /// following VT100 conventions. Use this instead of creating 79 | /// `VTPosition(row: 1, column: 1)` for better semantic clarity and 80 | /// consistency. 81 | public static var zero: VTPosition { 82 | VTPosition(row: 1, column: 1) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Types/VTColor.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// Standard ANSI color identifiers supported by most terminals. 5 | /// 6 | /// These colors correspond to the traditional 8-color ANSI palette that 7 | /// has been supported since early terminal systems. The actual appearance 8 | /// of these colors depends on the terminal's color scheme and user 9 | /// customization. 10 | public enum ANSIColor: Int, Equatable, Sendable { 11 | case black 12 | case red 13 | case green 14 | case yellow 15 | case blue 16 | case magenta 17 | case cyan 18 | case white 19 | /// Uses the terminal's configured default color. 20 | case `default` = 9 21 | } 22 | 23 | /// Intensity variations for ANSI colors. 24 | /// 25 | /// Most terminals support both normal and bright variants of the standard 26 | /// ANSI colors, effectively providing a 16-color palette. Bright colors 27 | /// are often rendered with higher luminosity or as completely different 28 | /// hues depending on the terminal's color scheme. 29 | public enum ANSIColorIntensity: Equatable, Sendable { 30 | /// Standard color intensity. 31 | case normal 32 | /// Bright or bold color intensity. 33 | case bright 34 | } 35 | 36 | /// Terminal color representation supporting both ANSI and RGB color spaces. 37 | /// 38 | /// `VTColor` provides a unified interface for terminal colors while 39 | /// maintaining optimal compatibility. ANSI colors work with all terminals 40 | /// and respect user theming, while RGB colors provide precise control 41 | /// for modern terminal emulators. 42 | /// 43 | /// ## Color Space Comparison 44 | /// 45 | /// - **ANSI**: Universal compatibility, user-customizable, accessibility- 46 | /// friendly 47 | /// - **RGB**: Precise colors, consistent across terminals, larger palette 48 | /// 49 | /// ## Usage Examples 50 | /// 51 | /// ```swift 52 | /// // ANSI colors (recommended for most use cases) 53 | /// let warning = VTColor.yellow 54 | /// let error = VTColor.red 55 | /// let normal = VTColor.default 56 | /// 57 | /// // RGB colors for precise branding 58 | /// let blue = VTColor.rgb(red: 0, green: 123, blue: 255) 59 | /// let green = VTColor.rgb(red: 40, green: 167, blue: 69) 60 | /// ``` 61 | public enum VTColor: Equatable, Hashable, Sendable { 62 | /// A 24-bit RGB color with precise component control. 63 | case rgb(red: UInt8, green: UInt8, blue: UInt8) 64 | /// An ANSI color that adapts to terminal themes and settings. 65 | case ansi(_ color: ANSIColor, intensity: ANSIColorIntensity = .normal) 66 | } 67 | 68 | extension VTColor { 69 | /// The terminal's default color, respecting user customization. 70 | public static var `default`: VTColor { 71 | .ansi(.default) 72 | } 73 | 74 | public static var black: VTColor { 75 | .ansi(.black) 76 | } 77 | 78 | public static var red: VTColor { 79 | .ansi(.red) 80 | } 81 | 82 | public static var green: VTColor { 83 | .ansi(.green) 84 | } 85 | 86 | public static var yellow: VTColor { 87 | .ansi(.yellow) 88 | } 89 | 90 | public static var blue: VTColor { 91 | .ansi(.blue) 92 | } 93 | 94 | public static var magenta: VTColor { 95 | .ansi(.magenta) 96 | } 97 | 98 | public static var cyan: VTColor { 99 | .ansi(.cyan) 100 | } 101 | 102 | public static var white: VTColor { 103 | .ansi(.white) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Geometry/Rect.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | @frozen 5 | public struct Rect { 6 | public let origin: Point 7 | public let size: Size 8 | 9 | public init(origin: Point, size: Size) { 10 | self.origin = origin 11 | self.size = size 12 | } 13 | } 14 | 15 | extension Rect: AdditiveArithmetic { 16 | public static var zero: Rect { 17 | Rect(origin: .zero, size: .zero) 18 | } 19 | 20 | public static func + (_ lhs: Rect, _ rhs: Rect) -> Rect { 21 | Rect(origin: lhs.origin + rhs.origin, size: lhs.size + rhs.size) 22 | } 23 | 24 | public static func - (_ lhs: Rect, _ rhs: Rect) -> Rect { 25 | Rect(origin: lhs.origin - rhs.origin, size: lhs.size - rhs.size) 26 | } 27 | } 28 | 29 | extension Rect: Codable { } 30 | 31 | extension Rect: CustomStringConvertible { 32 | public var description: String { 33 | "{\(origin), \(size)}" 34 | } 35 | } 36 | 37 | extension Rect: Equatable { } 38 | 39 | extension Rect: Hashable { } 40 | 41 | extension Rect: Sendable { } 42 | 43 | extension Rect { 44 | public var isEmpty: Bool { 45 | size.width <= 0 || size.height <= 0 46 | } 47 | } 48 | 49 | extension Rect { 50 | public var center: Point { 51 | Point(x: origin.x + size.width / 2, y: origin.y + size.height / 2) 52 | } 53 | } 54 | 55 | extension Rect { 56 | public func contains(_ point: Point) -> Bool { 57 | return point.x >= origin.x && point.x < (origin.x + size.width) && 58 | point.y >= origin.y && point.y < (origin.y + size.height) 59 | } 60 | 61 | public func contains(_ rect: Rect) -> Bool { 62 | return rect.origin.x >= origin.x && 63 | (rect.origin.x + rect.size.width) <= (origin.x + size.width) && 64 | rect.origin.y >= origin.y && 65 | (rect.origin.y + rect.size.height) <= (origin.y + size.height) 66 | } 67 | } 68 | 69 | extension Rect { 70 | public func intersects(_ rect: Rect) -> Bool { 71 | return (origin.x + size.width) > rect.origin.x && 72 | origin.x < (rect.origin.x + rect.size.width) && 73 | (origin.y + size.height) > rect.origin.y && 74 | origin.y < (rect.origin.y + rect.size.height) 75 | } 76 | } 77 | 78 | extension Rect { 79 | public func union(_ rect: Rect) -> Rect { 80 | let x = (min: min(origin.x, rect.origin.x), 81 | max: max(origin.x + size.width, rect.origin.x + rect.size.width)) 82 | let y = (min: min(origin.y, rect.origin.y), 83 | max: max(origin.y + size.height, rect.origin.y + rect.size.height)) 84 | 85 | return Rect(origin: Point(x: x.min, y: y.min), 86 | size: Size(width: x.max - x.min, height: y.max - y.min)) 87 | } 88 | } 89 | 90 | extension Rect { 91 | public func intersection(with rect: Rect) -> Rect? { 92 | let x = (min: max(origin.x, rect.origin.x), 93 | max: min(origin.x + size.width, rect.origin.x + rect.size.width)) 94 | let y = (min: max(origin.y, rect.origin.y), 95 | max: min(origin.y + size.height, rect.origin.y + rect.size.height)) 96 | 97 | guard x.min < x.max, y.min < y.max else { return nil } 98 | return Rect(origin: Point(x: x.min, y: y.min), 99 | size: Size(width: x.max - x.min, height: y.max - y.min)) 100 | } 101 | } 102 | 103 | extension Rect { 104 | public func offset(by point: Point) -> Rect { 105 | return Rect(origin: origin + point, size: size) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Terminal/VTTerminal.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | @_exported 5 | import Geometry 6 | 7 | /// A terminal interface that provides async access to terminal operations. 8 | /// 9 | /// `VTTerminal` defines the core protocol for terminal implementations, 10 | /// providing input event handling and output capabilities. Implementations 11 | /// handle platform-specific details while presenting a unified async interface. 12 | /// 13 | /// The protocol uses Swift's actor model to ensure thread-safe access to 14 | /// terminal resources, preventing data races and ensuring consistent state. 15 | /// 16 | /// ## Usage 17 | /// 18 | /// ```swift 19 | /// // Write text and control sequences 20 | /// await terminal <<< .CursorPosition(10, 5) 21 | /// <<< .SelectGraphicRendition([.bold, .foreground(.red)]) 22 | /// <<< "Hello, World in bold red!" 23 | /// 24 | /// // Handle input events 25 | /// for await event in terminal.input { 26 | /// switch event { 27 | /// case .key(let key): 28 | /// // Process keyboard input 29 | /// case .resize(let size): 30 | /// // Handle terminal size changes 31 | /// default: 32 | /// break 33 | /// } 34 | /// } 35 | /// ``` 36 | public protocol VTTerminal: Actor { 37 | /// The current dimensions of the terminal window. 38 | /// 39 | /// This property reflects the terminal's size in character cells and 40 | /// automatically updates when the terminal is resized. Access is 41 | /// non-isolated for performance since size queries are frequent. 42 | nonisolated var size: Size { get } 43 | 44 | /// An async stream of input events from the terminal. 45 | /// 46 | /// This stream provides keyboard input, mouse events, and terminal 47 | /// resize notifications. The stream remains active for the lifetime 48 | /// of the terminal and automatically handles platform-specific 49 | /// input parsing. 50 | /// 51 | /// Events are delivered in real-time as they occur, making this suitable 52 | /// for interactive applications and games. 53 | nonisolated var input: VTEventStream { get } 54 | 55 | /// Writes text or control sequences to the terminal output. 56 | /// 57 | /// Text is sent directly to the terminal and may include ANSI escape 58 | /// sequences for cursor movement, colors, and formatting. For structured 59 | /// output, consider using the `<<<` operators with `ControlSequence` 60 | /// values instead. 61 | /// 62 | /// - Parameter string: The text or escape sequences to write. 63 | func write(_ string: String) 64 | } 65 | 66 | extension VTTerminal { 67 | /// Writes a control sequence to the terminal using a fluent syntax. 68 | /// 69 | /// This operator provides a convenient way to send structured terminal 70 | /// commands while maintaining chainability for complex output operations. 71 | /// The sequence is automatically converted to its string representation. 72 | /// 73 | /// - Parameters: 74 | /// - terminal: The terminal to write to. 75 | /// - sequence: The control sequence to send. 76 | /// - Returns: The terminal instance for chaining additional operations. 77 | @inlinable 78 | @discardableResult 79 | public static func <<< (_ terminal: Self, _ sequence: ControlSequence) async -> Self { 80 | await terminal.write(sequence.description) 81 | return terminal 82 | } 83 | 84 | /// Writes a string to the terminal using a fluent syntax. 85 | /// 86 | /// This operator provides a chainable way to send text output while 87 | /// maintaining consistency with control sequence operations. 88 | /// 89 | /// - Parameters: 90 | /// - terminal: The terminal to write to. 91 | /// - string: The text to send. 92 | /// - Returns: The terminal instance for chaining additional operations. 93 | @inlinable 94 | @discardableResult 95 | public static func <<< (_ terminal: Self, _ string: String) async -> Self { 96 | await terminal.write(string) 97 | return terminal 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Buffer/TextMetrics.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if os(Windows) 5 | import WindowsCore 6 | #else 7 | #if GNU 8 | import libunistring 9 | #endif 10 | import POSIXCore 11 | import Synchronization 12 | 13 | private enum Locale { 14 | private static let utf8: Mutex = Mutex(nil) 15 | 16 | static var ID_UTF8: locale_t? { 17 | return utf8.withLock { locale in 18 | if let locale { return locale } 19 | locale = newlocale(LC_CTYPE_MASK, "en_US.UTF-8", nil) 20 | return locale 21 | } 22 | } 23 | } 24 | 25 | #endif 26 | 27 | extension UnicodeScalar { 28 | /// Determines if a unicode scalar is a wide character (occupies 2 columns) 29 | package var isWideCharacter: Bool { 30 | return switch value { 31 | case 0x01100 ... 0x0115f, // Hangul Jamo 32 | 0x02329 ... 0x0232a, // Angle brackets 33 | 0x02e80 ... 0x02eff, // CJK Radicals 34 | 0x03000 ... 0x0303e, // CJK Symbols 35 | 0x03041 ... 0x03096, // Hiragana 36 | 0x030a1 ... 0x030fa, // Katakana 37 | 0x03105 ... 0x0312d, // Bopomofo 38 | 0x03131 ... 0x0318e, // Hangul Compatibility Jamo 39 | 0x03190 ... 0x0319f, // Kanbun 40 | 0x031c0 ... 0x031e3, // CJK Strokes 41 | 0x031f0 ... 0x0321e, // Katakana Extension 42 | 0x03220 ... 0x03247, // Enclosed CJK 43 | 0x03250 ... 0x032fe, // Enclosed CJK 44 | 0x03300 ... 0x04dbf, // CJK Extension A 45 | 0x04e00 ... 0x09fff, // CJK Unified Ideographs 46 | 0x0a960 ... 0x0a97c, // Hangul Jamo Extended-A 47 | 0x0ac00 ... 0x0d7a3, // Hangul Syllables 48 | 0x0f900 ... 0x0faff, // CJK Compatibility 49 | 0x0fe10 ... 0x0fe19, // Vertical forms 50 | 0x0fe30 ... 0x0fe6f, // CJK Compatibility Forms 51 | 0x0ff00 ... 0x0ff60, // Fullwidth Forms 52 | 0x0ffe0 ... 0x0ffe6, // Fullwidth Forms 53 | 0x1f300 ... 0x1f5ff, // Misc Symbols and Pictographs 54 | 0x1f600 ... 0x1f64f, // Emoticons 55 | 0x1f680 ... 0x1f6ff, // Transport and Map 56 | 0x1f700 ... 0x1f77f, // Alchemical Symbols 57 | 0x1f780 ... 0x1f7ff, // Geometric Shapes Extended 58 | 0x1f800 ... 0x1f8ff, // Supplemental Arrows-C 59 | 0x1f900 ... 0x1f9ff, // Supplemental Symbols and Pictographs 60 | 0x20000 ... 0x2fffd, // CJK Extension B-F 61 | 0x30000 ... 0x3fffd: // CJK Extension G 62 | true 63 | default: 64 | false 65 | } 66 | } 67 | 68 | package var width: Int { 69 | #if os(Windows) 70 | // Control characters have zero width 71 | if value < 0x20 || (0x7f ..< 0xa0).contains(value) { return 0 } 72 | 73 | var CharType: WORD = 0 74 | let bSuccess = withUnsafePointer(to: value) { 75 | $0.withMemoryRebound(to: WCHAR.self, capacity: 1) { 76 | GetStringTypeW(CT_CTYPE3, $0, 1, &CharType) 77 | } 78 | } 79 | guard bSuccess else { 80 | // throw WindowsError() 81 | return isWideCharacter ? 2 : 1 82 | } 83 | return CharType & C3_FULLWIDTH == C3_FULLWIDTH ? 2 : 1 84 | #elseif GNU 85 | // Control character or invalid - zero width -> -1 86 | // Zero-width character (combining marks, etc.) -> 0 87 | // Normal width character -> 1 88 | // Wide character (CJK, etc.) -> 2 89 | return max(1, Int(uc_width(UInt32(value), "C.UTF-8"))) 90 | #else 91 | // Control character or invalid - zero width -> -1 92 | // Zero-width character (combining marks, etc.) -> 0 93 | // Normal width character -> 1 94 | // Wide character (CJK, etc.) -> 2 95 | return max(1, Int(wcwidth_l(wchar_t(value), Locale.ID_UTF8))) 96 | #endif 97 | } 98 | } 99 | 100 | extension Character { 101 | package var width: Int { 102 | // Handle common ASCII fast path 103 | if isASCII { return isWhitespace ? (self == " " ? 1 : 0) : 1 } 104 | // For non-ASCII characters, we need to check their unicode properties 105 | return unicodeScalars.reduce(0) { $0 + $1.width } 106 | } 107 | } 108 | 109 | extension String { 110 | package var width: Int { 111 | return unicodeScalars.reduce(0) { $0 + $1.width } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/CursorMotionOptimizer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// Cursor motion optimization extension for `VTBuffer`. 5 | /// 6 | /// This extension provides intelligent cursor positioning that minimizes 7 | /// the number of bytes sent to the terminal by choosing the most efficient 8 | /// movement strategy from multiple alternatives. 9 | extension VTBuffer { 10 | /// Generates the most efficient cursor movement sequence between two positions. 11 | /// 12 | /// This method analyzes multiple cursor movement strategies and selects 13 | /// the one that requires the fewest bytes when encoded. This optimization 14 | /// is particularly important for terminal applications that frequently 15 | /// reposition the cursor, as it can significantly reduce bandwidth usage 16 | /// and improve rendering performance. 17 | /// 18 | /// ## Movement Strategies 19 | /// 20 | /// The optimizer considers several approaches: 21 | /// - **Absolute positioning**: Direct jump to target coordinates 22 | /// - **Line-based movement**: Efficient moves to column 1 of different rows 23 | /// - **Horizontal-only**: Optimized left/right movement on the same row 24 | /// - **Relative movement**: Combination of vertical and horizontal steps 25 | /// 26 | /// ## Parameters 27 | /// - source: Current cursor position 28 | /// - target: Desired cursor position 29 | /// 30 | /// ## Returns 31 | /// An array of control sequences representing the optimal movement path. 32 | /// Returns an empty array if source and target positions are identical. 33 | /// 34 | /// ## Usage Example 35 | /// ```swift 36 | /// let buffer = VTBuffer(size: Size(width: 80, height: 24)) 37 | /// let currentPos = VTPosition(row: 5, column: 10) 38 | /// let targetPos = VTPosition(row: 8, column: 1) 39 | /// 40 | /// let movements = buffer.reposition(from: currentPos, to: targetPos) 41 | /// for sequence in movements { 42 | /// await terminal.write(sequence.description) 43 | /// } 44 | /// ``` 45 | /// 46 | /// ## Performance Benefits 47 | /// 48 | /// This optimization reduces the number of bytes transmitted to the terminal 49 | /// by selecting the most compact cursor movement strategy available. The 50 | /// byte savings are particularly noticeable in applications that frequently 51 | /// reposition the cursor, such as when rendering forms, menus, or other 52 | /// structured layouts. 53 | /// 54 | /// ## Implementation Notes 55 | /// 56 | /// All strategies are evaluated and the shortest encoded representation 57 | /// is selected. This ensures optimal performance across different terminal 58 | /// types and cursor movement patterns. 59 | package func reposition(from source: VTPosition, to target: VTPosition) 60 | -> [ControlSequence] { 61 | // If the source and target are the same, no motion is needed. 62 | if source == target { return [] } 63 | 64 | let ΔRow = target.row - source.row 65 | let ΔColumn = target.column - source.column 66 | 67 | // Generate all possible movement strategies 68 | var strategies: [[ControlSequence]] = [] 69 | 70 | // Strategy 1: Absolute positioning 71 | strategies.append([.CursorPosition(target.row, target.column)]) 72 | 73 | // Strategy 2: Line-based movement to column 1 (when applicable) 74 | if ΔRow > 0 && target.column == 1 { 75 | strategies.append([.CursorNextLine(ΔRow)]) 76 | } else if ΔRow < 0 && target.column == 1 { 77 | strategies.append([.CursorPreviousLine(-ΔRow)]) 78 | } 79 | 80 | // Strategy 3: Horizontal-only movements (when applicable) 81 | if ΔRow == 0 && ΔColumn > 0 { 82 | strategies.append([.CursorHorizontalAbsolute(target.column)]) 83 | strategies.append([.CursorForward(ΔColumn)]) 84 | } else if ΔRow == 0 && ΔColumn < 0 { 85 | strategies.append([.CursorHorizontalAbsolute(target.column)]) 86 | strategies.append([.CursorBackward(-ΔColumn)]) 87 | } 88 | 89 | // Strategy 4: Relative movement (vertical + horizontal) 90 | var motions: [ControlSequence] = [] 91 | 92 | if ΔRow > 0 { 93 | motions.append(.CursorDown(ΔRow)) 94 | } else if ΔRow < 0 { 95 | motions.append(.CursorUp(-ΔRow)) 96 | } 97 | 98 | if ΔColumn > 0 { 99 | motions.append(.CursorForward(ΔColumn)) 100 | } else if ΔColumn < 0 { 101 | motions.append(.CursorBackward(-ΔColumn)) 102 | } 103 | 104 | if motions.count > 0 { strategies.append(motions) } 105 | 106 | // Return the strategy with the minimum total character count 107 | return strategies.min { lhs, rhs in 108 | lhs.reduce(0) { $0 + $1.encoded(as: .b7).count } < rhs.reduce(0) { $0 + $1.encoded(as: .b7).count } 109 | } ?? [.CursorPosition(target.row, target.column)] 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirtualTerminal 2 | 3 | **Modern, high-performance terminal UI library for Swift** 4 | 5 | Build beautiful, fast command-line applications with native Swift. VirtualTerminal provides efficient rendering, cross-platform compatibility, and Swift 6 concurrency support—without the complexity of C bindings. 6 | 7 | ## Why VirtualTerminal? 8 | 9 | ### 🚀 **Built for Performance** 10 | - **Damage-based rendering**: Only redraw changed cells, not entire screens 11 | - **Intelligent cursor optimization**: Minimal escape sequences for movement 12 | - **Double buffering**: Smooth animations without screen tearing 13 | - **Output batching**: Batch multiple operations into fewer writes 14 | 15 | ### 🛡️ **Swift-Native Design** 16 | - **Memory safety**: No unsafe pointers or C interop required 17 | - **Modern concurrency**: Built on Swift 6 actors and async/await 18 | - **Type safety**: Compile-time guarantees for colors, positions, and styles 19 | - **Zero dependencies**: Pure Swift implementation 20 | 21 | ### 🌍 **True Cross-Platform** 22 | - **macOS, Linux, Windows**: Single codebase, platform-optimized internals 23 | - **Consistent APIs**: Write once, run everywhere 24 | - **Native input handling**: Platform-specific optimizations under the hood 25 | 26 | ## Quick Example 27 | 28 | ```swift 29 | import VirtualTerminal 30 | 31 | // Create a high-performance terminal renderer 32 | let renderer = try await VTRenderer(mode: .raw) 33 | 34 | // Render at 60 FPS with automatic optimization 35 | try await renderer.rendering(fps: 60) { buffer in 36 | buffer.write("Hello, World!", 37 | at: VTPosition(row: 1, column: 1), 38 | style: VTStyle(foreground: .green, attributes: [.bold])) 39 | } 40 | 41 | // Handle input events with modern Swift concurrency 42 | for await event in renderer.terminal.input { 43 | switch event { 44 | case .key(let key) where key.character == "q": 45 | return // Clean exit 46 | case .resize(let size): 47 | renderer.resize(to: size) 48 | default: 49 | break 50 | } 51 | } 52 | ``` 53 | 54 | ## Core Features 55 | 56 | ### Efficient Rendering 57 | - **Damage detection**: Only update changed regions 58 | - **Style optimization**: Minimize escape sequence overhead 59 | - **Cursor movement**: Intelligent positioning algorithms 60 | - **Unicode support**: Proper width calculation for CJK, emoji, and symbols 61 | 62 | ### Modern Input Handling 63 | ```swift 64 | // AsyncSequence-based input processing 65 | for await event in terminal.input { 66 | switch event { 67 | case .key(let key): 68 | handleKeyPress(key) 69 | case .mouse(let mouse): 70 | handleMouseEvent(mouse) 71 | case .resize(let size): 72 | handleResize(size) 73 | } 74 | } 75 | ``` 76 | 77 | ### Rich Styling 78 | ```swift 79 | let style = VTStyle(foreground: .rgb(red: 255, green: 100, blue: 50), 80 | background: .ansi(.blue), 81 | attributes: [.bold, .italic]) 82 | buffer.write("Styled text", at: position, style: style) 83 | ``` 84 | 85 | ### Structured Terminal Control Sequences 86 | 87 | Beyond high-level UI rendering, VirtualTerminal provides a structured, type-safe API for formulating terminal escape sequences. Rather than hardcoding string literals like `"\033[31;1m"`, you express terminal commands using semantic Swift types. 88 | 89 | The `ControlSequence` enum covers ISO 6429/ECMA-48 compliant terminal operations: 90 | 91 | ```swift 92 | import VirtualTerminal 93 | 94 | // Type-safe cursor positioning and styling 95 | await terminal <<< .CursorPosition(10, 20) 96 | await terminal <<< .SelectGraphicRendition([.bold, .foreground(.red)]) 97 | await terminal <<< "Important text" 98 | await terminal <<< .SelectGraphicRendition([.reset]) 99 | 100 | // Structured screen manipulation 101 | await terminal <<< .EraseDisplay(.EntireDisplay) 102 | await terminal <<< .SetMode([.DEC(.UseAlternateScreenBuffer)]) 103 | ``` 104 | 105 | This approach offers: 106 | - **Semantic clarity**: Express intent with Swift types, not escape code memorization 107 | - **Compile-time validation**: Prevents malformed sequences and parameter errors 108 | - **Encoding abstraction**: Handles 7-bit vs 8-bit encoding automatically 109 | - **Composability**: Chain operations with fluent syntax using the `<<<` operator 110 | 111 | The library generates correct ANSI/VT100 escape sequences from these structured commands, making it both a UI toolkit and a robust terminal control sequence generator. 112 | 113 | ## Installation 114 | 115 | Add to your `Package.swift`: 116 | 117 | ```swift 118 | dependencies: [ 119 | .package(url: "https://github.com/compnerd/VirtualTerminal.git", branch: "main") 120 | ], 121 | targets: [ 122 | .target(name: "YourCLI", dependencies: ["VirtualTerminal"]) 123 | ] 124 | ``` 125 | 126 | ## Requirements 127 | 128 | - **Swift 6.0+** 129 | - **macOS 14+**, **Linux**, or **Windows 10+** 130 | - Terminal with basic ANSI support (any modern terminal) 131 | - libunistring is required for Linux GNU 132 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Input/VTEventStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// An asynchronous stream of terminal input events. 5 | /// 6 | /// `VTEventStream` provides an efficient way to consume terminal input events 7 | /// using Swift's async/await concurrency model. The stream automatically 8 | /// handles the complexities of terminal input processing, delivering a clean 9 | /// sequence of `VTEvent` values that your application can process. 10 | /// 11 | /// The stream intelligently batches events from the underlying terminal APIs 12 | /// for optimal performance while providing a simple single-event interface 13 | /// to your application code. 14 | /// 15 | /// ## Usage Example 16 | /// ```swift 17 | /// for try await event in eventStream { 18 | /// switch event { 19 | /// case .key(let key): 20 | /// if key.character == "q" { 21 | /// break // Exit event loop 22 | /// } 23 | /// handleKeyInput(key) 24 | /// case .mouse(let mouse): 25 | /// handleMouseInput(mouse) 26 | /// case .resize(let resize): 27 | /// handleTerminalResize(resize) 28 | /// } 29 | /// } 30 | /// ``` 31 | /// 32 | /// ## Error Handling 33 | /// The stream can throw errors if the underlying terminal input fails. 34 | /// Handle errors appropriately in your event processing loop: 35 | /// 36 | /// ```swift 37 | /// do { 38 | /// for try await event in eventStream { 39 | /// processEvent(event) 40 | /// } 41 | /// } catch { 42 | /// handleInputError(error) 43 | /// } 44 | /// ``` 45 | public struct VTEventStream: AsyncSequence, Sendable { 46 | public typealias Element = VTEvent 47 | private let stream: AsyncThrowingStream<[VTEvent], Error> 48 | 49 | internal init(_ stream: AsyncThrowingStream<[VTEvent], Error>) { 50 | self.stream = stream 51 | } 52 | 53 | /// An iterator that flattens batched events into individual events. 54 | /// 55 | /// This iterator efficiently manages internal buffering to provide smooth 56 | /// single-event consumption while preserving the performance benefits 57 | /// of batch reading from the underlying terminal APIs. 58 | /// 59 | /// The iterator automatically handles the complexity of buffering and 60 | /// flattening batched events, so your application code can focus on 61 | /// processing individual events without worrying about batching details. 62 | /// 63 | /// ## Performance Characteristics 64 | /// 65 | /// The iterator maintains an internal buffer to minimize the number of 66 | /// async operations while ensuring events are delivered as quickly as 67 | /// possible. This design provides: 68 | /// - Low latency for interactive applications 69 | /// - High throughput for batch operations 70 | /// - Efficient memory usage through buffer reuse 71 | public struct AsyncIterator: AsyncIteratorProtocol { 72 | private var iterator: AsyncThrowingStream<[VTEvent], Error>.Iterator 73 | private var buffer: [VTEvent] = [] 74 | private var index: Array.Index? 75 | 76 | internal init(underlying: AsyncThrowingStream<[VTEvent], Error>.Iterator) { 77 | self.iterator = underlying 78 | } 79 | 80 | /// Advances to the next event in the stream. 81 | /// 82 | /// This method handles the complexity of managing batched events from 83 | /// the underlying terminal input system. It maintains an internal buffer 84 | /// to efficiently deliver individual events while minimizing system calls. 85 | /// 86 | /// The method automatically: 87 | /// - Serves buffered events when available 88 | /// - Fetches new batches when the buffer is exhausted 89 | /// - Skips empty batches that may occur during input processing 90 | /// - Returns `nil` when the stream reaches its end 91 | /// 92 | /// ## Error Propagation 93 | /// Any errors from the underlying terminal input system are propagated 94 | /// to the caller, allowing proper error handling in your application. 95 | /// 96 | /// - Returns: The next `VTEvent` in the stream, or `nil` if the stream 97 | /// has ended. 98 | /// - Throws: Any errors that occur while reading from the terminal input. 99 | public mutating func next() async throws -> Element? { 100 | if let index, index < buffer.endIndex { 101 | let event = buffer[index] 102 | self.index = buffer.index(after: index) 103 | return event 104 | } 105 | 106 | while let batch = try await iterator.next() { 107 | guard !batch.isEmpty else { continue } 108 | 109 | self.buffer = batch 110 | self.index = buffer.index(after: buffer.startIndex) 111 | 112 | return buffer[buffer.startIndex] 113 | } 114 | 115 | return nil 116 | } 117 | } 118 | 119 | /// Creates an async iterator for processing events from the stream. 120 | /// 121 | /// This method is called automatically when you use `for try await` loops 122 | /// or other async sequence operations. The returned iterator handles all 123 | /// the complexity of buffering and flattening batched events. 124 | /// 125 | /// You typically won't call this method directly, but instead use it 126 | /// implicitly through async sequence operations: 127 | /// 128 | /// ```swift 129 | /// // This automatically calls makeAsyncIterator() 130 | /// for try await event in eventStream { 131 | /// processEvent(event) 132 | /// } 133 | /// ``` 134 | /// 135 | /// - Returns: A new async iterator for consuming events from this stream. 136 | public func makeAsyncIterator() -> AsyncIterator { 137 | AsyncIterator(underlying: stream.makeAsyncIterator()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/DeltaCompression.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// Represents a contiguous range of changed cells with consistent styling. 5 | /// 6 | /// `DamageSpan` is the fundamental unit of delta compression in the terminal 7 | /// rendering system. Each span identifies a continuous region of the terminal 8 | /// buffer that has changed and shares the same text styling, allowing for 9 | /// efficient batch updates to the terminal. 10 | /// 11 | /// ## Memory Efficiency 12 | /// 13 | /// By grouping consecutive cells with identical styling, damage spans 14 | /// reduce the number of style change operations sent to the terminal, 15 | /// which improves both performance and reduces output size. 16 | package struct DamageSpan: Sendable { 17 | /// The range of buffer indices affected by this damage span. 18 | /// 19 | /// This range identifies the contiguous sequence of cells in the terminal 20 | /// buffer that have changed. The range uses linear buffer indices, where 21 | /// position `row * width + column` maps to the buffer index. 22 | package let range: Range 23 | 24 | /// The consistent text style applied to all cells in this span. 25 | /// 26 | /// All cells within the damage span share this same style, which allows 27 | /// the renderer to apply the style once and then output all the character 28 | /// data without additional style changes. 29 | package let style: VTStyle 30 | 31 | internal init(range: Range, style: VTStyle) { 32 | self.range = range 33 | self.style = style 34 | } 35 | } 36 | 37 | private func split(span: Range, from buffer: borrowing VTBuffer, into damages: inout [DamageSpan]) { 38 | guard !span.isEmpty else { return } 39 | 40 | var start = span.lowerBound 41 | var current = buffer.buffer[span.lowerBound].style 42 | 43 | for offset in span.dropFirst() { 44 | let style = buffer.buffer[offset].style 45 | if style == current { continue } 46 | damages.append(DamageSpan(range: start ..< offset, style: current)) 47 | start = offset 48 | current = style 49 | } 50 | 51 | damages.append(DamageSpan(range: start ..< span.upperBound, style: current)) 52 | } 53 | 54 | /// Computes the minimal set of changes between two terminal buffers. 55 | /// 56 | /// This function performs delta compression by comparing two `VTBuffer` 57 | /// instances and identifying only the regions that have actually changed. 58 | /// The result is an optimized list of damage spans that represent the 59 | /// minimal updates needed to transform the current buffer to the updated state. 60 | /// 61 | /// ## Delta Compression Benefits 62 | /// 63 | /// Instead of redrawing the entire terminal screen, delta compression: 64 | /// - Reduces terminal output by only updating changed regions 65 | /// - Minimizes bandwidth usage for remote terminal sessions 66 | /// - Decreases rendering latency by avoiding unnecessary updates 67 | /// - Preserves terminal scrollback by not clearing unchanged regions 68 | /// 69 | /// ## Parameters 70 | /// - current: The baseline buffer state (what's currently displayed) 71 | /// - updated: The target buffer state (what should be displayed) 72 | /// 73 | /// ## Returns 74 | /// An array of `DamageSpan` objects representing the minimal changes needed. 75 | /// Returns a single full-screen span if the buffer sizes differ. 76 | /// 77 | /// ## Usage Example 78 | /// ```swift 79 | /// let currentBuffer = VTBuffer(size: Size(width: 80, height: 24)) 80 | /// let updatedBuffer = currentBuffer.copy() 81 | /// 82 | /// // Make some changes to updatedBuffer 83 | /// updatedBuffer.setCursor(position: VTPosition(row: 5, column: 10)) 84 | /// updatedBuffer.write("Hello, World!") 85 | /// 86 | /// let changes = damages(from: currentBuffer, to: updatedBuffer) 87 | /// for span in changes { 88 | /// // Only update the changed regions 89 | /// await renderSpan(span, in: updatedBuffer) 90 | /// } 91 | /// ``` 92 | /// 93 | /// ## Algorithm Behavior 94 | /// 95 | /// The function scans through both buffers simultaneously, identifying 96 | /// contiguous regions of change. When style boundaries are encountered 97 | /// within a changed region, the span is automatically split to maintain 98 | /// the invariant that each damage span has consistent styling. 99 | /// 100 | /// ## Performance Characteristics 101 | /// 102 | /// The algorithm runs in O(n) time where n is the buffer size, making 103 | /// it efficient enough for real-time terminal applications running at 104 | /// high frame rates. 105 | package func damages(from current: borrowing VTBuffer, to updated: borrowing VTBuffer) -> [DamageSpan] { 106 | guard updated.size == current.size else { 107 | // Full screen damage if the size has changed. 108 | return [DamageSpan(range: 0 ..< updated.buffer.count, style: .default)] 109 | } 110 | 111 | var start: ContiguousArray.Index? 112 | var damages: [DamageSpan] = [] 113 | for index in updated.buffer.indices { 114 | switch (start, current.buffer[index] == updated.buffer[index]) { 115 | case let (.some(position), true): 116 | // If the current cell is unchanged and we have a start position, 117 | // end the current damage span. 118 | split(span: position ..< index, from: updated, into: &damages) 119 | start = nil 120 | case (.none, false): 121 | // If the current cell is changed and we don't have a start position, 122 | // start a new damage span. 123 | start = index 124 | default: 125 | continue 126 | } 127 | } 128 | 129 | // Handle the final damage span if it exists. 130 | if let position = start { 131 | split(span: position ..< updated.buffer.endIndex, from: updated, into: &damages) 132 | } 133 | 134 | return damages 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Primitives/RingBuffer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// A high-performance ring buffer implementation using safe Swift types 5 | /// with optimal memory layout and minimal overhead. 6 | package struct RingBuffer { 7 | private var storage: ContiguousArray 8 | // Points to first valid element (oldest) 9 | private var head: ContiguousArray.Index = 0 10 | // Points to next insertion position 11 | private var tail: ContiguousArray.Index = 0 12 | private var size: Int = 0 13 | 14 | /// The maximum number of elements this buffer can hold 15 | public let capacity: Int 16 | 17 | /// Creates a new ring buffer with the specified capacity. 18 | /// - Parameter capacity: The maximum number of elements the buffer can hold 19 | package init(capacity: Int) { 20 | precondition(capacity > 0, "Capacity must be positive") 21 | self.capacity = capacity 22 | // Pre-allocate storage to full capacity with nil values 23 | self.storage = ContiguousArray(repeating: nil, count: capacity) 24 | } 25 | 26 | /// The number of elements currently in the buffer 27 | @inline(__always) 28 | package var count: Int { size } 29 | 30 | /// Returns true if the buffer contains no elements 31 | @inline(__always) 32 | package var isEmpty: Bool { count == 0 } 33 | 34 | /// Returns true if the buffer is at maximum capacity 35 | @inline(__always) 36 | package var isFull: Bool { count == capacity } 37 | 38 | /// Pushes an element to the buffer, potentially overwriting the oldest element if full 39 | package mutating func push(_ element: Element) { 40 | if isFull { 41 | storage[head] = nil 42 | head = _index(after: head) 43 | } else { 44 | size += 1 45 | } 46 | 47 | storage[tail] = element 48 | tail = _index(after: tail) 49 | } 50 | 51 | /// Returns the oldest element without removing it 52 | package borrowing func peek() -> Element? { 53 | guard !isEmpty else { return nil } 54 | return storage[head] 55 | } 56 | 57 | /// Removes and returns the oldest element if available 58 | package mutating func pop() -> Element? { 59 | guard !isEmpty else { return nil } 60 | defer { 61 | storage[head] = nil 62 | head = _index(after: head) 63 | size -= 1 64 | } 65 | return storage[head] 66 | } 67 | } 68 | 69 | // MARK: - Private Helpers 70 | 71 | extension RingBuffer { 72 | @inline(__always) 73 | private func _index(after index: ContiguousArray.Index) 74 | -> ContiguousArray.Index { 75 | (index + 1) % capacity 76 | } 77 | 78 | @inline(__always) 79 | private func _index(before index: ContiguousArray.Index) 80 | -> ContiguousArray.Index { 81 | (index - 1 + capacity) % capacity 82 | } 83 | } 84 | 85 | // MARK: - Collection Conformance 86 | 87 | extension RingBuffer: Collection { 88 | package typealias Index = Int 89 | 90 | package var startIndex: Index { 0 } 91 | package var endIndex: Index { count } 92 | 93 | package borrowing func index(after i: Index) -> Index { 94 | precondition(i < count, "Index out of bounds") 95 | return i + 1 96 | } 97 | 98 | package subscript(position: Index) -> Element { 99 | @inline(__always) 100 | _read { 101 | precondition(position >= 0 && position < count, "Index out of bounds") 102 | let offset = (head + position) % capacity 103 | guard let element = storage[offset] else { 104 | fatalError("Ring buffer invariant violated: found nil at valid position \(position)") 105 | } 106 | yield element 107 | } 108 | 109 | @inline(__always) 110 | _modify { 111 | precondition(position >= 0 && position < count, "Index out of bounds") 112 | let offset = (head + position) % capacity 113 | guard var element = storage[offset] else { 114 | fatalError("Ring buffer invariant violated: found nil at valid position \(position)") 115 | } 116 | yield &element 117 | storage[offset] = element 118 | } 119 | } 120 | } 121 | 122 | // MARK: - Sequence Conformance for Enhanced Performance 123 | 124 | extension RingBuffer: Sequence { 125 | package struct Iterator: IteratorProtocol { 126 | private let buffer: RingBuffer 127 | private var currentIndex: Int = 0 128 | 129 | internal init(_ buffer: RingBuffer) { 130 | self.buffer = buffer 131 | } 132 | 133 | package mutating func next() -> Element? { 134 | guard currentIndex < buffer.count else { return nil } 135 | let element = buffer[currentIndex] 136 | currentIndex += 1 137 | return element 138 | } 139 | } 140 | 141 | package func makeIterator() -> Iterator { 142 | Iterator(self) 143 | } 144 | } 145 | 146 | // MARK: - Additional Access Methods 147 | 148 | extension RingBuffer { 149 | /// Returns the most recently added element without removing it 150 | package borrowing func last() -> Element? { 151 | guard !isEmpty else { return nil } 152 | let lastIndex = _index(before: tail) 153 | return storage[lastIndex] 154 | } 155 | 156 | /// Removes and returns the newest element if available 157 | package mutating func popLast() -> Element? { 158 | guard !isEmpty else { return nil } 159 | tail = _index(before: tail) 160 | defer { 161 | storage[tail] = nil 162 | size -= 1 163 | } 164 | return storage[tail] 165 | } 166 | 167 | /// Removes all elements from the buffer 168 | package mutating func removeAll() { 169 | // Clear all elements efficiently 170 | for i in 0.. Void) rethrows { 196 | for index in 0 ..< count { 197 | try body(self[index]) 198 | } 199 | } 200 | 201 | /// Efficiently maps over all elements maintaining order 202 | package borrowing func map(_ transform: (borrowing Element) throws -> U) rethrows -> [U] { 203 | var result: [U] = [] 204 | result.reserveCapacity(count) 205 | 206 | try forEach { element in 207 | result.append(try transform(element)) 208 | } 209 | 210 | return result 211 | } 212 | 213 | /// Efficiently reduces all elements 214 | package borrowing func reduce( 215 | _ initialResult: Result, 216 | _ nextPartialResult: (Result, borrowing Element) throws -> Result 217 | ) rethrows -> Result { 218 | var result = initialResult 219 | try forEach { element in 220 | result = try nextPartialResult(result, element) 221 | } 222 | return result 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/SGROptimizer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// Optimizes terminal text styling by tracking state and minimizing redundant commands. 5 | /// 6 | /// `SGRStateTracker` maintains the current terminal styling state and generates 7 | /// only the minimal SGR (Select Graphic Rendition) commands needed to transition 8 | /// between different text styles. This optimization reduces terminal output and 9 | /// improves rendering performance by avoiding redundant style changes. 10 | /// 11 | /// ## SGR Optimization Benefits 12 | /// 13 | /// - Eliminates redundant style commands when styles haven't changed 14 | /// - Minimizes terminal output by generating only necessary transitions 15 | /// - Handles complex attribute combinations intelligently 16 | /// - Manages terminal state consistency across style changes 17 | /// 18 | /// ## Usage Pattern 19 | /// 20 | /// The tracker is typically used in rendering pipelines where text styles 21 | /// change frequently: 22 | /// 23 | /// ```swift 24 | /// var tracker = SGRStateTracker() 25 | /// 26 | /// // First transition sets up initial styling 27 | /// let commands = tracker.transition(to: VTStyle(foreground: .red, 28 | /// attributes: [.bold])) 29 | /// // Outputs: [.Foreground(.red), .Bold] 30 | /// 31 | /// // Subsequent identical style produces no commands 32 | /// let commands2 = tracker.transition(to: VTStyle(foreground: .red, 33 | /// attributes: [.bold])) 34 | /// // Outputs: [] (no change needed) 35 | /// 36 | /// // Only changed attributes are updated 37 | /// let commands3 = tracker.transition(to: VTStyle(foreground: .blue, // changed 38 | /// attributes: [.bold] // unchanged)) 39 | /// // Outputs: [.Foreground(.blue)] 40 | /// ``` 41 | /// 42 | /// ## State Management 43 | /// 44 | /// The tracker maintains internal state and uses the non-copyable `~Copyable` 45 | /// constraint to ensure unique ownership and prevent state corruption. 46 | package struct SGRStateTracker: ~Copyable { 47 | private var current: VTStyle = .default 48 | 49 | private static var irreversible: VTAttributes { 50 | [] 51 | } 52 | 53 | package init() { } 54 | 55 | private static func rendition(for attribute: VTAttributes, disabled: Bool = false) -> GraphicRendition { 56 | switch attribute { 57 | case .bold: return disabled ? .Normal : .Bold 58 | case .italic: return disabled ? .ItalicOff : .Italic 59 | case .underline: return disabled ? .UnderlineOff : .Underline 60 | case .blink: return .SlowBlink 61 | case .strikethrough: return disabled ? .NotCrossedOut : .CrossedOut 62 | default: fatalError("Unsupported VTAttribute \(attribute)") 63 | } 64 | } 65 | 66 | /// Generates the minimal SGR commands to transition between text styles. 67 | /// 68 | /// This method compares the current tracked style with the target style 69 | /// and produces only the SGR commands necessary to achieve the transition. 70 | /// It handles complex scenarios like attribute removal, color changes, 71 | /// and combinations of multiple style properties. 72 | /// 73 | /// ## Optimization Logic 74 | /// 75 | /// The method analyzes differences between current and target styles: 76 | /// - **No-op optimization**: Returns empty array if styles are identical 77 | /// - **Attribute management**: Toggles only changed text attributes 78 | /// - **Color transitions**: Updates only foreground/background changes 79 | /// - **Reset handling**: Uses SGR reset only when necessary for irreversible changes 80 | /// 81 | /// ## Parameters 82 | /// - target: The desired text style to transition to 83 | /// 84 | /// ## Returns 85 | /// An array of `GraphicRendition` commands representing the minimal 86 | /// transition. Returns an empty array if no changes are needed. 87 | /// 88 | /// ## Usage Examples 89 | /// 90 | /// ### Basic Style Changes 91 | /// ```swift 92 | /// var tracker = SGRStateTracker() 93 | /// 94 | /// // Set initial style 95 | /// let initial = tracker.transition(to: VTStyle(foreground: .white, 96 | /// background: .black, 97 | /// attributes: [.bold])) 98 | /// // Result: [.Foreground(.white), .Background(.black), .Bold] 99 | /// 100 | /// // Change only color 101 | /// let colorChange = tracker.transition(to: VTStyle(foreground: .red, // changed 102 | /// background: .black, // unchanged 103 | /// attributes: [.bold])) // unchanged 104 | /// // Result: [.Foreground(.red)] 105 | /// ``` 106 | /// 107 | /// ### Complex Attribute Handling 108 | /// ```swift 109 | /// // Add italic, keep bold 110 | /// let addItalic = tracker.transition(to: VTStyle(foreground: .red, 111 | /// attributes: [.bold, .italic])) 112 | /// // Result: [.Italic] 113 | /// 114 | /// // Remove bold, keep italic 115 | /// let removeBold = tracker.transition(to: VTStyle(foreground: .red, 116 | /// attributes: [.italic])) 117 | /// // Result: [.Normal] 118 | /// ``` 119 | /// 120 | /// ## State Updates 121 | /// 122 | /// The tracker's internal state is automatically updated after each 123 | /// transition, ensuring subsequent calls have accurate baseline information 124 | /// for optimization decisions. 125 | /// 126 | /// ## Performance Characteristics 127 | /// 128 | /// This optimization is particularly effective in scenarios with: 129 | /// - Frequent style changes (syntax highlighting, UI elements) 130 | /// - Repetitive styling patterns (tables, formatted output) 131 | /// - Complex attribute combinations that would otherwise generate redundant commands 132 | package mutating func transition(to target: VTStyle) -> [GraphicRendition] { 133 | if current == target { return [] } 134 | 135 | var renditions: [GraphicRendition] = [] 136 | 137 | let removed = current.attributes.subtracting(target.attributes) 138 | // let added = target.attributes.subtracting(current.attributes) 139 | let toggled = current.attributes.symmetricDifference(target.attributes) 140 | 141 | // Only reset if we need to clear attributes which cannot be individually 142 | // toggled. 143 | if !removed.intersection(Self.irreversible).isEmpty { 144 | renditions.append(.Reset) 145 | current = .default 146 | } 147 | 148 | // Foreground color. 149 | if current.foreground != target.foreground { 150 | renditions.append(.Foreground(target.foreground ?? .default)) 151 | } 152 | 153 | // Background color. 154 | if current.background != target.background { 155 | renditions.append(.Background(target.background ?? .default)) 156 | } 157 | 158 | // Attributes. 159 | if toggled.contains(.bold) { 160 | renditions.append(Self.rendition(for: .bold, disabled: removed.contains(.bold))) 161 | } 162 | if toggled.contains(.italic) { 163 | renditions.append(Self.rendition(for: .italic, disabled: removed.contains(.italic))) 164 | } 165 | if toggled.contains(.underline) { 166 | renditions.append(Self.rendition(for: .underline, disabled: removed.contains(.underline))) 167 | } 168 | if toggled.contains(.blink) { 169 | renditions.append(Self.rendition(for: .blink, disabled: removed.contains(.blink))) 170 | } 171 | if toggled.contains(.strikethrough) { 172 | renditions.append(Self.rendition(for: .strikethrough, disabled: removed.contains(.strikethrough))) 173 | } 174 | 175 | current = target 176 | return renditions 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Buffer/VTStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// Text formatting attributes that can be combined for rich terminal styling. 5 | /// 6 | /// Use these attributes to make text bold, italic, underlined, or apply other 7 | /// visual effects. Multiple attributes can be combined using set operations: 8 | /// 9 | /// ```swift 10 | /// let emphasis: VTAttributes = [.bold, .italic] 11 | /// let decorated: VTAttributes = [.underline, .strikethrough] 12 | /// ``` 13 | public struct VTAttributes: OptionSet, Sendable, Equatable { 14 | public let rawValue: UInt8 15 | 16 | public init(rawValue: UInt8) { 17 | self.rawValue = rawValue 18 | } 19 | } 20 | 21 | extension VTAttributes { 22 | /// Makes text appear in bold or increased intensity. 23 | public static var bold: VTAttributes { 24 | VTAttributes(rawValue: 1 << 1) 25 | } 26 | 27 | /// Renders text in italic or oblique style. 28 | public static var italic: VTAttributes { 29 | VTAttributes(rawValue: 1 << 2) 30 | } 31 | 32 | /// Adds an underline beneath the text. 33 | public static var underline: VTAttributes { 34 | VTAttributes(rawValue: 1 << 3) 35 | } 36 | 37 | /// Draws a line through the text (crossed out). 38 | public static var strikethrough: VTAttributes { 39 | VTAttributes(rawValue: 1 << 5) 40 | } 41 | 42 | /// Makes text blink or flash intermittently. 43 | /// 44 | /// Note that blink support varies by terminal and may be disabled in 45 | /// some environments for accessibility reasons. 46 | public static var blink: VTAttributes { 47 | VTAttributes(rawValue: 1 << 4) 48 | } 49 | } 50 | 51 | private func pack(_ color: ANSIColor, _ intensity: ANSIColorIntensity) -> UInt64 { 52 | // [23-9: reserved][8: intensity][7-0: color] 53 | return (UInt64(intensity == .bright ? 1 : 0) << 8) | (UInt64(color.rawValue) & 0xff) 54 | } 55 | 56 | private func pack(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> UInt64 { 57 | // [23-16: red][15-8: green][7-0: blue] 58 | return (UInt64(red) << 16) | (UInt64(green) << 8) | (UInt64(blue) << 0) 59 | } 60 | 61 | private struct Flags: OptionSet { 62 | public let rawValue: UInt8 63 | 64 | public init(rawValue: UInt8) { 65 | self.rawValue = rawValue 66 | } 67 | } 68 | 69 | extension Flags { 70 | public static var ANSIForeground: Flags { 71 | Flags(rawValue: 1 << 0) 72 | } 73 | 74 | public static var RGBForeground: Flags { 75 | Flags(rawValue: 1 << 1) 76 | } 77 | 78 | public static var ANSIBackground: Flags { 79 | Flags(rawValue: 1 << 2) 80 | } 81 | 82 | public static var RGBBackground: Flags { 83 | Flags(rawValue: 1 << 3) 84 | } 85 | } 86 | 87 | /// Defines the complete visual appearance of terminal text. 88 | /// 89 | /// A style combines foreground color, background color, and text attributes 90 | /// into a single value. Styles are efficiently packed into a single 64-bit 91 | /// integer for optimal memory usage and comparison performance. 92 | /// 93 | /// ## Usage Examples 94 | /// 95 | /// ```swift 96 | /// // Basic colored text 97 | /// let red = VTStyle(foreground: .ansi(.init(color: .red, intensity: .bright))) 98 | /// 99 | /// // Styled text with multiple attributes 100 | /// let heading = VTStyle(foreground: .rgb(red: 255, green: 255, blue: 255), 101 | /// background: .ansi(.init(color: .blue, intensity: .normal)), 102 | /// attributes: [.bold, .underline]) 103 | /// 104 | /// // Use default system colors with formatting 105 | /// let emphasis = VTStyle(attributes: [.italic]) 106 | /// ``` 107 | public struct VTStyle: Sendable, Equatable { 108 | // [63-40: background][39-16: foreground][15-8: attributes][7-0: flags] 109 | private let representation: UInt64 110 | 111 | /// Creates a new text style with the specified visual properties. 112 | /// 113 | /// All parameters are optional, allowing you to specify only the styling 114 | /// you need. Omitted colors will use the terminal's default colors. 115 | /// 116 | /// - Parameters: 117 | /// - foreground: The text color, or `nil` for terminal default. 118 | /// - background: The background color, or `nil` for terminal default. 119 | /// - attributes: Text formatting attributes to apply. 120 | public init(foreground: VTColor? = nil, background: VTColor? = nil, attributes: VTAttributes = []) { 121 | var representation = (UInt64(attributes.rawValue) << 8) 122 | 123 | switch foreground { 124 | case .none: 125 | representation |= (pack(ANSIColor.default, .normal) << 16) | UInt64(Flags.ANSIForeground.rawValue) 126 | case let .some(.ansi(color, intensity)): 127 | representation |= (pack(color, intensity) << 16) | UInt64(Flags.ANSIForeground.rawValue) 128 | case let .some(.rgb(red, green, blue)): 129 | representation |= (pack(red, green, blue) << 16) | UInt64(Flags.RGBForeground.rawValue) 130 | } 131 | 132 | switch background { 133 | case .none: 134 | representation |= (pack(ANSIColor.default, .normal) << 40) | UInt64(Flags.ANSIBackground.rawValue) 135 | case let .some(.ansi(color, intensity)): 136 | representation |= (pack(color, intensity) << 40) | UInt64(Flags.ANSIBackground.rawValue) 137 | case let .some(.rgb(red, green, blue)): 138 | representation |= (pack(red, green, blue) << 40) | UInt64(Flags.RGBBackground.rawValue) 139 | } 140 | 141 | self.representation = representation 142 | } 143 | 144 | /// The foreground (text) color, or `nil` if using terminal default. 145 | public var foreground: VTColor? { 146 | let flags = Flags(rawValue: UInt8(representation & 0xff)) 147 | 148 | if flags.contains(.ANSIForeground) { 149 | let bits = representation >> 16 150 | guard let color = ANSIColor(rawValue: (Int(bits) & 0xff)) else { 151 | return nil 152 | } 153 | let intensity = (bits >> 8) & 1 == 1 ? ANSIColorIntensity.bright 154 | : ANSIColorIntensity.normal 155 | return .ansi(color, intensity: intensity) 156 | } 157 | 158 | if flags.contains(.RGBForeground) { 159 | // [15-8: red][7-0: green][0-0: blue] 160 | let bits = representation >> 16 161 | let red = UInt8((bits >> 0x10) & 0xff) 162 | let green = UInt8((bits >> 0x08) & 0xff) 163 | let blue = UInt8((bits >> 0x00) & 0xff) 164 | return .rgb(red: red, green: green, blue: blue) 165 | } 166 | 167 | return nil 168 | } 169 | 170 | /// The background color, or `nil` if using terminal default. 171 | public var background: VTColor? { 172 | let flags = Flags(rawValue: UInt8(representation & 0xff)) 173 | 174 | if flags.contains(.ANSIBackground) { 175 | let bits = representation >> 40 176 | guard let color = ANSIColor(rawValue: (Int(bits) & 0xff)) else { 177 | return nil 178 | } 179 | let intensity = (bits >> 8) & 1 == 1 ? ANSIColorIntensity.bright 180 | : ANSIColorIntensity.normal 181 | return .ansi(color, intensity: intensity) 182 | } 183 | 184 | if flags.contains(.RGBBackground) { 185 | let bits = representation >> 40 186 | let red = UInt8((bits >> 0x10) & 0xff) 187 | let green = UInt8((bits >> 0x08) & 0xff) 188 | let blue = UInt8((bits >> 0x00) & 0xff) 189 | return .rgb(red: red, green: green, blue: blue) 190 | } 191 | 192 | return nil 193 | } 194 | 195 | /// The set of text formatting attributes applied to this style. 196 | public var attributes: VTAttributes { 197 | return VTAttributes(rawValue: UInt8((representation >> 8) & 0xff)) 198 | } 199 | } 200 | 201 | extension VTStyle { 202 | internal func with(foreground: VTColor?) -> VTStyle { 203 | VTStyle(foreground: foreground, background: background, attributes: attributes) 204 | } 205 | 206 | internal func with(background: VTColor?) -> VTStyle { 207 | VTStyle(foreground: foreground, background: background, attributes: attributes) 208 | } 209 | 210 | internal func with(attributes: VTAttributes) -> VTStyle { 211 | VTStyle(foreground: foreground, background: background, attributes: attributes) 212 | } 213 | } 214 | 215 | extension VTStyle { 216 | /// A style with no formatting that uses terminal default colors. 217 | /// 218 | /// This is equivalent to creating `VTStyle()` with no parameters, but 219 | /// provides better semantic clarity when resetting text to unstyled 220 | /// appearance. 221 | public static var `default`: VTStyle { 222 | VTStyle(foreground: nil, background: nil, attributes: []) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/VTDisplayLink.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Geometry 5 | 6 | /// A precise frame timing system for smooth terminal animations and rendering. 7 | /// 8 | /// `VTDisplayLink` provides accurate frame rate control by synchronizing 9 | /// callback execution to a specified target frame rate. It handles timing 10 | /// precision, frame drops, and pause/resume functionality for creating 11 | /// smooth terminal-based animations and interactive applications. 12 | /// 13 | /// ## Key Features 14 | /// 15 | /// - **Precise timing**: Synchronizes to target frame intervals using system clocks 16 | /// - **Frame rate control**: Maintains consistent timing even under varying load 17 | /// - **Pause/resume**: Allows temporary suspension without losing timing accuracy 18 | /// - **Task group integration**: Works seamlessly with Swift's structured concurrency 19 | /// - **Frame drop handling**: Automatically catches up when frames are delayed 20 | /// 21 | /// ## Usage with VTRenderer 22 | /// 23 | /// The display link integrates automatically with `VTRenderer`: 24 | /// 25 | /// ```swift 26 | /// try await renderer.rendering(fps: 60) { buffer in 27 | /// // Your rendering code runs at exactly 60 FPS 28 | /// drawAnimatedContent(&buffer, frame: frameCounter++) 29 | /// } 30 | /// ``` 31 | /// 32 | /// ## Manual Usage 33 | /// 34 | /// For custom animation loops or precise timing control: 35 | /// 36 | /// ```swift 37 | /// let displayLink = VTDisplayLink(fps: 30) { link in 38 | /// guard !gameIsPaused else { return } 39 | /// updateGameLogic(deltaTime: link.duration.seconds) 40 | /// renderFrame(timestamp: link.timestamp) 41 | /// } 42 | /// 43 | /// try await withThrowingTaskGroup(of: Void.self) { group in 44 | /// displayLink.add(to: &group) 45 | /// try await group.next() 46 | /// } 47 | /// ``` 48 | /// 49 | /// ## Performance Characteristics 50 | /// 51 | /// The display link uses `ContinuousClock` for microsecond-level precision 52 | /// and employs `Task.sleep(for:)` for efficient CPU usage. It automatically 53 | /// handles frame catching-up when the system is under load, ensuring 54 | /// animations remain smooth even when individual frames are delayed. 55 | public final class VTDisplayLink: Sendable { 56 | /// The target time interval between frame callbacks. 57 | /// 58 | /// This duration represents the ideal time between frame updates based 59 | /// on the configured frame rate. For example, at 60 FPS this would be 60 | /// approximately 16.67 milliseconds. 61 | public let duration: Duration 62 | 63 | /// The configured target frame rate in frames per second. 64 | /// 65 | /// This computed property provides the frame rate that was specified 66 | /// during initialization. Use this to display current frame rate settings 67 | /// or verify the display link configuration. 68 | /// 69 | /// ## Usage Example 70 | /// ```swift 71 | /// print("Display link running at \(displayLink.preferredFramesPerSecond) FPS") 72 | /// ``` 73 | public var preferredFramesPerSecond: Double { 74 | 1.0 / duration.seconds 75 | } 76 | 77 | /// A Boolean value that indicates whether the system suspends the display 78 | /// link’s notifications to the target. 79 | public private(set) nonisolated(unsafe) var isPaused: Bool = false 80 | 81 | /// The instant that represents when the last frame displayed. 82 | public private(set) nonisolated(unsafe) var timestamp: ContinuousClock.Instant = .now 83 | 84 | /// The time stamp of the next frame. 85 | public var targetTimestamp: ContinuousClock.Instant { 86 | let elapsed = ContinuousClock.now - timestamp 87 | let intervals = elapsed.nanoseconds / duration.nanoseconds 88 | return timestamp + Duration.seconds(Double(intervals) * duration.seconds) 89 | } 90 | 91 | private let callback: @Sendable (borrowing VTDisplayLink) async throws -> Void 92 | 93 | /// Creates a display link with the specified frame rate and callback. 94 | /// 95 | /// The display link will attempt to call the provided callback at the 96 | /// specified frame rate. The callback receives a reference to the display 97 | /// link, allowing access to timing information and control methods. 98 | /// 99 | /// ## Parameters 100 | /// - fps: Target frames per second (must be > 0) 101 | /// - callback: Function to execute at each frame interval 102 | /// 103 | /// ## Usage Example 104 | /// ```swift 105 | /// let displayLink = VTDisplayLink(fps: 60) { link in 106 | /// // Frame callback executed 60 times per second 107 | /// await renderFrame() 108 | /// 109 | /// // Access timing for animations 110 | /// animateWithTime(link.timestamp) 111 | /// } 112 | /// ``` 113 | /// 114 | /// ## Error Handling 115 | /// If the callback throws an error, it will propagate up through the 116 | /// task group and terminate the display link execution. 117 | public init(fps: Double, _ callback: @escaping @Sendable (borrowing VTDisplayLink) async throws -> Void) { 118 | self.duration = Duration.seconds(1.0 / fps) 119 | self.callback = callback 120 | } 121 | 122 | /// Temporarily suspends frame callback execution. 123 | /// 124 | /// When paused, the display link continues running and maintaining 125 | /// accurate timing, but skips executing the frame callback. This 126 | /// allows you to temporarily suspend animations or rendering without 127 | /// losing synchronization when resumed. 128 | /// 129 | /// ## Usage Example 130 | /// ```swift 131 | /// // Pause during menu screens 132 | /// displayLink.pause() 133 | /// await showMenu() 134 | /// displayLink.resume() 135 | /// ``` 136 | /// 137 | /// ## Timing Behavior 138 | /// The display link continues tracking frame timestamps while paused, 139 | /// so when resumed, it will continue from the correct timing position 140 | /// without catching up on missed frames. 141 | public func pause() { 142 | self.isPaused = true 143 | } 144 | 145 | /// Resumes frame callback execution after being paused. 146 | /// 147 | /// Restores normal frame callback execution at the configured frame rate. 148 | /// The display link will continue from its current timing position 149 | /// without attempting to catch up on frames that occurred while paused. 150 | /// 151 | /// ## Usage Example 152 | /// ```swift 153 | /// displayLink.resume() 154 | /// // Frame callbacks resume at the next frame boundary 155 | /// ``` 156 | public func resume() { 157 | self.isPaused = false 158 | } 159 | 160 | /// Adds the display link to a task group for concurrent execution. 161 | /// 162 | /// This method integrates the display link into Swift's structured 163 | /// concurrency system by adding it as a task to a `ThrowingTaskGroup`. 164 | /// The display link will run concurrently with other tasks in the group, 165 | /// executing its callback at the configured frame rate. 166 | /// 167 | /// ## Parameters 168 | /// - group: A throwing task group that will manage the display link's execution 169 | /// 170 | /// ## Usage Example 171 | /// ```swift 172 | /// let displayLink = VTDisplayLink(fps: 60) { link in 173 | /// await updateAnimation() 174 | /// } 175 | /// 176 | /// try await withThrowingTaskGroup(of: Void.self) { group in 177 | /// displayLink.add(to: &group) 178 | /// 179 | /// // Add other concurrent tasks 180 | /// group.addTask { await handleUserInput() } 181 | /// 182 | /// // Wait for any task to complete or throw 183 | /// try await group.next() 184 | /// } 185 | /// ``` 186 | /// 187 | /// ## Behavior 188 | /// The display link task will continue running until: 189 | /// - The task is cancelled (via `Task.isCancelled`) 190 | /// - The callback throws an error 191 | /// - The task group is cancelled 192 | /// 193 | /// ## Performance Notes 194 | /// The task uses an unowned reference to `self` to avoid retain cycles, 195 | /// so ensure the display link instance remains alive while the task is running. 196 | public func add(to group: inout ThrowingTaskGroup) { 197 | group.addTask { [unowned self] in 198 | timestamp = .now 199 | repeat { 200 | // Synchronise to the display link interval. 201 | let remainder = targetTimestamp - ContinuousClock.now 202 | if remainder > .zero { 203 | try await Task.sleep(for: remainder) 204 | } 205 | 206 | // Update the frame timing. 207 | timestamp = targetTimestamp 208 | 209 | guard !isPaused else { continue } 210 | try await callback(self) 211 | } while !Task.isCancelled 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Protocol/Encoding.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// A control sequence paired with its encoding format. 5 | /// 6 | /// `EncodedControlSequence` represents a terminal control sequence that has 7 | /// been associated with a specific encoding (7-bit or 8-bit). This is useful 8 | /// when you need to store or pass around sequences with their encoding 9 | /// information intact. 10 | /// 11 | /// ## Usage Example 12 | /// ```swift 13 | /// let encoded = EncodedControlSequence(.CursorUp(5), encoding: .b7) 14 | /// // The sequence retains both the command and encoding information 15 | /// ``` 16 | public struct EncodedControlSequence: Sendable { 17 | public let sequence: ControlSequence 18 | public let encoding: ControlSequenceEncoding 19 | 20 | public init(_ sequence: ControlSequence, encoding: ControlSequenceEncoding) { 21 | self.sequence = sequence 22 | self.encoding = encoding 23 | } 24 | } 25 | 26 | /// Context for writing control sequences with a specific encoding. 27 | /// 28 | /// `EncodedStreamContext` provides a fluent interface for writing terminal 29 | /// control sequences and text with a consistent encoding format. This ensures 30 | /// all sequences sent through the context use the same 7-bit or 8-bit 31 | /// encoding, which is important for terminal compatibility. 32 | /// 33 | /// The context maintains a reference to the underlying terminal stream and 34 | /// automatically encodes all control sequences before sending them. 35 | /// 36 | /// ## Usage Example 37 | /// ```swift 38 | /// let terminal: any Terminal = ... 39 | /// let context = terminal(encoding: .b7) 40 | /// 41 | /// // All sequences will use 7-bit encoding 42 | /// await context <<< .CursorPosition(10, 20) 43 | /// <<< "Hello, World!" 44 | /// await context <<< .SelectGraphicRendition([.bold, .foreground(.red)]) 45 | /// ``` 46 | public final class EncodedStreamContext: Sendable { 47 | @usableFromInline 48 | internal nonisolated(unsafe) var stream: any VTTerminal 49 | 50 | @usableFromInline 51 | internal let encoding: ControlSequenceEncoding 52 | 53 | @inlinable 54 | public init(stream: any VTTerminal, encoding: ControlSequenceEncoding) { 55 | self.stream = stream 56 | self.encoding = encoding 57 | } 58 | } 59 | 60 | extension VTTerminal { 61 | /// Creates an encoded stream context with the specified encoding. 62 | /// 63 | /// This method provides a convenient way to create a context that will 64 | /// encode all control sequences using the specified format. Use this 65 | /// when you need consistent encoding across multiple terminal operations. 66 | /// 67 | /// - Parameter encoding: The encoding format to use for all sequences. 68 | /// - Returns: An encoded stream context bound to this terminal. 69 | @inlinable 70 | public func callAsFunction(encoding: ControlSequenceEncoding) -> EncodedStreamContext { 71 | EncodedStreamContext(stream: self, encoding: encoding) 72 | } 73 | } 74 | 75 | extension EncodedStreamContext { 76 | /// Writes a control sequence to the terminal using the context's encoding. 77 | /// 78 | /// This operator provides a fluent interface for sending control sequences 79 | /// through the encoded context. The sequence is automatically encoded 80 | /// using the context's encoding format before being sent to the terminal. 81 | /// 82 | /// - Parameters: 83 | /// - context: The encoded stream context to write through. 84 | /// - sequence: The control sequence to send. 85 | /// - Returns: The same context for method chaining. 86 | @inlinable 87 | @discardableResult 88 | public static func <<< (_ context: EncodedStreamContext, _ sequence: ControlSequence) async -> EncodedStreamContext { 89 | await context.stream.write(sequence.encoded(as: context.encoding)) 90 | return context 91 | } 92 | 93 | /// Writes plain text to the terminal through the encoded context. 94 | /// 95 | /// This operator allows writing text content through the encoded context. 96 | /// The text is sent directly to the terminal without encoding (only 97 | /// control sequences require encoding). 98 | /// 99 | /// - Parameters: 100 | /// - context: The encoded stream context to write through. 101 | /// - string: The text string to send. 102 | /// - Returns: The same context for method chaining. 103 | @inlinable 104 | @discardableResult 105 | public static func <<< (_ context: inout EncodedStreamContext, _ string: String) async -> EncodedStreamContext { 106 | await context.stream.write(string) 107 | return context 108 | } 109 | } 110 | 111 | /// Context for scoped encoding operations - operates directly on base stream. 112 | /// 113 | /// `EncodedBufferedStreamContext` provides a fluent interface for buffered 114 | /// terminal operations with consistent encoding. Unlike `EncodedStreamContext`, 115 | /// this operates on buffered streams where sequences are accumulated and 116 | /// then flushed together, improving performance for batch operations. 117 | /// 118 | /// This context is particularly useful when you need to build up a series 119 | /// of terminal commands before sending them all at once. 120 | /// 121 | /// ## Usage Example 122 | /// ```swift 123 | /// let buffer = VTBufferedTerminalStream(terminal) 124 | /// let context = buffer(encoding: .b8) 125 | /// 126 | /// // Build up a series of operations 127 | /// context <<< .SaveCursor 128 | /// <<< .CursorPosition(1, 1) 129 | /// <<< .SelectGraphicRendition([.bold]) 130 | /// <<< "Status: Ready" 131 | /// <<< .RestoreCursor 132 | /// 133 | /// // All operations are sent together when buffer is flushed 134 | /// await buffer.flush() 135 | /// ``` 136 | public final class EncodedBufferedStreamContext { 137 | @usableFromInline 138 | internal var stream: VTBufferedTerminalStream 139 | 140 | @usableFromInline 141 | internal let encoding: ControlSequenceEncoding 142 | 143 | @inlinable 144 | internal init(stream: VTBufferedTerminalStream, encoding: ControlSequenceEncoding) { 145 | self.stream = stream 146 | self.encoding = encoding 147 | } 148 | } 149 | 150 | extension VTBufferedTerminalStream { 151 | /// Creates an encoded buffered stream context with the specified encoding. 152 | /// 153 | /// This method provides a convenient way to create a context for buffered 154 | /// operations that will encode all control sequences using the specified 155 | /// format. The context accumulates operations until the underlying 156 | /// buffered stream is flushed. 157 | /// 158 | /// - Parameter encoding: The encoding format to use for all sequences. 159 | /// - Returns: An encoded buffered stream context bound to this stream. 160 | @inlinable 161 | public func callAsFunction(encoding: ControlSequenceEncoding) -> EncodedBufferedStreamContext { 162 | EncodedBufferedStreamContext(stream: self, encoding: encoding) 163 | } 164 | } 165 | 166 | extension EncodedBufferedStreamContext { 167 | /// Appends a control sequence to the buffer using the context's encoding. 168 | /// 169 | /// This operator provides a fluent interface for adding control sequences 170 | /// to the buffered stream. The sequence is encoded using the context's 171 | /// encoding format and added to the buffer. The actual transmission occurs 172 | /// when the buffer is flushed. 173 | /// 174 | /// - Parameters: 175 | /// - context: The encoded buffered stream context to append to. 176 | /// - sequence: The control sequence to add to the buffer. 177 | /// - Returns: The same context for method chaining. 178 | @inlinable 179 | @discardableResult 180 | public static func <<< (_ context: EncodedBufferedStreamContext, _ sequence: ControlSequence) -> EncodedBufferedStreamContext { 181 | context.stream.append(sequence.encoded(as: context.encoding)) 182 | return context 183 | } 184 | 185 | /// Appends plain text to the buffer through the encoded context. 186 | /// 187 | /// This operator allows adding text content to the buffered stream through 188 | /// the encoded context. The text is added directly to the buffer without 189 | /// encoding (only control sequences require encoding). The text will be 190 | /// sent when the buffer is flushed. 191 | /// 192 | /// - Parameters: 193 | /// - context: The encoded buffered stream context to append to. 194 | /// - string: The text string to add to the buffer. 195 | /// - Returns: The same context for method chaining. 196 | @inlinable 197 | @discardableResult 198 | public static func <<< (_ context: EncodedBufferedStreamContext, _ string: String) -> EncodedBufferedStreamContext { 199 | context.stream.append(string) 200 | return context 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Buffer/VTBuffer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Geometry 5 | 6 | /// A high-performance terminal buffer that manages a 2D grid of terminal cells. 7 | /// 8 | /// `VTBuffer` provides efficient storage and manipulation of terminal content 9 | /// using a contiguous array for optimal cache performance. It supports Unicode 10 | /// characters with proper width handling for CJK text, emoji, and other wide 11 | /// characters. 12 | /// 13 | /// The buffer uses `~Copyable` semantics to ensure efficient memory usage and 14 | /// prevent accidental expensive copies during rendering operations. 15 | public struct VTBuffer: ~Copyable, Sendable { 16 | /// The dimensions of the terminal buffer in columns and rows. 17 | public let size: Size 18 | 19 | /// Internal storage for terminal cells in row-major order. 20 | package private(set) var buffer: ContiguousArray 21 | 22 | /// Creates a new terminal buffer with the specified dimensions. 23 | /// 24 | /// The buffer is initialized with blank cells using the default style. 25 | /// 26 | /// - Parameter size: The width and height of the buffer in character cells. 27 | public init(size: Size) { 28 | self.size = size 29 | self.buffer = 30 | ContiguousArray(repeating: .blank, count: size.width * size.height) 31 | } 32 | } 33 | 34 | extension VTBuffer { 35 | public typealias Index = ContiguousArray.Index 36 | public typealias Element = VTCell 37 | 38 | /// Accesses the terminal cell at the specified position. 39 | /// 40 | /// If the position is outside the buffer bounds, reading returns a blank 41 | /// cell and writing operations are ignored. This provides safe access 42 | /// without runtime crashes for out-of-bounds coordinates. 43 | /// 44 | /// - Parameter position: The 1-based row and column position in the buffer. 45 | /// - Returns: The terminal cell at the specified position, or a blank cell 46 | /// if out of bounds. 47 | public subscript(position: VTPosition) -> Element { 48 | _read { 49 | guard position.valid(in: size) else { 50 | yield VTCell.blank 51 | return 52 | } 53 | yield buffer[position.offset(in: size)] 54 | } 55 | _modify { 56 | guard position.valid(in: size) else { 57 | var blank = VTCell.blank 58 | yield &blank 59 | return 60 | } 61 | yield &buffer[position.offset(in: size)] 62 | } 63 | } 64 | } 65 | 66 | extension VTBuffer { 67 | /// Converts a linear buffer offset to a terminal position. 68 | /// 69 | /// This method is used internally for converting between the buffer's 70 | /// linear storage and 2D terminal coordinates. 71 | /// 72 | /// - Parameter offset: The linear offset into the buffer array. 73 | /// - Returns: The corresponding 1-based terminal position. 74 | package func position(at offset: ContiguousArray.Index) -> VTPosition { 75 | let offset = max(0, min(offset, buffer.count - 1)) 76 | return VTPosition(row: 1 + (offset / size.width), 77 | column: 1 + (offset % size.width)) 78 | } 79 | } 80 | 81 | extension VTBuffer { 82 | /// Writes a string to the buffer starting at the specified position. 83 | /// 84 | /// This method handles various control characters and Unicode text: 85 | /// - `\n`: Moves to the same column on the next row 86 | /// - `\r`: Moves to the beginning of the current row 87 | /// - `\t`: Moves to the next tab stop (every 8 characters) 88 | /// - Wide characters: Automatically handles CJK text and emoji with 89 | /// continuation cells 90 | /// 91 | /// Text that extends beyond the buffer boundaries is clipped. Wide 92 | /// characters that don't fit at the end of a line are moved to the next 93 | /// line. 94 | /// 95 | /// - Parameters: 96 | /// - text: The string to write to the buffer. 97 | /// - position: The starting position for writing (1-based coordinates). 98 | /// - style: The visual style to apply to the text. Defaults to `.default`. 99 | public mutating func write(string text: String, at position: VTPosition, 100 | style: VTStyle = .default) { 101 | // Validate position is within buffer bounds 102 | guard position.valid(in: size) else { return } 103 | 104 | var cursor = position.offset(in: size) 105 | for character in text { 106 | switch character { 107 | case "\n": 108 | cursor = min(size.height - 1, (cursor / size.width) + 1) * size.width 109 | + (cursor % size.width) 110 | case "\r": 111 | cursor = (cursor / size.width) * size.width 112 | case "\t": 113 | // Move to the next tab stop, which is every 8 characters. 114 | cursor = cursor - (cursor % size.width) 115 | + min(size.width - 1, (((cursor % size.width) / 8) + 1) * 8) 116 | default: 117 | guard cursor < buffer.count else { return } 118 | let width = character.width 119 | 120 | // Check if wide character fits in current row 121 | if (cursor % size.width) + width > size.width { 122 | // Wide character doesn't fit, move to next line 123 | cursor = ((cursor / size.width) + 1) * size.width 124 | guard cursor < buffer.count else { return } 125 | } 126 | 127 | // Check if entire character (including continuation) fits in buffer 128 | guard cursor + width < buffer.count else { return } 129 | 130 | buffer[cursor] = VTCell(character: character, style: style) 131 | for offset in 1 ..< width { 132 | // Mark continuation cells with the NUL character 133 | buffer[cursor + offset] = VTCell(character: "\u{0000}", style: style) 134 | } 135 | cursor = cursor + width 136 | } 137 | } 138 | } 139 | 140 | /// Clears the entire buffer by filling it with space characters. 141 | /// 142 | /// This operation resets all cells in the buffer to contain a space 143 | /// character with the specified style, effectively clearing any existing 144 | /// content. 145 | /// 146 | /// - Parameter style: The style to apply to the cleared cells. Defaults 147 | /// to `.default`. 148 | public mutating func clear(style: VTStyle = .default) { 149 | for index in buffer.indices { 150 | buffer[index] = VTCell(character: " ", style: style) 151 | } 152 | } 153 | 154 | /// Fills a rectangular region of the buffer with a specific character. 155 | /// 156 | /// This method efficiently fills a rectangular area with the same character 157 | /// and style. It handles wide characters correctly by placing continuation 158 | /// cells as needed. The rectangle is automatically clipped to the buffer 159 | /// boundaries. 160 | /// 161 | /// - Parameters: 162 | /// - rect: The rectangular region to fill (0-based coordinates). 163 | /// - character: The character to fill the region with. 164 | /// - style: The visual style to apply. Defaults to `.default`. 165 | public mutating func fill(rect: Rect, with character: Character, 166 | style: VTStyle = .default) { 167 | guard !rect.isEmpty else { return } 168 | 169 | let fill = VTCell(character: character, style: style) 170 | let continuation = VTCell(character: "\u{0000}", style: style) 171 | let width = character.width 172 | 173 | // Clamp rectangle bounds to valid buffer coordinates 174 | let rows = (start: max(0, rect.origin.y), 175 | end: min(rect.origin.y + rect.size.height, size.height)) 176 | let columns = (start: max(0, rect.origin.x), 177 | end: min(rect.origin.x + rect.size.width, size.width)) 178 | 179 | // Early exit if clipped rectangle is empty 180 | guard rows.start < rows.end && columns.start < columns.end else { return } 181 | 182 | if width == 1 { 183 | // Fast path for single-width characters 184 | for row in rows.start ..< rows.end { 185 | for column in columns.start ..< columns.end { 186 | buffer[row * size.width + column] = fill 187 | } 188 | } 189 | } else { 190 | // Wide character path 191 | for row in rows.start ..< rows.end { 192 | for column in stride(from: columns.start, to: columns.end, by: width) { 193 | buffer[row * size.width + column] = fill 194 | // Fill continuation cells for wide characters 195 | for offset in 1 ..< min(width, columns.end - column) { 196 | let index = row * size.width + column + offset 197 | if index >= buffer.count { break } 198 | buffer[index] = continuation 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Input/VTEvent.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Geometry 5 | 6 | /// Terminal input events from user interactions. 7 | /// 8 | /// `VTEvent` represents all types of input that can occur in a terminal 9 | /// application, including keyboard input, mouse interactions, and terminal 10 | /// resize events. This unified event system allows applications to handle 11 | /// all user input through a single interface. 12 | /// 13 | /// ## Usage Example 14 | /// ```swift 15 | /// func handleEvent(_ event: VTEvent) { 16 | /// switch event { 17 | /// case .key(let key): 18 | /// handleKeyPress(key) 19 | /// case .mouse(let mouse): 20 | /// handleMouseInput(mouse) 21 | /// case .resize(let resize): 22 | /// handleTerminalResize(resize) 23 | /// } 24 | /// } 25 | /// ``` 26 | public enum VTEvent: Equatable, Sendable { 27 | case key(KeyEvent) 28 | case mouse(MouseEvent) 29 | case resize(ResizeEvent) 30 | } 31 | 32 | // MARK: - Key Event 33 | 34 | /// Modifier keys that can be held during keyboard input. 35 | /// 36 | /// `KeyModifiers` represents the state of modifier keys (Shift, Ctrl, Alt, 37 | /// Meta) during a key event. Multiple modifiers can be combined using the 38 | /// `OptionSet` interface to represent complex key combinations like 39 | /// Ctrl+Shift+A. 40 | /// 41 | /// ## Usage Example 42 | /// ```swift 43 | /// let modifiers: KeyModifiers = [.ctrl, .shift] 44 | /// if key.modifiers.contains(.ctrl) { 45 | /// // Handle Ctrl-modified key press 46 | /// } 47 | /// ``` 48 | public struct KeyModifiers: Equatable, OptionSet, Sendable { 49 | public let rawValue: UInt8 50 | 51 | public init(rawValue: UInt8) { 52 | self.rawValue = rawValue 53 | } 54 | } 55 | 56 | extension KeyModifiers { 57 | public static var shift: KeyModifiers { 58 | KeyModifiers(rawValue: 1 << 0) 59 | } 60 | 61 | public static var ctrl: KeyModifiers { 62 | KeyModifiers(rawValue: 1 << 1) 63 | } 64 | 65 | public static var alt: KeyModifiers { 66 | KeyModifiers(rawValue: 1 << 2) 67 | } 68 | 69 | public static var meta: KeyModifiers { 70 | KeyModifiers(rawValue: 1 << 3) 71 | } 72 | } 73 | 74 | /// The type of keyboard interaction that occurred. 75 | /// 76 | /// `KeyEventType` distinguishes between key press and release events, 77 | /// allowing applications to respond differently to each phase of user 78 | /// input. This is particularly useful for applications that need to 79 | /// track key hold duration or implement custom repeat behavior. 80 | public enum KeyEventType: Equatable, Sendable { 81 | case press 82 | case release 83 | } 84 | 85 | /// A keyboard input event with character and modifier information. 86 | /// 87 | /// `KeyEvent` captures complete information about a keyboard interaction, 88 | /// including the character generated, the raw keycode, any modifier keys 89 | /// held, and whether this was a press or release event. 90 | /// 91 | /// The character field contains the printable character for normal keys, 92 | /// or nil for special keys like arrows, function keys, or modifier-only 93 | /// events. 94 | /// 95 | /// ## Usage Example 96 | /// ```swift 97 | /// func handleKeyEvent(_ event: KeyEvent) { 98 | /// switch (event.character, event.modifiers) { 99 | /// case ("q", []): 100 | /// quit() 101 | /// case ("c", .ctrl): 102 | /// copySelection() 103 | /// case (nil, []): 104 | /// handleSpecialKey(event.keycode) 105 | /// default: 106 | /// insertText(event.character) 107 | /// } 108 | /// } 109 | /// ``` 110 | public struct KeyEvent: Equatable, Sendable { 111 | public let character: Character? 112 | public let keycode: UInt16 113 | public let modifiers: KeyModifiers 114 | public let type: KeyEventType 115 | 116 | internal init(character: Character?, keycode: UInt16, modifiers: KeyModifiers = [], type: KeyEventType) { 117 | self.character = character 118 | self.keycode = keycode 119 | self.modifiers = modifiers 120 | self.type = type 121 | } 122 | 123 | internal init(scalar: UnicodeScalar?, keycode: UInt16, modifiers: KeyModifiers = [], type: KeyEventType) { 124 | self.character = if let scalar { Character(scalar) } else { nil } 125 | self.keycode = keycode 126 | self.modifiers = modifiers 127 | self.type = type 128 | } 129 | } 130 | 131 | // MARK: - Mouse Event 132 | 133 | /// Mouse buttons that can be pressed or released. 134 | /// 135 | /// `MouseButton` represents the physical mouse buttons available for 136 | /// interaction. The standard three-button mouse (left, right, middle) 137 | /// plus additional buttons (4, 5) commonly used for navigation are 138 | /// supported. Multiple buttons can be combined for complex interactions. 139 | /// 140 | /// ## Usage Example 141 | /// ```swift 142 | /// let buttons: MouseButton = [.left, .right] // Both buttons pressed 143 | /// if mouse.type == .pressed(.left) { 144 | /// startSelection(at: mouseEvent.position) 145 | /// } 146 | /// ``` 147 | public struct MouseButton: Equatable, OptionSet, Sendable { 148 | public let rawValue: UInt8 149 | 150 | public init(rawValue: UInt8) { 151 | self.rawValue = rawValue 152 | } 153 | } 154 | 155 | extension MouseButton { 156 | public static var left: MouseButton { 157 | MouseButton(rawValue: 1 << 0) 158 | } 159 | 160 | public static var right: MouseButton { 161 | MouseButton(rawValue: 1 << 1) 162 | } 163 | 164 | public static var middle: MouseButton { 165 | MouseButton(rawValue: 1 << 2) 166 | } 167 | 168 | public static var button4: MouseButton { 169 | MouseButton(rawValue: 1 << 3) 170 | } 171 | 172 | public static var button5: MouseButton { 173 | MouseButton(rawValue: 1 << 4) 174 | } 175 | } 176 | 177 | /// The type of mouse interaction that occurred. 178 | /// 179 | /// `MouseEventType` captures different mouse actions including button 180 | /// presses, releases, movement, and scroll wheel operations. This 181 | /// comprehensive event model allows applications to implement rich 182 | /// mouse-based user interfaces. 183 | /// 184 | /// ## Usage Examples 185 | /// ```swift 186 | /// switch mouse.type { 187 | /// case .pressed(.left): 188 | /// beginDrag(at: mouse.position) 189 | /// case .released(.left): 190 | /// endDrag(at: mouse.position) 191 | /// case .move: 192 | /// updateHover(at: mouse.position) 193 | /// case .scroll(let deltaX, let deltaY): 194 | /// scrollContent(x: deltaX, y: deltaY) 195 | /// } 196 | /// ``` 197 | public enum MouseEventType: Equatable, Sendable { 198 | case pressed(MouseButton) 199 | case released(MouseButton) 200 | case move 201 | case scroll(deltaX: Int, deltaY: Int) 202 | } 203 | 204 | /// A mouse input event with position and interaction details. 205 | /// 206 | /// `MouseEvent` combines the location of a mouse interaction with the 207 | /// specific type of action that occurred. The position is given in 208 | /// terminal character cell coordinates, making it easy to map mouse 209 | /// interactions to specific locations in your terminal content. 210 | /// 211 | /// ## Usage Example 212 | /// ```swift 213 | /// func handleMouseEvent(_ event: MouseEvent) { 214 | /// let cell = (row: Int(event.position.y), column: Int(event.position.x)) 215 | /// switch event.type { 216 | /// case .pressed(.left): 217 | /// selectCell(row: cell.row, column: cell.column) 218 | /// case .released(.right): 219 | /// showContextMenu(row: cell.row, column: cell.column) 220 | /// default: 221 | /// break 222 | /// } 223 | /// } 224 | /// ``` 225 | public struct MouseEvent: Equatable, Sendable { 226 | public let position: Point 227 | public let type: MouseEventType 228 | 229 | internal init(position: Point, type: MouseEventType) { 230 | self.position = position 231 | self.type = type 232 | } 233 | } 234 | 235 | // MARK: - Resize Event 236 | 237 | /// A terminal window resize event. 238 | /// 239 | /// `ResizeEvent` is generated when the terminal window changes size, 240 | /// providing the new dimensions in character cells. Applications should 241 | /// respond to resize events by updating their layout and potentially 242 | /// redrawing content to fit the new terminal size. 243 | /// 244 | /// ## Usage Example 245 | /// ```swift 246 | /// func handleResizeEvent(_ event: ResizeEvent) { 247 | /// let target = Size(width: event.size.width, height: event.size.height) 248 | /// 249 | /// // Update application layout for new terminal size 250 | /// updateLayout(columns: target.width, rows: target.height) 251 | /// 252 | /// // Redraw content to fit new dimensions 253 | /// redrawScreen() 254 | /// } 255 | /// ``` 256 | public struct ResizeEvent: Equatable, Sendable { 257 | public let size: Size 258 | 259 | internal init(size: Size) { 260 | self.size = size 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/VTBufferedTerminalStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if os(Windows) 5 | import WindowsCore 6 | #else 7 | import POSIXCore 8 | #endif 9 | 10 | /// A high-performance buffered output stream for terminal operations. 11 | /// 12 | /// `VTBufferedTerminalStream` accumulates terminal output in memory before 13 | /// writing to the underlying terminal, significantly improving performance 14 | /// when rendering complex scenes or animations. It automatically flushes 15 | /// when the buffer reaches capacity and provides convenient operators for 16 | /// building terminal output. 17 | /// 18 | /// ## Performance Benefits 19 | /// 20 | /// Buffering reduces system call overhead by batching multiple write 21 | /// operations into fewer, larger writes. This is especially important 22 | /// for terminal applications that generate substantial output, such as 23 | /// full-screen interfaces or animations. 24 | /// 25 | /// ## Usage Pattern 26 | /// 27 | /// The typical usage pattern is through the `withBufferedOutput` function: 28 | /// 29 | /// ```swift 30 | /// await withBufferedOutput(terminal: myTerminal) { stream in 31 | /// stream <<< "Hello, " 32 | /// stream <<< ControlSequence.setForegroundColor(.red) 33 | /// stream <<< "World!" 34 | /// // Automatically flushed when the closure completes 35 | /// } 36 | /// ``` 37 | /// 38 | /// ## Buffer Management 39 | /// 40 | /// The stream automatically flushes when: 41 | /// - The buffer reaches its capacity 42 | /// - `flush()` is called explicitly 43 | /// - The `withBufferedOutput` closure completes 44 | /// 45 | /// This ensures output appears promptly while maintaining optimal performance. 46 | public final class VTBufferedTerminalStream { 47 | private var buffer: [UInt8] = [] 48 | private let terminal: any VTTerminal 49 | 50 | internal init(_ terminal: some VTTerminal, capacity: Int) { 51 | self.buffer.reserveCapacity(capacity) 52 | self.terminal = terminal 53 | } 54 | 55 | /// Appends string content to the output buffer. 56 | /// 57 | /// This method efficiently accumulates string data in the internal buffer. 58 | /// When the buffer approaches capacity, it automatically flushes to the 59 | /// terminal and continues buffering new content. 60 | /// 61 | /// The method handles UTF-8 encoding internally, so you can safely pass 62 | /// any Unicode string content including emoji and international characters. 63 | /// 64 | /// ## Performance Considerations 65 | /// Multiple small `append()` calls are more efficient than direct terminal 66 | /// writes, as the buffering amortizes the cost of system calls. 67 | /// 68 | /// ## Usage Example 69 | /// ```swift 70 | /// stream.append("Status: ") 71 | /// stream.append("✓ Connected") 72 | /// ``` 73 | /// 74 | /// ## Automatic Flushing 75 | /// If appending the string would exceed buffer capacity, the current 76 | /// buffer is flushed synchronously before adding the new content. 77 | public func append(_ string: String) { 78 | let view = string.utf8 79 | 80 | // If the buffer is full, flush it before appending 81 | if buffer.count + view.count > buffer.capacity { 82 | let output = String(decoding: buffer, as: UTF8.self) 83 | Task.synchronously { [terminal] in 84 | await terminal.write(output) 85 | } 86 | buffer.removeAll(keepingCapacity: true) 87 | } 88 | 89 | buffer.append(contentsOf: view) 90 | } 91 | 92 | /// Forces all buffered content to be written to the terminal. 93 | /// 94 | /// This method immediately sends any accumulated buffer content to the 95 | /// underlying terminal and clears the buffer. It's typically called 96 | /// automatically, but can be used when you need to ensure output 97 | /// appears immediately. 98 | /// 99 | /// ## When to Use 100 | /// - Before waiting for user input 101 | /// - When switching between buffered and unbuffered output 102 | /// - To ensure critical messages are displayed immediately 103 | /// 104 | /// ## Usage Example 105 | /// ```swift 106 | /// stream.append("Processing...") 107 | /// await stream.flush() // Ensure prompt appears 108 | /// let input = await readUserInput() 109 | /// ``` 110 | /// 111 | /// The method is safe to call multiple times and has no effect if the 112 | /// buffer is already empty. 113 | internal func flush() async { 114 | guard !buffer.isEmpty else { return } 115 | await terminal.write(String(decoding: buffer, as: UTF8.self)) 116 | buffer.removeAll(keepingCapacity: true) 117 | } 118 | } 119 | 120 | /// Convenient operators for building terminal output streams. 121 | /// 122 | /// These operators provide a fluent interface for constructing terminal 123 | /// output by chaining multiple operations together. The `<<<` operator 124 | /// is used (instead of `<<`) to avoid conflicts with bit shift operations. 125 | extension VTBufferedTerminalStream { 126 | /// Appends a control sequence to the buffered output. 127 | /// 128 | /// This operator provides a clean syntax for adding terminal control 129 | /// sequences like color changes, cursor movements, or text formatting. 130 | /// The sequence is converted to its string representation and buffered. 131 | /// 132 | /// ## Usage Example 133 | /// ```swift 134 | /// stream <<< ControlSequence.clearScreen 135 | /// <<< ControlSequence.moveCursor(to: Point(x: 10, y: 5)) 136 | /// <<< ControlSequence.setForegroundColor(.green) 137 | /// ``` 138 | /// 139 | /// ## Returns 140 | /// The same stream instance, enabling method chaining. 141 | @inlinable 142 | @discardableResult 143 | public static func <<< (_ stream: VTBufferedTerminalStream, _ sequence: ControlSequence) -> VTBufferedTerminalStream { 144 | stream.append(sequence.description) 145 | return stream 146 | } 147 | 148 | /// Appends a string to the buffered output. 149 | /// 150 | /// This operator provides a fluent interface for adding text content 151 | /// to the terminal output stream. It's equivalent to calling `append()` 152 | /// but allows for more readable chaining with other operations. 153 | /// 154 | /// ## Usage Example 155 | /// ```swift 156 | /// stream <<< "Username: " 157 | /// <<< ControlSequence.setStyle(.bold) 158 | /// <<< username 159 | /// <<< ControlSequence.resetStyle 160 | /// ``` 161 | /// 162 | /// ## Returns 163 | /// The same stream instance, enabling method chaining. 164 | @inlinable 165 | @discardableResult 166 | public static func <<< (_ stream: VTBufferedTerminalStream, _ string: String) -> VTBufferedTerminalStream { 167 | stream.append(string) 168 | return stream 169 | } 170 | } 171 | 172 | /// Creates a buffered terminal output context for efficient batch operations. 173 | /// 174 | /// This function provides the recommended way to perform multiple terminal 175 | /// output operations efficiently. It creates a buffered stream, executes 176 | /// your output operations, and automatically flushes the buffer when complete. 177 | /// 178 | /// ## Parameters 179 | /// - terminal: The target terminal for output 180 | /// - capacity: Buffer size in bytes (defaults to system page size for optimal performance) 181 | /// - body: Closure that performs the output operations 182 | /// 183 | /// ## Returns 184 | /// The result returned by the body closure 185 | /// 186 | /// ## Performance Benefits 187 | /// 188 | /// Using buffered output can improve performance by 10x or more when 189 | /// generating substantial terminal output, as it reduces system call 190 | /// overhead from potentially hundreds of small writes to a few large ones. 191 | /// 192 | /// ## Usage Examples 193 | /// 194 | /// ### Simple Text Output 195 | /// ```swift 196 | /// await withBufferedOutput(terminal: terminal) { stream in 197 | /// stream <<< "Hello, World!" 198 | /// } 199 | /// ``` 200 | /// 201 | /// ### Complex UI Rendering 202 | /// ```swift 203 | /// let menuItems = ["File", "Edit", "View", "Help"] 204 | /// await withBufferedOutput(terminal: terminal) { stream in 205 | /// stream <<< ControlSequence.clearScreen 206 | /// 207 | /// for (index, item) in menuItems.enumerated() { 208 | /// stream <<< ControlSequence.moveCursor(to: Point(x: 0, y: index)) 209 | /// stream <<< ControlSequence.setForegroundColor(.blue) 210 | /// stream <<< item 211 | /// } 212 | /// } 213 | /// ``` 214 | /// 215 | /// ### With Error Handling 216 | /// ```swift 217 | /// let result = try await withBufferedOutput(terminal: terminal) { stream in 218 | /// stream <<< "Processing data..." 219 | /// return try processComplexData() 220 | /// } 221 | /// ``` 222 | /// 223 | /// ## Automatic Cleanup 224 | /// The buffer is automatically flushed even if the closure throws an error, 225 | /// ensuring partial output is not lost. 226 | public func withBufferedOutput(terminal: any VTTerminal, capacity: Int = SystemInfo.PageSize, 227 | _ body: (inout VTBufferedTerminalStream) async throws -> Result) async rethrows -> Result { 228 | var stream = VTBufferedTerminalStream(terminal, capacity: capacity) 229 | let result = try await body(&stream) 230 | await stream.flush() 231 | return result 232 | } 233 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Platform/POSIXTerminal.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if !os(Windows) 5 | 6 | import Geometry 7 | import POSIXCore 8 | import Synchronization 9 | 10 | /// POSIX/Unix terminal implementation using standard file descriptors. 11 | /// 12 | /// `POSIXTerminal` provides a cross-platform Unix/Linux implementation that 13 | /// interfaces directly with POSIX terminal APIs. It handles terminal attribute 14 | /// management, input parsing, and output rendering using standard POSIX system 15 | /// calls like `tcgetattr`, `tcsetattr`, and terminal I/O operations. 16 | /// 17 | /// ## POSIX Terminal Features 18 | /// 19 | /// This implementation leverages standard POSIX terminal capabilities: 20 | /// - **Terminal Attributes**: Manages canonical vs. raw mode, echo, and flow control 21 | /// - **Window Size Detection**: Uses `TIOCGWINSZ` ioctl for accurate terminal dimensions 22 | /// - **Input Parsing**: Processes escape sequences and control characters 23 | /// - **Attribute Restoration**: Automatically restores original terminal state on cleanup 24 | /// 25 | /// ## Terminal Modes 26 | /// 27 | /// The implementation supports two primary terminal interaction modes: 28 | /// 29 | /// ### Raw Mode 30 | /// ```swift 31 | /// let terminal = try await POSIXTerminal(mode: .raw) 32 | /// ``` 33 | /// - Disables line buffering (canonical mode) 34 | /// - Disables echo of typed characters 35 | /// - Disables XON/XOFF flow control 36 | /// - Disables CR-to-NL translation 37 | /// - Ideal for interactive applications and games 38 | /// 39 | /// ### Canonical Mode 40 | /// ```swift 41 | /// let terminal = try await POSIXTerminal(mode: .canonical) 42 | /// ``` 43 | /// - Enables line buffering (input available after Enter) 44 | /// - Enables character echo 45 | /// - Enables XON/XOFF flow control 46 | /// - Enables CR-to-NL translation 47 | /// - Suitable for line-oriented applications 48 | /// 49 | /// ## Usage Example 50 | /// 51 | /// ```swift 52 | /// // Create terminal for interactive application 53 | /// let terminal = try await POSIXTerminal(mode: .raw) 54 | /// 55 | /// // Clear screen and position cursor 56 | /// await terminal.write("\u{1B}[2J\u{1B}[H") 57 | /// await terminal.write("Interactive Terminal Application\n") 58 | /// 59 | /// // Process keyboard input 60 | /// for await events in terminal.input { 61 | /// for event in events { 62 | /// switch event { 63 | /// case .key(let keyEvent): 64 | /// if keyEvent.key == .escape { 65 | /// return // Exit application 66 | /// } 67 | /// // Handle other keys 68 | /// } 69 | /// } 70 | /// } 71 | /// // Terminal attributes automatically restored on deinit 72 | /// ``` 73 | /// 74 | /// ## Platform Compatibility 75 | /// 76 | /// This implementation works on all POSIX-compliant systems including: 77 | /// - Linux distributions 78 | /// - macOS 79 | /// - FreeBSD, OpenBSD, NetBSD 80 | /// - Other Unix-like systems 81 | /// 82 | /// ## Thread Safety 83 | /// 84 | /// The actor-based design ensures thread-safe access to terminal file 85 | /// descriptors and prevents race conditions in terminal attribute management. 86 | internal final actor POSIXTerminal: VTTerminal { 87 | private let hIn: CInt 88 | private let hOut: CInt 89 | private let sAttributes: termios 90 | 91 | /// Stream of terminal input events parsed from POSIX terminal input. 92 | /// 93 | /// This stream continuously reads from the terminal's input file descriptor 94 | /// and parses escape sequences, control characters, and regular key presses 95 | /// into structured `VTEvent` instances. The parsing handles complex sequences 96 | /// like function keys, arrow keys, and mouse events. 97 | public nonisolated let input: VTEventStream 98 | 99 | /// Current terminal dimensions in character units. 100 | /// 101 | /// This property reflects the terminal window size obtained from the 102 | /// `TIOCGWINSZ` ioctl call. It represents the visible character grid 103 | /// available for output and is determined during initialization. 104 | /// 105 | /// ## Note 106 | /// Window resize detection is not yet implemented (SIGWINCH handler). 107 | /// The size remains static after terminal initialization. 108 | private let _size: Mutex 109 | public nonisolated var size: Size { 110 | return _size.withLock { $0 } 111 | } 112 | 113 | /// Creates a new POSIX terminal interface with the specified mode. 114 | /// 115 | /// This initializer configures the terminal attributes according to the 116 | /// requested mode and sets up input parsing. It preserves the original 117 | /// terminal configuration for restoration during cleanup. 118 | /// 119 | /// ## Parameters 120 | /// - mode: Terminal interaction mode (`.raw` or `.canonical`) 121 | /// 122 | /// ## Initialization Process 123 | /// 1. Queries current terminal attributes with `tcgetattr` 124 | /// 2. Saves original attributes for later restoration 125 | /// 3. Modifies attributes based on the requested mode 126 | /// 4. Applies new attributes with `tcsetattr` 127 | /// 5. Determines terminal window size using `TIOCGWINSZ` 128 | /// 6. Starts asynchronous input parsing task 129 | /// 130 | /// ## Mode Differences 131 | /// 132 | /// ### Raw Mode Configuration 133 | /// - Disables `ICANON`: No line buffering, characters available immediately 134 | /// - Disables `ECHO`: Typed characters are not echoed to terminal 135 | /// - Disables `IXON`: No XON/XOFF software flow control 136 | /// - Disables `ICRNL`: Carriage return not translated to newline 137 | /// 138 | /// ### Canonical Mode Configuration 139 | /// - Enables `ICANON`: Line buffering, input available after newline 140 | /// - Enables `ECHO`: Characters are echoed as typed 141 | /// - Enables `IXON`: XON/XOFF flow control active 142 | /// - Enables `ICRNL`: Carriage return translated to newline 143 | /// 144 | /// ## Usage Examples 145 | /// 146 | /// ### Interactive Application (Raw Mode) 147 | /// ```swift 148 | /// let terminal = try await POSIXTerminal(mode: .raw) 149 | /// // Immediate character response, no echo 150 | /// // Suitable for games, editors, interactive UIs 151 | /// ``` 152 | /// 153 | /// ### Command-Line Tool (Canonical Mode) 154 | /// ```swift 155 | /// let terminal = try await POSIXTerminal(mode: .canonical) 156 | /// // Line-based input with echo 157 | /// // Suitable for traditional command-line interfaces 158 | /// ``` 159 | /// 160 | /// ## Error Conditions 161 | /// Throws `POSIXError` if: 162 | /// - Terminal attribute queries fail (`tcgetattr`) 163 | /// - Terminal attribute setting fails (`tcsetattr`) 164 | /// - Window size query fails (`ioctl` with `TIOCGWINSZ`) 165 | /// - Terminal dimensions are invalid (zero width or height) 166 | /// 167 | /// ## Cleanup Behavior 168 | /// Original terminal attributes are automatically restored when the 169 | /// terminal is deallocated, ensuring the shell remains usable. 170 | public init(mode: VTMode) async throws { 171 | self.hIn = STDIN_FILENO 172 | self.hOut = STDOUT_FILENO 173 | 174 | var attr: termios = termios() 175 | guard tcgetattr(hIn, &attr) == 0 else { 176 | throw POSIXError() 177 | } 178 | 179 | // Save the original terminal attributes 180 | self.sAttributes = attr 181 | 182 | switch mode { 183 | case .raw: 184 | // Disable canonical mode, echo, XON/XOFF, and CR to NL translation 185 | attr.c_lflag &= ~(ICANON | ECHO | IXON | ICRNL) 186 | case .canonical: 187 | // Enable canonical mode, echo, XON/XOFF, and CR to NL translation 188 | attr.c_lflag |= (ICANON | ECHO | IXON | ICRNL) 189 | } 190 | 191 | guard tcsetattr(hOut, TCSANOW, &attr) == 0 else { 192 | throw POSIXError() 193 | } 194 | 195 | var ws = winsize() 196 | guard ioctl(hOut, TIOCGWINSZ, &ws) == 0 else { 197 | throw POSIXError() 198 | } 199 | 200 | let size = Size(width: Int(ws.ws_col), height: Int(ws.ws_row)) 201 | guard size.width > 0 && size.height > 0 else { 202 | throw POSIXError(EINVAL) 203 | } 204 | _size = Mutex(size) 205 | 206 | // TODO(compnerd): setup SIGWINCH handler to update size 207 | 208 | self.input = VTEventStream(AsyncThrowingStream { [hIn] continuation in 209 | Task { 210 | var parser = VTInputParser() 211 | 212 | while !Task.isCancelled { 213 | do { 214 | let events = try withUnsafeTemporaryAllocation(of: CChar.self, capacity: 128) { 215 | guard let baseAddress = $0.baseAddress else { throw POSIXError() } 216 | let count = read(hIn, baseAddress, $0.count) 217 | guard count >= 0 else { throw POSIXError() } 218 | 219 | let sequences = baseAddress.withMemoryRebound(to: UInt8.self, capacity: count) { 220 | let buffer = UnsafeBufferPointer(start: $0, count: count) 221 | return parser.parse(ArraySlice(buffer)) 222 | } 223 | 224 | return sequences.compactMap { $0.event.map { VTEvent.key($0) } } 225 | } 226 | continuation.yield(events) 227 | } catch { 228 | continuation.finish(throwing: error) 229 | } 230 | } 231 | continuation.finish() 232 | } 233 | }) 234 | } 235 | 236 | deinit { 237 | // Restore the original terminal attributes on deinitialization 238 | var attr = self.sAttributes 239 | _ = tcsetattr(self.hOut, TCSANOW, &attr) 240 | } 241 | 242 | /// Writes string data directly to the terminal output. 243 | /// 244 | /// This method sends UTF-8 encoded string data to the terminal using the 245 | /// POSIX `write` system call. The string can contain VT100/ANSI escape 246 | /// sequences which will be interpreted by the terminal emulator. 247 | /// 248 | /// ## Parameters 249 | /// - string: The text to write, including any escape sequences 250 | /// 251 | /// ## Usage Examples 252 | /// ```swift 253 | /// // Write plain text 254 | /// await terminal.write("Hello, Unix Terminal!") 255 | /// 256 | /// // Write with ANSI color codes 257 | /// await terminal.write("\u{1B}[32mGreen text\u{1B}[0m") 258 | /// 259 | /// // Complex cursor positioning 260 | /// await terminal.write("\u{1B}[10;5H") // Move to row 10, column 5 261 | /// await terminal.write("Positioned text") 262 | /// ``` 263 | /// 264 | /// ## Performance Characteristics 265 | /// Each call results in a single `write` system call. For applications 266 | /// generating substantial output, consider using `VTBufferedTerminalStream` 267 | /// to batch writes and reduce system call overhead. 268 | /// 269 | /// ## Error Handling 270 | /// Write failures are silently ignored in this implementation. The POSIX 271 | /// `write` call may fail if the output file descriptor is closed or the 272 | /// process lacks write permissions, but these errors are not propagated. 273 | /// 274 | /// ## Terminal Interpretation 275 | /// The terminal emulator will interpret escape sequences in the string: 276 | /// - Color and style changes (SGR sequences) 277 | /// - Cursor positioning and movement 278 | /// - Screen clearing and scrolling commands 279 | /// - Other VT100/ANSI control sequences 280 | public func write(_ string: String) { 281 | #if GNU 282 | _ = Glibc.write(self.hOut, string, string.utf8.count) 283 | #else 284 | _ = unistd.write(self.hOut, string, string.utf8.count) 285 | #endif 286 | } 287 | } 288 | 289 | #endif 290 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Platform/WindowsTerminal.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if os(Windows) 5 | 6 | import Geometry 7 | import Synchronization 8 | import WindowsCore 9 | 10 | /// A Sendable wrapper for Windows HANDLE values. 11 | /// 12 | /// This wrapper enables safe sharing of Windows handle values across 13 | /// async task boundaries by marking them as `@unchecked Sendable`. 14 | /// While HANDLE values are raw pointers, they represent opaque system 15 | /// resources that can be safely shared in this context. 16 | private struct SendableHANDLE: @unchecked Sendable { 17 | var handle: HANDLE 18 | } 19 | 20 | /// Windows Console API implementation of the VTTerminal protocol. 21 | /// 22 | /// `WindowsTerminal` provides a native Windows implementation that interfaces 23 | /// directly with the Windows Console API to enable VT100/ANSI escape sequence 24 | /// support. It handles console mode management, input event processing, and 25 | /// output rendering using Windows-specific system calls. 26 | /// 27 | /// ## Windows Console Features 28 | /// 29 | /// This implementation leverages modern Windows Console capabilities: 30 | /// - **Virtual Terminal Processing**: Enables ANSI/VT100 escape sequence support 31 | /// - **Input Event Handling**: Processes keyboard, mouse, and resize events 32 | /// - **Console Mode Management**: Automatically configures and restores console settings 33 | /// - **UTF-8 Output**: Handles Unicode text rendering through Windows APIs 34 | /// 35 | /// ## Initialization and Setup 36 | /// 37 | /// The terminal automatically configures the Windows console for VT100 support: 38 | /// 39 | /// ```swift 40 | /// // Create terminal with full interactive support 41 | /// let terminal = try await WindowsTerminal(mode: .cooked) 42 | /// 43 | /// // Terminal is ready for VT100 escape sequences 44 | /// await terminal.write("\u{1B}[31mRed text\u{1B}[0m") 45 | /// 46 | /// // Process input events 47 | /// for await events in terminal.input { 48 | /// for event in events { 49 | /// switch event { 50 | /// case .key(let keyEvent): 51 | /// // Handle keyboard input 52 | /// break 53 | /// case .mouse(let mouseEvent): 54 | /// // Handle mouse events 55 | /// break 56 | /// case .resize(let resizeEvent): 57 | /// // Handle terminal resize 58 | /// break 59 | /// } 60 | /// } 61 | /// } 62 | /// ``` 63 | /// 64 | /// ## Console Mode Management 65 | /// 66 | /// The implementation automatically: 67 | /// - Enables `ENABLE_VIRTUAL_TERMINAL_PROCESSING` for escape sequence support 68 | /// - Configures `DISABLE_NEWLINE_AUTO_RETURN` for precise cursor control 69 | /// - Preserves original console settings and restores them on cleanup 70 | /// 71 | /// ## Thread Safety 72 | /// 73 | /// The actor-based design ensures thread-safe access to Windows Console APIs, 74 | /// preventing race conditions that could occur with concurrent console operations. 75 | /// 76 | /// ## Platform Availability 77 | /// 78 | /// This implementation is only available on Windows platforms and requires 79 | /// Windows 10 version 1607 (Anniversary Update) or later for full VT100 support. 80 | internal final actor WindowsTerminal: VTTerminal { 81 | private let hIn: SendableHANDLE 82 | private let hOut: SendableHANDLE 83 | private let dwMode: DWORD 84 | 85 | /// Stream of terminal input events from Windows Console API. 86 | /// 87 | /// This stream processes Windows console input events (keyboard, mouse, 88 | /// resize) and converts them to VTEvent instances. The stream operates 89 | /// asynchronously and continues until the terminal is deallocated or 90 | /// an unrecoverable error occurs. 91 | public nonisolated let input: VTEventStream 92 | 93 | /// Current terminal dimensions in character units. 94 | /// 95 | /// This property reflects the console window size (not the buffer size) 96 | /// and is updated automatically when console resize events are processed. 97 | /// The size represents the visible character grid available for output. 98 | private let _size: Mutex 99 | public nonisolated var size: Size { 100 | return _size.withLock { $0 } 101 | } 102 | 103 | /// Creates a new Windows terminal interface with the specified mode. 104 | /// 105 | /// This initializer configures the Windows console for VT100 compatibility 106 | /// and sets up input event processing. It automatically enables virtual 107 | /// terminal processing and configures appropriate console modes. 108 | /// 109 | /// ## Parameters 110 | /// - mode: The terminal interaction mode (typically `.cooked` for full functionality) 111 | /// 112 | /// ## Setup Process 113 | /// 1. Obtains standard input/output handles 114 | /// 2. Queries current console configuration 115 | /// 3. Determines terminal dimensions from console buffer info 116 | /// 4. Enables VT100 escape sequence processing 117 | /// 5. Starts asynchronous input event monitoring 118 | /// 119 | /// ## Usage Example 120 | /// ```swift 121 | /// do { 122 | /// let terminal = try await WindowsTerminal(mode: .cooked) 123 | /// 124 | /// // Terminal is ready for VT100 sequences 125 | /// await terminal.write("\u{1B}[2J\u{1B}[H") // Clear screen 126 | /// await terminal.write("Welcome to Windows Terminal!") 127 | /// 128 | /// // Process input events 129 | /// for await events in terminal.input { 130 | /// // Handle user input 131 | /// } 132 | /// } catch { 133 | /// print("Failed to initialize terminal: \(error)") 134 | /// } 135 | /// ``` 136 | /// 137 | /// ## Error Conditions 138 | /// Throws `WindowsError` if: 139 | /// - Standard handles cannot be obtained 140 | /// - Console mode queries or configuration fail 141 | /// - Console screen buffer information is unavailable 142 | /// 143 | /// ## Console Mode Changes 144 | /// The initializer modifies the console output mode to enable: 145 | /// - `ENABLE_VIRTUAL_TERMINAL_PROCESSING`: VT100/ANSI escape sequences 146 | /// - `DISABLE_NEWLINE_AUTO_RETURN`: Precise cursor positioning 147 | /// 148 | /// Original console modes are preserved and restored during cleanup. 149 | public init(mode: VTMode) async throws { 150 | self.hIn = SendableHANDLE(handle: GetStdHandle(STD_INPUT_HANDLE)) 151 | if self.hIn.handle == INVALID_HANDLE_VALUE { 152 | throw WindowsError() 153 | } 154 | 155 | self.hOut = SendableHANDLE(handle: GetStdHandle(STD_OUTPUT_HANDLE)) 156 | if self.hOut.handle == INVALID_HANDLE_VALUE { 157 | throw WindowsError() 158 | } 159 | 160 | var dwMode: DWORD = 0 161 | guard GetConsoleMode(self.hOut.handle, &dwMode) else { 162 | throw WindowsError() 163 | } 164 | 165 | var csbi = CONSOLE_SCREEN_BUFFER_INFO() 166 | guard GetConsoleScreenBufferInfo(self.hOut.handle, &csbi) else { 167 | throw WindowsError() 168 | } 169 | 170 | let size = Size(width: Int(csbi.srWindow.Right - csbi.srWindow.Left + 1), 171 | height: Int(csbi.srWindow.Bottom - csbi.srWindow.Top + 1)) 172 | _size = Mutex(size) 173 | 174 | // Save the original console mode so that we can restore it later. 175 | self.dwMode = dwMode 176 | 177 | dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN 178 | guard SetConsoleMode(self.hOut.handle, dwMode) else { 179 | throw WindowsError() 180 | } 181 | 182 | self.input = VTEventStream(AsyncThrowingStream { [hIn] continuation in 183 | Task { 184 | repeat { 185 | do { 186 | guard WaitForSingleObject(hIn.handle, INFINITE) == WAIT_OBJECT_0 else { 187 | throw WindowsError() 188 | } 189 | 190 | var cNumberOfEvents: DWORD = 0 191 | guard GetNumberOfConsoleInputEvents(hIn.handle, &cNumberOfEvents) else { 192 | throw WindowsError() 193 | } 194 | guard cNumberOfEvents > 0 else { continue } 195 | 196 | let events = try Array(unsafeUninitializedCapacity: Int(cNumberOfEvents)) { 197 | var NumberOfEventsRead: DWORD = 0 198 | guard ReadConsoleInputW(hIn.handle, $0.baseAddress, DWORD($0.count), &NumberOfEventsRead) else { 199 | throw WindowsError() 200 | } 201 | $1 = Int(NumberOfEventsRead) 202 | } 203 | .compactMap { 204 | return switch $0.EventType { 205 | case KEY_EVENT: 206 | VTEvent.key(.from($0.Event.KeyEvent)) 207 | case MOUSE_EVENT: 208 | VTEvent.mouse(.from($0.Event.MouseEvent)) 209 | case WINDOW_BUFFER_SIZE_EVENT: 210 | VTEvent.resize(.from($0.Event.WindowBufferSizeEvent)) 211 | default: 212 | nil 213 | } 214 | } 215 | 216 | continuation.yield(events) 217 | } catch { 218 | continuation.finish(throwing: error) 219 | } 220 | } while !Task.isCancelled 221 | continuation.finish() 222 | } 223 | }) 224 | } 225 | 226 | deinit { 227 | // Restore the original console mode. 228 | _ = SetConsoleMode(self.hOut.handle, self.dwMode) 229 | } 230 | 231 | /// Writes string data directly to the Windows console output. 232 | /// 233 | /// This method sends UTF-8 encoded string data to the console using the 234 | /// Windows `WriteFile` API. The string can contain VT100/ANSI escape 235 | /// sequences which will be processed by the console if virtual terminal 236 | /// processing is enabled. 237 | /// 238 | /// ## Parameters 239 | /// - string: The text to write, including any escape sequences 240 | /// 241 | /// ## Usage Examples 242 | /// ```swift 243 | /// // Write plain text 244 | /// await terminal.write("Hello, World!") 245 | /// 246 | /// // Write with ANSI color codes 247 | /// await terminal.write("\u{1B}[31mRed text\u{1B}[0m") 248 | /// 249 | /// // Complex escape sequences 250 | /// await terminal.write("\u{1B}[2J\u{1B}[H") // Clear screen, home cursor 251 | /// ``` 252 | /// 253 | /// ## Performance Characteristics 254 | /// The method performs synchronous I/O to the console. For high-frequency 255 | /// output, consider using `VTBufferedTerminalStream` to batch multiple 256 | /// writes and reduce system call overhead. 257 | /// 258 | /// ## Error Handling 259 | /// Write failures are silently ignored in this implementation. The 260 | /// Windows `WriteFile` API may fail if the console handle is invalid 261 | /// or the process lacks write permissions. 262 | internal func write(_ string: consuming String) { 263 | var dwNumberOfBytesWritten: DWORD = 0 264 | _ = string.withUTF8 { 265 | WriteFile(self.hOut.handle, $0.baseAddress, DWORD($0.count), &dwNumberOfBytesWritten, nil) 266 | } 267 | } 268 | } 269 | 270 | /// Convenience operators for Windows terminal output. 271 | extension WindowsTerminal { 272 | /// Writes a string to the terminal using operator syntax. 273 | /// 274 | /// This operator provides a fluent interface for terminal output that 275 | /// mirrors common stream operator patterns. It's particularly useful 276 | /// for chaining multiple output operations in sequence. 277 | /// 278 | /// ## Parameters 279 | /// - terminal: The Windows terminal to write to 280 | /// - string: The text content to write 281 | /// 282 | /// ## Returns 283 | /// The same terminal instance, enabling method chaining. 284 | /// 285 | /// ## Usage Example 286 | /// ```swift 287 | /// // Chain multiple writes 288 | /// await terminal <<< "Line 1\n" 289 | /// <<< "Line 2\n" 290 | /// <<< "\u{1B}[31mRed Line 3\u{1B}[0m\n" 291 | /// 292 | /// // Equivalent to multiple write() calls 293 | /// await terminal.write("Line 1\n") 294 | /// await terminal.write("Line 2\n") 295 | /// await terminal.write("\u{1B}[31mRed Line 3\u{1B}[0m\n") 296 | /// ``` 297 | /// 298 | /// ## Performance Notes 299 | /// Each operator call results in a separate `WriteFile` system call. 300 | /// For bulk output, consider accumulating strings or using buffered 301 | /// output streams for better performance. 302 | @discardableResult 303 | public static func <<< (_ terminal: WindowsTerminal, _ string: String) async -> WindowsTerminal { 304 | await terminal.write(string) 305 | return terminal 306 | } 307 | } 308 | 309 | #endif 310 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/VTProfiler.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Primitives 5 | 6 | /// Comprehensive rendering performance metrics over a sliding time window. 7 | /// 8 | /// `FrameStatistics` provides detailed insights into terminal rendering 9 | /// performance, including frame rate analysis, timing data, and dropped 10 | /// frame tracking. These metrics help optimize application performance 11 | /// and identify rendering bottlenecks. 12 | /// 13 | /// ## Usage Example 14 | /// ```swift 15 | /// let stats = profiler.statistics 16 | /// print("Current FPS: \(stats.fps.current)") 17 | /// print("Average frame time: \(stats.frametime.average)") 18 | /// print("Dropped frames: \(stats.frames.dropped)") 19 | /// 20 | /// if stats.frames.dropped > 0 { 21 | /// print("Warning: Frame drops detected - consider optimizing render loop") 22 | /// } 23 | /// ``` 24 | public struct FrameStatistics: Sendable { 25 | /// Frame rate metrics across different time scales. 26 | /// 27 | /// - `current`: Instantaneous FPS based on the most recent frame 28 | /// - `average`: Mean FPS over the entire sampling window 29 | /// - `max`: Peak FPS achieved (from shortest frame time) 30 | /// - `min`: Lowest FPS recorded (from longest frame time) 31 | public let fps: (current: Double, average: Double, max: Double, min: Double) 32 | 33 | /// Frame timing measurements for performance analysis. 34 | /// 35 | /// - `current`: Duration of the most recent frame 36 | /// - `average`: Mean frame duration over the sampling window 37 | /// 38 | /// Use these metrics to identify performance regressions and optimize 39 | /// rendering code. Consistent frame times indicate stable performance. 40 | public let frametime: (current: Duration, average: Duration) 41 | 42 | /// Frame processing counters for drop detection. 43 | /// 44 | /// - `rendered`: Total frames processed by the profiler 45 | /// - `dropped`: Frames that exceeded the target frame time 46 | /// 47 | /// Dropped frames indicate the rendering loop is taking longer than 48 | /// the target frame duration, potentially causing visual stuttering. 49 | public let frames: (rendered: Int, dropped: Int) 50 | } 51 | 52 | /// High-performance frame timing profiler for terminal rendering. 53 | /// 54 | /// `VTProfiler` provides efficient performance monitoring for terminal 55 | /// applications with minimal overhead. It uses a ring buffer to maintain 56 | /// a sliding window of recent frame times and incrementally updates 57 | /// statistics to avoid expensive recalculations. 58 | /// 59 | /// ## Key Features 60 | /// 61 | /// - **Low overhead**: Ring buffer design minimizes memory allocations 62 | /// - **Incremental statistics**: Efficient min/max tracking without full scans 63 | /// - **Configurable window**: Adapts sampling window to target frame rate 64 | /// - **Drop detection**: Identifies frames exceeding target duration 65 | /// - **~Copyable**: Prevents accidental expensive copies 66 | /// 67 | /// ## Usage with VTRenderer 68 | /// 69 | /// The profiler integrates seamlessly with `VTRenderer`'s automatic 70 | /// rendering loop: 71 | /// 72 | /// ```swift 73 | /// try await renderer.rendering(fps: 60) { buffer in 74 | /// // Your rendering code here 75 | /// drawContent(&buffer) 76 | /// } 77 | /// 78 | /// // Access performance metrics 79 | /// let stats = renderer.statistics 80 | /// if stats.fps.current < 30 { 81 | /// optimizeRenderingPath() 82 | /// } 83 | /// ``` 84 | /// 85 | /// ## Manual Profiling 86 | /// 87 | /// For custom timing scenarios outside of the renderer: 88 | /// 89 | /// ```swift 90 | /// var profiler = VTProfiler(target: 60.0) 91 | /// 92 | /// // Profile a specific operation 93 | /// let (duration, result) = profiler.measure { 94 | /// performExpensiveOperation() 95 | /// } 96 | /// 97 | /// // Check if operation was too slow 98 | /// let stats = profiler.statistics 99 | /// if stats.frames.dropped > 0 { 100 | /// print("Operation exceeded target time") 101 | /// } 102 | /// ``` 103 | /// 104 | /// ## Performance Considerations 105 | /// 106 | /// The profiler automatically sizes its sampling window based on the 107 | /// target frame rate (minimum 60 samples, or 2 seconds worth of frames). 108 | /// This provides stable statistics while keeping memory usage bounded. 109 | public struct VTProfiler: ~Copyable { 110 | /// Target frame duration for drop detection. 111 | private let target: Duration 112 | /// Ring buffer maintaining recent frame time samples. 113 | private var samples: RingBuffer 114 | 115 | /// Frame counters for rendered and dropped frame tracking. 116 | private var frames: (rendered: Int, dropped: Int) = (0, 0) 117 | /// Incrementally maintained min/max values for efficient statistics. 118 | private var extrema: (min: Duration, max: Duration) = 119 | (.nanoseconds(Int64.max), .zero) 120 | 121 | /// Creates a profiler configured for the specified target frame rate. 122 | /// 123 | /// The profiler automatically configures its sampling window size based 124 | /// on the target frame rate to provide stable statistics. The minimum 125 | /// window size is 60 samples, with larger windows for higher frame rates 126 | /// to maintain approximately 2 seconds of sample history. 127 | /// 128 | /// ## Parameters 129 | /// - fps: Target frames per second (must be > 0) 130 | /// 131 | /// ## Usage Example 132 | /// ```swift 133 | /// // Create profiler for 60 FPS target 134 | /// let profiler = VTProfiler(target: 60.0) 135 | /// 136 | /// // For high-refresh displays 137 | /// let profiler120 = VTProfiler(target: 120.0) 138 | /// ``` 139 | /// 140 | /// ## Preconditions 141 | /// The target FPS must be greater than zero. Invalid values will trigger 142 | /// a runtime assertion in debug builds. 143 | public init(target fps: Double) { 144 | precondition(fps > 0, "Target FPS must be greater than zero") 145 | self.target = .nanoseconds(Int64(1_000_000_000.0 / fps)) 146 | self.samples = RingBuffer(capacity: max(Int(fps * 2.0), 60)) 147 | } 148 | 149 | /// Records a frame time sample and updates performance statistics. 150 | /// 151 | /// This method efficiently maintains all profiling statistics: 152 | /// - Increments frame counters 153 | /// - Tracks dropped frames (exceeding target duration) 154 | /// - Updates min/max extrema incrementally 155 | /// - Handles ring buffer overflow with extrema recalculation 156 | /// 157 | /// The incremental approach avoids expensive full-buffer scans on 158 | /// every sample, maintaining O(1) complexity in the common case. 159 | /// 160 | /// ## Performance Notes 161 | /// When the ring buffer evicts a sample that was a minimum or maximum 162 | /// value, the method performs a full scan to recalculate extrema. 163 | /// This ensures accuracy while keeping the common case efficient. 164 | private mutating func record(sample: Duration) { 165 | frames.rendered += 1 166 | if sample > target { 167 | frames.dropped += 1 168 | } 169 | 170 | // Handle ring buffer capacity 171 | let evicted: Duration? = samples.isFull ? samples.peek() : nil 172 | 173 | samples.push(sample) 174 | if evicted == extrema.min || evicted == extrema.max { 175 | // Recalculate min/max if evicted sample was an extremum 176 | extrema.min = samples.reduce(.nanoseconds(Int64.max)) { min($0, $1) } 177 | extrema.max = samples.reduce(.zero) { max($0, $1) } 178 | } else { 179 | extrema.min = min(extrema.min, sample) 180 | extrema.max = max(extrema.max, sample) 181 | } 182 | } 183 | 184 | /// Measures the execution time of a synchronous operation. 185 | /// 186 | /// This method provides precise timing measurement with automatic 187 | /// sample recording. It's ideal for profiling specific operations 188 | /// or integrating with custom rendering loops that need detailed 189 | /// performance analysis. 190 | /// 191 | /// ## Usage Examples 192 | /// ```swift 193 | /// // Profile a specific rendering operation 194 | /// let (duration, result) = profiler.measure { 195 | /// return complexRenderingOperation() 196 | /// } 197 | /// print("Operation took \(duration)") 198 | /// 199 | /// // Profile without caring about the result 200 | /// profiler.measure { 201 | /// updateGameState() 202 | /// } 203 | /// ``` 204 | /// 205 | /// ## Error Handling 206 | /// If the operation throws an error, the timing is still recorded 207 | /// before the error is re-thrown, ensuring accurate profiling 208 | /// even for failed operations. 209 | /// 210 | /// - Parameter operation: The operation to time and profile 211 | /// - Returns: Tuple containing measured duration and operation result 212 | /// - Throws: Any error thrown by the operation 213 | @discardableResult 214 | public mutating func measure(_ operation: () throws -> Result) rethrows -> (Duration, Result) { 215 | let start = ContinuousClock.now 216 | let result = try operation() 217 | let delta = ContinuousClock.now - start 218 | 219 | record(sample: delta) 220 | return (delta, result) 221 | } 222 | 223 | /// Measures the execution time of an asynchronous operation. 224 | /// 225 | /// This async variant provides the same timing capabilities for 226 | /// asynchronous operations, making it perfect for profiling async 227 | /// rendering operations, I/O operations, or any async work that 228 | /// affects frame timing. 229 | /// 230 | /// ## Usage Examples 231 | /// ```swift 232 | /// // Profile async rendering operations 233 | /// let (duration, _) = await profiler.measure { 234 | /// await renderComplexScene() 235 | /// } 236 | /// 237 | /// // Profile async I/O that affects rendering 238 | /// await profiler.measure { 239 | /// await loadTextureAsync() 240 | /// } 241 | /// ``` 242 | /// 243 | /// ## Concurrency Notes 244 | /// The profiler itself is not thread-safe and should be used from 245 | /// a single actor or protected by appropriate synchronization when 246 | /// accessed from multiple concurrent contexts. 247 | /// 248 | /// - Parameter operation: The async operation to time and profile 249 | /// - Returns: Tuple containing measured duration and operation result 250 | /// - Throws: Any error thrown by the async operation 251 | @discardableResult 252 | public mutating func measure(_ operation: @Sendable () async throws -> Result) async rethrows -> (Duration, Result) { 253 | let start = ContinuousClock.now 254 | let result = try await operation() 255 | let delta = ContinuousClock.now - start 256 | 257 | record(sample: delta) 258 | return (delta, result) 259 | } 260 | 261 | /// Comprehensive performance statistics computed from current samples. 262 | /// 263 | /// This property provides real-time performance analysis based on all 264 | /// samples in the current window. The statistics are computed on-demand 265 | /// using efficient algorithms to minimize overhead. 266 | /// 267 | /// ## Statistical Accuracy 268 | /// 269 | /// - **FPS calculations**: Computed as 1/duration for each metric 270 | /// - **Averages**: True arithmetic mean over the sample window 271 | /// - **Extrema**: Tracked incrementally for efficiency 272 | /// 273 | /// ## Empty Sample Handling 274 | /// 275 | /// When no samples are available, returns zero values for all metrics. 276 | /// This ensures the property is always safe to access without 277 | /// additional nil checking. 278 | /// 279 | /// ## Usage Examples 280 | /// ```swift 281 | /// let stats = profiler.statistics 282 | /// 283 | /// // Performance monitoring 284 | /// if stats.fps.current < targetFps * 0.8 { 285 | /// print("Performance warning: FPS dropped to \(stats.fps.current)") 286 | /// } 287 | /// 288 | /// // Frame drop analysis 289 | /// let dropRate = Double(stats.frames.dropped) / Double(stats.frames.rendered) 290 | /// if dropRate > 0.05 { 291 | /// print("High drop rate: \(dropRate * 100)% of frames dropped") 292 | /// } 293 | /// 294 | /// // Timing analysis 295 | /// print("Average frame time: \(stats.frametime.average)") 296 | /// ``` 297 | public var statistics: FrameStatistics { 298 | guard !samples.isEmpty, let current = samples.last() else { 299 | return FrameStatistics(fps: (current: 0, average: 0, max: 0, min: 0), 300 | frametime: (current: .zero, average: .zero), 301 | frames: (rendered: 0, dropped: 0)) 302 | } 303 | 304 | func fps(_ duration: Duration) -> Double { 305 | guard duration > .zero else { return 0 } 306 | return 1_000_000_000.0 / Double(duration.nanoseconds) 307 | } 308 | 309 | let total = samples.reduce(0) { $0 + $1.nanoseconds } 310 | let average = Duration.nanoseconds(total / Int64(samples.count)) 311 | 312 | return FrameStatistics(fps: (current: fps(current), 313 | average: fps(average), 314 | max: fps(extrema.min), 315 | min: fps(extrema.max)), 316 | frametime: (current: current, average: average), 317 | frames: frames) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Sources/VirtualTerminal/Rendering/VTRenderer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Geometry 5 | 6 | #if os(Windows) 7 | internal typealias PlatformTerminal = WindowsTerminal 8 | #else 9 | internal typealias PlatformTerminal = POSIXTerminal 10 | #endif 11 | 12 | /// Optimized segment types for efficient terminal output. 13 | /// 14 | /// The renderer analyzes buffer content to identify patterns that can be 15 | /// optimized during output. Repeated characters are encoded as run-length 16 | /// segments, while diverse content is sent as literal strings. 17 | package enum Segment { 18 | /// A run of repeated characters that can be optimized with repeat commands. 19 | case run(Character, Int) 20 | /// A literal string segment containing diverse characters. 21 | case literal(String) 22 | } 23 | 24 | extension VTBuffer { 25 | /// Analyzes a buffer range to create optimized output segments. 26 | /// 27 | /// This method performs run-length analysis to identify sequences of 28 | /// repeated characters that can be efficiently encoded using terminal 29 | /// repeat commands. Short runs below the minimum length threshold are 30 | /// grouped into literal segments for optimal output. 31 | /// 32 | /// ## Performance Optimization 33 | /// 34 | /// The segmentation process reduces terminal output by: 35 | /// - Using repeat commands for character runs ≥ `minlength` 36 | /// - Grouping short runs into efficient literal segments 37 | /// - Minimizing the number of escape sequences sent 38 | /// 39 | /// ## Parameters 40 | /// - range: Linear buffer range to analyze for segments 41 | /// - minlength: Minimum run length to qualify for run-length encoding 42 | /// 43 | /// ## Returns 44 | /// Array of segments optimized for terminal transmission 45 | package borrowing func segment(_ range: Range, minlength: Int = 5) -> [Segment] { 46 | assert(!range.isEmpty, "Range must not be empty") 47 | 48 | var segments: [Segment] = [] 49 | 50 | var index: ContiguousArray.Index = range.lowerBound 51 | while index < range.upperBound { 52 | let start = index 53 | let character = buffer[index].character 54 | 55 | while index < range.upperBound && buffer[index].character == character { 56 | index += 1 57 | } 58 | 59 | let length = index - start 60 | if length >= minlength { 61 | // This is a repeated character run, so we store it as a run segment. 62 | segments.append(.run(character, length)) 63 | continue 64 | } 65 | 66 | // This is a short run, so we store it as a literal segment. Continue 67 | // to the next run. 68 | var end = index 69 | 70 | while index < range.upperBound { 71 | let start = index 72 | let character = buffer[index].character 73 | 74 | while index < range.upperBound && buffer[index].character == character { 75 | index += 1 76 | } 77 | 78 | let length = index - start 79 | if length >= minlength { 80 | // This will form a new run, stop the literal segment here. 81 | index = start 82 | break 83 | } 84 | 85 | // Otherwise, we extend the literal segment to include this short run. 86 | end = index 87 | } 88 | 89 | segments.append(.literal(String(buffer[start ..< end].map(\.character)))) 90 | } 91 | 92 | return segments 93 | } 94 | } 95 | 96 | /// A high-performance double-buffered terminal renderer. 97 | /// 98 | /// `VTRenderer` implements an efficient rendering system that minimizes 99 | /// terminal output through damage-based updates and intelligent optimization. 100 | /// The renderer uses double buffering to track changes between frames and 101 | /// only redraws modified areas. 102 | /// 103 | /// ## Architecture 104 | /// 105 | /// The renderer maintains two buffers: 106 | /// - **Back buffer**: Where your application draws new content 107 | /// - **Front buffer**: The current displayed state 108 | /// 109 | /// During `present()`, the renderer compares buffers to identify changes 110 | /// (damage) and sends only the necessary updates to the terminal. 111 | /// 112 | /// ## Performance Features 113 | /// 114 | /// - **Damage-based rendering**: Only updates changed areas 115 | /// - **Run-length encoding**: Optimizes repeated character output 116 | /// - **Cursor optimization**: Minimizes cursor movement commands 117 | /// - **Synchronized updates**: Uses terminal synchronization for flicker-free rendering 118 | /// - **SGR state tracking**: Minimizes style change commands 119 | /// 120 | /// ## Usage Example 121 | /// 122 | /// ```swift 123 | /// let renderer = try await VTRenderer(mode: .raw) 124 | /// 125 | /// // Render loop with automatic frame rate control 126 | /// try await renderer.rendering(fps: 60) { buffer in 127 | /// // Draw your content to the buffer 128 | /// buffer.write(string: "Hello, World!", at: VTPosition(row: 1, column: 1)) 129 | /// buffer.fill(rect: Rect(x: 0, y: 10, width: 20, height: 5), 130 | /// with: "█", style: .default) 131 | /// } 132 | /// ``` 133 | public final class VTRenderer: Sendable { 134 | /// The underlying platform-specific terminal implementation. 135 | private let _terminal: PlatformTerminal 136 | 137 | /// The currently displayed buffer state (visible to the user). 138 | package nonisolated(unsafe) var front: VTBuffer 139 | 140 | /// The buffer where new content is drawn (back buffer for double buffering). 141 | public nonisolated(unsafe) var back: VTBuffer 142 | 143 | /// Performance profiler for tracking rendering statistics (optional). 144 | public private(set) nonisolated(unsafe) var profiler: VTProfiler? 145 | 146 | /// Creates a new renderer with the specified terminal mode. 147 | /// 148 | /// Initializes the double-buffered rendering system and establishes 149 | /// connection to the terminal. The renderer automatically detects 150 | /// the terminal size and creates appropriately sized buffers. 151 | /// 152 | /// ## Error Conditions 153 | /// Throws if terminal initialization fails, which can happen if: 154 | /// - Terminal is not available (non-interactive environment) 155 | /// - Terminal capabilities are insufficient 156 | /// - Platform-specific terminal setup fails 157 | /// 158 | /// - Parameter mode: Terminal mode configuration for capabilities and behavior 159 | /// - Throws: Terminal initialization errors 160 | public init(mode: VTMode) async throws { 161 | self._terminal = try await PlatformTerminal(mode: mode) 162 | self.front = VTBuffer(size: _terminal.size) 163 | self.back = VTBuffer(size: _terminal.size) 164 | } 165 | 166 | /// Provides access to the underlying terminal for direct operations. 167 | /// 168 | /// Use this property when you need to send control sequences directly 169 | /// or access terminal-specific functionality that isn't part of the 170 | /// standard rendering pipeline. 171 | /// 172 | /// ## Usage Example 173 | /// ```swift 174 | /// // Send a direct control sequence 175 | /// await renderer.terminal.write(.SetMode(.DEC(.UseAlternateScreenBuffer))) 176 | /// 177 | /// // Access terminal properties 178 | /// let size = renderer.terminal.size 179 | /// ``` 180 | public var terminal: some VTTerminal { 181 | self._terminal 182 | } 183 | 184 | /// Current rendering performance statistics. 185 | /// 186 | /// Provides real-time performance metrics when profiling is enabled 187 | /// through the `rendering(fps:_:)` method. Returns zero values when 188 | /// profiling is not active. 189 | /// 190 | /// ## Metrics Available 191 | /// - **FPS**: Current, average, minimum, and maximum frame rates 192 | /// - **Frame time**: Current and average frame rendering duration 193 | /// - **Frame counts**: Total rendered and dropped frame counts 194 | /// 195 | /// ## Usage Example 196 | /// ```swift 197 | /// let stats = renderer.statistics 198 | /// print("FPS: \(stats.fps.current), Frame time: \(stats.frametime.current)") 199 | /// ``` 200 | public nonisolated var statistics: FrameStatistics { 201 | profiler?.statistics 202 | ?? FrameStatistics(fps: (current: 0, average: 0, max: 0, min: 0), 203 | frametime: (current: .zero, average: .zero), 204 | frames: (rendered: 0, dropped: 0)) 205 | } 206 | 207 | /// Renders damage spans to the terminal with optimized output. 208 | /// 209 | /// This is the core rendering method that converts buffer differences 210 | /// into efficient terminal commands. It performs several optimizations: 211 | /// 212 | /// - **Synchronized updates**: Prevents flicker during complex updates 213 | /// - **Cursor optimization**: Minimizes cursor movement by leveraging auto-wrap 214 | /// - **SGR state tracking**: Reduces style change commands 215 | /// - **Run-length encoding**: Optimizes repeated character sequences 216 | /// 217 | /// The method uses terminal synchronization to ensure atomic updates 218 | /// and maintains minimal cursor movement for optimal performance. 219 | private borrowing func paint(_ damages: [DamageSpan]) async { 220 | // If there is no damage, we can skip the reconciliation. 221 | guard !damages.isEmpty else { return } 222 | 223 | await withBufferedOutput(terminal: terminal) { stream in 224 | stream <<< .SetMode([.DEC(.SynchronizedUpdate)]) 225 | defer { stream <<< .ResetMode([.DEC(.SynchronizedUpdate)]) } 226 | 227 | var tracker = SGRStateTracker() 228 | var current = VTPosition(row: .max, column: .max) 229 | 230 | for span in damages { 231 | let position = back.position(at: span.range.lowerBound) 232 | 233 | // Only move cursor if we're not already at the right position. This 234 | // leverages terminal auto-wrapping for contiguous spans 235 | if position != current { 236 | for motion in back.reposition(from: current, to: position) { 237 | stream <<< motion 238 | } 239 | } 240 | 241 | // Update rendition state. 242 | let transition = tracker.transition(to: span.style) 243 | if !transition.isEmpty { 244 | stream <<< .SelectGraphicRendition(transition) 245 | } 246 | 247 | // Write all characters in the span - terminal will auto-wrap at row boundaries 248 | for segment in back.segment(span.range) { 249 | switch segment { 250 | case .run(let character, let count): 251 | stream <<< String(character) <<< .Repeat(count - 1) 252 | case .literal(let string): 253 | stream <<< string 254 | } 255 | } 256 | 257 | // "deferred wrap" or "soft-wrap" puts the parser into a pending wrap 258 | // state where the cursor is still at the end of the line, but the next 259 | // character will be written at the start of the next line. 260 | let deferred = 261 | back.position(at: span.range.upperBound - 1).column == back.size.width 262 | // Update the current position. 263 | current = back.position(at: span.range.upperBound - (deferred ? 1 : 0)) 264 | } 265 | 266 | stream <<< .SelectGraphicRendition([.Reset]) 267 | } 268 | } 269 | 270 | /// Presents the back buffer to the terminal and swaps buffers. 271 | /// 272 | /// This method performs the core double-buffering operation: 273 | /// 1. Compares back and front buffers to identify damaged areas 274 | /// 2. Sends optimized updates for only the changed regions 275 | /// 3. Swaps buffers to prepare for the next frame 276 | /// 277 | /// The damage detection ensures minimal terminal output by sending 278 | /// only the changes since the last frame, dramatically improving 279 | /// performance for applications with partial screen updates. 280 | /// 281 | /// ## Usage in Manual Rendering 282 | /// ```swift 283 | /// // Draw content to back buffer 284 | /// renderer.back.write(string: "Updated content", at: position) 285 | /// 286 | /// // Present changes and swap buffers 287 | /// await renderer.present() 288 | /// 289 | /// // Back buffer is now ready for next frame 290 | /// renderer.back.clear() 291 | /// ``` 292 | /// 293 | /// ## Performance Characteristics 294 | /// - Only changed areas are redrawn 295 | /// - Cursor movement is optimized 296 | /// - Style changes are minimized 297 | /// - Output is synchronized to prevent flicker 298 | public borrowing func present() async { 299 | // Compute the damage between the front and back buffers and repaint. 300 | await paint(damages(from: front, to: back)) 301 | // Swap the front and back buffers to prepare for the next frame. 302 | swap(&front, &back) 303 | } 304 | 305 | /// Runs an automatic rendering loop with frame rate control and profiling. 306 | /// 307 | /// This method provides a complete rendering solution with automatic 308 | /// frame rate control, performance profiling, and buffer management. 309 | /// Your render callback is called at the specified frame rate, and 310 | /// the renderer handles all timing and optimization automatically. 311 | /// 312 | /// ## Features 313 | /// - **Frame rate control**: Maintains consistent timing 314 | /// - **Performance profiling**: Tracks FPS and frame time metrics 315 | /// - **Automatic buffer management**: Handles present and clear operations 316 | /// - **Structured concurrency**: Properly manages the rendering task 317 | /// 318 | /// ## Parameters 319 | /// - fps: Target frame rate (frames per second) 320 | /// - render: Callback that draws content to the back buffer 321 | /// 322 | /// ## Usage Example 323 | /// ```swift 324 | /// try await renderer.rendering(fps: 60) { buffer in 325 | /// // Draw your application content 326 | /// drawUI(&buffer) 327 | /// drawGame(&buffer) 328 | /// } 329 | /// ``` 330 | /// 331 | /// ## Error Handling 332 | /// The method propagates any errors thrown by your render callback 333 | /// and properly cleans up the rendering loop. The rendering task 334 | /// is automatically cancelled when the method exits. 335 | /// 336 | /// ## Performance Monitoring 337 | /// While this method runs, use the `statistics` property to monitor 338 | /// rendering performance and detect frame drops or timing issues. 339 | public func rendering(fps: Double, _ render: @escaping @Sendable (inout VTBuffer) throws -> Void) async throws { 340 | self.profiler = VTProfiler(target: fps) 341 | let link = VTDisplayLink(fps: fps) { [unowned self] _ in 342 | try render(&back) 343 | await profiler!.measure { await present() } 344 | back.clear() 345 | } 346 | 347 | try await withThrowingTaskGroup(of: Void.self) { group in 348 | defer { group.cancelAll() } 349 | 350 | // Add the display link task to the group. 351 | link.add(to: &group) 352 | 353 | // Wait for the display link task to complete. 354 | try await group.next() 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /Sources/VTDemo/VTDemo.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Saleem Abdulrasool 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Foundation 5 | import Geometry 6 | import Primitives 7 | import VirtualTerminal 8 | #if !os(Windows) 9 | import POSIXCore 10 | #endif 11 | 12 | extension VTBuffer { 13 | internal var center: Point { 14 | Point(x: size.width / 2, y: size.height / 2) 15 | } 16 | } 17 | 18 | #if swift(<6.2) 19 | private protocol SendableMetatype: ~Copyable, ~Escapable { 20 | } 21 | #endif 22 | 23 | // MARK: - Scene 24 | 25 | private protocol Scene: SendableMetatype { 26 | static var name: String { get } 27 | static var description: String { get } 28 | 29 | init(size: Size) 30 | 31 | mutating func update(ΔTime: Duration) 32 | mutating func render(into buffer: inout VTBuffer) 33 | mutating func process(input: VTEvent) 34 | } 35 | 36 | extension Scene { 37 | mutating func update(ΔTime: Duration) { } 38 | mutating func process(input: VTEvent) { } 39 | } 40 | 41 | // MARK: - Matrix Rain Effect 42 | 43 | private struct MatrixRain: Scene { 44 | private struct MatrixDrop { 45 | static var Characters: String { 46 | "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789" 47 | } 48 | 49 | let column: Int 50 | private let height: Int 51 | private let speed: Int = Int.random(in: 1 ... 5) 52 | private var length: Int = 0 53 | 54 | private var position: Int = 0 55 | private var trail: [Character] = [] 56 | 57 | private static func trailLength() -> Int { 58 | return switch Int.random(in: 1 ... 100) { 59 | case 1 ... 15: // 15% chance - very short drops (3-8 chars) 60 | Int.random(in: 3 ... 8) 61 | case 16 ... 70: // 55% chance - normal drops (6-20 chars) 62 | Int.random(in: 6 ... 20) 63 | case 71 ... 90: // 20% chance - long drops (20-35 chars) 64 | Int.random(in: 20 ... 35) 65 | case 91 ... 100: // 10% chance - very long drops (35-50 chars) 66 | Int.random(in: 35 ... 50) 67 | default: 68 | Int.random(in: 6 ... 35) 69 | } 70 | } 71 | 72 | private mutating func reset() { 73 | length = Self.trailLength() 74 | position = -length 75 | trail = (0 ..< length).map { _ in MatrixDrop.Characters.randomElement() ?? "0" } 76 | } 77 | 78 | public init(column: Int, height: Int) { 79 | self.column = column 80 | self.height = height 81 | 82 | reset() 83 | self.length = Self.trailLength() 84 | } 85 | 86 | internal mutating func update() { 87 | position += speed 88 | guard position < height + length else { return reset() } 89 | } 90 | 91 | public func render(into buffer: inout VTBuffer) { 92 | guard (1 ... buffer.size.width).contains(column) else { return } 93 | 94 | for character in trail.enumerated() { 95 | let row = position + character.offset 96 | guard (1 ... buffer.size.height).contains(row) else { continue } 97 | 98 | let intensity = 99 | character.offset == 0 ? 255 : max(50, 255 - 20 * character.offset) 100 | let style = VTStyle(foreground: .rgb(red: 0, green: UInt8(intensity), blue: 0), 101 | attributes: [.bold]) 102 | buffer.write(string: String(character.element), 103 | at: VTPosition(row: row, column: column), style: style) 104 | } 105 | } 106 | } 107 | 108 | private var drops: [MatrixDrop] = [] 109 | private var update: ContinuousClock.Instant = .now 110 | private var fade: ContinuousClock.Instant = .now 111 | 112 | public init(size: Size) { 113 | drops = stride(from: 1, to: size.width, by: 2) 114 | .map { MatrixDrop(column: $0, height: size.height) } 115 | } 116 | 117 | public mutating func render(into buffer: inout VTBuffer) { 118 | if ContinuousClock.now - update >= Duration.milliseconds(150) { 119 | for column in drops.indices { 120 | drops[column].update() 121 | } 122 | update = .now 123 | } 124 | 125 | // Fade every ~200ms 126 | if ContinuousClock.now - fade >= Duration.milliseconds(200) { 127 | defer { fade = .now } 128 | 129 | for row in 1 ... buffer.size.height { 130 | for column in 1 ... buffer.size.width { 131 | let position = VTPosition(row: row, column: column) 132 | let character = buffer[position].character 133 | if character == " " { continue } 134 | buffer[position] = 135 | VTCell(character: character, 136 | style: VTStyle(foreground: .rgb(red: 0, green: 64, blue: 0))) 137 | } 138 | } 139 | } 140 | 141 | for drop in drops { 142 | drop.render(into: &buffer) 143 | } 144 | } 145 | } 146 | 147 | extension MatrixRain { 148 | static var name: String { 149 | "Matrix Terminal Rain" 150 | } 151 | 152 | static var description: String { 153 | "The iconic Matrix terminal rain - random characters falling down the screen." 154 | } 155 | } 156 | 157 | // MARK: - Menu Scene 158 | 159 | private struct Menu: Scene { 160 | package struct Option: Sendable { 161 | let scene: Scene.Type 162 | let hotkey: Character 163 | } 164 | 165 | public init(size: Size) { } 166 | 167 | package static let Options: [Option] = [ 168 | Option(scene: MatrixRain.self, hotkey: "1"), 169 | ] 170 | 171 | private let start: ContinuousClock.Instant = .now 172 | 173 | private var selection: Array