clojure-lanterna/terminals/index.html @ 0f6bab39c0f4 default tip

adopt: Update site.
author Steve Losh <steve@stevelosh.com>
date Thu, 13 Jun 2024 13:05:28 -0400
parents be9b7c8cdbbc
children (none)
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Terminals / clojure-lanterna</title>
        <link rel="stylesheet" href="../_dmedia/tango.css"/>
        <link rel="stylesheet/less" type="text/css" href="../_dmedia/style.less"/>
        <script src="../_dmedia/less.js" type="text/javascript">
        </script>
    </head>
    <body class="content">
        <div class="wrap">
            <header><h1><a href="..">clojure-lanterna</a></h1></header>
                <div class="markdown">
<h1 id="terminals"><a href="">Terminals</a></h1><p>The lowest layer of Lanterna (and thus clojure-lanterna) is a Terminal.</p>
<p>You can use terminals to do the stuff you normally think of using a curses
library for.</p>
<div class="toc">
<ul>
<li><a href="#getting-a-terminal">Getting a Terminal</a></li>
<li><a href="#writing-text">Writing Text</a></li>
<li><a href="#moving-the-cursor">Moving the Cursor</a></li>
<li><a href="#colors">Colors</a></li>
<li><a href="#styles">Styles</a></li>
<li><a href="#input">Input</a></li>
<li><a href="#sizing">Sizing</a></li>
<li><a href="#whats-next">What's Next?</a></li>
</ul></div>
<h2 id="getting-a-terminal">Getting a Terminal</h2>
<p>Let's try it out.  Open up a REPL and pull in the namespace:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">require</span> <span class="o">'</span><span class="p">[</span><span class="nv">lanterna.terminal</span> <span class="ss">:as</span> <span class="nv">t</span><span class="p">])</span>
</pre></div>


<p>Now get a Terminal:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="k">def </span><span class="nv">term</span> <span class="p">(</span><span class="nf">t/get-terminal</span> <span class="ss">:swing</span><span class="p">))</span>
</pre></div>


<p>Now we've got a Terminal called <code>term</code>.</p>
<p>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.</p>
<p>When you write a standalone program you can use <code>:text</code> to force the terminal to
use the console, but we'll talk more about that later.</p>
<p>You may have noticed that nothing seems to have happened.  Now you need to
"start" the terminal to initialize it:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/start</span> <span class="nv">term</span><span class="p">)</span>
</pre></div>


<p>Now you should see a blank terminal that looks like this:</p>
<p><img alt="Blank Terminal" src="http://i.imgur.com/sQIHO.png"/></p>
<h2 id="writing-text">Writing Text</h2>
<p>You can print characters to the current cursor location with <code>put-character</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/put-character</span> <span class="nv">term</span> <span class="sc">\H</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/put-character</span> <span class="nv">term</span> <span class="sc">\i</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/put-character</span> <span class="nv">term</span> <span class="sc">\!</span><span class="p">)</span>
</pre></div>


<p><img alt="Terminal that says Hi" src="http://i.imgur.com/YyGEz.png"/></p>
<p>Notice how the characters didn't overwrite each other?  <code>put-character</code> not only
writes the character, it also move the cursor one column to the right.</p>
<p>You might want to make this a bit more convenient:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="k">def </span><span class="nv">put-character-to-term</span> <span class="p">(</span><span class="nb">partial </span><span class="nv">t/put-character</span> <span class="nv">term</span><span class="p">))</span>
<span class="p">(</span><span class="k">def </span><span class="nv">write</span> <span class="o">#</span><span class="p">(</span><span class="nb">dorun </span><span class="p">(</span><span class="nb">map </span><span class="nv">put-character-to-term</span> <span class="nv">%</span><span class="p">)))</span>

<span class="p">(</span><span class="nf">write</span> <span class="s">" My name is Steve!"</span><span class="p">)</span>
</pre></div>


<p><img alt="Terminal that says Hi my name is Steve" src="http://i.imgur.com/LECAv.png"/></p>
<p>But of course clojure-lanterna already provides that as the function
<code>put-string</code>.</p>
<h2 id="moving-the-cursor">Moving the Cursor</h2>
<p>You can move the cursor with <code>move-cursor</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/move-cursor</span> <span class="nv">term</span> <span class="mi">40</span> <span class="mi">12</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/put-character</span> <span class="nv">term</span> <span class="sc">\@</span><span class="p">)</span>
</pre></div>


<p>This moves the cursor to column 40, row 12 and prints an @.</p>
<p><img alt="Terminal with Rogue" src="http://i.imgur.com/rN8lK.png"/></p>
<p>Let's move the cursor back over the @ so it looks like a proper Roguelike game:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/move-cursor</span> <span class="nv">term</span> <span class="mi">40</span> <span class="mi">12</span><span class="p">)</span>
</pre></div>


