Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/VirtualTerminal/Input/VTEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ import Geometry
/// }
/// ```
public enum VTEvent: Equatable, Sendable {
/// A keyboard input event.
case key(KeyEvent)

/// A mouse input event.
case mouse(MouseEvent)

/// A terminal resize event.
case resize(ResizeEvent)

/// A device response event.
case response(VTDeviceAttributesResponse)
}

// MARK: - Key Event
Expand Down
2 changes: 2 additions & 0 deletions Sources/VirtualTerminal/Input/VTEventStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
/// handleMouseInput(mouse)
/// case .resize(let resize):
/// handleTerminalResize(resize)
/// case .response(let response):
/// handleTerminalResponse(response)
/// }
/// }
/// ```
Expand Down
71 changes: 54 additions & 17 deletions Sources/VirtualTerminal/Input/VTInputParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ internal enum ParseResult<Output> {
case indeterminate
}

/// Device attribute response types based on intermediate characters.
///
/// Different device attribute queries return different types of information.
/// The intermediate character in the CSI sequence indicates which type of
/// response is being provided.
public enum VTDeviceAttributesResponse: Equatable, Sendable {
/// Primary device attributes (DA1) - basic terminal identification
case primary([Int])

/// Secondary device attributes (DA2) - version and capability info
case secondary([Int])

/// Tertiary device attributes (DA3) - unit identification
case tertiary([Int])
}

