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)))
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 but one day it will be amazing