Files
apple--containerization/Sources/ContainerizationOS/Terminal.swift
T
wehub-resource-sync 680845cb1c
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

210 lines
6.6 KiB
Swift

//===----------------------------------------------------------------------===//
// 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)!)
}
}