Skip to content

Commit 65d38af

Browse files
committed
Core: introduce VTCapabilities
Introduce the infrastructure to query the terminal what it actually supports. This allows the safe use of advanced features and will allow defaulting to 8-bit encoding if the terminal supports 8-bit encoding rather than the 7-bit encoding.
1 parent b755e32 commit 65d38af

4 files changed

Lines changed: 167 additions & 2 deletions

File tree

Sources/VirtualTerminal/Input/VTEvent.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ extension KeyModifiers {
7878
/// input. This is particularly useful for applications that need to
7979
/// track key hold duration or implement custom repeat behavior.
8080
public enum KeyEventType: Equatable, Sendable {
81-
case press
82-
case release
81+
case press // Key press event
82+
case release // Key release event
83+
case response([Int]) // Response from the hardware
8384
}
8485

8586
/// A keyboard input event with character and modifier information.

Sources/VirtualTerminal/Input/VTInputParser.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ internal enum ParseResult<Output> {
8686
internal enum ParsedSequence {
8787
case character(Character)
8888
case cursor(direction: Direction, count: Int)
89+
case DeviceAttributes(_ attributes: [Int])
8990
case function(number: Int, modifiers: KeyModifiers)
9091
case unknown(sequence: [UInt8])
9192
}
@@ -307,6 +308,11 @@ extension VTInputParser {
307308
input = input.dropFirst()
308309
return parse(next: &input)
309310

311+
case 0x3f: // '?' (DEC or Private Namespace)
312+
state = .CSI(parameters: parameters, intermediate: intermediate)
313+
input = input.dropFirst()
314+
return parse(next: &input)
315+
310316
case 0x40 ... 0x7e: // command
311317
state = .normal
312318
input = input.dropFirst()
@@ -416,6 +422,8 @@ extension VTInputParser {
416422
return .cursor(direction: .right, count: count)
417423
case 0x44: // 'D' (CUB)
418424
return .cursor(direction: .left, count: count)
425+
case 0x63: // 'c' (DA)
426+
return .DeviceAttributes(parameters)
419427
default:
420428
let sequence: [UInt8] = [UInt8(0x1b), UInt8(0x5b)] + parameters.flatMap { String($0).utf8 } + [UInt8(0x3b)] + intermediate + [command]
421429
return .unknown(sequence: sequence)
@@ -476,6 +484,10 @@ extension ParsedSequence {
476484
type: .press)
477485
}
478486

487+
case let .DeviceAttributes(attributes):
488+
KeyEvent(character: nil, keycode: 0, modifiers: [],
489+
type: .response(attributes))
490+
479491
case let .function(number, modifiers):
480492
KeyEvent(character: nil,
481493
keycode: UInt16(Int(VTKeyCode.F1) + number - 1),

Sources/VirtualTerminal/Rendering/VTRenderer.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ extension VTBuffer {
133133
public final class VTRenderer: Sendable {
134134
/// The underlying platform-specific terminal implementation.
135135
private let _terminal: PlatformTerminal
136+
/// The terminal capabilities.
137+
private let capabilities: VTCapabilities
136138
/// The currently displayed buffer state (visible to the user).
137139
package nonisolated(unsafe) var front: VTBuffer
138140
/// The buffer where new content is drawn (back buffer for double buffering).
@@ -157,6 +159,7 @@ public final class VTRenderer: Sendable {
157159
/// - Throws: Terminal initialization errors
158160
public init(mode: VTMode) async throws {
159161
self._terminal = try await PlatformTerminal(mode: mode)
162+
self.capabilities = await VTCapabilities.query(self._terminal) ?? .unknown
160163
self.front = VTBuffer(size: _terminal.size)
161164
self.back = VTBuffer(size: _terminal.size)
162165
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright © 2025 Saleem Abdulrasool <compnerd@compnerd.org>
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
public enum DECTerminalType: UInt8 {
5+
// VT100 Series
6+
7+
// VT125 Series (Graphics)
8+
case VT125 = 12 // VT125 with ReGIS graphics
9+
10+
// VT200 Series
11+
case VT220 = 62 // VT220
12+
13+
// VT300 Series
14+
case VT320 = 63 // VT320
15+
16+
// VT400 Series
17+
case VT420 = 64 // VT420
18+
}
19+
20+
extension DECTerminalType: Sendable { }
21+
extension DECTerminalType: CaseIterable { }
22+
23+
public enum VTDAFeature: UInt8 {
24+
case ExtendedColumns = 1 // 132 columns
25+
case PrinterPort = 2
26+
case ReGISGraphics = 3
27+
case SixelGraphics = 4
28+
case SelectiveErase = 6
29+
case SoftCharacterSet = 7
30+
case UserDefinedKeys = 8
31+
case NationalReplacementCharacterSet = 9
32+
case SerboCroatianCharacterSet = 12
33+
case EightBitInterfaceArchitecture = 14
34+
case TechnicalCharacterSet = 15
35+
case LocatorPort = 16
36+
case TerminalStateInterrogation = 17
37+
case WindowingCapability = 18
38+
case Sessions = 19
39+
case HorizontalScrolling = 21
40+
case ANSIColor = 22
41+
case GreekCharacterSet = 23
42+
case TurkishCharacterSet = 24
43+
case RectangularAreaOperations = 28
44+
case ANSITextLocator = 29
45+
case TextMacros = 32
46+
case ISOLatin2CharacterSet = 42
47+
case PCTerm = 44
48+
case SoftKeyMapping = 45
49+
case ASCIIEmulation = 46
50+
case ClipboardAccess = 52
51+
}
52+
53+
public struct VTExtensions: Sendable, OptionSet {
54+
public typealias RawValue = VTDAFeature.RawValue
55+
56+
public let rawValue: RawValue
57+
58+
public init(rawValue: RawValue) {
59+
self.rawValue = rawValue
60+
}
61+
62+
internal init(_ parameter: VTDAFeature) {
63+
self.init(rawValue: 1 << parameter.rawValue)
64+
}
65+
}
66+
67+
extension VTExtensions {
68+
public static var none: VTExtensions {
69+
VTExtensions(rawValue: 0)
70+
}
71+
}
72+
73+
public struct VTCapabilities: Sendable {
74+
public let standard: DECTerminalType?
75+
public let features: VTExtensions
76+
77+
}
78+
79+
extension VTCapabilities {
80+
/// Queries the terminal to determine its capabilities and supported features.
81+
///
82+
/// Use this method to detect what your terminal supports instead of making assumptions
83+
/// or relying on environment variables. This allows your application to gracefully
84+
/// adapt its behavior based on actual terminal capabilities.
85+
///
86+
/// ```swift
87+
/// if let capabilities = await VTCapabilities.query(terminal) {
88+
/// if capabilities.features.contains(.ANSIColor) {
89+
/// // Safe to use colors
90+
/// await terminal <<< .SelectGraphicRendition([.foreground(.red)])
91+
/// }
92+
/// } else {
93+
/// // Use basic functionality only
94+
/// }
95+
/// ```
96+
///
97+
/// - Parameters:
98+
/// - terminal: The terminal to query
99+
/// - timeout: How long to wait for a response (default: 250ms)
100+
/// - Returns: Terminal capabilities, or `nil` if detection failed
101+
public static func query(_ terminal: some VTTerminal,
102+
timeout: Duration = .milliseconds(250)) async
103+
-> VTCapabilities? {
104+
return await withTaskGroup(of: Void.self) { group in
105+
defer { group.cancelAll() }
106+
107+
var capabilities: VTCapabilities?
108+
109+
group.addTask {
110+
capabilities = try? await Task<VTCapabilities, Error>.withTimeout(timeout: timeout) {
111+
for try await event in terminal.input {
112+
if case let .key(event) = event, case let .response(attributes) = event.type {
113+
let standard: DECTerminalType? =
114+
if case let .some(value) = attributes.first,
115+
let id = UInt8(exactly: value) {
116+
DECTerminalType(rawValue: id)
117+
} else {
118+
nil
119+
}
120+
121+
let features = attributes.dropFirst()
122+
.compactMap(UInt8.init(exactly:))
123+
.compactMap(VTDAFeature.init(rawValue:))
124+
.compactMap(VTExtensions.init)
125+
.reduce(into: VTExtensions()) { $0.formUnion($1) }
126+
127+
return VTCapabilities(standard: standard, features: features)
128+
}
129+
}
130+
return .unknown
131+
}
132+
}
133+
134+
group.addTask {
135+
await terminal <<< .DeviceAttributes(.Request)
136+
}
137+
138+
await group.waitForAll()
139+
140+
return capabilities
141+
}
142+
}
143+
}
144+
145+
extension VTCapabilities {
146+
public static var unknown: VTCapabilities {
147+
VTCapabilities(standard: nil, features: [])
148+
}
149+
}

0 commit comments

Comments
 (0)