Timed wait for key press

I am new to Julia but I can’t seem to find out how to do this…
I want a function that allows an input of number of seconds to wait for a key to be pressed, if zero it returns the next key pressed in the buffer. Then it returns info as to what key was pressed, ASCII is ok but the Ins (Insert) key returns 27 + something else. The Esc (Escape) key returns just 27. The function keys don’t return ASCII but something else. This would be similar to inkey() in FoxPlus (if anybody knows what that is) which doesn’t work quite the way I want it to, either. The return can be 1-127 for ASCII and higher numbers for the rest of the keys. I am not looking for just the Shift, Ctrl, or Alt keys by themselves, but I want the up-arrow, down-arrow, Home, PgUp, etc. to return info as to which one was pressed. It would be nice if I could clear the type ahead buffer when I want to but I can’t find a command that does that. Maybe I just don’t know where to look.

1 Like

This isn’t so much a language thing as it is an operating system / graphics framework sort of thing.

What sort of context do you want this to work/run in?

1 Like

This is to be a terminal type interface, wasn’t even thinking of mouse interaction, using Linux as the server. I hadn’t even looked at a graphics interface. The programs i’m trying to convert are written in FoxPlus, totally text, no graphics.

1 Like

I think I have a rough idea of what you’re talking about, I’m on my phone but can give a more full answer tomorrow. But here is my rough idea:

println("Please enter a key")

if bytesavailable(stdin) > 0 # if there are any bytes, there has been terminal input
    while bytesavailable(stdin) > 0
        c = read(stdin, Char)
        # different keys like home or arrow keys are mapped to different sequences of characters
    println("No input") 

This code likely doesn’t work as I haven’t tested it, but this is my rough idea.

In order to determine which key has been hit, you would need to test each key and see what character sequence it corresponds to.

Not sure it’s much help, I’ll try to revisit this and give a more thorough answer when I’m at my computer.

Yes, I just tried it out over an ssh connection and it’s not behaving like I thought. If this is still unsolved tomorrow I’ll try to provide a better answer.

It is certainly possible though; this is done in the package VimBindings.jl. As you say, the Escape key sends a special character, whereas (for example) an arrow key sends that same special character followed by a character sequence. In order to tell whether the key was just an Escape key press or another key, the terminal must wait to see if any new data comes in.

c = read(stdin, Char)
doesn’t like BackSpace

Right, that’s exactly why I talked about operating systems and graphical toolkits. Command line interfaces (regardless of the language) only get the text that the controlling terminal sends them. This can sometimes include some control chars like backspace, but it’s very configuration-dependent in the terminal itself. If you need to get the raw keypresses, you typically need to have a framework that talks more directly with the operating system, like a graphics framework or entire windowing system or web frontend or somesuch.

I’ve been able to sit down at my machine and figure this out, I think.

Perhaps surprisingly, this actually doesn’t relate to graphical toolkits (although it is somewhat related to operating systems and the way an OS terminal handles characters).

This is not the case; programs which are run at the terminal are able to access all bytes sent to stdin by setting the terminal to “raw mode”, also called “non-canonical mode”. In this mode, the terminal sends input byte-by-byte to the program, with no buffering or processing of keys or control sequences. Even the Julia REPL runs in raw mode, and the REPL code processes and handles control sequences byte for byte.

Still, that doesn’t solve the ability to read the raw terminal input directly from stdin. While julia code can call read(stdin, Char), it seems to only be able to do with line-by-line buffering, effectively in “canonical” mode (also called “cooked” mode). But the code below uses the utility function with_raw_tty to set the input to raw mode, and read raw bytes directly from the terminal, adapted from the function getpass in base/utils.jl

Just as a warning, this bypasses all of the processing and safety measures Julia offers, so Ctrl-c does not work! You are reading directly from the terminal input. In this example, you can escape the loop with the Enter key.

function raw_input_example()
    # based on the function getpass in base/utils.jl#343
    Base.with_raw_tty(stdin) do
        while true
            c = Base._getch() # get raw character. ONLY works in with_raw_tty
            print("Byte: $c\r\n")
            print("Char: " * escape_string(string(Char(c))) * "\r\n\n")
            # use Enter to escape this loop
            if c == 0xff || c == UInt8('\n') || c == UInt8('\r') || c == 0x04
                break # EOF or return

