clojure-lanterna

Terminals

The lowest layer of Lanterna (and thus clojure-lanterna) is a Terminal.

You can use terminals to do the stuff you normally think of using a curses library for.

Getting a Terminal

Let's try it out. Open up a REPL and pull in the namespace:

(require '[lanterna.terminal :as t])

Now get a Terminal:

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

Now we've got a Terminal called term.

We're going to force this to be a Swing-based terminal instead of it being in the console, because we don't want to mess with our REPL.

When you write a standalone program you can use :text to force the terminal to use the console, but we'll talk more about that later.

You may have noticed that nothing seems to have happened. Now you need to "start" the terminal to initialize it:

(t/start term)

Now you should see a blank terminal that looks like this:

Blank Terminal

Writing Text

You can print characters to the current cursor location with put-character:

(t/put-character term \H)
(t/put-character term \i)
(t/put-character term \!)

Terminal that says Hi

Notice how the characters didn't overwrite each other? put-character not only writes the character, it also move the cursor one column to the right.

You might want to make this a bit more convenient:

(def put-character-to-term (partial t/put-character term))
(def write #(dorun (map put-character-to-term %)))

(write " My name is Steve!")

Terminal that says Hi my name is Steve

But of course clojure-lanterna already provides that as the function put-string.

Moving the Cursor

You can move the cursor with move-cursor:

(t/move-cursor term 40 12)
(t/put-character term \@)

This moves the cursor to column 40, row 12 and prints an @.

Terminal with Rogue

Let's move the cursor back over the @ so it looks like a proper Roguelike game:

(t/move-cursor term 40 12)

Terminal with Highlighted Rogue

Colors

You can change the foreground and background colors:

(t/move-cursor term 0 6)
(t/set-fg-color term :red)
(t/put-string term "Red")

(t/move-cursor term 0 7)
(t/set-fg-color term :blue)
(t/put-string term "Blue")

(t/move-cursor term 0 8)
(t/set-fg-color term :black)
(t/set-bg-color term :green)
(t/put-string term "Green")

Terminal with Colors

When you set a foreground or background color, all subsequent characters you write will use that color. To reset the colors back to the default you can use the special color :default:

(t/set-fg-color term :default)
(t/set-bg-color term :default)

Styles

Styles are not currently implemented for Terminals. Pull requests are welcome, I'm pretty sure it's a Clojure/Java interop problem.

Input

Lanterna will buffer the user's keystrokes for you so you can retrieve them later.

Focus the Swing terminal and type "abc". Now try running get-key in your REPL:

(t/get-key term)
; => \a

Lanterna returns the first letter that you typed, as a standard Java/Clojure Character.

Run it three more times:

(t/get-key term)
; => \b

(t/get-key term)
; => \c

(t/get-key term)
; => nil

Each call to get-key pops one character off the input buffer and returns it. If there isn't anything on the buffer, it returns nil.

If you want to make sure you get a key back (by waiting for the user to press one if there's none already buffered) you can use get-key-blocking:

(t/get-key-blocking term)
;
; Nothing happens until you press a key in the Swing terminal,
; then the key is returned.
;
; => \a

get-key-blocking optionally accepts a check interval and/or timeout:

(t/get-key-blocking term {:interval 100 :timeout 5000})
;
; Key presses will be checked for every 100 msecs. If you wait
; 5 seconds, nil will be returned.
;
; => nil

Normal alphanumeric keys are returned as simple Character objects like \a.

Note that there's no special attribute to determine if the Shift key was pressed, but the Characters will be the correct ones. For example, if the user presses "Shift-a" the Character you get will be \A instead of \a.

Special keys are returned as Clojure keywords like :enter, :page-up, and :backspace.

You can get a full list of the supported special keys by peeking in Lanterna's constants namespace (or just consult the reference documentation):

(require '[lanterna.constants :as c])
(vals c/key-codes)
; => (:end :cursor-location :backspace :unknown :right
;     :delete :tab :insert :enter :left :page-up :page-down
;     :escape :reverse-tab :home :down :normal :up)

Sizing

The final piece of Lantera's terminal layer is the concept of terminal sizes.

When writing a terminal application, you're at the mercy of the user when it comes to how big (or small) the window is going to be.

Obviously in a console environment the user can resize their xterm window. Lanterna's Swing terminal emulator can be resized by dragging normally as well.

First of all, you can get the size of the terminal at any time with get-size:

(t/get-size term)
; => [80 24]

But getting the size at a single point in time usually won't be enough.

Your application needs to be able to handle resized windows. To do this you can provide a function when you create the terminal. This function will be called by Lanterna whenever the window is resized and passed the new columns and rows.

Let's try it out. First close your old terminal:

(t/stop term)

You'll notice the Swing emulator vanishes. Let's make a simple listener function that will update a ref whenever the terminal size changes:

(def terminal-size (ref [0 0]))

(defn handle-resize [cols rows]
  (dosync (ref-set terminal-size [cols rows])))

Create a new Swing terminal, passing an options map containing the listener function:

(def term (t/get-terminal :swing {:resize-listener handle-resize}))

(t/start term)

If you try to check the size right away, you'll still get [0 0]:

@terminal-size
; => [0 0]

Now resize the Swing window and try again:

@terminal-size
; => [78 24]

TODO: Figure out how to get the initial size.

What you do in your resize listener is up to you. You might want to record the size as we did here, and you might also want to redraw your UI, because it'll probably look strange otherwise.

That wraps up the terminal layer. Go ahead and close your terminal:

(t/stop term)

One more thing: if you get tired of manually starting and stoping terminals, you can use the in-terminal macro to do it for you:

(let [term (t/get-terminal :swing)]
  (t/in-terminal term
    (t/put-string term "Hello!  Press any key to end.")
    (t/get-key-blocking term)))

What's Next?

Now that you've covered all of the major concepts of Lanterna's terminal layer, it's time to move on to the next layer: screens.