<p><img alt="Terminal with Highlighted Rogue" src="http://i.imgur.com/MADt8.png"/></p>
<h2 id="colors">Colors</h2>
<p>You can change the foreground and background colors:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/move-cursor</span> <span class="nv">term</span> <span class="mi">0</span> <span class="mi">6</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/set-fg-color</span> <span class="nv">term</span> <span class="ss">:red</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/put-string</span> <span class="nv">term</span> <span class="s">"Red"</span><span class="p">)</span>

<span class="p">(</span><span class="nf">t/move-cursor</span> <span class="nv">term</span> <span class="mi">0</span> <span class="mi">7</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/set-fg-color</span> <span class="nv">term</span> <span class="ss">:blue</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/put-string</span> <span class="nv">term</span> <span class="s">"Blue"</span><span class="p">)</span>

<span class="p">(</span><span class="nf">t/move-cursor</span> <span class="nv">term</span> <span class="mi">0</span> <span class="mi">8</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/set-fg-color</span> <span class="nv">term</span> <span class="ss">:black</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/set-bg-color</span> <span class="nv">term</span> <span class="ss">:green</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/put-string</span> <span class="nv">term</span> <span class="s">"Green"</span><span class="p">)</span>
</pre></div>


<p><img alt="Terminal with Colors" src="http://i.imgur.com/ZkxhC.png"/></p>
<p>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 <code>:default</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/set-fg-color</span> <span class="nv">term</span> <span class="ss">:default</span><span class="p">)</span>
<span class="p">(</span><span class="nf">t/set-bg-color</span> <span class="nv">term</span> <span class="ss">:default</span><span class="p">)</span>
</pre></div>


<h2 id="styles">Styles</h2>
<p>Styles are not currently implemented for Terminals.  Pull requests are welcome,
I'm pretty sure it's a Clojure/Java interop problem.</p>
<h2 id="input">Input</h2>
<p>Lanterna will buffer the user's keystrokes for you so you can retrieve them
later.</p>
<p>Focus the Swing terminal and type "abc".  Now try running <code>get-key</code> in your
REPL:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/get-key</span> <span class="nv">term</span><span class="p">)</span>
<span class="c1">; =&gt; \a</span>
</pre></div>


<p>Lanterna returns the first letter that you typed, as a standard Java/Clojure
Character.</p>
<p>Run it three more times:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/get-key</span> <span class="nv">term</span><span class="p">)</span>
<span class="c1">; =&gt; \b</span>

<span class="p">(</span><span class="nf">t/get-key</span> <span class="nv">term</span><span class="p">)</span>
<span class="c1">; =&gt; \c</span>

<span class="p">(</span><span class="nf">t/get-key</span> <span class="nv">term</span><span class="p">)</span>
<span class="c1">; =&gt; nil</span>
</pre></div>


<p>Each call to <code>get-key</code> pops one character off the input buffer and returns it.
If there isn't anything on the buffer, it returns <code>nil</code>.</p>
<p>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 <code>get-key-blocking</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/get-key-blocking</span> <span class="nv">term</span><span class="p">)</span>
<span class="c1">;</span>
<span class="c1">; Nothing happens until you press a key in the Swing terminal,</span>
<span class="c1">; then the key is returned.</span>
<span class="c1">;</span>
<span class="c1">; =&gt; \a</span>
</pre></div>


<p><code>get-key-blocking</code> optionally accepts a check interval and/or timeout:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/get-key-blocking</span> <span class="nv">term</span> <span class="p">{</span><span class="ss">:interval</span> <span class="mi">100</span> <span class="ss">:timeout</span> <span class="mi">5000</span><span class="p">})</span>
<span class="c1">;</span>
<span class="c1">; Key presses will be checked for every 100 msecs. If you wait</span>
<span class="c1">; 5 seconds, nil will be returned.</span>
<span class="c1">;</span>
<span class="c1">; =&gt; nil</span>
</pre></div>


<p>Normal alphanumeric keys are returned as simple Character objects like <code>\a</code>.</p>
<p>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 <code>\A</code> instead of <code>\a</code>.</p>
<p>Special keys are returned as Clojure keywords like <code>:enter</code>, <code>:page-up</code>, and
<code>:backspace</code>.</p>
<p>You can get a full list of the supported special keys by peeking in Lanterna's
constants namespace (or just consult the reference documentation):</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">require</span> <span class="o">'</span><span class="p">[</span><span class="nv">lanterna.constants</span> <span class="ss">:as</span> <span class="nv">c</span><span class="p">])</span>
<span class="p">(</span><span class="nb">vals </span><span class="nv">c/key-codes</span><span class="p">)</span>
<span class="c1">; =&gt; (:end :cursor-location :backspace :unknown :right</span>
<span class="c1">;     :delete :tab :insert :enter :left :page-up :page-down</span>
<span class="c1">;     :escape :reverse-tab :home :down :normal :up)</span>
</pre></div>


