Listening to stdin in Swift - swift

Currently I am trying to listen to user input from the command line in my swift application.
I am aware of the readLine() method but it does not really fit my needs. I want to listen for data being inserted on the command line. Like when a user is pressing the ‘up key’ inside the terminal.
Something like what can be done in Node.js:
stdin.on( 'data', function( key ){
if (key === '\u0003' ) {
process.exit();
} // write the key to stdout all normal like
process.stdout.write( key );
});
I tried searching but I couldn’t find an equivalent to this in Swift. I thought maybe something with ‘Inputstream’ but didn’t a find a appropriate solution either.
If someone could give me some hints on how to do something like this in Swift I would highly appreciate it.

Normally standard input buffers everything until a newline is entered, that's why a typical standard input is read by lines:
while let line = readLine() {
print(line)
}
(press CTRL+D to send EOF, that is end the input)
To really read every character separately, you need to enter raw mode and that means use the low level terminal functions:
// see https://stackoverflow.com/a/24335355/669586
func initStruct<S>() -> S {
let struct_pointer = UnsafeMutablePointer<S>.allocate(capacity: 1)
let struct_memory = struct_pointer.pointee
struct_pointer.deallocate()
return struct_memory
}
func enableRawMode(fileHandle: FileHandle) -> termios {
var raw: termios = initStruct()
tcgetattr(fileHandle.fileDescriptor, &raw)
let original = raw
raw.c_lflag &= ~(UInt(ECHO | ICANON))
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &raw);
return original
}
func restoreRawMode(fileHandle: FileHandle, originalTerm: termios) {
var term = originalTerm
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &term);
}
let stdIn = FileHandle.standardInput
let originalTerm = enableRawMode(fileHandle: stdIn)
var char: UInt8 = 0
while read(stdIn.fileDescriptor, &char, 1) == 1 {
if char == 0x04 { // detect EOF (Ctrl+D)
break
}
print(char)
}
// It would be also nice to disable raw input when exiting the app.
restoreRawMode(fileHandle: stdIn, originalTerm: originalTerm)
Reference https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html

You probably want FileHandle.standardInput.
Something like:
let file = FileHandle.standardInput
while true {
let data = file.availableData
print("\(String(bytes: data, encoding: .utf8))")
}
will echo out input the way I think you want it. Standard disclaimers about being careful with input and that this is probably a dangerous activity, sanitise your inputs and so on.
I'm not exactly sure how you'd go about matching specific control and arrow keys, but this is a start.

Related

getch() equivalent in Swift: read a single character from stdin without a newline

I'm looking for a Swift function like getch() from C to read a single character from terminal input without requiring the user to press the return key. getchar() and readLine() are not sufficient, as they both require return.
There's a getch() function from ncurses which looked promising, but unfortunately seems to require taking over the display of the whole window.
After searching for a while online, I landed on the following (partly based on this answer):
import Foundation
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)
}
}
func getch() -> UInt8 {
let handle = FileHandle.standardInput
let term = handle.enableRawMode()
defer { handle.restoreRawMode(originalTerm: term) }
var byte: UInt8 = 0
read(handle.fileDescriptor, &byte, 1)
return byte
}
fputs("Press any key to continue... ", stdout)
fflush(stdout)
let x = getch()
print()
print("Got character: \(UnicodeScalar(x))")

How to print a content of the CharacterSet.decimalDigits?

I tried to print a content of the CharacterSet.decimalDigits with:
print(CharacterSet.decimalDigits)
output: CFCharacterSet Predefined DecimalDigit Set
But my expectation was something like this:
[1, 2, 3, 4 ...]
So my question is: How to print content of the CharacterSet.decimalDigits?
This is not easy. Character sets are not made to be iterated, they are made to check whether a character is inside them or not. They don't contain the characters themselves and the ranges cannot be accessed.
The only thing you can do is to iterate over all characters and check every one of them against the character set, e.g.:
let set = CharacterSet.decimalDigits
let allCharacters = UInt32.min ... UInt32.max
allCharacters
.lazy
.compactMap { UnicodeScalar($0) }
.filter { set.contains($0) }
.map { String($0) }
.forEach { print($0) }
However, note that such a thing takes significant time and shouldn't be used inside a production application.
I don't think you can to that, at least not directly. If you look at the output of
let data = CharacterSet.decimalDigits.bitmapRepresentation
for byte in data {
print(String(format: "%02x", byte))
}
you'll see that the set internally stores bits at the code positions where the decimal digits are.

Convert Swift String to wchar_t

