From 1c2da31580a90b432779ce52e24d933ad0be1e0c Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Fri, 9 Jan 2026 22:08:42 -0700 Subject: [PATCH] WIP: Termios --- Sources/CSystem/include/CSystemDarwin.h | 19 + Sources/CSystem/include/CSystemLinux.h | 7 + Sources/CSystem/include/module.modulemap | 1 + Sources/CSystem/shims.c | 24 + Sources/Samples/PasswordReader.swift | 55 ++ Sources/Samples/RawMode.swift | 102 +++ Sources/Samples/TerminalSize.swift | 108 ++++ Sources/Samples/main.swift | 5 + Sources/System/Internals/CInterop.swift | 15 + Sources/System/Internals/Constants.swift | 498 ++++++++++++++ Sources/System/Internals/Syscalls.swift | 177 +++++ Sources/System/Terminal/BaudRate.swift | 263 ++++++++ .../System/Terminal/ControlCharacters.swift | 160 +++++ .../Terminal/TerminalAttributes+Serial.swift | 127 ++++ .../System/Terminal/TerminalAttributes.swift | 198 ++++++ .../System/Terminal/TerminalDescriptor.swift | 98 +++ Sources/System/Terminal/TerminalFlags.swift | 607 ++++++++++++++++++ .../System/Terminal/TerminalOperations.swift | 184 ++++++ .../System/Terminal/TerminalWindowSize.swift | 96 +++ Tests/SystemTests/TerminalTests.swift | 518 +++++++++++++++ 20 files changed, 3262 insertions(+) create mode 100644 Sources/CSystem/include/CSystemDarwin.h create mode 100644 Sources/Samples/PasswordReader.swift create mode 100644 Sources/Samples/RawMode.swift create mode 100644 Sources/Samples/TerminalSize.swift create mode 100644 Sources/System/Terminal/BaudRate.swift create mode 100644 Sources/System/Terminal/ControlCharacters.swift create mode 100644 Sources/System/Terminal/TerminalAttributes+Serial.swift create mode 100644 Sources/System/Terminal/TerminalAttributes.swift create mode 100644 Sources/System/Terminal/TerminalDescriptor.swift create mode 100644 Sources/System/Terminal/TerminalFlags.swift create mode 100644 Sources/System/Terminal/TerminalOperations.swift create mode 100644 Sources/System/Terminal/TerminalWindowSize.swift create mode 100644 Tests/SystemTests/TerminalTests.swift diff --git a/Sources/CSystem/include/CSystemDarwin.h b/Sources/CSystem/include/CSystemDarwin.h new file mode 100644 index 00000000..b0ea2fff --- /dev/null +++ b/Sources/CSystem/include/CSystemDarwin.h @@ -0,0 +1,19 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if defined(__APPLE__) + +#include +#include + +// Terminal ioctl shims +int _system_ioctl_TIOCGWINSZ(int fd, struct winsize *ws); +int _system_ioctl_TIOCSWINSZ(int fd, const struct winsize *ws); + +#endif diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h index 6489c4f3..8c67335e 100644 --- a/Sources/CSystem/include/CSystemLinux.h +++ b/Sources/CSystem/include/CSystemLinux.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,13 @@ #include #include #include +#include #include #include "io_uring.h" + +// Terminal ioctl shims +int _system_ioctl_TIOCGWINSZ(int fd, struct winsize *ws); +int _system_ioctl_TIOCSWINSZ(int fd, const struct winsize *ws); + #endif diff --git a/Sources/CSystem/include/module.modulemap b/Sources/CSystem/include/module.modulemap index 6e8b89e9..803be8bf 100644 --- a/Sources/CSystem/include/module.modulemap +++ b/Sources/CSystem/include/module.modulemap @@ -1,4 +1,5 @@ module CSystem { + header "CSystemDarwin.h" header "CSystemLinux.h" header "CSystemWASI.h" header "CSystemWindows.h" diff --git a/Sources/CSystem/shims.c b/Sources/CSystem/shims.c index f492a2ae..784855f1 100644 --- a/Sources/CSystem/shims.c +++ b/Sources/CSystem/shims.c @@ -11,6 +11,30 @@ #include +// Terminal ioctl shims for Linux +int _system_ioctl_TIOCGWINSZ(int fd, struct winsize *ws) { + return ioctl(fd, TIOCGWINSZ, ws); +} + +int _system_ioctl_TIOCSWINSZ(int fd, const struct winsize *ws) { + return ioctl(fd, TIOCSWINSZ, ws); +} + +#endif + +#if defined(__APPLE__) + +#include + +// Terminal ioctl shims for Darwin +int _system_ioctl_TIOCGWINSZ(int fd, struct winsize *ws) { + return ioctl(fd, TIOCGWINSZ, ws); +} + +int _system_ioctl_TIOCSWINSZ(int fd, const struct winsize *ws) { + return ioctl(fd, TIOCSWINSZ, ws); +} + #endif #if defined(_WIN32) diff --git a/Sources/Samples/PasswordReader.swift b/Sources/Samples/PasswordReader.swift new file mode 100644 index 00000000..281947a8 --- /dev/null +++ b/Sources/Samples/PasswordReader.swift @@ -0,0 +1,55 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if !os(Windows) + +import ArgumentParser +import SystemPackage + +struct PasswordReader: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Securely read a password with echo disabled" + ) + + @Option(name: .shortAndLong, help: "Prompt to display") + var prompt: String = "Password:" + + func run() throws { + // Only works if stdin is a terminal + guard let terminal = TerminalDescriptor(.standardInput) else { + complain("Error: stdin is not a terminal") + throw ExitCode.failure + } + + // Display prompt (without newline) + print(prompt, terminator: " ") + + // Read password with echo disabled + let password = try terminal.withAttributes({ attrs in + attrs.localFlags.remove(.echo) + }) { + readLine() ?? "" + } + + // Print newline since echo was disabled + print() + + // Display result (in real app, you'd validate/use the password) + if password.isEmpty { + print("No password entered") + } else { + print("Password read successfully (\(password.count) characters)") + + // Show it's actually hidden + print("Password was: \(String(repeating: "*", count: password.count))") + } + } +} + +#endif diff --git a/Sources/Samples/RawMode.swift b/Sources/Samples/RawMode.swift new file mode 100644 index 00000000..5c1b0ff6 --- /dev/null +++ b/Sources/Samples/RawMode.swift @@ -0,0 +1,102 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if !os(Windows) + +import ArgumentParser +import SystemPackage + +struct RawMode: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Demonstrate raw mode terminal input (character-at-a-time)" + ) + + func run() throws { + guard let terminal = TerminalDescriptor(.standardInput) else { + complain("Error: stdin is not a terminal") + throw ExitCode.failure + } + + print("Raw Mode Key Reader") + print("==================") + print() + print("In raw mode, characters are available immediately without") + print("waiting for Enter. Press 'q' to quit.") + print() + print("Try pressing keys, arrow keys, or special keys...") + print() + + try terminal.withRawMode { + var buffer = [UInt8](repeating: 0, count: 16) + + while true { + // Read a single character (or escape sequence) + let bytesRead = try buffer.withUnsafeMutableBytes { bufferPtr in + try FileDescriptor.standardInput.read(into: bufferPtr) + } + + guard bytesRead > 0 else { + continue + } + + let bytes = Array(buffer[..= 0x20 && bytes[0] <= 0x7E { + let char = Character(UnicodeScalar(bytes[0])) + print(" ('\(char)')", terminator: "") + } + } + + // Recognize common escape sequences + if bytesRead == 3 && bytes[0] == 0x1B && bytes[1] == 0x5B { + switch bytes[2] { + case 0x41: print(" [UP ARROW]", terminator: "") + case 0x42: print(" [DOWN ARROW]", terminator: "") + case 0x43: print(" [RIGHT ARROW]", terminator: "") + case 0x44: print(" [LEFT ARROW]", terminator: "") + default: break + } + } else if bytesRead == 1 { + switch bytes[0] { + case 0x1B: print(" [ESC]", terminator: "") + case 0x0D: print(" [ENTER]", terminator: "") + case 0x7F: print(" [DELETE]", terminator: "") + case 0x09: print(" [TAB]", terminator: "") + default: break + } + } + + // Clear to end of line and move cursor back + print("\u{1B}[K", terminator: "") + } + } + + print() + print("Terminal restored to normal mode.") + } +} + +#endif diff --git a/Sources/Samples/TerminalSize.swift b/Sources/Samples/TerminalSize.swift new file mode 100644 index 00000000..8b7cd3cb --- /dev/null +++ b/Sources/Samples/TerminalSize.swift @@ -0,0 +1,108 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if !os(Windows) + +import ArgumentParser +import SystemPackage + +struct TerminalSize: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Display terminal window dimensions" + ) + + @Flag(name: .shortAndLong, help: "Show verbose output") + var verbose: Bool = false + + @Flag(name: .long, help: "Draw a border using the terminal dimensions") + var border: Bool = false + + @Option(name: .long, help: "Try to set terminal rows (height)") + var setRows: UInt16? + + @Option(name: .long, help: "Try to set terminal columns (width)") + var setColumns: UInt16? + + func run() throws { + guard let terminal = TerminalDescriptor(.standardOutput) else { + complain("Error: stdout is not a terminal") + throw ExitCode.failure + } + + // Try to set size if requested + if let rows = setRows, let cols = setColumns { + print("Attempting to set terminal size to \(cols)x\(rows)...") + let newSize = TerminalDescriptor.WindowSize(rows: rows, columns: cols) + do { + try terminal.setWindowSize(newSize) + print("✓ Successfully set terminal size") + } catch { + print("✗ Failed to set terminal size: \(error)") + print("Note: Setting terminal size may not be supported by your terminal emulator") + } + print() + } else if setRows != nil || setColumns != nil { + complain("Error: Both --set-rows and --set-columns must be specified together") + throw ExitCode.failure + } + + let size = try terminal.windowSize() + + if verbose { + print("Terminal Window Size:") + print(" Rows (height): \(size.rows)") + print(" Columns (width): \(size.columns)") + + // Access underlying C struct for pixel dimensions + let xpixel = size.rawValue.ws_xpixel + let ypixel = size.rawValue.ws_ypixel + if xpixel > 0 || ypixel > 0 { + print(" X pixels: \(xpixel)") + print(" Y pixels: \(ypixel)") + } else { + print(" Pixel dimensions: not available") + } + print() + print("This is useful for:") + print(" - Formatting output to fit the terminal") + print(" - Creating full-screen terminal UIs") + print(" - Responsive command-line applications") + } else { + print("\(size.columns)x\(size.rows)") + } + + if border { + print() + drawBorder(width: Int(size.columns), height: Int(size.rows)) + } + } + + private func drawBorder(width: Int, height: Int) { + // Top border + print("┌" + String(repeating: "─", count: width - 2) + "┐") + + // Middle rows + for row in 2.. UnsafeMutablePointer? { return getenv(name) } + +// MARK: - Terminal Control (termios) + +#if !os(Windows) +@usableFromInline +internal func system_isatty( + _ fd: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd) + } +#endif + return isatty(fd) +} + +internal func system_tcgetattr( + _ fd: CInt, + _ termios_p: UnsafeMutablePointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, termios_p) + } +#endif + return tcgetattr(fd, termios_p) +} + +internal func system_tcsetattr( + _ fd: CInt, + _ optional_actions: CInt, + _ termios_p: UnsafePointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, optional_actions, termios_p) + } +#endif + return tcsetattr(fd, optional_actions, termios_p) +} + +internal func system_tcdrain( + _ fd: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd) + } +#endif + return tcdrain(fd) +} + +internal func system_tcflush( + _ fd: CInt, + _ queue_selector: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, queue_selector) + } +#endif + return tcflush(fd, queue_selector) +} + +internal func system_tcflow( + _ fd: CInt, + _ action: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, action) + } +#endif + return tcflow(fd, action) +} + +internal func system_tcsendbreak( + _ fd: CInt, + _ duration: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, duration) + } +#endif + return tcsendbreak(fd, duration) +} + + +@usableFromInline +internal func system_cfgetispeed( + _ termios_p: UnsafePointer +) -> CInterop.SpeedT { + return cfgetispeed(termios_p) +} + + +@usableFromInline +internal func system_cfgetospeed( + _ termios_p: UnsafePointer +) -> CInterop.SpeedT { + return cfgetospeed(termios_p) +} + + +@usableFromInline +internal func system_cfsetispeed( + _ termios_p: UnsafeMutablePointer, + _ speed: CInterop.SpeedT +) -> CInt { + return cfsetispeed(termios_p, speed) +} + + +@usableFromInline +internal func system_cfsetospeed( + _ termios_p: UnsafeMutablePointer, + _ speed: CInterop.SpeedT +) -> CInt { + return cfsetospeed(termios_p, speed) +} + + +@usableFromInline +internal func system_cfsetspeed( + _ termios_p: UnsafeMutablePointer, + _ speed: CInterop.SpeedT +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(termios_p, speed) + } +#endif + return cfsetspeed(termios_p, speed) +} + + +@usableFromInline +internal func system_cfmakeraw( + _ termios_p: UnsafeMutablePointer +) { + cfmakeraw(termios_p) +} + +// Window size ioctls + +@usableFromInline +internal func system_tiocgwinsz( + _ fd: CInt, + _ winsize_p: UnsafeMutablePointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, winsize_p) + } +#endif + return _system_ioctl_TIOCGWINSZ(fd, winsize_p) +} + + +@usableFromInline +internal func system_tiocswinsz( + _ fd: CInt, + _ winsize_p: UnsafePointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, winsize_p) + } +#endif + return _system_ioctl_TIOCSWINSZ(fd, winsize_p) +} +#endif diff --git a/Sources/System/Terminal/BaudRate.swift b/Sources/System/Terminal/BaudRate.swift new file mode 100644 index 00000000..b42f34c2 --- /dev/null +++ b/Sources/System/Terminal/BaudRate.swift @@ -0,0 +1,263 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +/// A terminal baud rate. +/// +/// POSIX defines baud rates as symbolic constants rather than numeric values. +/// On modern systems, these typically correspond directly to the bits-per-second +/// rate (e.g., `b9600` represents 9600 bps). The API uses symbolic constants for +/// portability across platforms. +/// +/// Use the predefined static constants rather than constructing arbitrary values. +/// +/// **Platform Notes:** +/// - Darwin supports rates up to `b230400` via symbolic constants. Higher rates +/// can be set using numeric values passed directly to `cfsetspeed()`. +/// - Linux defines symbolic constants for rates up to 4 Mbps (`b4000000`). +/// +/// See also: [POSIX termios.h](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/termios.h.html) +@frozen +@available(System 99, *) +public struct BaudRate: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.SpeedT + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.SpeedT) { + self.rawValue = rawValue + } + + /// Hang up (zero baud rate). + /// + /// Setting the output speed to this value causes the modem to hang up. + /// + /// The corresponding C constant is `B0`. + @_alwaysEmitIntoClient + public static var hangUp: BaudRate { BaudRate(rawValue: _B0) } + + /// 50 baud. + /// + /// The corresponding C constant is `B50`. + @_alwaysEmitIntoClient + public static var b50: BaudRate { BaudRate(rawValue: _B50) } + + /// 75 baud. + /// + /// The corresponding C constant is `B75`. + @_alwaysEmitIntoClient + public static var b75: BaudRate { BaudRate(rawValue: _B75) } + + /// 110 baud. + /// + /// The corresponding C constant is `B110`. + @_alwaysEmitIntoClient + public static var b110: BaudRate { BaudRate(rawValue: _B110) } + + /// 134.5 baud. + /// + /// The corresponding C constant is `B134`. + @_alwaysEmitIntoClient + public static var b134: BaudRate { BaudRate(rawValue: _B134) } + + /// 150 baud. + /// + /// The corresponding C constant is `B150`. + @_alwaysEmitIntoClient + public static var b150: BaudRate { BaudRate(rawValue: _B150) } + + /// 200 baud. + /// + /// The corresponding C constant is `B200`. + @_alwaysEmitIntoClient + public static var b200: BaudRate { BaudRate(rawValue: _B200) } + + /// 300 baud. + /// + /// The corresponding C constant is `B300`. + @_alwaysEmitIntoClient + public static var b300: BaudRate { BaudRate(rawValue: _B300) } + + /// 600 baud. + /// + /// The corresponding C constant is `B600`. + @_alwaysEmitIntoClient + public static var b600: BaudRate { BaudRate(rawValue: _B600) } + + /// 1200 baud. + /// + /// The corresponding C constant is `B1200`. + @_alwaysEmitIntoClient + public static var b1200: BaudRate { BaudRate(rawValue: _B1200) } + + /// 1800 baud. + /// + /// The corresponding C constant is `B1800`. + @_alwaysEmitIntoClient + public static var b1800: BaudRate { BaudRate(rawValue: _B1800) } + + /// 2400 baud. + /// + /// The corresponding C constant is `B2400`. + @_alwaysEmitIntoClient + public static var b2400: BaudRate { BaudRate(rawValue: _B2400) } + + /// 4800 baud. + /// + /// The corresponding C constant is `B4800`. + @_alwaysEmitIntoClient + public static var b4800: BaudRate { BaudRate(rawValue: _B4800) } + + /// 9600 baud. + /// + /// The corresponding C constant is `B9600`. + @_alwaysEmitIntoClient + public static var b9600: BaudRate { BaudRate(rawValue: _B9600) } + + /// 19200 baud. + /// + /// The corresponding C constant is `B19200`. + @_alwaysEmitIntoClient + public static var b19200: BaudRate { BaudRate(rawValue: _B19200) } + + /// 38400 baud. + /// + /// The corresponding C constant is `B38400`. + @_alwaysEmitIntoClient + public static var b38400: BaudRate { BaudRate(rawValue: _B38400) } + + /// 57600 baud. + /// + /// The corresponding C constant is `B57600`. + @_alwaysEmitIntoClient + public static var b57600: BaudRate { BaudRate(rawValue: _B57600) } + + /// 115200 baud. + /// + /// The corresponding C constant is `B115200`. + @_alwaysEmitIntoClient + public static var b115200: BaudRate { BaudRate(rawValue: _B115200) } + + /// 230400 baud. + /// + /// The corresponding C constant is `B230400`. + @_alwaysEmitIntoClient + public static var b230400: BaudRate { BaudRate(rawValue: _B230400) } + + #if canImport(Darwin) + /// 7200 baud. + /// + /// This is a Darwin-specific extension. + /// + /// The corresponding C constant is `B7200`. + @_alwaysEmitIntoClient + public static var b7200: BaudRate { BaudRate(rawValue: _B7200) } + + /// 14400 baud. + /// + /// This is a Darwin-specific extension. + /// + /// The corresponding C constant is `B14400`. + @_alwaysEmitIntoClient + public static var b14400: BaudRate { BaudRate(rawValue: _B14400) } + + /// 28800 baud. + /// + /// This is a Darwin-specific extension. + /// + /// The corresponding C constant is `B28800`. + @_alwaysEmitIntoClient + public static var b28800: BaudRate { BaudRate(rawValue: _B28800) } + + /// 76800 baud. + /// + /// This is a Darwin-specific extension. + /// + /// The corresponding C constant is `B76800`. + @_alwaysEmitIntoClient + public static var b76800: BaudRate { BaudRate(rawValue: _B76800) } + #endif + + #if os(Linux) + /// 460800 baud. + /// + /// The corresponding C constant is `B460800`. + @_alwaysEmitIntoClient + public static var b460800: BaudRate { BaudRate(rawValue: _B460800) } + + /// 500000 baud. + /// + /// The corresponding C constant is `B500000`. + @_alwaysEmitIntoClient + public static var b500000: BaudRate { BaudRate(rawValue: _B500000) } + + /// 576000 baud. + /// + /// The corresponding C constant is `B576000`. + @_alwaysEmitIntoClient + public static var b576000: BaudRate { BaudRate(rawValue: _B576000) } + + /// 921600 baud. + /// + /// The corresponding C constant is `B921600`. + @_alwaysEmitIntoClient + public static var b921600: BaudRate { BaudRate(rawValue: _B921600) } + + /// 1000000 baud (1 Mbps). + /// + /// The corresponding C constant is `B1000000`. + @_alwaysEmitIntoClient + public static var b1000000: BaudRate { BaudRate(rawValue: _B1000000) } + + /// 1152000 baud. + /// + /// The corresponding C constant is `B1152000`. + @_alwaysEmitIntoClient + public static var b1152000: BaudRate { BaudRate(rawValue: _B1152000) } + + /// 1500000 baud (1.5 Mbps). + /// + /// The corresponding C constant is `B1500000`. + @_alwaysEmitIntoClient + public static var b1500000: BaudRate { BaudRate(rawValue: _B1500000) } + + /// 2000000 baud (2 Mbps). + /// + /// The corresponding C constant is `B2000000`. + @_alwaysEmitIntoClient + public static var b2000000: BaudRate { BaudRate(rawValue: _B2000000) } + + /// 2500000 baud (2.5 Mbps). + /// + /// The corresponding C constant is `B2500000`. + @_alwaysEmitIntoClient + public static var b2500000: BaudRate { BaudRate(rawValue: _B2500000) } + + /// 3000000 baud (3 Mbps). + /// + /// The corresponding C constant is `B3000000`. + @_alwaysEmitIntoClient + public static var b3000000: BaudRate { BaudRate(rawValue: _B3000000) } + + /// 3500000 baud (3.5 Mbps). + /// + /// The corresponding C constant is `B3500000`. + @_alwaysEmitIntoClient + public static var b3500000: BaudRate { BaudRate(rawValue: _B3500000) } + + /// 4000000 baud (4 Mbps). + /// + /// The corresponding C constant is `B4000000`. + @_alwaysEmitIntoClient + public static var b4000000: BaudRate { BaudRate(rawValue: _B4000000) } + #endif +} + +#endif diff --git a/Sources/System/Terminal/ControlCharacters.swift b/Sources/System/Terminal/ControlCharacters.swift new file mode 100644 index 00000000..d1d01284 --- /dev/null +++ b/Sources/System/Terminal/ControlCharacters.swift @@ -0,0 +1,160 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +/// An index identifying a specific control character. +@frozen +@available(System 99, *) +public struct ControlCharacter: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public static var endOfFile: Self { Self(rawValue: _VEOF) } + + @_alwaysEmitIntoClient + public static var endOfLine: Self { Self(rawValue: _VEOL) } + + @_alwaysEmitIntoClient + public static var erase: Self { Self(rawValue: _VERASE) } + + @_alwaysEmitIntoClient + public static var interrupt: Self { Self(rawValue: _VINTR) } + + @_alwaysEmitIntoClient + public static var kill: Self { Self(rawValue: _VKILL) } + + @_alwaysEmitIntoClient + public static var minimum: Self { Self(rawValue: _VMIN) } + + @_alwaysEmitIntoClient + public static var quit: Self { Self(rawValue: _VQUIT) } + + @_alwaysEmitIntoClient + public static var start: Self { Self(rawValue: _VSTART) } + + @_alwaysEmitIntoClient + public static var stop: Self { Self(rawValue: _VSTOP) } + + @_alwaysEmitIntoClient + public static var suspend: Self { Self(rawValue: _VSUSP) } + + @_alwaysEmitIntoClient + public static var time: Self { Self(rawValue: _VTIME) } + + @_alwaysEmitIntoClient + public static var endOfLine2: Self { Self(rawValue: _VEOL2) } + + @_alwaysEmitIntoClient + public static var wordErase: Self { Self(rawValue: _VWERASE) } + + @_alwaysEmitIntoClient + public static var reprint: Self { Self(rawValue: _VREPRINT) } + + @_alwaysEmitIntoClient + public static var discard: Self { Self(rawValue: _VDISCARD) } + + @_alwaysEmitIntoClient + public static var literalNext: Self { Self(rawValue: _VLNEXT) } + + #if canImport(Darwin) + @_alwaysEmitIntoClient + public static var status: Self { Self(rawValue: _VSTATUS) } + + @_alwaysEmitIntoClient + public static var delayedSuspend: Self { Self(rawValue: _VDSUSP) } + #endif + + #if os(Linux) + @_alwaysEmitIntoClient + public static var switchCharacter: Self { Self(rawValue: _VSWTC) } + #endif +} + +/// The control characters for a terminal. +@frozen +@available(System 99, *) +public struct ControlCharacters: Sendable { + #if SYSTEM_PACKAGE_DARWIN + @usableFromInline + internal var storage: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + #elseif os(Linux) + @usableFromInline + internal var storage: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + #endif + + @_alwaysEmitIntoClient + public init() { + #if SYSTEM_PACKAGE_DARWIN + self.storage = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + #elseif os(Linux) + self.storage = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + #endif + } + + @_alwaysEmitIntoClient + public static var count: Int { Int(_NCCS) } + + @_alwaysEmitIntoClient + public static var disabled: CInterop.ControlCharacterValue { __POSIX_VDISABLE } + + @_alwaysEmitIntoClient + public subscript(_ character: ControlCharacter) -> CInterop.ControlCharacterValue { + get { + withUnsafeBytes(of: storage) { buffer in + buffer[Int(character.rawValue)] + } + } + set { + withUnsafeMutableBytes(of: &storage) { buffer in + buffer[Int(character.rawValue)] = newValue + } + } + } + + @_alwaysEmitIntoClient + public subscript(rawIndex index: Int) -> CInterop.ControlCharacterValue { + get { + precondition(index >= 0 && index < Self.count, "Index out of bounds") + return withUnsafeBytes(of: storage) { $0[index] } + } + set { + precondition(index >= 0 && index < Self.count, "Index out of bounds") + withUnsafeMutableBytes(of: &storage) { $0[index] = newValue } + } + } +} + +#endif + +// Manual Hashable conformance +@available(System 99, *) +extension ControlCharacters: Hashable { + @_alwaysEmitIntoClient + public static func == (lhs: ControlCharacters, rhs: ControlCharacters) -> Bool { + withUnsafeBytes(of: lhs.storage) { lhsBytes in + withUnsafeBytes(of: rhs.storage) { rhsBytes in + lhsBytes.elementsEqual(rhsBytes) + } + } + } + + @_alwaysEmitIntoClient + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: storage) { bytes in + hasher.combine(bytes: bytes) + } + } +} diff --git a/Sources/System/Terminal/TerminalAttributes+Serial.swift b/Sources/System/Terminal/TerminalAttributes+Serial.swift new file mode 100644 index 00000000..48386000 --- /dev/null +++ b/Sources/System/Terminal/TerminalAttributes+Serial.swift @@ -0,0 +1,127 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +// MARK: - Serial Port Conveniences + +extension TerminalAttributes { + /// The number of data bits per character. + @frozen + @available(System 99, *) + public struct CharacterSize: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public static var bits5: CharacterSize { CharacterSize(rawValue: _CS5) } + @_alwaysEmitIntoClient + public static var bits6: CharacterSize { CharacterSize(rawValue: _CS6) } + @_alwaysEmitIntoClient + public static var bits7: CharacterSize { CharacterSize(rawValue: _CS7) } + @_alwaysEmitIntoClient + public static var bits8: CharacterSize { CharacterSize(rawValue: _CS8) } + } + + /// The parity mode for error detection. + @frozen + @available(System 99, *) + public struct Parity: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public static var none: Parity { Parity(rawValue: 0) } + @_alwaysEmitIntoClient + public static var even: Parity { Parity(rawValue: _PARENB) } + @_alwaysEmitIntoClient + public static var odd: Parity { Parity(rawValue: _PARENB | _PARODD) } + + #if os(Linux) + @_alwaysEmitIntoClient + public static var mark: Parity { Parity(rawValue: _PARENB | _PARODD | _CMSPAR) } + @_alwaysEmitIntoClient + public static var space: Parity { Parity(rawValue: _PARENB | _CMSPAR) } + #endif + } + + /// The number of stop bits per character. + @frozen + @available(System 99, *) + public struct StopBits: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public static var one: StopBits { StopBits(rawValue: 0) } + @_alwaysEmitIntoClient + public static var two: StopBits { StopBits(rawValue: _CSTOPB) } + } + + /// The number of data bits per character. + @_alwaysEmitIntoClient + public var characterSize: CharacterSize { + get { + CharacterSize(rawValue: controlFlags.rawValue & _CSIZE) + } + set { + // CRITICAL: Clear CSIZE mask first to avoid combining bits + controlFlags.rawValue = (controlFlags.rawValue & ~_CSIZE) | newValue.rawValue + } + } + + /// The parity mode for error detection. + @_alwaysEmitIntoClient + public var parity: Parity { + get { + #if os(Linux) + let parityBits = controlFlags.rawValue & (_PARENB | _PARODD | _CMSPAR) + #else + let parityBits = controlFlags.rawValue & (_PARENB | _PARODD) + #endif + return Parity(rawValue: parityBits) + } + set { + #if os(Linux) + let mask: CInterop.TerminalFlags = _PARENB | _PARODD | _CMSPAR + #else + let mask: CInterop.TerminalFlags = _PARENB | _PARODD + #endif + controlFlags.rawValue = (controlFlags.rawValue & ~mask) | newValue.rawValue + } + } + + /// The number of stop bits per character. + @_alwaysEmitIntoClient + public var stopBits: StopBits { + get { + StopBits(rawValue: controlFlags.rawValue & _CSTOPB) + } + set { + controlFlags.rawValue = (controlFlags.rawValue & ~_CSTOPB) | newValue.rawValue + } + } +} + +#endif diff --git a/Sources/System/Terminal/TerminalAttributes.swift b/Sources/System/Terminal/TerminalAttributes.swift new file mode 100644 index 00000000..d8a137b7 --- /dev/null +++ b/Sources/System/Terminal/TerminalAttributes.swift @@ -0,0 +1,198 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +/// Terminal I/O attributes. +/// +/// This type represents the POSIX `termios` structure, providing type-safe access +/// to input modes, output modes, control modes, local modes, control characters, +/// and baud rates. +@frozen +@available(System 99, *) +public struct TerminalAttributes: RawRepresentable, Equatable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.Termios + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Termios) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public init() { + self.rawValue = CInterop.Termios() + } + + /// Input mode flags controlling input preprocessing. + @_alwaysEmitIntoClient + public var inputFlags: InputFlags { + get { InputFlags(rawValue: rawValue.c_iflag) } + set { rawValue.c_iflag = newValue.rawValue } + } + + /// Output mode flags controlling output postprocessing. + @_alwaysEmitIntoClient + public var outputFlags: OutputFlags { + get { OutputFlags(rawValue: rawValue.c_oflag) } + set { rawValue.c_oflag = newValue.rawValue } + } + + /// Control mode flags for hardware control settings. + @_alwaysEmitIntoClient + public var controlFlags: ControlFlags { + get { ControlFlags(rawValue: rawValue.c_cflag) } + set { rawValue.c_cflag = newValue.rawValue } + } + + /// Local mode flags controlling terminal behavior. + @_alwaysEmitIntoClient + public var localFlags: LocalFlags { + get { LocalFlags(rawValue: rawValue.c_lflag) } + set { rawValue.c_lflag = newValue.rawValue } + } + + /// Control characters for special input handling. + @_alwaysEmitIntoClient + public var controlCharacters: ControlCharacters { + get { + withUnsafePointer(to: rawValue.c_cc) { ptr in + ptr.withMemoryRebound(to: ControlCharacters.self, capacity: 1) { $0.pointee } + } + } + set { + withUnsafeMutablePointer(to: &rawValue.c_cc) { ptr in + ptr.withMemoryRebound(to: ControlCharacters.self, capacity: 1) { $0.pointee = newValue } + } + } + } + + /// The input baud rate. + @_alwaysEmitIntoClient + public var inputSpeed: BaudRate { + get { + withUnsafePointer(to: rawValue) { ptr in + BaudRate(rawValue: system_cfgetispeed(ptr)) + } + } + set { + withUnsafeMutablePointer(to: &rawValue) { ptr in + _ = system_cfsetispeed(ptr, newValue.rawValue) + } + } + } + + /// The output baud rate. + @_alwaysEmitIntoClient + public var outputSpeed: BaudRate { + get { + withUnsafePointer(to: rawValue) { ptr in + BaudRate(rawValue: system_cfgetospeed(ptr)) + } + } + set { + withUnsafeMutablePointer(to: &rawValue) { ptr in + _ = system_cfsetospeed(ptr, newValue.rawValue) + } + } + } + + /// Sets both input and output baud rates simultaneously. + @_alwaysEmitIntoClient + public mutating func setSpeed(_ speed: BaudRate) { + withUnsafeMutablePointer(to: &rawValue) { ptr in + _ = system_cfsetspeed(ptr, speed.rawValue) + } + } + + /// Configures these attributes for raw mode in place. + /// + /// Raw mode configures the terminal for character-at-a-time input with no processing. + public mutating func makeRaw() { + localFlags.subtract([.canonical, .echo, .signals, .extendedInput]) + inputFlags.subtract([.breakInterrupt, .mapCRToNL, .ignoreCR, .parityCheck, .stripHighBit, .startStopOutput]) + outputFlags.remove(.postProcess) + controlFlags.remove(.characterSizeMask) + controlFlags.insert(.characterSize8) + controlCharacters[.minimum] = 1 + controlCharacters[.time] = 0 + } + + /// Returns a copy of these attributes configured for raw mode. + @_alwaysEmitIntoClient + public func raw() -> TerminalAttributes { + var copy = self + copy.makeRaw() + return copy + } + + // MARK: - Equatable & Hashable + + @_alwaysEmitIntoClient + public static func == (lhs: TerminalAttributes, rhs: TerminalAttributes) -> Bool { + withUnsafeBytes(of: lhs.rawValue) { lhsBytes in + withUnsafeBytes(of: rhs.rawValue) { rhsBytes in + lhsBytes.elementsEqual(rhsBytes) + } + } + } + + @_alwaysEmitIntoClient + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: rawValue) { bytes in + hasher.combine(bytes: bytes) + } + } +} + +// MARK: - SetAction + +extension TerminalAttributes { + /// Specifies when to apply terminal attribute changes. + @frozen + @available(System 99, *) + public struct SetAction: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { + self.rawValue = rawValue + } + + /// Apply changes immediately. + /// + /// The corresponding C constant is `TCSANOW`. + @_alwaysEmitIntoClient + public static var now: Self { Self(rawValue: _TCSANOW) } + + /// Apply changes after all pending output has been transmitted. + /// + /// The corresponding C constant is `TCSADRAIN`. + @_alwaysEmitIntoClient + public static var afterDrain: Self { Self(rawValue: _TCSADRAIN) } + + /// Apply changes after all pending output has been transmitted, + /// and discard any unread input. + /// + /// The corresponding C constant is `TCSAFLUSH`. + @_alwaysEmitIntoClient + public static var afterFlush: Self { Self(rawValue: _TCSAFLUSH) } + + #if canImport(Darwin) + /// Modifier flag: don't alter hardware state. + /// + /// The corresponding C constant is `TCSASOFT`. + @_alwaysEmitIntoClient + public static var soft: Self { Self(rawValue: _TCSASOFT) } + #endif + } +} + +#endif diff --git a/Sources/System/Terminal/TerminalDescriptor.swift b/Sources/System/Terminal/TerminalDescriptor.swift new file mode 100644 index 00000000..b21ec41a --- /dev/null +++ b/Sources/System/Terminal/TerminalDescriptor.swift @@ -0,0 +1,98 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +/// A file descriptor referring to a terminal device. +/// +/// `TerminalDescriptor` wraps a `FileDescriptor` and provides terminal-specific +/// operations defined by POSIX. Use the failable initializer to safely +/// convert a file descriptor, or the `unchecked` initializer when you know the +/// descriptor refers to a terminal. +/// +/// ```swift +/// // Safe conversion +/// if let terminal = TerminalDescriptor(FileDescriptor.standardInput) { +/// var attrs = try terminal.attributes() +/// attrs.localFlags.remove(.echo) +/// try terminal.setAttributes(attrs, when: .now) +/// } +/// +/// // Unchecked (use when you know it's a terminal) +/// let terminal = TerminalDescriptor(unchecked: .standardInput) +/// ``` +@frozen +@available(System 99, *) +public struct TerminalDescriptor: RawRepresentable, Hashable, Codable { + /// The raw C file descriptor value. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a terminal descriptor from a raw file descriptor value. + /// + /// - Precondition: `rawValue` must refer to a terminal device. + /// Operations on a `TerminalDescriptor` created from a non-terminal + /// file descriptor will throw. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { + self.rawValue = rawValue + } + + /// Creates a terminal descriptor from a file descriptor if it refers to a terminal. + /// + /// Returns `nil` if `fileDescriptor` does not refer to a terminal device. + /// + /// The corresponding C function is `isatty`. + @_alwaysEmitIntoClient + public init?(_ fileDescriptor: FileDescriptor) { + guard system_isatty(fileDescriptor.rawValue) != 0 else { + return nil + } + self.rawValue = fileDescriptor.rawValue + } + + /// Creates a terminal descriptor without validating that the file descriptor + /// refers to a terminal. + /// + /// - Precondition: `fileDescriptor` must refer to a terminal device. + /// Operations on a `TerminalDescriptor` created from a non-terminal + /// file descriptor will throw. + @_alwaysEmitIntoClient + public init(unchecked fileDescriptor: FileDescriptor) { + self.rawValue = fileDescriptor.rawValue + } + + /// The underlying file descriptor. + @_alwaysEmitIntoClient + public var fileDescriptor: FileDescriptor { + FileDescriptor(rawValue: rawValue) + } +} + +// MARK: - FileDescriptor conveniences + +@available(System 99, *) +extension FileDescriptor { + /// Returns whether this file descriptor refers to a terminal device. + /// + /// The corresponding C function is `isatty`. + @_alwaysEmitIntoClient + public var isTerminal: Bool { + system_isatty(rawValue) != 0 + } + + /// Returns this file descriptor as a terminal descriptor, or `nil` if + /// it does not refer to a terminal device. + @_alwaysEmitIntoClient + public var asTerminal: TerminalDescriptor? { + TerminalDescriptor(self) + } +} + +#endif diff --git a/Sources/System/Terminal/TerminalFlags.swift b/Sources/System/Terminal/TerminalFlags.swift new file mode 100644 index 00000000..0658114f --- /dev/null +++ b/Sources/System/Terminal/TerminalFlags.swift @@ -0,0 +1,607 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +// MARK: - InputFlags + +/// Input mode flags for terminal attributes. +/// +/// These flags control preprocessing of input characters before they are +/// made available to a reading process. +@frozen +@available(System 99, *) +public struct InputFlags: OptionSet, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + /// Signal interrupt on break condition. + /// + /// The corresponding C constant is \`BRKINT\`. + @_alwaysEmitIntoClient + public static var breakInterrupt: Self { Self(rawValue: _BRKINT) } + + /// Map carriage return to newline on input. + /// + /// The corresponding C constant is \`ICRNL\`. + @_alwaysEmitIntoClient + public static var mapCRToNL: Self { Self(rawValue: _ICRNL) } + + /// Ignore break condition. + /// + /// The corresponding C constant is \`IGNBRK\`. + @_alwaysEmitIntoClient + public static var ignoreBreak: Self { Self(rawValue: _IGNBRK) } + + /// Ignore carriage return on input. + /// + /// The corresponding C constant is \`IGNCR\`. + @_alwaysEmitIntoClient + public static var ignoreCR: Self { Self(rawValue: _IGNCR) } + + /// Ignore characters with parity errors. + /// + /// The corresponding C constant is \`IGNPAR\`. + @_alwaysEmitIntoClient + public static var ignoreParityErrors: Self { Self(rawValue: _IGNPAR) } + + /// Map newline to carriage return on input. + /// + /// The corresponding C constant is \`INLCR\`. + @_alwaysEmitIntoClient + public static var mapNLToCR: Self { Self(rawValue: _INLCR) } + + /// Enable input parity checking. + /// + /// The corresponding C constant is \`INPCK\`. + @_alwaysEmitIntoClient + public static var parityCheck: Self { Self(rawValue: _INPCK) } + + /// Strip the eighth bit from input characters. + /// + /// The corresponding C constant is \`ISTRIP\`. + @_alwaysEmitIntoClient + public static var stripHighBit: Self { Self(rawValue: _ISTRIP) } + + /// Enable any character to restart output. + /// + /// The corresponding C constant is \`IXANY\`. + @_alwaysEmitIntoClient + public static var restartAny: Self { Self(rawValue: _IXANY) } + + /// Enable start/stop input flow control. + /// + /// The corresponding C constant is \`IXOFF\`. + @_alwaysEmitIntoClient + public static var startStopInput: Self { Self(rawValue: _IXOFF) } + + /// Enable start/stop output flow control. + /// + /// The corresponding C constant is \`IXON\`. + @_alwaysEmitIntoClient + public static var startStopOutput: Self { Self(rawValue: _IXON) } + + /// Mark parity and framing errors in the input stream. + /// + /// The corresponding C constant is \`PARMRK\`. + @_alwaysEmitIntoClient + public static var markParityErrors: Self { Self(rawValue: _PARMRK) } + + /// Ring bell when input queue is full. + /// + /// The corresponding C constant is \`IMAXBEL\`. + @_alwaysEmitIntoClient + public static var ringBellOnFull: Self { Self(rawValue: _IMAXBEL) } + + /// Assume input is UTF-8 encoded for correct VERASE handling. + /// + /// The corresponding C constant is \`IUTF8\`. + @_alwaysEmitIntoClient + public static var utf8Input: Self { Self(rawValue: _IUTF8) } + + #if os(Linux) + /// Map uppercase characters to lowercase on input. + /// + /// The corresponding C constant is \`IUCLC\`. + @_alwaysEmitIntoClient + public static var mapUpperToLower: Self { Self(rawValue: _IUCLC) } + #endif +} + +// MARK: - OutputFlags + +/// Output mode flags for terminal attributes. +/// +/// These flags control postprocessing of output characters. +@frozen +@available(System 99, *) +public struct OutputFlags: OptionSet, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + /// Enable output processing. + /// + /// The corresponding C constant is \`OPOST\`. + @_alwaysEmitIntoClient + public static var postProcess: Self { Self(rawValue: _OPOST) } + + /// Map newline to carriage return-newline on output. + /// + /// The corresponding C constant is \`ONLCR\`. + @_alwaysEmitIntoClient + public static var mapNLToCRNL: Self { Self(rawValue: _ONLCR) } + + /// Map carriage return to newline on output. + /// + /// The corresponding C constant is \`OCRNL\`. + @_alwaysEmitIntoClient + public static var mapCRToNL: Self { Self(rawValue: _OCRNL) } + + /// Don't output carriage return at column 0. + /// + /// The corresponding C constant is \`ONOCR\`. + @_alwaysEmitIntoClient + public static var noCRAtColumn0: Self { Self(rawValue: _ONOCR) } + + /// Newline performs carriage return function. + /// + /// The corresponding C constant is \`ONLRET\`. + @_alwaysEmitIntoClient + public static var nlPerformsCR: Self { Self(rawValue: _ONLRET) } + + /// Use fill characters for delay. + /// + /// The corresponding C constant is \`OFILL\`. + @_alwaysEmitIntoClient + public static var useFillCharacters: Self { Self(rawValue: _OFILL) } + + /// Fill character is DEL (0x7F), otherwise NUL (0x00). + /// + /// The corresponding C constant is \`OFDEL\`. + @_alwaysEmitIntoClient + public static var fillIsDEL: Self { Self(rawValue: _OFDEL) } + + /// Newline delay mask. + /// + /// The corresponding C constant is \`NLDLY\`. + @_alwaysEmitIntoClient + public static var newlineDelayMask: Self { Self(rawValue: _NLDLY) } + + /// Newline delay type 0 (no delay). + /// + /// The corresponding C constant is \`NL0\`. + @_alwaysEmitIntoClient + public static var newlineDelay0: Self { Self(rawValue: _NL0) } + + /// Newline delay type 1. + /// + /// The corresponding C constant is \`NL1\`. + @_alwaysEmitIntoClient + public static var newlineDelay1: Self { Self(rawValue: _NL1) } + + /// Carriage return delay mask. + /// + /// The corresponding C constant is \`CRDLY\`. + @_alwaysEmitIntoClient + public static var crDelayMask: Self { Self(rawValue: _CRDLY) } + + /// Carriage return delay type 0 (no delay). + /// + /// The corresponding C constant is \`CR0\`. + @_alwaysEmitIntoClient + public static var crDelay0: Self { Self(rawValue: _CR0) } + + /// Carriage return delay type 1. + /// + /// The corresponding C constant is \`CR1\`. + @_alwaysEmitIntoClient + public static var crDelay1: Self { Self(rawValue: _CR1) } + + /// Carriage return delay type 2. + /// + /// The corresponding C constant is \`CR2\`. + @_alwaysEmitIntoClient + public static var crDelay2: Self { Self(rawValue: _CR2) } + + /// Carriage return delay type 3. + /// + /// The corresponding C constant is \`CR3\`. + @_alwaysEmitIntoClient + public static var crDelay3: Self { Self(rawValue: _CR3) } + + /// Horizontal tab delay mask. + /// + /// The corresponding C constant is \`TABDLY\`. + @_alwaysEmitIntoClient + public static var tabDelayMask: Self { Self(rawValue: _TABDLY) } + + /// Horizontal tab delay type 0 (no delay). + /// + /// The corresponding C constant is \`TAB0\`. + @_alwaysEmitIntoClient + public static var tabDelay0: Self { Self(rawValue: _TAB0) } + + /// Horizontal tab delay type 1. + /// + /// The corresponding C constant is \`TAB1\`. + @_alwaysEmitIntoClient + public static var tabDelay1: Self { Self(rawValue: _TAB1) } + + /// Horizontal tab delay type 2. + /// + /// The corresponding C constant is \`TAB2\`. + @_alwaysEmitIntoClient + public static var tabDelay2: Self { Self(rawValue: _TAB2) } + + #if SYSTEM_PACKAGE_DARWIN + /// Expand tabs to spaces. + /// + /// The corresponding C constant is \`TAB3\`. + @_alwaysEmitIntoClient + public static var expandTabs: Self { Self(rawValue: _TAB3) } + #elseif os(Linux) + /// Expand tabs to spaces. + /// + /// The corresponding C constant is \`XTABS\`. + @_alwaysEmitIntoClient + public static var expandTabs: Self { Self(rawValue: _XTABS) } + #endif + + /// Backspace delay mask. + /// + /// The corresponding C constant is \`BSDLY\`. + @_alwaysEmitIntoClient + public static var backspaceDelayMask: Self { Self(rawValue: _BSDLY) } + + /// Backspace delay type 0 (no delay). + /// + /// The corresponding C constant is \`BS0\`. + @_alwaysEmitIntoClient + public static var backspaceDelay0: Self { Self(rawValue: _BS0) } + + /// Backspace delay type 1. + /// + /// The corresponding C constant is \`BS1\`. + @_alwaysEmitIntoClient + public static var backspaceDelay1: Self { Self(rawValue: _BS1) } + + /// Vertical tab delay mask. + /// + /// The corresponding C constant is \`VTDLY\`. + @_alwaysEmitIntoClient + public static var vtabDelayMask: Self { Self(rawValue: _VTDLY) } + + /// Vertical tab delay type 0 (no delay). + /// + /// The corresponding C constant is \`VT0\`. + @_alwaysEmitIntoClient + public static var vtabDelay0: Self { Self(rawValue: _VT0) } + + /// Vertical tab delay type 1. + /// + /// The corresponding C constant is \`VT1\`. + @_alwaysEmitIntoClient + public static var vtabDelay1: Self { Self(rawValue: _VT1) } + + /// Form feed delay mask. + /// + /// The corresponding C constant is \`FFDLY\`. + @_alwaysEmitIntoClient + public static var formFeedDelayMask: Self { Self(rawValue: _FFDLY) } + + /// Form feed delay type 0 (no delay). + /// + /// The corresponding C constant is \`FF0\`. + @_alwaysEmitIntoClient + public static var formFeedDelay0: Self { Self(rawValue: _FF0) } + + /// Form feed delay type 1. + /// + /// The corresponding C constant is \`FF1\`. + @_alwaysEmitIntoClient + public static var formFeedDelay1: Self { Self(rawValue: _FF1) } + + #if canImport(Darwin) + /// Expand tabs to spaces (Darwin-specific name for TAB3). + /// + /// The corresponding C constant is \`OXTABS\`. + @_alwaysEmitIntoClient + public static var oxtabs: Self { Self(rawValue: _OXTABS) } + + /// Discard EOT (^D) characters on output. + /// + /// The corresponding C constant is \`ONOEOT\`. + @_alwaysEmitIntoClient + public static var discardEOT: Self { Self(rawValue: _ONOEOT) } + #endif + + #if os(Linux) + /// Map lowercase characters to uppercase on output. + /// + /// The corresponding C constant is \`OLCUC\`. + @_alwaysEmitIntoClient + public static var mapLowerToUpper: Self { Self(rawValue: _OLCUC) } + #endif +} + +// MARK: - ControlFlags + +/// Control mode flags for terminal attributes. +/// +/// These flags control hardware characteristics of the terminal. +@frozen +@available(System 99, *) +public struct ControlFlags: OptionSet, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + /// Enable the receiver. + /// + /// The corresponding C constant is \`CREAD\`. + @_alwaysEmitIntoClient + public static var enableReceiver: Self { Self(rawValue: _CREAD) } + + /// Use two stop bits (instead of one). + /// + /// The corresponding C constant is \`CSTOPB\`. + @_alwaysEmitIntoClient + public static var twoStopBits: Self { Self(rawValue: _CSTOPB) } + + /// Hang up the modem connection on last close. + /// + /// The corresponding C constant is \`HUPCL\`. + @_alwaysEmitIntoClient + public static var hangUpOnClose: Self { Self(rawValue: _HUPCL) } + + /// Ignore modem status lines. + /// + /// The corresponding C constant is \`CLOCAL\`. + @_alwaysEmitIntoClient + public static var local: Self { Self(rawValue: _CLOCAL) } + + /// Enable parity generation and detection. + /// + /// The corresponding C constant is \`PARENB\`. + @_alwaysEmitIntoClient + public static var parityEnable: Self { Self(rawValue: _PARENB) } + + /// Use odd parity (instead of even). + /// + /// The corresponding C constant is \`PARODD\`. + @_alwaysEmitIntoClient + public static var oddParity: Self { Self(rawValue: _PARODD) } + + /// Mask for character size bits. + /// + /// The corresponding C constant is \`CSIZE\`. + @_alwaysEmitIntoClient + public static var characterSizeMask: Self { Self(rawValue: _CSIZE) } + + /// 5-bit characters. + /// + /// The corresponding C constant is \`CS5\`. + @_alwaysEmitIntoClient + public static var characterSize5: Self { Self(rawValue: _CS5) } + + /// 6-bit characters. + /// + /// The corresponding C constant is \`CS6\`. + @_alwaysEmitIntoClient + public static var characterSize6: Self { Self(rawValue: _CS6) } + + /// 7-bit characters. + /// + /// The corresponding C constant is \`CS7\`. + @_alwaysEmitIntoClient + public static var characterSize7: Self { Self(rawValue: _CS7) } + + /// 8-bit characters. + /// + /// The corresponding C constant is \`CS8\`. + @_alwaysEmitIntoClient + public static var characterSize8: Self { Self(rawValue: _CS8) } + + #if canImport(Darwin) + /// CTS flow control of output. + /// + /// The corresponding C constant is \`CCTS_OFLOW\`. + @_alwaysEmitIntoClient + public static var ctsOutputFlowControl: Self { Self(rawValue: _CCTS_OFLOW) } + + /// RTS flow control of input. + /// + /// The corresponding C constant is \`CRTS_IFLOW\`. + @_alwaysEmitIntoClient + public static var rtsInputFlowControl: Self { Self(rawValue: _CRTS_IFLOW) } + + /// DTR flow control of input. + /// + /// The corresponding C constant is \`CDTR_IFLOW\`. + @_alwaysEmitIntoClient + public static var dtrInputFlowControl: Self { Self(rawValue: _CDTR_IFLOW) } + + /// DSR flow control of output. + /// + /// The corresponding C constant is \`CDSR_OFLOW\`. + @_alwaysEmitIntoClient + public static var dsrOutputFlowControl: Self { Self(rawValue: _CDSR_OFLOW) } + + /// DCD (Carrier Detect) flow control of output. + /// + /// The corresponding C constant is \`CCAR_OFLOW\`. + @_alwaysEmitIntoClient + public static var carrierFlowControl: Self { Self(rawValue: _CCAR_OFLOW) } + + /// Enable RTS/CTS (hardware) full-duplex flow control. + /// + /// The corresponding C constant is \`CRTSCTS\`. + @_alwaysEmitIntoClient + public static var hardwareFlowControl: Self { Self(rawValue: _CRTSCTS) } + #endif + + #if os(Linux) + /// Enable RTS/CTS (hardware) flow control. + /// + /// The corresponding C constant is \`CRTSCTS\`. + @_alwaysEmitIntoClient + public static var hardwareFlowControl: Self { Self(rawValue: _CRTSCTS) } + + /// Use "stick" (mark/space) parity. + /// + /// The corresponding C constant is \`CMSPAR\`. + @_alwaysEmitIntoClient + public static var markSpaceParity: Self { Self(rawValue: _CMSPAR) } + #endif +} + +// MARK: - LocalFlags + +/// Local mode flags for terminal attributes. +/// +/// These flags control terminal functions that affect local processing. +@frozen +@available(System 99, *) +public struct LocalFlags: OptionSet, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TerminalFlags + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TerminalFlags) { + self.rawValue = rawValue + } + + /// Enable echo of input characters. + /// + /// The corresponding C constant is \`ECHO\`. + @_alwaysEmitIntoClient + public static var echo: Self { Self(rawValue: _ECHO) } + + /// Echo the ERASE character as backspace-space-backspace. + /// + /// The corresponding C constant is \`ECHOE\`. + @_alwaysEmitIntoClient + public static var echoErase: Self { Self(rawValue: _ECHOE) } + + /// Echo newline after the KILL character. + /// + /// The corresponding C constant is \`ECHOK\`. + @_alwaysEmitIntoClient + public static var echoKill: Self { Self(rawValue: _ECHOK) } + + /// Echo newline even if ECHO is not set. + /// + /// The corresponding C constant is \`ECHONL\`. + @_alwaysEmitIntoClient + public static var echoNL: Self { Self(rawValue: _ECHONL) } + + /// Enable canonical (line-buffered) input mode. + /// + /// The corresponding C constant is \`ICANON\`. + @_alwaysEmitIntoClient + public static var canonical: Self { Self(rawValue: _ICANON) } + + /// Enable extended input character processing. + /// + /// The corresponding C constant is \`IEXTEN\`. + @_alwaysEmitIntoClient + public static var extendedInput: Self { Self(rawValue: _IEXTEN) } + + /// Enable signal generation. + /// + /// The corresponding C constant is \`ISIG\`. + @_alwaysEmitIntoClient + public static var signals: Self { Self(rawValue: _ISIG) } + + /// Disable flushing after interrupt or quit. + /// + /// The corresponding C constant is \`NOFLSH\`. + @_alwaysEmitIntoClient + public static var noFlushAfterInterrupt: Self { Self(rawValue: _NOFLSH) } + + /// Send SIGTTOU for background output. + /// + /// The corresponding C constant is \`TOSTOP\`. + @_alwaysEmitIntoClient + public static var stopBackgroundOutput: Self { Self(rawValue: _TOSTOP) } + + /// Echo control characters as ^X. + /// + /// The corresponding C constant is \`ECHOCTL\`. + @_alwaysEmitIntoClient + public static var echoControl: Self { Self(rawValue: _ECHOCTL) } + + /// Visual erase for line kill. + /// + /// The corresponding C constant is \`ECHOKE\`. + @_alwaysEmitIntoClient + public static var echoKillErase: Self { Self(rawValue: _ECHOKE) } + + /// Visual erase mode for hardcopy terminals. + /// + /// The corresponding C constant is \`ECHOPRT\`. + @_alwaysEmitIntoClient + public static var echoPrint: Self { Self(rawValue: _ECHOPRT) } + + /// Output is being flushed (read-only state flag). + /// + /// The corresponding C constant is \`FLUSHO\`. + @_alwaysEmitIntoClient + public static var flushingOutput: Self { Self(rawValue: _FLUSHO) } + + /// Retype pending input at next read (state flag). + /// + /// The corresponding C constant is \`PENDIN\`. + @_alwaysEmitIntoClient + public static var retypePending: Self { Self(rawValue: _PENDIN) } + + #if canImport(Darwin) + /// Use alternate word erase algorithm. + /// + /// The corresponding C constant is \`ALTWERASE\`. + @_alwaysEmitIntoClient + public static var alternateWordErase: Self { Self(rawValue: _ALTWERASE) } + + /// External processing (for pseudo-terminals). + /// + /// The corresponding C constant is \`EXTPROC\`. + @_alwaysEmitIntoClient + public static var externalProcessing: Self { Self(rawValue: _EXTPROC) } + + /// Disable kernel status message from VSTATUS character. + /// + /// The corresponding C constant is \`NOKERNINFO\`. + @_alwaysEmitIntoClient + public static var noKernelInfo: Self { Self(rawValue: _NOKERNINFO) } + #endif + + #if os(Linux) + /// External processing (for pseudo-terminals). + /// + /// The corresponding C constant is \`EXTPROC\`. + @_alwaysEmitIntoClient + public static var externalProcessing: Self { Self(rawValue: _EXTPROC) } + #endif +} + +#endif diff --git a/Sources/System/Terminal/TerminalOperations.swift b/Sources/System/Terminal/TerminalOperations.swift new file mode 100644 index 00000000..087a75ba --- /dev/null +++ b/Sources/System/Terminal/TerminalOperations.swift @@ -0,0 +1,184 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +// MARK: - Terminal Operations + +@available(System 99, *) +extension TerminalDescriptor { + /// Returns the current terminal attributes. + @_alwaysEmitIntoClient + public func attributes() throws(Errno) -> TerminalAttributes { + try _attributes().get() + } + + @usableFromInline + internal func _attributes() -> Result { + var attrs = CInterop.Termios() + let result = withUnsafeMutablePointer(to: &attrs) { attrsPtr in + nothingOrErrno(retryOnInterrupt: false) { + system_tcgetattr(rawValue, attrsPtr) + } + } + return result.map { TerminalAttributes(rawValue: attrs) } + } + + /// Sets the terminal attributes. + @_alwaysEmitIntoClient + public func setAttributes( + _ attributes: TerminalAttributes, + when action: TerminalAttributes.SetAction, + retryOnInterrupt: Bool = true + ) throws(Errno) { + try _setAttributes(attributes, when: action, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _setAttributes( + _ attributes: TerminalAttributes, + when action: TerminalAttributes.SetAction, + retryOnInterrupt: Bool + ) -> Result { + var attrs = attributes.rawValue + return withUnsafePointer(to: &attrs) { attrsPtr in + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_tcsetattr(rawValue, action.rawValue, attrsPtr) + } + } + } + + /// Blocks until all output written to the terminal has been transmitted. + @_alwaysEmitIntoClient + public func drain(retryOnInterrupt: Bool = true) throws(Errno) { + try _drain(retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _drain(retryOnInterrupt: Bool) -> Result { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_tcdrain(rawValue) + } + } + + /// Discards data in the specified queue(s). + @_alwaysEmitIntoClient + public func flush(_ queue: Queue) throws(Errno) { + try _flush(queue).get() + } + + @usableFromInline + internal func _flush(_ queue: Queue) -> Result { + nothingOrErrno(retryOnInterrupt: false) { + system_tcflush(rawValue, queue.rawValue) + } + } + + /// Suspends or resumes data transmission or reception. + @_alwaysEmitIntoClient + public func flow(_ action: FlowAction) throws(Errno) { + try _flow(action).get() + } + + @usableFromInline + internal func _flow(_ action: FlowAction) -> Result { + nothingOrErrno(retryOnInterrupt: false) { + system_tcflow(rawValue, action.rawValue) + } + } + + /// Sends a break signal on the terminal. + @_alwaysEmitIntoClient + public func sendBreak(duration: CInt = 0) throws(Errno) { + try _sendBreak(duration: duration).get() + } + + @usableFromInline + internal func _sendBreak(duration: CInt) -> Result { + nothingOrErrno(retryOnInterrupt: false) { + system_tcsendbreak(rawValue, duration) + } + } + + /// Executes a closure with modified terminal attributes. + public func withAttributes( + appliedWhen action: TerminalAttributes.SetAction = .afterFlush, + retryOnInterrupt: Bool = true, + _ modify: (inout TerminalAttributes) throws -> Void, + do body: () throws -> T + ) throws -> T { + let original = try attributes() + var modified = original + try modify(&modified) + try setAttributes(modified, when: action, retryOnInterrupt: retryOnInterrupt) + defer { + _ = try? setAttributes(original, when: .now) + } + return try body() + } + + /// Executes a closure with the terminal in raw mode. + public func withRawMode(retryOnInterrupt: Bool = true, do body: () throws -> T) throws -> T { + try withAttributes(retryOnInterrupt: retryOnInterrupt, { $0.makeRaw() }, do: body) + } +} + +// MARK: - Supporting Types + +@available(System 99, *) +extension TerminalDescriptor { + /// Specifies which queue(s) to flush. + @frozen + @available(System 99, *) + public struct Queue: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public static var input: Self { Self(rawValue: _TCIFLUSH) } + + @_alwaysEmitIntoClient + public static var output: Self { Self(rawValue: _TCOFLUSH) } + + @_alwaysEmitIntoClient + public static var both: Self { Self(rawValue: _TCIOFLUSH) } + } + + /// Flow control actions. + @frozen + @available(System 99, *) + public struct FlowAction: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public static var suspendOutput: Self { Self(rawValue: _TCOOFF) } + + @_alwaysEmitIntoClient + public static var resumeOutput: Self { Self(rawValue: _TCOON) } + + @_alwaysEmitIntoClient + public static var sendStop: Self { Self(rawValue: _TCIOFF) } + + @_alwaysEmitIntoClient + public static var sendStart: Self { Self(rawValue: _TCION) } + } +} + +#endif diff --git a/Sources/System/Terminal/TerminalWindowSize.swift b/Sources/System/Terminal/TerminalWindowSize.swift new file mode 100644 index 00000000..141b8269 --- /dev/null +++ b/Sources/System/Terminal/TerminalWindowSize.swift @@ -0,0 +1,96 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +// MARK: - Window Size + +@available(System 99, *) +extension TerminalDescriptor { + /// The size of a terminal window in characters. + @frozen + @available(System 99, *) + public struct WindowSize: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.WinSize + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.WinSize) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public init(rows: UInt16, columns: UInt16) { + var size = CInterop.WinSize() + size.ws_row = rows + size.ws_col = columns + self.rawValue = size + } + + @_alwaysEmitIntoClient + public var rows: UInt16 { + get { rawValue.ws_row } + set { rawValue.ws_row = newValue } + } + + @_alwaysEmitIntoClient + public var columns: UInt16 { + get { rawValue.ws_col } + set { rawValue.ws_col = newValue } + } + + // MARK: - Equatable & Hashable + + @_alwaysEmitIntoClient + public static func == (lhs: WindowSize, rhs: WindowSize) -> Bool { + lhs.rows == rhs.rows && lhs.columns == rhs.columns + } + + @_alwaysEmitIntoClient + public func hash(into hasher: inout Hasher) { + hasher.combine(rows) + hasher.combine(columns) + } + } + + /// Returns the current window size. + @_alwaysEmitIntoClient + public func windowSize() throws(Errno) -> WindowSize { + try _windowSize().get() + } + + @usableFromInline + internal func _windowSize() -> Result { + var size = CInterop.WinSize() + let result = withUnsafeMutablePointer(to: &size) { sizePtr in + valueOrErrno(retryOnInterrupt: false) { + system_tiocgwinsz(rawValue, sizePtr) + } + } + return result.map { _ in WindowSize(rawValue: size) } + } + + /// Sets the window size. + @_alwaysEmitIntoClient + public func setWindowSize(_ size: WindowSize) throws(Errno) { + try _setWindowSize(size).get() + } + + @usableFromInline + internal func _setWindowSize(_ size: WindowSize) -> Result { + var ws = size.rawValue + return withUnsafePointer(to: &ws) { sizePtr in + nothingOrErrno(retryOnInterrupt: false) { + system_tiocswinsz(rawValue, sizePtr) + } + } + } +} + +#endif diff --git a/Tests/SystemTests/TerminalTests.swift b/Tests/SystemTests/TerminalTests.swift new file mode 100644 index 00000000..09705dbf --- /dev/null +++ b/Tests/SystemTests/TerminalTests.swift @@ -0,0 +1,518 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if !os(Windows) + +import XCTest +import SystemPackage + +final class TerminalTests: XCTestCase { + + // MARK: - TerminalDescriptor Initialization Tests + + func testTerminalDescriptorInitWithNonTerminal() throws { + // Create a pipe - pipes are not terminals + let fds = try FileDescriptor.pipe() + defer { + try? fds.readEnd.close() + try? fds.writeEnd.close() + } + + // Should return nil for non-terminal file descriptors + XCTAssertNil(TerminalDescriptor(fds.readEnd)) + XCTAssertNil(TerminalDescriptor(fds.writeEnd)) + XCTAssertFalse(fds.readEnd.isTerminal) + XCTAssertFalse(fds.writeEnd.isTerminal) + } + + func testTerminalDescriptorUncheckedInit() throws { + // Unchecked init doesn't validate - just wraps the descriptor + let fd = FileDescriptor.standardInput + let terminal = TerminalDescriptor(unchecked: fd) + XCTAssertEqual(terminal.rawValue, fd.rawValue) + XCTAssertEqual(terminal.fileDescriptor, fd) + } + + func testFileDescriptorIsTerminalProperty() throws { + // Test isTerminal property - may vary depending on test environment + // Just verify it doesn't crash + let _ = FileDescriptor.standardInput.isTerminal + let _ = FileDescriptor.standardOutput.isTerminal + let _ = FileDescriptor.standardError.isTerminal + } + + func testFileDescriptorAsTerminalProperty() throws { + // Create a pipe to ensure we have a non-terminal + let fds = try FileDescriptor.pipe() + defer { + try? fds.readEnd.close() + try? fds.writeEnd.close() + } + + XCTAssertNil(fds.readEnd.asTerminal) + XCTAssertNil(fds.writeEnd.asTerminal) + } + + // MARK: - TerminalAttributes Basic Tests + + func testTerminalAttributesInit() { + let attrs = TerminalAttributes() + // Should create zero-initialized attributes + XCTAssertNotNil(attrs) + } + + func testTerminalAttributesEquatable() { + let attrs1 = TerminalAttributes() + let attrs2 = TerminalAttributes() + + XCTAssertEqual(attrs1, attrs2) + + var attrs3 = TerminalAttributes() + attrs3.inputFlags.insert(.breakInterrupt) + + XCTAssertNotEqual(attrs1, attrs3) + } + + func testTerminalAttributesHashable() { + let attrs1 = TerminalAttributes() + let attrs2 = TerminalAttributes() + + XCTAssertEqual(attrs1.hashValue, attrs2.hashValue) + + var set = Set() + set.insert(attrs1) + XCTAssertTrue(set.contains(attrs2)) + } + + // MARK: - Flag Tests + + func testInputFlags() { + var flags = InputFlags() + XCTAssertTrue(flags.isEmpty) + + flags.insert(.breakInterrupt) + XCTAssertTrue(flags.contains(.breakInterrupt)) + XCTAssertFalse(flags.contains(.ignoreCR)) + + flags.insert(.mapCRToNL) + XCTAssertTrue(flags.contains(.breakInterrupt)) + XCTAssertTrue(flags.contains(.mapCRToNL)) + + flags.remove(.breakInterrupt) + XCTAssertFalse(flags.contains(.breakInterrupt)) + XCTAssertTrue(flags.contains(.mapCRToNL)) + + // Test set operations + let flags2: InputFlags = [.ignoreBreak, .ignoreCR] + let union = flags.union(flags2) + XCTAssertTrue(union.contains(.mapCRToNL)) + XCTAssertTrue(union.contains(.ignoreBreak)) + XCTAssertTrue(union.contains(.ignoreCR)) + } + + func testOutputFlags() { + var flags = OutputFlags() + XCTAssertTrue(flags.isEmpty) + + flags.insert(.postProcess) + XCTAssertTrue(flags.contains(.postProcess)) + + flags.insert(.mapNLToCRNL) + XCTAssertTrue(flags.contains(.postProcess)) + XCTAssertTrue(flags.contains(.mapNLToCRNL)) + } + + func testControlFlags() { + var flags = ControlFlags() + XCTAssertTrue(flags.isEmpty) + + flags.insert(.enableReceiver) + XCTAssertTrue(flags.contains(.enableReceiver)) + + flags.insert(.characterSize8) + XCTAssertTrue(flags.contains(.characterSize8)) + } + + func testLocalFlags() { + var flags = LocalFlags() + XCTAssertTrue(flags.isEmpty) + + flags.insert(.echo) + XCTAssertTrue(flags.contains(.echo)) + + flags.insert(.canonical) + XCTAssertTrue(flags.contains(.canonical)) + + flags.remove(.echo) + XCTAssertFalse(flags.contains(.echo)) + XCTAssertTrue(flags.contains(.canonical)) + } + + // MARK: - TerminalAttributes Flag Properties + + func testTerminalAttributesFlagProperties() { + var attrs = TerminalAttributes() + + // Test input flags + attrs.inputFlags.insert(.breakInterrupt) + XCTAssertTrue(attrs.inputFlags.contains(.breakInterrupt)) + + // Test output flags + attrs.outputFlags.insert(.postProcess) + XCTAssertTrue(attrs.outputFlags.contains(.postProcess)) + + // Test control flags + attrs.controlFlags.insert(.enableReceiver) + XCTAssertTrue(attrs.controlFlags.contains(.enableReceiver)) + + // Test local flags + attrs.localFlags.insert(.echo) + XCTAssertTrue(attrs.localFlags.contains(.echo)) + } + + // MARK: - Control Character Tests + + func testControlCharacterConstants() { + // Just verify the constants exist and have distinct values + XCTAssertNotEqual(ControlCharacter.endOfFile.rawValue, ControlCharacter.interrupt.rawValue) + XCTAssertNotEqual(ControlCharacter.erase.rawValue, ControlCharacter.kill.rawValue) + } + + func testControlCharactersSubscript() { + var cc = ControlCharacters() + + // Test typed subscript + cc[.interrupt] = 0x03 // Ctrl-C + XCTAssertEqual(cc[.interrupt], 0x03) + + cc[.endOfFile] = 0x04 // Ctrl-D + XCTAssertEqual(cc[.endOfFile], 0x04) + + // Test minimum/time for non-canonical mode + cc[.minimum] = 1 + cc[.time] = 0 + XCTAssertEqual(cc[.minimum], 1) + XCTAssertEqual(cc[.time], 0) + } + + func testControlCharactersRawIndexSubscript() { + var cc = ControlCharacters() + + // Test raw index subscript + XCTAssertGreaterThan(ControlCharacters.count, 0) + + for i in 0..() + set.insert(size1) + XCTAssertTrue(set.contains(size2)) + } + + // MARK: - Integration Tests (require actual terminal) + + func testTerminalAttributesRoundtrip() throws { + // Only run if we actually have a terminal + guard let terminal = TerminalDescriptor(.standardInput) else { + throw XCTSkip("stdin is not a terminal") + } + + // Get current attributes + let original = try terminal.attributes() + + // Verify we can read them back + let readBack = try terminal.attributes() + XCTAssertEqual(original, readBack) + } + + func testTerminalSetAttributes() throws { + guard let terminal = TerminalDescriptor(.standardInput) else { + throw XCTSkip("stdin is not a terminal") + } + + let original = try terminal.attributes() + defer { + // Always restore original attributes + try? terminal.setAttributes(original, when: .now) + } + + // Make a modification + var modified = original + modified.localFlags.insert(.echoNL) + + // Set the modified attributes + try terminal.setAttributes(modified, when: .now) + + // Read them back + let readBack = try terminal.attributes() + XCTAssertTrue(readBack.localFlags.contains(.echoNL)) + } + + func testWithAttributes() throws { + guard let terminal = TerminalDescriptor(.standardInput) else { + throw XCTSkip("stdin is not a terminal") + } + + let original = try terminal.attributes() + + // Modify attributes within scope + try terminal.withAttributes({ attrs in + attrs.localFlags.insert(.echoNL) + }) { + // Inside the block, attributes should be modified + let current = try terminal.attributes() + XCTAssertTrue(current.localFlags.contains(.echoNL)) + } + + // After the block, attributes should be restored + let restored = try terminal.attributes() + XCTAssertEqual(original.localFlags, restored.localFlags) + } + + func testWithRawMode() throws { + guard let terminal = TerminalDescriptor(.standardInput) else { + throw XCTSkip("stdin is not a terminal") + } + + let original = try terminal.attributes() + + try terminal.withRawMode { + // Inside raw mode + let current = try terminal.attributes() + XCTAssertFalse(current.localFlags.contains(.canonical)) + XCTAssertFalse(current.localFlags.contains(.echo)) + } + + // After exiting raw mode + let restored = try terminal.attributes() + XCTAssertEqual(original.localFlags, restored.localFlags) + } + + func testWindowSizeOperation() throws { + guard let terminal = TerminalDescriptor(.standardOutput) else { + throw XCTSkip("stdout is not a terminal") + } + + // Just verify we can get window size without crashing + let size = try terminal.windowSize() + XCTAssertGreaterThan(size.rows, 0) + XCTAssertGreaterThan(size.columns, 0) + } + + func testTerminalOperationsDontCrash() throws { + guard let terminal = TerminalDescriptor(.standardOutput) else { + throw XCTSkip("stdout is not a terminal") + } + + // These should not crash, though they may fail with errors + // We're just testing they're callable + try? terminal.drain() + try? terminal.flush(.output) + try? terminal.flow(.resumeOutput) + } +} + +#endif