Getch() Equivalent in Swift: Read a Single Character from Stdin Without a Newline

Xcode Swift Command Line Tool reads 1 char from keyboard without echo or need to press return

Here is the function for use with Swift, written in Swift:

func GetKeyPress () -> Int
{
var key: Int = 0
var c: cc_t = 0
var cct = (c, c, c, c, c, c, c, c, c, c, c, c, c, c, c, c, c, c, c, c) // Set of 20 Special Characters
var oldt: termios = termios(c_iflag: 0, c_oflag: 0, c_cflag: 0, c_lflag: 0, c_cc: cct, c_ispeed: 0, c_ospeed: 0)

tcgetattr(STDIN_FILENO, &oldt) // 1473
var newt = oldt
newt.c_lflag = 1217 // Reset ICANON and Echo off
tcsetattr( STDIN_FILENO, TCSANOW, &newt)
key = Int(getchar()) // works like "getch()"
tcsetattr( STDIN_FILENO, TCSANOW, &oldt)
return key
}

Listening to stdin in Swift

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

Swift: readLine with timeout

Not the prettiest Swift code, but it does the job. aio (asynchronous input output) low level system API in raw terminal mode https://stackoverflow.com/a/59795707/5329717 is used to read user input without pressing enter.

We're doing multiple reads using aio_read (paired by following aio_return) because the user may be inputting keys we don't want.

Since Xcode debug console isn't a standard console please run this in a standalone terminal.

The only caveat I've run into with this code is in the scenario when time runs out aio_read sets terminal standard input into still expecting user input (e.g. enter key for the shell to appear again). I'll be trying to circumvent this problem.

import Foundation

// https://stackoverflow.com/a/59000106/5329717
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)
}
}
}

// https://stackoverflow.com/a/59795707/5329717
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($0)
CFRunLoopRunInMode(CFRunLoopMode.defaultMode,
0.5, //choose the interval value depending on the fps you need
false)
//Async IO return
aio_return($0)
}
}
}
FileHandle.standardInput.restoreRawMode(originalTerm: originalTermios)
userChoice! ? print("Thanks for choosing YES. Bye") : print("Thanks for choosing NO. Bye")

Read a character input from Erlang without requiring the Return key pressed from the prompt

Strap yourself in quantumpotato, you are about to enter a world of pain you never knew existed.


A responsible programmer would tell you that there is no way to do this, certainly not a portable one.

Now, the phrase "not portable" has been diluted by pedantic fever dreams like CHAR_BIT != 8 to subtly persuade everyone that 'not portable' means either "some LSD-addled system built in the late 60s can't run this" or "Won't run on Windows".

No, what you're about to encounter is the sort of portability problem which caused our ancestors to volunteer to use autotools. Really, truly, not portable across different, even POSIX "compliant", systems.

As you might imagine, the sane way to do this is to rely on something like Ncurses, but as is true of so many of the necessities of life, no non-NIF implementation exists for Erlang.

In spite of all these warnings I just gave you, here's what I have done in a pinch in the past:

-module(thing).
-compile(export_all).

get_char() ->
Ch = io:get_chars("p: ", 1),
io:fwrite("Ch: ~w", [Ch]).

start() ->
io:setopts([{binary, true}]),
get_char().

Alone, this won't work of course. So you need to invoke your script from another that set's the appropriate termios input flags. As I mentioned earlier, this has been a real crapshoot in the past and I haven't heard anything that would lead me to believe the situation has changed.

#!/bin/sh
stty --f /dev/tty icanon raw
erl -pa ./ -run thing start -run init stop -noshell
stty echo echok icanon -raw

It is possible to invoke "stty" or send magic chars from within an Erlang program as well, but if memory serves you need to invoke erl with -noshell and add additional stty incantations like time and echo 0.

Another problem I've experienced in the past is that while you can use get_chars safely, other io functions will fail because some of them have their own built-in 'line cooking'.

If all of this hasn't yet persuaded you that computers have been a disaster for the human race, you can read up more about the details of this in the excellent book The Linux Programming Interface.

Good luck.

Non-blocking getch(), ncurses

The curses library is a package deal. You can't just pull out one routine and hope for the best without properly initializing the library. Here's a code that correctly blocks on getch():

#include <curses.h>

int main(void) {
initscr();
timeout(-1);
int c = getch();
endwin();
printf ("%d %c\n", c, c);
return 0;
}


Related Topics



Leave a reply



Submit