<h2 id="sizing">Sizing</h2>
<p>The final piece of Lantera's terminal layer is the concept of terminal sizes.</p>
<p>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.</p>
<p>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.</p>
<p>First of all, you can get the size of the terminal at any time with <code>get-size</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/get-size</span> <span class="nv">term</span><span class="p">)</span>
<span class="c1">; =&gt; [80 24]</span>
</pre></div>


<p>But getting the size at a single point in time usually won't be enough.</p>
<p>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.</p>
<p>Let's try it out.  First close your old terminal:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/stop</span> <span class="nv">term</span><span class="p">)</span>
</pre></div>


<p>You'll notice the Swing emulator vanishes.  Let's make a simple listener
function that will update a ref whenever the terminal size changes:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="k">def </span><span class="nv">terminal-size</span> <span class="p">(</span><span class="nb">ref </span><span class="p">[</span><span class="mi">0</span> <span class="mi">0</span><span class="p">]))</span>

<span class="p">(</span><span class="kd">defn </span><span class="nv">handle-resize</span> <span class="p">[</span><span class="nv">cols</span> <span class="nv">rows</span><span class="p">]</span>
  <span class="p">(</span><span class="nb">dosync </span><span class="p">(</span><span class="nb">ref-set </span><span class="nv">terminal-size</span> <span class="p">[</span><span class="nv">cols</span> <span class="nv">rows</span><span class="p">])))</span>
</pre></div>


<p>Create a new Swing terminal, passing an options map containing the listener
function:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="k">def </span><span class="nv">term</span> <span class="p">(</span><span class="nf">t/get-terminal</span> <span class="ss">:swing</span> <span class="p">{</span><span class="ss">:resize-listener</span> <span class="nv">handle-resize</span><span class="p">}))</span>

<span class="p">(</span><span class="nf">t/start</span> <span class="nv">term</span><span class="p">)</span>
</pre></div>


<p>If you try to check the size right away, you'll still get <code>[0 0]</code>:</p>
<div class="codehilite"><pre><span class="o">@</span><span class="nv">terminal-size</span>
<span class="c1">; =&gt; [0 0]</span>
</pre></div>


<p>Now resize the Swing window and try again:</p>
<div class="codehilite"><pre><span class="o">@</span><span class="nv">terminal-size</span>
<span class="c1">; =&gt; [78 24]</span>
</pre></div>


<p>TODO: Figure out how to get the initial size.</p>
<p>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.</p>
<p>That wraps up the terminal layer.  Go ahead and close your terminal:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">t/stop</span> <span class="nv">term</span><span class="p">)</span>
</pre></div>


<p>One more thing: if you get tired of manually <code>start</code>ing and <code>stop</code>ing terminals,
you can use the <code>in-terminal</code> macro to do it for you:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="k">let </span><span class="p">[</span><span class="nv">term</span> <span class="p">(</span><span class="nf">t/get-terminal</span> <span class="ss">:swing</span><span class="p">)]</span>
  <span class="p">(</span><span class="nf">t/in-terminal</span> <span class="nv">term</span>
    <span class="p">(</span><span class="nf">t/put-string</span> <span class="nv">term</span> <span class="s">"Hello!  Press any key to end."</span><span class="p">)</span>
    <span class="p">(</span><span class="nf">t/get-key-blocking</span> <span class="nv">term</span><span class="p">)))</span>
</pre></div>


<h2 id="whats-next">What's Next?</h2>
<p>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: <a href="../screens/">screens</a>.</p>
                </div>
            <footer><p>Created by <a href="http://stevelosh.com">Steve Losh</a>.
Documentation created with <a href="http://sjl.bitbucket.org/d/">d</a>.</p>
<p><br/><a id="rochester-made" href="http://rochestermade.com" title="Rochester Made"><img src="http://rochestermade.com/media/images/rochester-made-dark-on-light.png" alt="Rochester Made" title="Rochester Made"/></a></p>
<script type="text/javascript">
  var _gauges = _gauges || [];
  (function() {
    var t   = document.createElement('script');
    t.type  = 'text/javascript';
    t.async = true;
    t.id    = 'gauges-tracker';
    t.setAttribute('data-site-id', '4f843f8c613f5d65280000e6');
    t.src = '//secure.gaug.es/track.js';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(t, s);
  })();
</script></footer>
        </div>
    </body>
</html>