• Stars
    star
    145
  • Rank 254,144 (Top 6 %)
  • Language
    Clojure
  • License
    Mozilla Public Li...
  • Created about 6 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Terminal UI library for Clojure

Trikl

"Terminal React for Clojure" => Trikl

CircleCI cljdoc badge Clojars Project

Trikl is a long-term slow moving research project. It is not intended for general consumption. There are useful bits in there, but expect to get intimately familiar with the implementation if you want to get value out of them.

The README below is from the first incarnation of Trikl, which had severe shortcomings around event handling and how it handles components. The groundwork for a new version has started in the lambdaisland.trikl1.* namespaces.

Regal is a spin-off library created to support this work.


Trikl lets you write terminal applications in a way that's similar to React/Reagent. It's main intended use case is for hobbyist/indy games.

With Trikl you use (a dialect of) Hiccup to create your Terminal UI. As your application state changes, Trikl re-renders the UI, diffs the output with what's currently on the screen, and sends the necessary commands to the terminal to bring it up to date.

This is still very much work in progress and subject to change.

Example

You can use Trikl directly by hooking it up to STDIN/STDOUT, or you can use it as a telnet server. The telnet server is great because it makes it easy to try stuff out from the REPL.

For instance you can do something like this:

(require '[trikl.core :as t])

;; store clients so we can poke at them from the REPL
(def clients (atom []))

;; Start the server on port 1357, as an accept handler just store the client in
;; the atom.
(def stop-server (t/start-server #(swap! clients conj %) 1357))

;; in a terminal: telnet localhost 1357

#_(stop-server) ;; disconnect all clients and stop listening for connections

;; Render hiccup! Re-run this as often as you like, only changes are sent to the client.
(t/render (last @clients) #_(t/stdio-client)
          [:box {:x 10 :y 5 :width 20 :height 10 :styles {:bg [50 50 200]}}
           [:box {:x 1 :y 1 :width 18 :height 8 :styles {:bg [200 50 0]}}
            [:box {:x 3 :y 1}
             [:span {:styles {:fg [30 30 150]}} "hello\n"]
             [:span {:styles {:fg [100 250 100]}} "  world"]]]])

;; Listen for input events
(t/add-listener (last @clients)
                ::my-listener
                (fn [event]
                  (prn event)))

Use stdio-client to hook up the terminal the process is running in.

Result:

Things you can render

You can render the following things

String

A string is simply written to to the screen as-is. Note that strings are limited to their current bounding box, so long strings don't wrap. You can use newlines to use multiple lines.

(t/render client "hello, world!\nWhat a lovely day we're having")

Sequence

A seq (so the result of calling list, map, for, etc. Not vectors!) will simply render each item in the list.

(t/render client (repeat 20 "la"))

Elements

Elements are vectors, they contain the element type (first item in the vector), a map of attributes (optional, second element in the vector), and any remaining child elements.

Trikl currently knows of the following elements.

:span

A :span can be used to add styling, i.e. foreground and background colors. Colors are specified using RGB (red-green-blue) values. Each value can go from 0 to 255.

(t/render client [:span {:styles {:fg [100 200 0] :bg [50 50 200]}} "Oh my!"])

:box

The :box element changes the current bounding box. You give it a position with :x and :y, and a size with :width and :height, and anything inside the box will be contained within those coordinates.

If you don't supply :x or :y it will default to the top-left corner of its surrounding bounding box. If you omit :width and :height it will take up as much space as it has available.

You can also supply :styles to the box, as with :span. Setting a :bg on the box will color the whole box.

Note for instance that this example truncates the string to "Hello,", because it doesn't fit in the box.

(t/render client [:box {:x 20 :y 10, :width 7, :height 3} "Hello, world!"])

:line-box

A :line-box is like a box, but it gets a fancy border. This border is drawn on the inside of the box, so you lose two rows and two columns of space to put stuff inside, but it looks pretty nice!

By default it uses lines with rounded corners, but you can use any border decoration you like by supplying a :lines attribute. This can be a string or sequence containing the characters to use, starting from the top left corner and moving clockwise. The default value for :lines is "╭─╮│╯─╰│"

(t/render client [:line-box {:x 20 :y 10, :width 10, :height 3} "Hello, world!"])

:cols and :rows

The :cols and :rows elements will split their children into columns and rows respectively. If any of the children have a :width or :height that will be respected. Any remaining space is distributed equally among the ones that don't have a fixed :width/:height already.

So you could divide the screen in four equally sized sections using:

(t/render (last @clients)
          [:cols
           [:rows
            [:line-box "1"]
            [:line-box "2"]]
           [:rows
            [:line-box "3"]
            [:line-box "4"]]])

Custom Components

You can define custom components by creating a two-argument function, attributes and children, and using the function as the first element in the vector. You can return any of the above renderable things from the function.

(defn app [attrs children]
  [:rows
   [:line-box "Hiiiiii"]
   [:line-box {:height 15 :styles {:bg [100 50 50]}}]])

(t/render client [app])

App state

If you keep your app state in an atom, then you can use render-watch! to automatically re-render when the atom changes.

(def app-state (atom {:pos [10 10]}))

(t/render-watch! client
                 (fn [{[x y] :pos} _]
                   [:box {:x x :y y} "X"])
                 app-state)

(swap! app-state update-in [:pos 0] inc)
(swap! app-state update-in [:pos 1] inc)

(t/unwatch! app-state)

Querying the screen size and bounding box

During rendering you can use t/*screen-size* and t/*bounding-box* to find the current dimensions you are working in. There's also a (box-size) helpers which only returns the [width height] of the bounding box, rather than [x y width height]. This should greatly alleviate the need to write your own drawing functions, as you can now do everything with custom elements and a combination of [:span ...] and [:box ...]. The main reason to write custom drawing functions would be for performance, when what you are drawing does not easily fit in the span/box paradigm. If you find yourself creating a span per character then maybe a custom drawing function makes sense.

Custom drawing functions

To implement custom elements, extend the t/draw multimethod. The method takes two arguments, the element (the vector), and a "virtual screen". Your method needs to return an updated version of the virtual screen.

The main reasons to do this are because this gives you access to the current screen size (bounding box), and for performance reasons.

The VirtualScreen has a ":charels" key (character elements, analogues to pixels), This is a vector of rows, each row is a vector of "charels", which have a :char, :bg, :fg key. Make sure the :char is set to an actual char, not to a String.

After drawing the virtual screen is diffed with the previous virtual screen to figure out the minimum commands to send to the terminal to update it.

(defmethod t/draw :x-marks-the-spot [element screen]
  (assoc-in screen [:charels 5 5 :char] \X))

Things to watch out for:

  • Normalize your element with t/split-el, this always returns a three element vector with element type, attribute map, children sequence.

  • Stick to your bounding box! You probably want to start with this

  (let [[_ attrs children] (split-el element)
        [x y width height] (apply-bounding-box attrs screen)]
    ,,,)

You should not touch anything outside [(range x (+ x width)) (range y (+ y height))]

  • If your element takes :styles, then use push-styles and pop-styles to correctly restore the styles from a surrounding context.

True Color

In theory ANSI compatible terminals are able to render 24 bit colors (16 million shades), but in practice they don't always do.

You can try this snippet, if you see nice continuous gradients from blue to purple then you're all set.

(defn app [_]
  [:box
   (for [y (range 50)]
     [:span
      (for [x (range 80)]
        [:span {:styles {:bg [(* x 2) (* y 2) (+ x (* 4 y))]}} " "])
      "\n"])])

iTerm and gnome-terminal should both be fine, but if you're using Tmux and you're not getting the desired results, then add this to your ~/.tmux.conf

set -g default-terminal "xterm-256color"
set-option -ga terminal-overrides ",xterm-256color:Tc"

Using netcat

Not all systems come with telnet installed, notably recent versions of Mac OS X have stopped bundling it. The common advice you'll find is to use Netcat (nc) instead, but these two are not the same. Telnet understands certain binary codes to configure your terminal, which Trikl needs to function correctly.

You can brew install telnet, or in a pinch you can use stty to configure your terminal to not echo input, and to enable "raw" (direct, unbuffered) mode.

Make sure to invoke stty and nc as a single command like this:

stty -echo -icanon && nc localhost 1357

To undo the changes to your terminal do

stty +echo +icanon

Graal compatibility

Trikl contains enough type hints to prevent Clojure's type reflection, which makes it compatible with GraalVM. This means you can compile your project to native binaries that boot instantly. Great for tooling!

License

Copyright © 2018 Arne Brasseur

Licensed under the term of the Mozilla Public License 2.0, see LICENSE.

More Repositories

1

kaocha

Full featured next gen Clojure test runner
Clojure
792
star
2

regal

Royally reified regular expressions
Clojure
326
star
3

deep-diff2

Deep diff Clojure data structures and pretty print the result
Clojure
295
star
4

uri

A pure Clojure/ClojureScript URI library
Clojure
243
star
5

witchcraft

Clojure API for manipulating Minecraft, based on Bukkit
Clojure
135
star
6

fetch

ClojureScript wrapper for the JavaScript fetch API
Clojure
122
star
7

glogi

A ClojureScript logging library based on goog.log
Clojure
119
star
8

ornament

Clojure Styled Components
Clojure
118
star
9

uniontypes

Union Types (ADTs, sum types) built on clojure.spec
Clojure
115
star
10

launchpad

Clojure/nREPL launcher
Clojure
87
star
11

classpath

Classpath/classloader/deps.edn related utilities
Clojure
84
star
12

corgi

Emacs Lisp
75
star
13

metabase-datomic

Datomic driver for Metabase
Clojure
65
star
14

chui

Clojure
62
star
15

npmdemo

Demo of using Node+Express with ClojureScript
Clojure
60
star
16

funnel

Transit-over-WebSocket Message Relay
Clojure
58
star
17

deja-fu

ClojureScript local time/date library with a delightful API
Clojure
48
star
18

facai

Factories for fun and profit. 恭喜發財!
Clojure
45
star
19

open-source

A collection of Clojure/ClojureScript tools and libraries
Clojure
43
star
20

witchcraft-workshop

materials and code for the ClojureD 2022 workshop on Minecraft+Clojure
Clojure
40
star
21

kaocha-cljs

ClojureScript support for Kaocha
Clojure
40
star
22

cljbox2d

Clojure
40
star
23

thirdpartyjs

Demonstration of how to use third party JS in ClojureScript
Clojure
38
star
24

kaocha-cucumber

Cucumber support for Kaocha
Clojure
37
star
25

dom-types

Implement ClojureScript print handlers, as well Datify/Navigable for various built-in browser types.
Clojure
36
star
26

kaocha-cloverage

Code coverage analysis for Kaocha
Clojure
32
star
27

ansi

Parse ANSI color escape sequences to Hiccup syntax
Clojure
31
star
28

embedkit

Metabase as a Dashboard Engine
Clojure
30
star
29

plenish

Clojure
30
star
30

pennon

A feature flag library for Clojure
Clojure
30
star
31

hiccup

Enlive-backed Hiccup implementation (clj-only)
Clojure
28
star
32

edn-lines

Library for dealing with newline separated EDN files
Shell
27
star
33

kaocha-cljs2

Run ClojureScript tests from Kaocha (major rewrite)
Clojure
26
star
34

witchcraft-plugin

Add Clojure support (and an nREPL) to any Bukkit-based Minecraft server
Clojure
23
star
35

cli

Opinionated command line argument handling, with excellent support for subcommands
Clojure
22
star
36

garden-watcher

A component that watches-and-recompiles your Garden stylesheets.
Clojure
22
star
37

reitit-jaatya

Freeze your reitit routes and create a static site out of it
Clojure
21
star
38

nrepl-proxy

Proxy for debugging nREPL interactions
Clojure
18
star
39

data-printers

Quickly define print handlers for tagged literals across print/pprint implementations.
Clojure
18
star
40

lambdaisland-guides

In depth guides into Clojure and ClojureScript by Lambda Island
TeX
17
star
41

specmonstah-malli

Clojure
17
star
42

faker

Port of the Ruby Faker gem
Clojure
15
star
43

puck

ClojureScript wrapper around Pixi.js, plus other game dev utils
Clojure
15
star
44

kaocha-junit-xml

JUnit XML output for Kaocha
Clojure
11
star
45

aoc_2020

Advent of Code 2020
Clojure
11
star
46

harvest

Flexible factory library, successor to Facai
Clojure
11
star
47

gaiwan_co

Website for Gaiwan GmbH
Clojure
8
star
48

nrepl

Main namespace for starting an nREPL server with `clj`
Clojure
8
star
49

zipper-viz

Visualize Clojure zippers using Graphviz
Clojure
8
star
50

exoscale

Clojure/Babashka wrapper for the Exoscale HTTP API
Clojure
7
star
51

webstuff

The web as it was meant to be
Clojure
7
star
52

birch

A ClojureScript/Lumo version of the Unix "tree" command
Clojure
7
star
53

funnel-client

Websocket client for Funnel + examples
Clojure
7
star
54

corgi-packages

Emacs Packages developed as part of Corgi
Emacs Lisp
7
star
55

kanban

Episode 9. Reagent
Clojure
6
star
56

react-calculator

A calculator built with ClojureScript and React
JavaScript
6
star
57

souk

Clojure
6
star
58

logback-clojure-filter

Logback appender filter that takes a Clojure expression
Clojure
6
star
59

breakout

The retro game "Breakout". re-frame/Reagent/React/SVG.
Clojure
5
star
60

activities

Clojure
5
star
61

booklog

Keep track of the books you read (Auth with Buddy)
Clojure
5
star
62

li40-ultimate

Code from episode 40: The Ultimate Dev Setup
Shell
5
star
63

l33t

Demo ClojureScript+Node.js app
JavaScript
5
star
64

ep47-interceptors

Accompanying code to Lambda Island episode 47. Interceptors.
Clojure
5
star
65

daedalus

"Path finding and Delaunay triangulation in 2D, cljs wrapper for hxdaedalus-js"
Clojure
5
star
66

component_example

Example code for the Lambda Island episodes about Component
Clojure
4
star
67

ep43-data-science-kixi-stats

Clojure
4
star
68

lambwiki

A small wiki app to demonstrate Luminus
Clojure
4
star
69

new-project

Template for new projects
Emacs Lisp
3
star
70

kaocha-boot

Kaocha support for Boot
Clojure
3
star
71

datomic-quick-start

Datomic Quickstart sample code
Clojure
3
star
72

redolist

TodoMVC in re-frame
Clojure
3
star
73

rolodex-gui

Reagent app for testing the Rolodex API
Clojure
2
star
74

ep33testcljs

Testing ClojureScript with multiple backends
Clojure
2
star
75

elpa

Lambda Island Emacs Lisp Package Archive
Emacs Lisp
2
star
76

datalog-benchmarks

Clojure
2
star
77

kaocha-doctest

Doctest test type for Kaocha
Clojure
1
star
78

morf

Clojure
1
star
79

land-of-regal

Playground for Regal
Clojure
1
star
80

compobook

An example Compojure app
Clojure
1
star
81

ep41-react-components-reagent

Demo code from Episode 41, using React Components from Reagent
Clojure
1
star
82

repl-tools

Clojure
1
star
83

li45_polymorphism

Code for Lambda Island episode 45 and 46 about Polymorphism
Clojure
1
star
84

dotenv

Clojure
1
star
85

kaocha-midje

Midje integration for Kaocha
Clojure
1
star
86

rolodex

Clojure
1
star
87

laoban

Clojure
1
star
88

cookie-cutter

Auto-generate Clojure test namespaces in bulk.
Clojure
1
star
89

shellutils

Globbing and other shell/file utils
Clojure
1
star
90

kaocha-cljs2-demo

Example setups for kaocha-cljs2. WIP
Clojure
1
star
91

kaocha-demo

Clojure
1
star
92

kaocha-nauseam

Example project with a large (artificial) test suite
Clojure
1
star
93

li39-integrant

Accompanying code for Lambda Island episode 38 about Integrant
Clojure
1
star
94

webbing

Clojure
1
star
95

slack-backfill

Save Slack history to JSON files
Clojure
1
star
96

janus

Parser for Changelog files
Clojure
1
star
97

xdemo

Demo of xforms/redux/kixi.stats
Clojure
1
star
98

ep23deftype

Code for Lambda Island Episode 23, deftype and definterface
Clojure
1
star
99

ep24defrecord

Code for Lambda Island Episode 24, defrecord and defprotocol
Clojure
1
star
100

ep32testing

Code for Lambda Island Episode 32, Introduction to Clojure Testing
Clojure
1
star