macOS / Linux terminal app - get single keyboard input

Hi everyone!

I'm working on a macOS Terminal / Linux app that needs to capture keyboard characters on a per key press basis. As its a command line app, I can't rely on NSWindows or NSEvent.

I found the getchar() c function that almost does what I need: it captures character presses, but only after pressing enter:

var c: Int32 = 0
while true {
    c = getchar()
    print("You entered: \(c)")
}

// output: 
// a                (+Enter)
// You entered: 97

Apparently this is default getchar() behavior.
Is there any built in function in Foundation or elsewhere that can capture a key press without needing to press enter?

I'm no ANSI terminal expert myself, but it appears to me, like you want to read directly from stdin. There is probably some pipe available in Swift somewhere, but I found this article 2. Entering raw mode | Build Your Own Text Editor in C and I assume all the APIs are available in Swift.

If your use-case is some kind of TUI (terminal user interface), I would probably look for some good ncurses (libncurses) Swift wrappers.

Yup, it sounds like you're basically looking for raw mode.
Erica Sadun wrote an article that illustrates this. I haven't tried this myself as the last time I needed it was when I wrote terrible, terrible C code for a uni project...

1 Like

There's a Swift package called Linenoise that provides lightweight readline style functionality, also my not very well tested fork which handles UTF-8 better. These rely on reading raw terminal input as it arrives. By default getchar() buffers input until it reads a line ending.

The function Terminal.withRawMode() is the example code you need to put stdin into raw mode (and take it out again).

Took this approach. Doesn't work for (high) unicode characters such as emoji's, but good enough for now!

Apologies. I pointed you at the original code. My version can read UTF-8. Look at LNIO.swift

I wrote it to support a REPL for the Lambda Calculus when I found that the original version couldn't handle a Ξ» character. I've just checked: it is fine with emoji too.

1 Like

Note that emoji support in terminal is not complete. For example when I paste a family of four character :family_man_woman_girl_boy: it is entered as πŸ‘¨<200d>πŸ‘©<200d>πŸ‘§<200d>πŸ‘¦ and when I paste dark fist :fist:t5: character it is entered as two characters, when I delete the second one the first is converted into :fist:. Handling unicode properly is a tricky business!

Well I'm pleased to report that my code is better than terminal then!

>  (Ξ»x.x) πŸ‘¨β€πŸ‘¨β€πŸ‘§β€πŸ‘¦
1 reductions in 7.928e-06 = 126135.21695257317 reductions per second
: πŸ‘¨β€πŸ‘¨β€πŸ‘§β€πŸ‘¦

I'm not telling the whole truth though: After typing in the expression at the top, there were three spaces after the emoji and before the cursor. The library is erroneously assuming that each UTF-8 code point is exactly one Character/glyph.

I’ll give it a go! Thanks :pray: