Swift: readLine 超时
Swift: readLine with timeout
我正在 Swift 中编写一个命令行工具,我想获得一些用户输入。我为此使用 readLine()
。但我想在 select 默认选项中添加一个超时,如果用户没有在时间范围内响应。这可能吗?
> Do you want to proceed? [y/n, will continue automatically in 2:00]: _
(实际更新提示中的剩余时间加分。)
不是最漂亮的 Swift 代码,但它完成了工作。 aio (asynchronous input output) low level system API in raw terminal mode is used to read user input without pressing enter.
我们正在使用 aio_read
(与 aio_return
配对)进行多次读取,因为用户可能输入了我们不想要的键。
由于 Xcode 调试控制台不是标准控制台,请 运行 在独立终端中使用它。
我 运行 对这段代码的唯一警告是在时间 运行 结束 aio_read
将终端标准输入设置为仍然期待用户输入(例如输入shell 再次出现的键)。我会尽量避免这个问题。
import Foundation
//
extension TimeInterval{
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
var formatString = ""
if hours == 0 {
if(minutes < 10) {
formatString = "%2d:%0.2d"
} else {
formatString = "%0.2d:%0.2d"
}
return String(format: formatString,minutes,seconds)
} else {
formatString = "%2d:%0.2d:%0.2d"
return String(format: formatString,hours,minutes,seconds)
}
}
}
//
extension FileHandle {
func enableRawMode() -> termios {
var raw = termios()
tcgetattr(self.fileDescriptor, &raw)
let original = raw
raw.c_lflag &= ~UInt(ECHO | ICANON)
tcsetattr(self.fileDescriptor, TCSADRAIN, &raw)
return original
}
func restoreRawMode(originalTerm: termios) {
var term = originalTerm
tcsetattr(self.fileDescriptor, TCSADRAIN, &term)
}
}
let bufferForReadSize = 100
var bufferForRead: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: bufferForReadSize, alignment: 1)
//Give the user slightly bit more than 2 minutes so that the 2:00 countdown initial value can be seen
let endTime = Date().addingTimeInterval(TimeInterval(exactly: 120.5)!)
//struct for using aio_ calls
var aio: aiocb = aiocb(aio_fildes: FileHandle.standardInput.fileDescriptor,
aio_offset: 0,
aio_buf: bufferForRead,
aio_nbytes: bufferForReadSize,
aio_reqprio: 0,
aio_sigevent: sigevent(),
aio_lio_opcode: 0)
var userChoice: Bool?
print()
let originalTermios = FileHandle.standardInput.enableRawMode()
withUnsafeMutablePointer(to: &aio) {
while userChoice == nil {
let timeLeft = endTime.timeIntervalSince(Date())
print("\u{1B}[A" + //rewind to previous line +
"Hello, World? (y/n)" + timeLeft.stringFromTimeInterval())
let inputString = String(cString: bufferForRead.bindMemory(to: Int8.self, capacity: bufferForReadSize))
if inputString.starts(with: "y") || inputString.starts(with: "Y") {
userChoice = true
break
} else if inputString.starts(with: "n") || inputString.starts(with: "N") {
userChoice = false
break
}
if timeLeft <= 0 {
userChoice = true
break
} else {
//Async IO read
aio_read([=10=])
CFRunLoopRunInMode(CFRunLoopMode.defaultMode,
0.5, //choose the interval value depending on the fps you need
false)
//Async IO return
aio_return([=10=])
}
}
}
FileHandle.standardInput.restoreRawMode(originalTerm: originalTermios)
userChoice! ? print("Thanks for choosing YES. Bye") : print("Thanks for choosing NO. Bye")
我正在 Swift 中编写一个命令行工具,我想获得一些用户输入。我为此使用 readLine()
。但我想在 select 默认选项中添加一个超时,如果用户没有在时间范围内响应。这可能吗?
> Do you want to proceed? [y/n, will continue automatically in 2:00]: _
(实际更新提示中的剩余时间加分。)
不是最漂亮的 Swift 代码,但它完成了工作。 aio (asynchronous input output) low level system API in raw terminal mode is used to read user input without pressing enter.
我们正在使用 aio_read
(与 aio_return
配对)进行多次读取,因为用户可能输入了我们不想要的键。
由于 Xcode 调试控制台不是标准控制台,请 运行 在独立终端中使用它。
我 运行 对这段代码的唯一警告是在时间 运行 结束 aio_read
将终端标准输入设置为仍然期待用户输入(例如输入shell 再次出现的键)。我会尽量避免这个问题。
import Foundation
//
extension TimeInterval{
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
var formatString = ""
if hours == 0 {
if(minutes < 10) {
formatString = "%2d:%0.2d"
} else {
formatString = "%0.2d:%0.2d"
}
return String(format: formatString,minutes,seconds)
} else {
formatString = "%2d:%0.2d:%0.2d"
return String(format: formatString,hours,minutes,seconds)
}
}
}
//
extension FileHandle {
func enableRawMode() -> termios {
var raw = termios()
tcgetattr(self.fileDescriptor, &raw)
let original = raw
raw.c_lflag &= ~UInt(ECHO | ICANON)
tcsetattr(self.fileDescriptor, TCSADRAIN, &raw)
return original
}
func restoreRawMode(originalTerm: termios) {
var term = originalTerm
tcsetattr(self.fileDescriptor, TCSADRAIN, &term)
}
}
let bufferForReadSize = 100
var bufferForRead: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: bufferForReadSize, alignment: 1)
//Give the user slightly bit more than 2 minutes so that the 2:00 countdown initial value can be seen
let endTime = Date().addingTimeInterval(TimeInterval(exactly: 120.5)!)
//struct for using aio_ calls
var aio: aiocb = aiocb(aio_fildes: FileHandle.standardInput.fileDescriptor,
aio_offset: 0,
aio_buf: bufferForRead,
aio_nbytes: bufferForReadSize,
aio_reqprio: 0,
aio_sigevent: sigevent(),
aio_lio_opcode: 0)
var userChoice: Bool?
print()
let originalTermios = FileHandle.standardInput.enableRawMode()
withUnsafeMutablePointer(to: &aio) {
while userChoice == nil {
let timeLeft = endTime.timeIntervalSince(Date())
print("\u{1B}[A" + //rewind to previous line +
"Hello, World? (y/n)" + timeLeft.stringFromTimeInterval())
let inputString = String(cString: bufferForRead.bindMemory(to: Int8.self, capacity: bufferForReadSize))
if inputString.starts(with: "y") || inputString.starts(with: "Y") {
userChoice = true
break
} else if inputString.starts(with: "n") || inputString.starts(with: "N") {
userChoice = false
break
}
if timeLeft <= 0 {
userChoice = true
break
} else {
//Async IO read
aio_read([=10=])
CFRunLoopRunInMode(CFRunLoopMode.defaultMode,
0.5, //choose the interval value depending on the fps you need
false)
//Async IO return
aio_return([=10=])
}
}
}
FileHandle.standardInput.restoreRawMode(originalTerm: originalTermios)
userChoice! ? print("Thanks for choosing YES. Bye") : print("Thanks for choosing NO. Bye")