Reading standard input one character at a time

As a learning exercise I’m making a terminal based interactive program (a game). I want the program to process input one character at a time. Every time a user presses a button, the program processes it and gives output. I couldn’t find a way to do it using standard library. I know read and read-line functions but they take an entire line (until enter is pressed). That is not what I want. I searched online and found this solution that uses jline3 Java library: https://stackoverflow.com/questions/58571928/how-can-i-read-a-single-character-from-stdin-in-clojure. I tried to follow it but when I run my program I’m getting the following warning and error.

WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
Exception in thread "main" Syntax error compiling at (/private/var/folders/nt/26pz2dqj4y70z38vrtgd8t4m0000gn/T/form-init7411586405251765923.clj:1:125).
Caused by: java.lang.IllegalArgumentException: No matching field found: read for class org.jline.terminal.impl.DumbTerminal

I don’t know how to enable debug logging. I’m working on OSX and iTerm2.

I made some progress using clojure-lanterna library. With the following code I can read and process one character but there is big side effect. The terminal window gets cleared when the program starts. Is there a way to avoid it?

(ns lonely-dobble.user-input
  (:require [clojure.string :as str])
  (:require [lanterna.terminal :as t])
  (:gen-class))

(def term (t/get-terminal :text))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (t/start term)
  (let [character (t/get-key-blocking term)]
    (t/stop term)
    (println character)))

Try:

(.read *in*)

or

(.read (clojure.java.io/reader *in*))

I think one of those two does it.

Oh I see now what you mean, you want to bind directly to commands of the terminal, and not the input sent to the shell.

In that case, I’d recommend you try the jline3 solution that is described inside your stackoverflow link.

Reading a single keypress from the terminal is one of those things that sounds trivial but isn’t, because your operating system kernel gets in the way. The TTY line discipline provides basic line editing capabilities, but that also means that it buffers input until it receives a newline. So it’s not that your program needs to do more, it’s that the TTY driver needs to do less.

You can change the TTY driver settings with stty. Here’s a snippet I’ve been using for that.

(defn exec-stty
  "Excute the `stty(1)` command to configure TTY options like input echoing, and
  line handling."
  [& args]
  (let [^Process
        process (-> (ProcessBuilder. (util/string-array (cons "stty" args)))
                    (.redirectInput (ProcessBuilder$Redirect/from (io/file "/dev/tty")))
                    (.start))

        ^StringWriter err (StringWriter.)]
    {:exit (.waitFor process)
     :err @(future
             (with-open [^StringWriter err (StringWriter.)]
               (io/copy (.getErrorStream process) err)
               (.toString err)))
     :out @(future
             (with-open [^StringWriter out (StringWriter.)]
               (io/copy (.getInputStream process) out)
               (.toString out)))}))

The redirectInput line is key, as that makes sure stty is operating on the right tty device. I’ve only tested this on Linux though, so curious to hear if this works for you on OS X.

You use it like this:

(stty "-echo" "-icanon")
(.read System/in)

And before you exit you switch it back. If you don’t do this your terminal will end up in a weird state and you need to issue a reset to get it back to normal.

(stty "+echo" "+icanon")

See man stty for details. echo enables/disables echoing of the input characters, I’m guessing this is what you want. icanon switches “canonical mode”, also referred to as “cooked” vs “raw” input. In cooked/canonical mode it processes backspace, delete, and enter. In raw mode it gets out of the way and lets you just read characters.

Yes I have looked into this stuff way too deeply while working on Trikl, which grew out of dissatisfaction with Laterna and the need for something more idiomatic and clojure-y. It’s a long running research project at this point, midway through its third rewrite :slight_smile: but one day it will be amazing :stuck_out_tongue:

2 Likes

Oops I guess that snippet isn’t really standalone. Here are the necessary imports

(:import java.io.StringWriter java.lang.ProcessBuilder$Redirect)

and the implementation of string-array. I’m type hinting everything because this code is meant to be GraalVM compatible.

(defn ^"[Ljava.lang.String;" string-array [args]
  (into-array String args))

That is some nice digging you have done @plexus! :muscle:

From your description, allow me to suggest a with-stty functionality, similar to with-open, to handle resetting stty.

I hope I will remember this when I at some point need it. :grinning:

Thank you @plexus. I will give it a try!