chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// `Terminal` provides a clean interface to deal with terminal interactions on Unix platforms.
|
||||
public struct Terminal: Sendable {
|
||||
private let initState: termios?
|
||||
|
||||
private var descriptor: Int32 {
|
||||
handle.fileDescriptor
|
||||
}
|
||||
public let handle: FileHandle
|
||||
|
||||
public init(descriptor: Int32, setInitState: Bool = true) throws {
|
||||
if setInitState {
|
||||
self.initState = try Self.getattr(descriptor)
|
||||
} else {
|
||||
initState = nil
|
||||
}
|
||||
self.handle = .init(fileDescriptor: descriptor, closeOnDealloc: false)
|
||||
}
|
||||
|
||||
/// Write the provided data to the tty device.
|
||||
public func write(_ data: Data) throws {
|
||||
try handle.write(contentsOf: data)
|
||||
}
|
||||
|
||||
/// The winsize for a pty.
|
||||
public struct Size: Sendable {
|
||||
let size: winsize
|
||||
|
||||
/// The width or `col` of the pty.
|
||||
public var width: UInt16 {
|
||||
size.ws_col
|
||||
}
|
||||
/// The height or `rows` of the pty.
|
||||
public var height: UInt16 {
|
||||
size.ws_row
|
||||
}
|
||||
|
||||
init(_ size: winsize) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
/// Set the size for use with a pty.
|
||||
public init(width cols: UInt16, height rows: UInt16) {
|
||||
self.size = winsize(ws_row: rows, ws_col: cols, ws_xpixel: 0, ws_ypixel: 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the current pty attached to any of the STDIO descriptors.
|
||||
public static var current: Terminal {
|
||||
get throws {
|
||||
for i in [STDERR_FILENO, STDOUT_FILENO, STDIN_FILENO] {
|
||||
do {
|
||||
return try Terminal(descriptor: i)
|
||||
} catch {}
|
||||
}
|
||||
throw Error.notAPty
|
||||
}
|
||||
}
|
||||
|
||||
/// The current window size for the pty.
|
||||
public var size: Size {
|
||||
get throws {
|
||||
var ws = winsize()
|
||||
try fromSyscall(ioctl(descriptor, UInt(TIOCGWINSZ), &ws))
|
||||
return Size(ws)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new pty pair.
|
||||
/// - Parameter initialSize: An initial size of the child pty.
|
||||
public static func create(initialSize: Size? = nil) throws -> (parent: Terminal, child: Terminal) {
|
||||
var parent: Int32 = 0
|
||||
var child: Int32 = 0
|
||||
let size = initialSize ?? Size(width: 120, height: 40)
|
||||
var ws = size.size
|
||||
|
||||
try fromSyscall(openpty(&parent, &child, nil, nil, &ws))
|
||||
return (
|
||||
parent: try Terminal(descriptor: parent, setInitState: false),
|
||||
child: try Terminal(descriptor: child, setInitState: false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Errors
|
||||
|
||||
extension Terminal {
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case notAPty
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notAPty:
|
||||
return "the provided fd is not a pty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Terminal {
|
||||
/// Resize the current pty from the size of the provided pty.
|
||||
/// - Parameter pty: A pty to resize from.
|
||||
public func resize(from pty: Terminal) throws {
|
||||
var ws = try pty.size
|
||||
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
|
||||
}
|
||||
|
||||
/// Resize the pty to the provided window size.
|
||||
/// - Parameter size: A window size for a pty.
|
||||
public func resize(size: Size) throws {
|
||||
var ws = size.size
|
||||
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
|
||||
}
|
||||
|
||||
/// Resize the pty to the provided window size.
|
||||
/// - Parameter width: A width or cols of the terminal.
|
||||
/// - Parameter height: A height or rows of the terminal.
|
||||
public func resize(width: UInt16, height: UInt16) throws {
|
||||
var ws = Size(width: width, height: height)
|
||||
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
|
||||
}
|
||||
}
|
||||
|
||||
extension Terminal {
|
||||
/// Enable raw mode for the pty.
|
||||
public func setraw() throws {
|
||||
var attr = try Self.getattr(descriptor)
|
||||
cfmakeraw(&attr)
|
||||
attr.c_oflag = attr.c_oflag | tcflag_t(OPOST)
|
||||
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
||||
}
|
||||
|
||||
/// Enable echo support.
|
||||
/// Chars typed will be displayed to the terminal.
|
||||
public func enableEcho() throws {
|
||||
var attr = try Self.getattr(descriptor)
|
||||
attr.c_iflag &= ~tcflag_t(ICRNL)
|
||||
attr.c_lflag &= ~tcflag_t(ICANON | ECHO)
|
||||
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
||||
}
|
||||
|
||||
/// Disable echo support.
|
||||
/// Chars typed will not be displayed back to the terminal.
|
||||
public func disableEcho() throws {
|
||||
var attr = try Self.getattr(descriptor)
|
||||
attr.c_lflag &= ~tcflag_t(ECHO)
|
||||
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
||||
}
|
||||
|
||||
private static func getattr(_ fd: Int32) throws -> termios {
|
||||
var attr = termios()
|
||||
try fromSyscall(tcgetattr(fd, &attr))
|
||||
return attr
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Reset
|
||||
|
||||
extension Terminal {
|
||||
/// Close this pty's file descriptor.
|
||||
public func close() throws {
|
||||
do {
|
||||
// Use FileHandle's close directly as it sets the underlying fd in the object
|
||||
// to -1 for us.
|
||||
try self.handle.close()
|
||||
} catch {
|
||||
if let error = error as NSError?, error.domain == NSPOSIXErrorDomain {
|
||||
throw POSIXError(.init(rawValue: Int32(error.code))!)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the pty to its initial state.
|
||||
public func reset() throws {
|
||||
if var attr = initState {
|
||||
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the pty to its initial state masking any errors.
|
||||
/// This is commonly used in a `defer` body to reset the current pty where the error code is not generally useful.
|
||||
public func tryReset() {
|
||||
try? reset()
|
||||
}
|
||||
}
|
||||
|
||||
private func fromSyscall(_ status: Int32) throws {
|
||||
guard status == 0 else {
|
||||
throw POSIXError(.init(rawValue: errno)!)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user