Search code examples
swiftswift4stdin

Listening to stdin in 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.


Solution

  • 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