/// Represents different types of parsed terminal input sequences.
///
/// Terminal input consists of various sequence types, from simple characters
Expand All @@ -86,6 +102,7 @@ internal enum ParseResult<Output> {
internal enum ParsedSequence {
case character(Character)
case cursor(direction: Direction, count: Int)
case DeviceAttributes(VTDeviceAttributesResponse)
case function(number: Int, modifiers: KeyModifiers)
case unknown(sequence: [UInt8])
}
Expand Down Expand Up @@ -307,6 +324,21 @@ extension VTInputParser {
input = input.dropFirst()
return parse(next: &input)

case 0x3d: // '=' (DEC Private Mode)
state = .CSI(parameters: parameters, intermediate: intermediate + [byte])
input = input.dropFirst()
return parse(next: &input)

case 0x3e: // '>' (DEC Private Mode)
state = .CSI(parameters: parameters, intermediate: intermediate + [byte])
input = input.dropFirst()
return parse(next: &input)

case 0x3f: // '?' (DEC Private Mode)
state = .CSI(parameters: parameters, intermediate: intermediate + [byte])
input = input.dropFirst()
return parse(next: &input)

case 0x40 ... 0x7e: // command
state = .normal
input = input.dropFirst()
Expand Down Expand Up @@ -376,7 +408,8 @@ extension VTInputParser {
}
input = input.dropFirst(2)
state = .normal
return .success(.unknown(sequence: [0x1b, 0x50] + data), buffer: input)
return .success(.unknown(sequence: [0x1b, 0x50] + data + [0x1b, byte]),
Copy link

Copilot AI Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original sequence construction is incomplete. The updated version correctly includes the terminating escape sequence, but the change appears to be formatting the sequence incorrectly by adding the byte at the end rather than properly constructing the DCS termination sequence.

Suggested change
return .success(.unknown(sequence: [0x1b, 0x50] + data + [0x1b, byte]),
return .success(.unknown(sequence: [0x1b, 0x50] + data + [0x1b, 0x5c]),

Copilot uses AI. Check for mistakes.
buffer: input)
}

input = input.dropFirst()
Expand Down Expand Up @@ -416,6 +449,12 @@ extension VTInputParser {
return .cursor(direction: .right, count: count)
case 0x44: // 'D' (CUB)
return .cursor(direction: .left, count: count)
case 0x63 where intermediate == [0x3f]: // '\033[?...c' (DA1)
return .DeviceAttributes(.primary(parameters))
case 0x63 where intermediate == [0x3e]: // '\033[>...c' (DA2)
return .DeviceAttributes(.secondary(parameters))
case 0x63 where intermediate == [0x3d]: // '\033[=...c' (DA3)
return .DeviceAttributes(.tertiary(parameters))
default:
let sequence: [UInt8] = [UInt8(0x1b), UInt8(0x5b)] + parameters.flatMap { String($0).utf8 } + [UInt8(0x3b)] + intermediate + [command]
return .unknown(sequence: sequence)
Expand Down Expand Up @@ -449,37 +488,35 @@ extension ParsedSequence {
/// }
/// ```
///
/// - Returns: A `KeyEvent` if the sequence represents keyboard input,
/// `nil` otherwise.
internal var event: KeyEvent? {
/// - Returns: A `VTEvent` if the sequence represents keyboard input,
/// or a terminal response, `nil` otherwise.
internal var event: VTEvent? {
return switch self {
case let .character(character):
if character == "\u{1b}" {
KeyEvent(character: character, keycode: VTKeyCode.escape, modifiers: [], type: .press)
.key(.init(character: character, keycode: VTKeyCode.escape, modifiers: [], type: .press))
} else {
KeyEvent(character: character, keycode: 0, modifiers: [], type: .press)
.key(.init(character: character, keycode: 0, modifiers: [], type: .press))
}

case let .cursor(direction, _):
switch direction {
case .up:
KeyEvent(character: nil, keycode: VTKeyCode.up, modifiers: [],
type: .press)
.key(.init(character: nil, keycode: VTKeyCode.up, modifiers: [], type: .press))
case .down:
KeyEvent(character: nil, keycode: VTKeyCode.down, modifiers: [],
type: .press)
.key(.init(character: nil, keycode: VTKeyCode.down, modifiers: [], type: .press))
case .left:
KeyEvent(character: nil, keycode: VTKeyCode.left, modifiers: [],
type: .press)
.key(.init(character: nil, keycode: VTKeyCode.left, modifiers: [], type: .press))
case .right:
KeyEvent(character: nil, keycode: VTKeyCode.right, modifiers: [],
type: .press)
.key(.init(character: nil, keycode: VTKeyCode.right, modifiers: [], type: .press))
}

case let .DeviceAttributes(attributes):
.response(attributes)

case let .function(number, modifiers):
KeyEvent(character: nil,
keycode: UInt16(Int(VTKeyCode.F1) + number - 1),
modifiers: modifiers, type: .press)
.key(.init(character: nil, keycode: UInt16(Int(VTKeyCode.F1) + number - 1),
modifiers: modifiers, type: .press))

case .unknown(_):
nil
Expand Down
2 changes: 1 addition & 1 deletion Sources/VirtualTerminal/Platform/POSIXTerminal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ internal final actor POSIXTerminal: VTTerminal {
return parser.parse(ArraySlice(buffer))
}

return sequences.compactMap { $0.event.map { VTEvent.key($0) } }
return sequences.compactMap(\.event)
}
continuation.yield(events)
} catch {
Expand Down
4 changes: 4 additions & 0 deletions Sources/VirtualTerminal/Rendering/VTRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ public final class VTRenderer: Sendable {
/// The underlying platform-specific terminal implementation.
private let _terminal: PlatformTerminal

/// The terminal capabilities.
private let capabilities: VTCapabilities
Copy link

Copilot AI Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capabilities property is stored but never used within the visible code. Consider making it accessible via a public getter so consumers can query the detected capabilities.

Copilot uses AI. Check for mistakes.

/// The currently displayed buffer state (visible to the user).
package nonisolated(unsafe) var front: VTBuffer

Expand All @@ -159,6 +162,7 @@ public final class VTRenderer: Sendable {
/// - Throws: Terminal initialization errors
public init(mode: VTMode) async throws {
self._terminal = try await PlatformTerminal(mode: mode)
self.capabilities = await VTCapabilities.query(self._terminal)
self.front = VTBuffer(size: _terminal.size)
self.back = VTBuffer(size: _terminal.size)
}
Expand Down
Loading