5

I've got a loop in which some things happen according to the state it runs in (manual/automatic/learning). I now want to be able to let the program switch between these states by pressing the accompanying letters on the keyboard ("m" for manual, "a" for automatic and "l" for learning).

So to do this I need to be able to catch a keypress during the loop and change the variable status accordingly. I now have the following, which can catch a keypress followed by an enter:

ch := make(chan string)
go func(ch chan string) {
    reader := bufio.NewReader(os.Stdin)
    for {
        s, _ := reader.ReadString('\n')
        ch <- s
    }
}(ch)

for {
    select {
        case stdin, _ := <-ch:
            fmt.Println("Keys pressed:", stdin)
        default:
            fmt.Println("Working..")
    }
    time.Sleep(time.Second)
}

But the fact that I need to hit the enter button is not acceptable.

Does anybody know a non-blocking way to catch a keypress of a normal letter (not a SIGINT) without the need to hit enter afterwards?

E_net4
  • 27,810
  • 13
  • 101
  • 139
kramer65
  • 50,427
  • 120
  • 308
  • 488
  • 2
    You could just use `os.Stdin.Read()` to read bytes off the wire. Your code explicitly wraps it and goes around it in order to buffer up to the next time the user presses enter. – Adrian Jan 29 '19 at 13:55

3 Answers3

5

After reading about os.Stdin.Read() and finding this answer I created the following code:

package main

import (
    "fmt"
    "os"
    "time"
    "os/exec"
)

func main() {
    ch := make(chan string)
    go func(ch chan string) {
        // disable input buffering
        exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
        // do not display entered characters on the screen
        exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
        var b []byte = make([]byte, 1)
        for {
            os.Stdin.Read(b)
            ch <- string(b)
        }
    }(ch)

    for {
        select {
            case stdin, _ := <-ch:
                fmt.Println("Keys pressed:", stdin)
            default:
                fmt.Println("Working..")
        }
        time.Sleep(time.Millisecond * 100)
    }
}

This works like a charm.

halfer
  • 19,824
  • 17
  • 99
  • 186
kramer65
  • 50,427
  • 120
  • 308
  • 488
  • Depends on an external utility, won't work on Windows for sure. But thanks for the code. – PePa Apr 11 '21 at 04:52
1

If you want to check if some key was pressed without blocking the process where it is in, you should use something like this:

import (
   ...
   "golang.org/x/sys/windows"
)

var user32_dll  = windows.NewLazyDLL("user32.dll")
var GetKeyState = user32_dll.NewProc("GetKeyState")

func wasESCKeyPressed() bool {
    r1, _, _ := GetKeyState.Call(27) // Call API to get ESC key state.
    return r1 == 65409               // Code for KEY_UP event of ESC key.
}

func loop() {
    for {
       // Do something...
       if wasESCKeyPressed() {
           break
       }
       // Do something...
       time.Sleep(time.Millisecond * 10)
    }
}
0

Because you're using ReadString which expects whichever parameter you give it, in your case - the return key. According to the docs:

ReadString reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter.

This means that the method won't return until you hit the return key.

You can use the regular Read method instead, to read the characters you need. See also this Stackoverflow question for reference.

noamt
  • 7,397
  • 2
  • 37
  • 59