And here is the output when I strike the following keys on my keyboard: a b c <Backspace> <Up Arrow> <Escape> <Enter>, with annotated comments. Note that this successfully records the <Backspace> key strike. Also note the full control sequence with the <Up Arrow> key strike.

julia> raw_input_example()
Byte: 97
Char: a

Byte: 98
Char: b

Byte: 99
Char: c

Byte: 127 # Backspace
Char: \x7f

Byte: 27 # Up Arrow
Char: \e

Byte: 91
Char: [

Byte: 65
Char: A

Byte: 27 # Escape
Char: \e

Byte: 13 # Enter
Char: \r

Ah yes, I had forgotten about raw mode! That won’t get the fine-grained keydown/keyup events or the modifier keys by themselves as I understand it, though. Not sure if that’s a requirement here, but that’s why my head went to UI frameworks.

1 Like

Ah yes definitely - not something you could write a more modern UI application with for sure. And you are right, no modifier key events, unlike something like an X11 application.

1 Like

Based on the information you gave me last time I wrote a function that works like a little bit like inkey() in FoxPlus except it returns strings instead of numbers. If you call it with inkey(“”) it checks for a key pressed one time and returns with “” (empty string), if no key has been pressed. If a letter key (32-127) was pressed, it returns with that letter. If the tab key was pressed it returns with “Tab”, likewise for “Ctrl-A” - “Ctrl-Z” except for “BackSpace” and “Enter”, function keys are returned with “F1” to “F12”, likewise for “Ins” “Del” “Home” “End” “PgUp” “PgDn” “UpArrow” “DnArrow” “LeftArrow” “RightArrow” If called with inkey(“60”) it will only wait for a key for 60 seconds, if called with inkey(“0”) it will wait for ever. The text file is 192 lines long and using include(inkey_function.jl") allowed me to fix typos. Now I’m trying to figure out how to get it into REPL to try it out (I am new to julia)

1 Like

How come c = read(stdin, Char) quit working? It didn’t used to require Enter… Even closed REPL an reloaded it to n avail… by the way, Caleb-Allen, your routine doesn’t differentiate between “A” and “F1” or “Home” (excuse me, it does work, I was only seeing the last byte) apparently bytesavailable doesn’t work. I have it sleep for a second and then try bytesavailable(stdin) > 0 and it fails

Using your info, Caleb, and the info at

I was able to write the function inkey() the way I want it to work. Somebody else might like this function, where do I put it? it’s 186 lines.

1 Like

I’m glad to hear you got it working!

You can place the code directly in this thread and putting three tic marks (```) before and after it to indicate that it is a code snippet, that way it will be scrollable.

You could also upload it to https://gist.github.com/ if you have a github account, and get a sharable link.

function inkey(s)
    requires   import REPL   before using
    This is for a single key stroke and returns what key it was
    based on a terminal setting in PuTTY of Ctrl-H for BackSpace and
    Linux function keys
    s is a string
         if null, checks bytesavailable one time
         if parse(Int, s) == 0 , has no time out
         otherwise, time out after parse(Int, s) seconds
    key pressed is returned as:
         Key Char
         number                   return
         ------                   ------
         1 - 7                    Ctrl-A to Ctrl-G
         8                        BackSpace (Ctrl-H)
         9                        Tab (Ctrl-I)
         13                       Enter (Ctrl-M)
         11 - 26                  Ctrl-K to   Ctrl-Z
         0x1b                     Esc
         0x1b 0x5b 0x31 0x31 0x7e F1
         0x1b 0x5b 0x31 0x32 0x7e F2
         0x1b 0x5b 0x31 0x33 0x7e F3
         0x1b 0x5b 0x31 0x34 0x7e F4
         0x1b 0x5b 0x31 0x35 0x7e F5
         0x1b 0x5b 0x31 0x37 0x7e F6
         0x1b 0x5b 0x31 0x38 0x7e F7
         0x1b 0x5b 0x31 0x39 0x7e F8
         0x1b 0x5b 0x32 0x30 0x7e F9
         0x1b 0x5b 0x32 0x31 0x7e F10
         0x1b 0x5b 0x32 0x33 0x7e F11
         0x1b 0x5b 0x32 0x34 0x7e F12
         0x1b 0x5b 0x32 0x7e      Ins
         0x1b 0x5b 0x33 0x7e      Del
         0x1b 0x5b 0x31 0x7e      Home
         0x1b 0x5b 0x34 0x7e      End
         0x1b 0x5b 0x35 0x7e      PgUp
         0x1b 0x5b 0x36 0x7e      PgDn
         0x1b 0x5b 0x41           UpArrow
         0x1b 0x5b 0x42           DnArrow
         0x1b 0x5b 0x43           RightArrow
         0x1b 0x5b 0x44           LeftArrow
    seconds = -1
    term = REPL.Terminals.TTYTerminal("xterm",stdin,stdout,stderr)
    while true
        ba = bytesavailable(stdin)
        if ba > 0
            str = read(stdin, ba)
            if str[1] == 0x1b
                # We have an Escape
                if length(str) == 1
                   return "ESC"
                if length(str) > 4
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x31 &&
                                         str[5] == 0x7e
                        return "F1"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x32 &&
                                         str[5] == 0x7e
                        return "F2"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x33 &&
                                         str[5] == 0x7e
                        return "F3"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x34 &&
                                         str[5] == 0x7e
                        return "F4"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x35 &&
                                         str[5] == 0x7e
                        return "F5"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x37 &&
                                         str[5] == 0x7e
                        return "F6"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x38 &&
                                         str[5] == 0x7e
                        return "F7"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x39 &&
                                         str[5] == 0x7e
                        return "F8"
                    if str[2] == 0x5b && str[3] == 0x32 && str[4] == 0x30 &&
                                         str[5] == 0x7e
                        return "F9"
                    if str[2] == 0x5b && str[3] == 0x32 && str[4] == 0x31 &&
                                         str[5] == 0x7e
                        return "F10"
                    if str[2] == 0x5b && str[3] == 0x32 && str[4] == 0x33 &&
                                         str[5] == 0x7e
                        return "F11"
                    if str[2] == 0x5b && str[3] == 0x32 && str[4] == 0x34 &&
                                         str[5] == 0x7e
                        return "F12"
                    return ""
                end # if length(str) > 4
                if length(str) > 3
                    if str[2] == 0x5b && str[3] == 0x32 && str[4] == 0x7e
                       return "Ins"
                    if str[2] == 0x5b && str[3] == 0x33 && str[4] == 0x7e
                       return "Del"
                    if str[2] == 0x5b && str[3] == 0x31 && str[4] == 0x7e
                       return "Home"
                    if str[2] == 0x5b && str[3] == 0x34 && str[4] == 0x7e
                       return "End"
                    if str[2] == 0x5b && str[3] == 0x35 && str[4] == 0x7e
                       return "PgUp"
                    if str[2] == 0x5b && str[3] == 0x36 && str[4] == 0x7e
                       return "PgDn"
                    return ""
                end # if length(str) > 3
                if str[2] == 0x5b && sr[3] == 0x41
                    return "UpArrow"
                if str[2] == 0x5b && sr[3] == 0x42
                    return "DnArrow"
                if str[2] == 0x5b && sr[3] == 0x43
                    return "RightArrow"
                if str[2] == 0x5b && sr[3] == 0x44
                    return "LeftArrow"
                return ""
                # We had an Escape
            end # if str[1] == 0x1b
            x = convert(Int, str[1])
            if x == 8
                return "BackSpace"
            if x == 9
                return "Tab"
            if x == 13
                return "Enter"
            if x < 27
                y = 64 + x
                return "Ctrl-" * Char(y)
            if x < 128 && x > 31
                return String(str)
            return ""
            # We had a key pressed!
        end # if ba > 0
        if s == ""
           # this is a one time test
           return s
        if s != "0"
            # this means we have a timed wait
            if seconds < 0
                # first time
                seconds = parse(Int, s)
                seconds = seconds - 1
                if seconds == 0
                   return ""
    #   else
            # this means we wait until aa key is pressed
    end # while true
end # function inkey(s)
1 Like