-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathPOSIXTerminal.swift
More file actions
292 lines (272 loc) · 9.99 KB
/
Copy pathPOSIXTerminal.swift
File metadata and controls
292 lines (272 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// Copyright © 2025 Saleem Abdulrasool <compnerd@compnerd.org>
// SPDX-License-Identifier: BSD-3-Clause
#if !os(Windows)
import Geometry
import POSIXCore
import Synchronization
/// POSIX/Unix terminal implementation using standard file descriptors.
///
/// `POSIXTerminal` provides a cross-platform Unix/Linux implementation that
/// interfaces directly with POSIX terminal APIs. It handles terminal attribute
/// management, input parsing, and output rendering using standard POSIX system
/// calls like `tcgetattr`, `tcsetattr`, and terminal I/O operations.
///
/// ## POSIX Terminal Features
///
/// This implementation leverages standard POSIX terminal capabilities:
/// - **Terminal Attributes**: Manages canonical vs. raw mode, echo, and flow control
/// - **Window Size Detection**: Uses `TIOCGWINSZ` ioctl for accurate terminal dimensions
/// - **Input Parsing**: Processes escape sequences and control characters
/// - **Attribute Restoration**: Automatically restores original terminal state on cleanup
///
/// ## Terminal Modes
///
/// The implementation supports two primary terminal interaction modes:
///
/// ### Raw Mode
/// ```swift
/// let terminal = try await POSIXTerminal(mode: .raw)
/// ```
/// - Disables line buffering (canonical mode)
/// - Disables echo of typed characters
/// - Disables XON/XOFF flow control
/// - Disables CR-to-NL translation
/// - Ideal for interactive applications and games
///
/// ### Canonical Mode
/// ```swift
/// let terminal = try await POSIXTerminal(mode: .canonical)
/// ```
/// - Enables line buffering (input available after Enter)
/// - Enables character echo
/// - Enables XON/XOFF flow control
/// - Enables CR-to-NL translation
/// - Suitable for line-oriented applications
///
/// ## Usage Example
///
/// ```swift
/// // Create terminal for interactive application
/// let terminal = try await POSIXTerminal(mode: .raw)
///
/// // Clear screen and position cursor
/// await terminal.write("\u{1B}[2J\u{1B}[H")
/// await terminal.write("Interactive Terminal Application\n")
///
/// // Process keyboard input
/// for await events in terminal.input {
/// for event in events {
/// switch event {
/// case .key(let keyEvent):
/// if keyEvent.key == .escape {
/// return // Exit application
/// }
/// // Handle other keys
/// }
/// }
/// }
/// // Terminal attributes automatically restored on deinit
/// ```
///
/// ## Platform Compatibility
///
/// This implementation works on all POSIX-compliant systems including:
/// - Linux distributions
/// - macOS
/// - FreeBSD, OpenBSD, NetBSD
/// - Other Unix-like systems
///
/// ## Thread Safety
///
/// The actor-based design ensures thread-safe access to terminal file
/// descriptors and prevents race conditions in terminal attribute management.
internal final actor POSIXTerminal: VTTerminal {
private let hIn: CInt
private let hOut: CInt
private let sAttributes: termios
/// Stream of terminal input events parsed from POSIX terminal input.
///
/// This stream continuously reads from the terminal's input file descriptor
/// and parses escape sequences, control characters, and regular key presses
/// into structured `VTEvent` instances. The parsing handles complex sequences
/// like function keys, arrow keys, and mouse events.
public nonisolated let input: VTEventStream
/// Current terminal dimensions in character units.
///
/// This property reflects the terminal window size obtained from the
/// `TIOCGWINSZ` ioctl call. It represents the visible character grid
/// available for output and is determined during initialization.
///
/// ## Note
/// Window resize detection is not yet implemented (SIGWINCH handler).
/// The size remains static after terminal initialization.
private let _size: Mutex<Size>
public nonisolated var size: Size {
return _size.withLock { $0 }
}
/// Creates a new POSIX terminal interface with the specified mode.
///
/// This initializer configures the terminal attributes according to the
/// requested mode and sets up input parsing. It preserves the original
/// terminal configuration for restoration during cleanup.
///
/// ## Parameters
/// - mode: Terminal interaction mode (`.raw` or `.canonical`)
///
/// ## Initialization Process
/// 1. Queries current terminal attributes with `tcgetattr`
/// 2. Saves original attributes for later restoration
/// 3. Modifies attributes based on the requested mode
/// 4. Applies new attributes with `tcsetattr`
/// 5. Determines terminal window size using `TIOCGWINSZ`
/// 6. Starts asynchronous input parsing task
///
/// ## Mode Differences
///
/// ### Raw Mode Configuration
/// - Disables `ICANON`: No line buffering, characters available immediately
/// - Disables `ECHO`: Typed characters are not echoed to terminal
/// - Disables `IXON`: No XON/XOFF software flow control
/// - Disables `ICRNL`: Carriage return not translated to newline
///
/// ### Canonical Mode Configuration
/// - Enables `ICANON`: Line buffering, input available after newline
/// - Enables `ECHO`: Characters are echoed as typed
/// - Enables `IXON`: XON/XOFF flow control active
/// - Enables `ICRNL`: Carriage return translated to newline
///
/// ## Usage Examples
///
/// ### Interactive Application (Raw Mode)
/// ```swift
/// let terminal = try await POSIXTerminal(mode: .raw)
/// // Immediate character response, no echo
/// // Suitable for games, editors, interactive UIs
/// ```
///
/// ### Command-Line Tool (Canonical Mode)
/// ```swift
/// let terminal = try await POSIXTerminal(mode: .canonical)
/// // Line-based input with echo
/// // Suitable for traditional command-line interfaces
/// ```
///
/// ## Error Conditions
/// Throws `POSIXError` if:
/// - Terminal attribute queries fail (`tcgetattr`)
/// - Terminal attribute setting fails (`tcsetattr`)
/// - Window size query fails (`ioctl` with `TIOCGWINSZ`)
/// - Terminal dimensions are invalid (zero width or height)
///
/// ## Cleanup Behavior
/// Original terminal attributes are automatically restored when the
/// terminal is deallocated, ensuring the shell remains usable.
public init(mode: VTMode) async throws {
self.hIn = STDIN_FILENO
self.hOut = STDOUT_FILENO
var attr: termios = termios()
guard tcgetattr(hIn, &attr) == 0 else {
throw POSIXError()
}
// Save the original terminal attributes
self.sAttributes = attr
switch mode {
case .raw:
// Disable canonical mode, echo, XON/XOFF, and CR to NL translation
attr.c_lflag &= ~(ICANON | ECHO | IXON | ICRNL)
case .canonical:
// Enable canonical mode, echo, XON/XOFF, and CR to NL translation
attr.c_lflag |= (ICANON | ECHO | IXON | ICRNL)
}
guard tcsetattr(hOut, TCSANOW, &attr) == 0 else {
throw POSIXError()
}
var ws = winsize()
guard ioctl(hOut, TIOCGWINSZ, &ws) == 0 else {
throw POSIXError()
}
let size = Size(width: Int(ws.ws_col), height: Int(ws.ws_row))
guard size.width > 0 && size.height > 0 else {
throw POSIXError(EINVAL)
}
_size = Mutex(size)
// TODO(compnerd): setup SIGWINCH handler to update size
self.input = VTEventStream(AsyncThrowingStream { [hIn] continuation in
Task {
var parser = VTInputParser()
while !Task.isCancelled {
do {
let events = try withUnsafeTemporaryAllocation(of: CChar.self, capacity: 128) {
guard let baseAddress = $0.baseAddress else { throw POSIXError() }
let count = read(hIn, baseAddress, $0.count)
guard count >= 0 else { throw POSIXError() }
let sequences = baseAddress.withMemoryRebound(to: UInt8.self, capacity: count) {
let buffer = UnsafeBufferPointer<UInt8>(start: $0, count: count)
return parser.parse(ArraySlice(buffer))
}
return sequences.compactMap { $0.event.map { VTEvent.key($0) } }
}
continuation.yield(events)
} catch {
continuation.finish(throwing: error)
}
}
continuation.finish()
}
})
}
deinit {
// Restore the original terminal attributes on deinitialization
var attr = self.sAttributes
_ = tcsetattr(self.hOut, TCSANOW, &attr)
}
/// Writes string data directly to the terminal output.
///
/// This method sends UTF-8 encoded string data to the terminal using the
/// POSIX `write` system call. The string can contain VT100/ANSI escape
/// sequences which will be interpreted by the terminal emulator.
///
/// ## Parameters
/// - string: The text to write, including any escape sequences
///
/// ## Usage Examples
/// ```swift
/// // Write plain text
/// await terminal.write("Hello, Unix Terminal!")
///
/// // Write with ANSI color codes
/// await terminal.write("\u{1B}[32mGreen text\u{1B}[0m")
///
/// // Complex cursor positioning
/// await terminal.write("\u{1B}[10;5H") // Move to row 10, column 5
/// await terminal.write("Positioned text")
/// ```
///
/// ## Performance Characteristics
/// Each call results in a single `write` system call. For applications
/// generating substantial output, consider using `VTBufferedTerminalStream`
/// to batch writes and reduce system call overhead.
///
/// ## Error Handling
/// Write failures are silently ignored in this implementation. The POSIX
/// `write` call may fail if the output file descriptor is closed or the
/// process lacks write permissions, but these errors are not propagated.
///
/// ## Terminal Interpretation
/// The terminal emulator will interpret escape sequences in the string:
/// - Color and style changes (SGR sequences)
/// - Cursor positioning and movement
/// - Screen clearing and scrolling commands
/// - Other VT100/ANSI control sequences
public func write(_ string: String) {
#if GNU
let pfnWrite = Glibc.write
#elseif os(macOS)
let pfnWrite = Darwin.write
#else
let pfnWrite = unistd.write
#endif
_ = pfnWrite(self.hOut, string, string.utf8.count)
}
}
#endif