1

I am creating a simple console-based game in Go. I want some way to accept unbuffered input (as in, you type in one key and it is immediately returned). I started out with this code:

func InitInput() {
  exec.Command("stty", "-f", "/dev/tty", "cbreak", "min", "1").Run()
  exec.Command("stty", "-f", "/dev/tty", "-echo").Run()
}
func StopInput() {
  exec.Command("stty", "-f", "/dev/tty", "echo").Run()
}
func GetInput() string {
  var b []byte = make([]byte, 1)
  for {
    os.Stdin.Read(b)
    return string(b)
  }
}

This was amazing, but it only works on a *nix-based os, and requires 3 functions. Next, someone recommended this code for me:

/*
// Works also for 64 bits
#ifdef _WIN32

// Lib for console management in windows
#include "conio.h"

#else

// Libs terminal management in Unix, Linux...
#include <stdio.h>
#include <unistd.h>
#include <termios.h>

// Implement reading a key pressed in terminal
char getch(){
    char ch = 0;
    struct termios old = {0};
    fflush(stdout);
    if( tcgetattr(0, &old) < 0 ) perror("tcsetattr()");
    old.c_lflag &= ~ICANON;
    old.c_lflag &= ~ECHO;
    old.c_cc[VMIN] = 1;
    old.c_cc[VTIME] = 0;
    if( tcsetattr(0, TCSANOW, &old) < 0 ) perror("tcsetattr ICANON");
    if( read(0, &ch,1) < 0 ) perror("read()");
    old.c_lflag |= ICANON;
    old.c_lflag |= ECHO;
    if(tcsetattr(0, TCSADRAIN, &old) < 0) perror("tcsetattr ~ICANON");
    return ch;
}
#endif
*/
import "C"

And then you only need 1 function:

func GetInput() string {
    return string(byte(C.getch()))
}

This works perfectly, except that because of the way that cgo works, it is very slow which is not ideal for a game. Also, the original code for testing for a newline, if extras.GetInput() == "\n" {} doesn't work anymore. Is there a way to get a single-character unbuffered input manager to work in Go without using a big, thick, external library?

kognise
  • 612
  • 1
  • 8
  • 23
  • 1
    You mean like https://github.com/nsf/termbox-go ? – icza Feb 16 '18 at 17:15
  • @icza That is an example of exactly what I *don't* want. It is a huge, bloated library, where I only want a single, lightweight input manager. – kognise Feb 16 '18 at 17:16
  • 2
    The go compiler does not include many stuff you don't use from an imported package, so I don't see it as a problem. – icza Feb 16 '18 at 17:18
  • 1
    And termbox-go is rather lightweight: _"Termbox is a library that provides a minimalistic API..."_ – icza Feb 16 '18 at 17:26
  • To be fair, a small *API* doesn't necessarily mean a small *library*. It just means that library doesn't *export* very much. – Adrian Feb 16 '18 at 17:37
  • I doubt that you really want unbuffered input for a game. I wouldn't want a program to ignore key presses while it's dealing ("thinking" if you will) with my last input. – Peter Feb 16 '18 at 17:38
  • @icza I'm pretty sure that `termbox-go` takes over the terminal screen. I don't want that. – kognise Feb 16 '18 at 17:39
  • @Peter Maybe I'm getting the terminology wrong. I want an input manager that will return input as soon as it is entered, without the user having to hit enter. – kognise Feb 16 '18 at 17:40
  • I had the same question: https://stackoverflow.com/questions/15159118/read-a-character-from-standard-input-in-go-without-pressing-enter – Kaveh Shahbazian Feb 17 '18 at 18:06
  • 1
    @KavehShahbazian Again I *do not* want `termbox-go`. Read [this](https://github.com/nsf/termbox-go/issues/162). – kognise Feb 19 '18 at 14:23

1 Answers1

0

I wrote some console handling from scratch sources, in C, Perl and PHP and was about to do one in Python.

Then I discovered Go and I'm asking the same questions. I agree with the author of the thread, we have to do the minimum. I haven't tried your code yet (The one which includes termio code).

There are many reasons why it may be slow:

1 / You make a C call every time you hit a key: You may change this topology: first, you place the console in non-buffering mode and return the saved parameters to Golang.

2/ You hit keys according to this code:

package main
import (
    "fmt"
    "os"
    "os/exec"
)

func main() {
    // 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()
    // restore the echoing state when exiting
    defer exec.Command("stty", "-F", "/dev/tty", "echo").Run()

    var b []byte = make([]byte, 1)
    LOOP:
    for {
        os.Stdin.Read(b)
        var n uint8 = b[0]
        switch n {
        case 27, 113, 81 : fmt.Println("ESC, or 'q' or 'Q' was hitted!")
            break LOOP
        default:
            fmt.Printf("You typed : %d\n", n)
        }
    }
}

Then at last you restore console settings.

In this code : I use stty but I suppose I could replace it with 2 C routine calls. Well, I need to try to implement this...

Also, a question: should we return a byte, a char a widechar? This can be more complicated for sure.

Notice that you have also extended keys on keyboard and must read forward one or more bytes to get the whole sequence, but this is not a problem.

OctaveL
  • 1,017
  • 8
  • 18