piotrmurach/tty

Problems with detecting a keypress

janko opened this issue · 7 comments

Hello Peter,

First of all, thank you for creating this very nice gem, I really like how it will be this ultimate Terminal gem.

I'm making a console application, and I want to detect a keypress (more specifically, arrow keys). What I want is that when the user presses an arrow key, that something happens.

So I was trying things out, but they don't work as I expected. I tried the following code:

require "tty"

shell = TTY::Shell.new
p shell.ask("").echo(false).read_char

I expected it to return after I type a single character, but it waited for me to press "Enter". I also expected that the input won't be displayed at any point, but it was displayed after I pressed "Enter". Concretely, when I typed foo<Enter>, I got the following output:

foo"f"

So, my input has been displayed + the first character inspected. I'm aware that detecting arrow keys will probably be a bit trickier, because they're two characters (e.g. ^[[B), but I first have to figure out how to read letters without "Enter" needing to be pressed.

I saw on StackOverflow that the raw setting needs to be enabled for "character input":

system "stty raw"
# do some work
system "stty -raw"

I just found out that there is IO.raw, which both turns off the echo and does the "character" input. It also resets it back at the end of the block.

require "io/console"

IO.raw do |stdin|
  p stdin.getc
end

I discovered another thing, IO#readpartial, which perfectly solved my problem.

require "io/console"

loop do
  keypress = STDIN.raw { |stdin| stdin.readpartial(3) }

  case keypress
  when "\e[A" then puts "Up"
  when "\e[B" then puts "Down"
  when "\e[C" then puts "Right"
  when "\e[D" then puts "Left"
  when "\x03" # Ctrl-C
    raise Interrupt
  when "\x04" # Ctrl-D
    exit
  end
end

IO#readpartial(n) reads at most n characters from the IO, and sees a "keypress" as a single input, which was exactly what I wanted. Maybe this is what you want to use in your gem, a "keypress" instead of a "character", because it's much more powerful (each "character" is a "keypress" as well).

Hi Janko,

Great that you are giving this lib a go! Before I talk about this issue, my goal is for this library to become modular so that individual components can become much more robust and basically stand on their own merits. I realised I was short sighted trying to lump everything into one big blob. I have extracted few components such as pastel that I think got significantly better and got major issues fixed.

Regarding terminal queries and user input, I'm gonna extract that to separate component that going forward will allow for asking robust questions such as choose an option from menu, yes/no and basically nice way to gather input. I will work on it this coming weekend.

Usage of 'io/console' is good, however I found that for instance the JRuby doesn't support it. But obviously that's not a show stopper. I'd like to work with you on getting the interface to work intuitively. I'm open to suggestions and of course PRs. Agree that `shell.ask('...').read_keypress would better explain the intention. We could also provide some callback system that would yield on each key.

Any thoughts?

Best

Piotr

Yeah, it's great that you started separating, now people can just use gems like pastel standalone (which probably has a frequent use case; I needed once just the coloring, for a custom logger).

That's great, I would like a separate #read_keypress, and then #read_char doesn't need changing, because it actually does what it's supposed to. I don't think that #read_keypress needs a callback (because it's blocking), it will just return when the user presses a key (returning that key). And if people want to wait for more keypresses, they could just put it in a loop, right?

One more thing that I wanted to ask, what if the key pressed is Ctrl-C? Because inside of STDIN.raw Ctrl-C won't terminate the process, it will just be treated as a regular keypress. My intuition says that we should just leave it to the programmer to rescue that case, since maybe he would like to handle Ctrl-C differently? But then again, if we raise an Interrupt error (it's what Ruby raises on Ctrl-C), then the user can also just rescue it. What do you think we should do?

Agree with your point of separating the two methods. Also, agree that blocking io doesn't need callbacks. I suppose I was thinking of more general approach, for instance, allow users to subscribe to keepress to accomplish things like autocompletion, but that's besides this point.

From user point of view I imagine that Ctrl-C should bum you out of the client, that would be at least my expectation. Thus I wouldn't do anything special but stay consistent and throw an error and let developer handle that. Also, I would probably be tempted and make this behaviour the default for all the other methods. Thoughts?

All other methods will already throw an error on Ctrl-C, it's just when you enable the "raw" method with IO#raw that this gets disabled. Ok, enough philosophing, I will work on the PR now 😃

Hi @janko-m, just to let you know that I've released v0.1.2 version with your code. The release includes necromancer that helps parsing user input and will help with extraction of shell component later on. In future people will be able to use just the shell interface without other dependencies but I need to extract a bit more. I will keep your posted as I would like your input/opinion. Btw, I like your tic-tac-toe curses usage.