Skip to content

Commit 4a7d001

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 4a7d001

File tree

4 files changed

+197
-1
lines changed

4 files changed

+197
-1
lines changed

Sources/VirtualTerminal/Input/VTEvent.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ extension KeyModifiers {
8080
public enum KeyEventType: Equatable, Sendable {
8181
case press
8282
case release
83+
case response(VTDeviceAttributesResponse)
8384
}
8485

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

Sources/VirtualTerminal/Input/VTInputParser.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ internal enum ParseResult<Output> {
7272
case indeterminate
7373
}
7474

75+
/// Device attribute response types based on intermediate characters.
76+
///
77+
/// Different device attribute queries return different types of information.
78+
/// The intermediate character in the CSI sequence indicates which type of
79+
/// response is being provided.
80+
public enum VTDeviceAttributesResponse: Equatable, Sendable {
81+
/// Primary device attributes (DA1) - basic terminal identification
82+
case primary([Int])
83+
84+
/// Secondary device attributes (DA2) - version and capability info
85+
case secondary([Int])
86+
87+
/// Tertiary device attributes (DA3) - unit identification
88+
case tertiary([Int])
89+
}
90+
7591
/// Represents different types of parsed terminal input sequences.
7692
///
7793
/// Terminal input consists of various sequence types, from simple characters
@@ -86,6 +102,7 @@ internal enum ParseResult<Output> {
86102
internal enum ParsedSequence {
87103
case character(Character)
88104
case cursor(direction: Direction, count: Int)
105+
case DeviceAttributes(VTDeviceAttributesResponse)
89106
case function(number: Int, modifiers: KeyModifiers)
90107
case unknown(sequence: [UInt8])
91108
}
@@ -307,6 +324,21 @@ extension VTInputParser {
307324
input = input.dropFirst()
308325
return parse(next: &input)
309326

327+
case 0x3d: // '=' (DEC Private Mode)
328+
state = .CSI(parameters: parameters, intermediate: intermediate + [byte])
329+
input = input.dropFirst()
330+
return parse(next: &input)
331+
332+
case 0x3e: // '>' (DEC Private Mode)
333+
state = .CSI(parameters: parameters, intermediate: intermediate + [byte])
334+
input = input.dropFirst()
335+
return parse(next: &input)
336+
337+
case 0x3f: // '?' (DEC Private Mode)
338+
state = .CSI(parameters: parameters, intermediate: intermediate + [byte])
339+
input = input.dropFirst()
340+
return parse(next: &input)
341+
310342
case 0x40 ... 0x7e: // command
311343
state = .normal
312344
input = input.dropFirst()
@@ -376,7 +408,8 @@ extension VTInputParser {
376408
}
377409
input = input.dropFirst(2)
378410
state = .normal
379-
return .success(.unknown(sequence: [0x1b, 0x50] + data), buffer: input)
411+
return .success(.unknown(sequence: [0x1b, 0x50] + data + [0x1b, byte]),
412+
buffer: input)
380413
}
381414

382415
input = input.dropFirst()
@@ -416,6 +449,12 @@ extension VTInputParser {
416449
return .cursor(direction: .right, count: count)
417450
case 0x44: // 'D' (CUB)
418451
return .cursor(direction: .left, count: count)
452+
case 0x63 where intermediate == [0x3f]: // '\033[?...c' (DA1)
453+
return .DeviceAttributes(.primary(parameters))
454+
case 0x63 where intermediate == [0x3e]: // '\033[>...c' (DA2)
455+
return .DeviceAttributes(.secondary(parameters))
456+
case 0x63 where intermediate == [0x3d]: // '\033[=...c' (DA3)
457+
return .DeviceAttributes(.tertiary(parameters))
419458
default:
420459
let sequence: [UInt8] = [UInt8(0x1b), UInt8(0x5b)] + parameters.flatMap { String($0).utf8 } + [UInt8(0x3b)] + intermediate + [command]
421460
return .unknown(sequence: sequence)
@@ -476,6 +515,10 @@ extension ParsedSequence {
476515
type: .press)
477516
}
478517

518+
case let .DeviceAttributes(attributes):
519+
KeyEvent(character: nil, keycode: 0, modifiers: [],
520+
type: .response(attributes))
521+
479522
case let .function(number, modifiers):
480523
KeyEvent(character: nil,
481524
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)