For context: I'm trying to use the very handy LibXL. I've used it with success in Obj-C and C++ but am now trying to port over to Swift. In order to better support Unicode, I need to sent all strings to the LibXL api as wchar_t*.
So, for this purpose I've cobbled together this code:
extension String {
///Function to convert a String into a wchar_t buffer.
///Don't forget to free the buffer!
var wideChar: UnsafeMutablePointer<wchar_t>? {
get {
guard let _cString = self.cString(using: .utf16) else {
return nil
}
let buffer = UnsafeMutablePointer<wchar_t>.allocate(capacity: _cString.count)
memcpy(buffer, _cString, _cString.count)
return buffer
}
}
The calls to LibXL appear to be working (getting a print of the error messages returns 'Ok'). Except when I try to actually write to a cell in a test spreadsheet. I get can't write row 0 in trial version:
if let name = "John Doe".wideChar, let passKey = "mac-f.....lots of characters...3".wideChar {
xlBookSetKeyW(book, name, passKey)
print(">: " + String.init(cString: xlBookErrorMessageW(book)))
}
if let sheetName = "Output".wideChar, let path = savePath.wideChar, let test = "Hello".wideChar {
let sheet: SheetHandle = xlBookAddSheetW(book, sheetName, nil)
xlSheetWriteStrW(sheet, 0, 0, test, sectionTitleFormat)
print(">: " + String.init(cString: xlBookErrorMessageW(book)))
let success = xlBookSaveW(book, path)
dump(success)
print(">: " + String.init(cString: xlBookErrorMessageW(book)))
}
I'm presuming that my code for converting to wchar_t* is incorrect. Can someone point me in the right direction for that..?
ADDENDUM: Thanks to #MartinR for the answer. It appears that the block 'consumes' any pointers that are used in it. So, for example, when writing a string using
("Hello".withWideChars({ wCharacters in
xlSheetWriteStrW(newSheet, destRow, destColumn, wCharacters, aFormatHandle)
})
The aFormatHandle will become invalid after the writeStr line executes and isn't re-useable. It's necessary to create a new FormatHandle for each write command.
There are different problems here. First, String.cString(using:) does
not work well with multi-byte encodings:
print("ABC".cString(using: .utf16)!)
// [65, 0] ???
Second, wchar_t contains UTF-32 code points, not UTF-16.
Finally, in
let buffer = UnsafeMutablePointer<wchar_t>.allocate(capacity: _cString.count)
memcpy(buffer, _cString, _cString.count)
the allocation size does not include the trailing null character,
and the copy copies _cString.count bytes, not characters.
All that can be fixed, but I would suggest a different API
(similar to the String.withCString(_:) method):
extension String {
/// Calls the given closure with a pointer to the contents of the string,
/// represented as a null-terminated wchar_t array.
func withWideChars<Result>(_ body: (UnsafePointer<wchar_t>) -> Result) -> Result {
let u32 = self.unicodeScalars.map { wchar_t(bitPattern: $0.value) } + [0]
return u32.withUnsafeBufferPointer { body($0.baseAddress!) }
}
}
which can then be used like
let name = "John Doe"
let passKey = "secret"
name.withWideChars { wname in
passKey.withWideChars { wpass in
xlBookSetKeyW(book, wname, wpass)
}
}
and the clean-up is automatic.

How to read Unicode codepoints from an unbuffered file in a mio event loop?

I'd like to use the mio crate to read keypresses as they arrive in an unbuffered fashion. I already have the code to unbuffer stdin, and I have scaffolded the event loop:
extern crate mio;
extern crate termios;
use termios::{Termios, TCSANOW, ICANON, ECHO, tcsetattr};
use mio::*;
use mio::unix::EventedFd;
fn unbuffer_stdin() {
let termios = Termios::from_fd(0).unwrap();
let mut new_termios = termios.clone();
new_termios.c_lflag &= !(ICANON | ECHO);
tcsetattr(0, TCSANOW, &mut new_termios).unwrap();
}
fn main() {
let stdin = 0;
unbuffer_stdin();
let poll = Poll::new().unwrap();
const STDIN: Token = Token(0);
let ev_fd = EventedFd(&stdin);
poll.register(&ev_fd, STDIN, Ready::readable(), PollOpt::edge()).unwrap();
let mut events = Events::with_capacity(1024);
loop {
poll.poll(&mut events, None).unwrap();
for event in events.iter() {
match event.token() {
STDIN => {
println!("keypress");
// XXX read in ready codepoints to a buffer
}
_ => unreachable!(),
}
}
}
}
How do I implement the part marked XXX? There are a couple of challenges:
How do I know how many bytes to read? I'm not sure mio tells me this.
How do I deal with partial codepoints, where the read spans a multi-byte character.
One solution that might work is to use oneshot events instead of edge events, then read one byte per event into a temporary buffer. Each time the buffer makes sense as a complete codepoint, I could then convert it to a char and store it away, and clear the scratch buffer.
This seems a little inefficient though. What is the best way?

How to get multiple lines of stdin Swift HackerRank?

I just tried out a HackerRank challenge, and if a question gives you x lines of input, putting x lines of let someVariable = readLine() simply doesn't cut it, because there are lot's of test cases that shoot way more input to the code we write, so hard coded readLine() for each line of input won't fly.
Is there some way to get multiple lines of input into one variable?
For anyone else out there who's trying a HackerRank challenge for the first time, you might need to know a couple of things that you may have never come across. I only recently learned about this piece of magic called the readLine() command, which is a native function in Swift.
When the HackerRank system executes your code, it passes your code lines of input and this is a way of retrieving that input.
let line1 = readLine()
let line2 = readLine()
let line3 = readLine()
line1 is now given the value of the first line of input mentioned in the question (or delivered to your code by one of the test cases), with line2 being the second and so on.
Your code may work just great but may fail on a bunch of other test cases. These test cases don't send your code the same number of lines of input. Here's food for thought:
var string = ""
while let thing = readLine() {
string += thing + " "
}
print(string)
Now the string variable contains all the input there was to receive (as a String, in this case).
Hope that helps someone
:)
Definitely you shouldn't do this:
while let readString = readLine() {
s += readString
}
This because Swift will expect an input string (from readLine) forever and will never terminate, causing your application die by timeout.
Instead you should think in a for loop assuming you know how many lines you need to read, which is usually this way in HackerRank ;)
Try something like this:
let n = Int(readLine()!)! // Number of test cases
for _ in 1 ... n { // Loop from 1 to n
let line = readLine()! // Read a single line
// do something with input
}
If you know that each line is an integer, you can use this:
let line = Int(readLine()!)!
Or if you know each line is an array of integers, use this:
let line = readLine()!.characters.split(" ").map{ Int(String($0))! }
Or if each line is an array of strings:
let line = readLine()!.characters.split(" ").map{ String($0) }
I hope this helps.
For new version, to get an array of numbers separated by space
let numbers = readLine()!.components(separatedBy: [" "]).map { Int($0)! }
Using readLine() and AnyGenerator to construct a String array of the std input lines
readLine() will read from standard input line-by-line until EOF is hit, whereafter it returns nil.
Returns Characters read from standard input through the end of the
current line or until EOF is reached, or nil if EOF has already been
reached.
This is quite neat, as it makes readLine() a perfect candidate for generating a sequence using the AnyGenerator initializer init(body:) which recursively (as next()) invokes body, terminating in case body equals nil.
AnyGenerator
init(body: () -> Element?)
Create a GeneratorType instance whose next method invokes body
and returns the result.
With this, there's no need to actually supply the amount of lines we expect from standard input, and hence, we can catch all input from standard input e.g. into a String array, where each element corresponds to an input line:
let allLines = AnyGenerator { readLine() }.map{ $0 }
// type: Array<String>
After which we can work with the String array to apply whatever operations needed to solve a given task (/HackerRank task).
// example standard input
4 3
<tag1 value = "HelloWorld">
<tag2 name = "Name1">
</tag2>
</tag1>
tag1.tag2~name
tag1~name
tag1~value
/* resulting allLines array:
["4 3", "<tag1 value = \"HelloWorld\">",
"<tag2 name = \"Name1\">",
"</tag2>",
"</tag1>",
"tag1.tag2~name",
"tag1~name",
"tag1~value"] */
I recently discovered a neat trick to get a certain amount of lines. I'm gonna assume the first line gives you the amount of lines you get:
guard let count = readLine().flatMap({ Int($0) }) else { fatalError("No count") }
let lines = AnyGenerator{ readLine() }.prefix(count)
for line in lines {
}
I usually use this form.
if let line = readLine(), let cnt = Int(line) {
for _ in 1...cnt {
if let line = readLine() {
// your code for a line
}
}
}
Following the answer from dfrib, for Swift 3+, AnyIterator can be used instead of AnyGenerator, in the same way:
let allLines = AnyIterator { readLine() }.map{ $0 }
// type: Array<String>