{"id":13801339,"url":"https://github.com/lambdaisland/trikl","last_synced_at":"2025-06-28T13:43:07.612Z","repository":{"id":66561990,"uuid":"157243867","full_name":"lambdaisland/trikl","owner":"lambdaisland","description":"Terminal UI library for Clojure","archived":false,"fork":false,"pushed_at":"2022-11-15T15:48:51.000Z","size":227,"stargazers_count":150,"open_issues_count":1,"forks_count":8,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-05-07T15:34:32.395Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lambdaisland.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-11-12T16:35:22.000Z","updated_at":"2025-04-30T04:13:26.000Z","dependencies_parsed_at":"2023-02-28T04:16:14.732Z","dependency_job_id":null,"html_url":"https://github.com/lambdaisland/trikl","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ftrikl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ftrikl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ftrikl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ftrikl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lambdaisland","download_url":"https://codeload.github.com/lambdaisland/trikl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253657444,"owners_count":21943291,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T00:01:21.756Z","updated_at":"2025-05-13T11:31:02.468Z","avatar_url":"https://github.com/lambdaisland.png","language":"Clojure","funding_links":[],"categories":["GUI"],"sub_categories":[],"readme":"# Trikl\n\n\"Terminal React for Clojure\" =\u003e Trikl\n\n[![CircleCI](https://circleci.com/gh/lambdaisland/trikl.svg?style=svg)](https://circleci.com/gh/lambdaisland/trikl) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/trikl)](https://cljdoc.org/d/lambdaisland/trikl) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/trikl.svg)](https://clojars.org/lambdaisland/trikl)\n\nTrikl 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.\n\nThe 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.\n\n[Regal](https://lambdaisland.github.io/land-of-regal/) is a spin-off library created to support this work.\n\n----\n\nTrikl lets you write terminal applications in a way that's similar to\nReact/Reagent. It's main intended use case is for hobbyist/indy games.\n\nWith Trikl you use (a dialect of) Hiccup to create your Terminal UI. As your\napplication state changes, Trikl re-renders the UI, diffs the output with what's\ncurrently on the screen, and sends the necessary commands to the terminal to\nbring it up to date.\n\nThis is still very much work in progress and subject to change.\n\n## Example\n\nYou can use Trikl directly by hooking it up to STDIN/STDOUT, or you can use it\nas a telnet server. The telnet server is great because it makes it easy to try\nstuff out from the REPL.\n\nFor instance you can do something like this:\n\n``` clojure\n(require '[trikl.core :as t])\n\n;; store clients so we can poke at them from the REPL\n(def clients (atom []))\n\n;; Start the server on port 1357, as an accept handler just store the client in\n;; the atom.\n(def stop-server (t/start-server #(swap! clients conj %) 1357))\n\n;; in a terminal: telnet localhost 1357\n\n#_(stop-server) ;; disconnect all clients and stop listening for connections\n\n;; Render hiccup! Re-run this as often as you like, only changes are sent to the client.\n(t/render (last @clients) #_(t/stdio-client)\n          [:box {:x 10 :y 5 :width 20 :height 10 :styles {:bg [50 50 200]}}\n           [:box {:x 1 :y 1 :width 18 :height 8 :styles {:bg [200 50 0]}}\n            [:box {:x 3 :y 1}\n             [:span {:styles {:fg [30 30 150]}} \"hello\\n\"]\n             [:span {:styles {:fg [100 250 100]}} \"  world\"]]]])\n\n;; Listen for input events\n(t/add-listener (last @clients)\n                ::my-listener\n                (fn [event]\n                  (prn event)))\n```\n\nUse `stdio-client` to hook up the terminal the process is running in.\n\nResult:\n\n![](example.png)\n\n## Things you can render\n\nYou can render the following things\n\n### String\n\nA string is simply written to to the screen as-is. Note that strings are limited\nto their current bounding box, so long strings don't wrap. You can use newlines\nto use multiple lines.\n\n``` clojure\n(t/render client \"hello, world!\\nWhat a lovely day we're having\")\n```\n\n### Sequence\n\nA seq (so the result of calling `list`, `map`, `for`, etc. Not vectors!) will\nsimply render each item in the list.\n\n``` clojure\n(t/render client (repeat 20 \"la\"))\n```\n\n### Elements\n\nElements are vectors, they contain the element type (first item in the vector),\na map of attributes (optional, second element in the vector), and any remaining\nchild elements.\n\nTrikl currently knows of the following elements.\n\n#### :span\n\nA `:span` can be used to add styling, i.e. foreground and background colors. Colors\nare specified using RGB (red-green-blue) values. Each value can go from 0 to\n255.\n\n``` clojure\n(t/render client [:span {:styles {:fg [100 200 0] :bg [50 50 200]}} \"Oh my!\"])\n```\n\n#### :box\n\nThe `:box` element changes the current bounding box. You give it a position with\n`:x` and `:y`, and a size with `:width` and `:height`, and anything inside the\nbox will be contained within those coordinates.\n\nIf you don't supply `:x` or `:y` it will default to the top-left corner of its\nsurrounding bounding box. If you omit `:width` and `:height` it will take up as\nmuch space as it has available.\n\nYou can also supply `:styles` to the box, as with `:span`. Setting a `:bg` on\nthe box will color the whole box.\n\nNote for instance that this example truncates the string to \"Hello,\", because it\ndoesn't fit in the box.\n\n``` clojure\n(t/render client [:box {:x 20 :y 10, :width 7, :height 3} \"Hello, world!\"])\n```\n\n#### :line-box\n\nA `:line-box` is like a box, but it gets a fancy border. This border is drawn on\nthe inside of the box, so you lose two rows and two columns of space to put\nstuff inside, but it looks pretty nice!\n\nBy default it uses lines with rounded corners, but you can use any border\ndecoration you like by supplying a `:lines` attribute. This can be a string or\nsequence containing the characters to use, starting from the top left corner and\nmoving clockwise. The default value for `:lines` is ` \"╭─╮│╯─╰│\"`\n\n``` clojure\n(t/render client [:line-box {:x 20 :y 10, :width 10, :height 3} \"Hello, world!\"])\n```\n\n#### :cols and :rows\n\nThe `:cols` and `:rows` elements will split their children into columns and rows\nrespectively. If any of the children have a `:width` or `:height` that will be\nrespected. Any remaining space is distributed equally among the ones that don't\nhave a fixed `:width`/`:height` already.\n\nSo you could divide the screen in four equally sized sections using:\n\n``` clojure\n(t/render (last @clients)\n          [:cols\n           [:rows\n            [:line-box \"1\"]\n            [:line-box \"2\"]]\n           [:rows\n            [:line-box \"3\"]\n            [:line-box \"4\"]]])\n\n```\n\n#### Custom Components\n\nYou can define custom components by creating a two-argument function, attributes\nand children, and using the function as the first element in the vector. You can\nreturn any of the above renderable things from the function.\n\n``` clojure\n(defn app [attrs children]\n  [:rows\n   [:line-box \"Hiiiiii\"]\n   [:line-box {:height 15 :styles {:bg [100 50 50]}}]])\n\n(t/render client [app])\n```\n\n## App state\n\nIf you keep your app state in an atom, then you can use `render-watch!` to\nautomatically re-render when the atom changes.\n\n``` clojure\n(def app-state (atom {:pos [10 10]}))\n\n(t/render-watch! client\n                 (fn [{[x y] :pos} _]\n                   [:box {:x x :y y} \"X\"])\n                 app-state)\n\n(swap! app-state update-in [:pos 0] inc)\n(swap! app-state update-in [:pos 1] inc)\n\n(t/unwatch! app-state)\n```\n\n## Querying the screen size and bounding box\n\nDuring rendering you can use `t/*screen-size*` and `t/*bounding-box*` to find\nthe current dimensions you are working in. There's also a `(box-size)` helpers\nwhich only returns the `[width height]` of the bounding box, rather than `[x y\nwidth height]`. This should greatly alleviate the need to write your own drawing\nfunctions, as you can now do everything with custom elements and a combination\nof `[:span ...]` and `[:box ...]`. The main reason to write custom drawing\nfunctions would be for performance, when what you are drawing does not easily\nfit in the span/box paradigm. If you find yourself creating a span per character\nthen maybe a custom drawing function makes sense.\n\n## Custom drawing functions\n\nTo implement custom elements, extend the `t/draw` multimethod. The method takes\ntwo arguments, the element (the vector), and a \"virtual screen\". Your method\nneeds to return an updated version of the virtual screen.\n\nThe main reasons to do this are because this gives you access to the current\nscreen size (bounding box), and for performance reasons.\n\nThe `VirtualScreen` has a \":charels\" key (character elements, analogues to\npixels), This is a vector of rows, each row is a vector of \"charels\", which have\na `:char`, `:bg`, `:fg` key. Make sure the `:char` is set to an actual `char`,\nnot to a String.\n\nAfter drawing the virtual screen is diffed with the previous virtual screen to\nfigure out the minimum commands to send to the terminal to update it.\n\n``` clojure\n(defmethod t/draw :x-marks-the-spot [element screen]\n  (assoc-in screen [:charels 5 5 :char] \\X))\n```\n\nThings to watch out for:\n\n- Normalize your element with `t/split-el`, this always returns a three element\n  vector with element type, attribute map, children sequence.\n\n- Stick to your bounding box! You probably want to start with this\n\n``` clojure\n  (let [[_ attrs children] (split-el element)\n        [x y width height] (apply-bounding-box attrs screen)]\n    ,,,)\n```\n\nYou should not touch anything outside `[(range x (+ x width)) (range y (+ y height))]`\n\n- If your element takes `:styles`, then use `push-styles` and `pop-styles` to\ncorrectly restore the styles from a surrounding context.\n\n\n## True Color\n\nIn theory ANSI compatible terminals are able to render 24 bit colors (16 million\nshades), but in practice they don't always do.\n\nYou can try this snippet, if you see nice continuous gradients from blue to\npurple then you're all set.\n\n``` clojure\n(defn app [_]\n  [:box\n   (for [y (range 50)]\n     [:span\n      (for [x (range 80)]\n        [:span {:styles {:bg [(* x 2) (* y 2) (+ x (* 4 y))]}} \" \"])\n      \"\\n\"])])\n```\n\niTerm 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`\n\n``` conf\nset -g default-terminal \"xterm-256color\"\nset-option -ga terminal-overrides \",xterm-256color:Tc\"\n```\n\n## Using netcat\n\nNot all systems come with telnet installed, notably recent versions of Mac OS X\nhave 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.\n\nYou can `brew install telnet`, or in a pinch you can use `stty` to configure\nyour terminal to not echo input, and to enable \"raw\" (direct, unbuffered) mode.\n\nMake sure to invoke `stty` and `nc` as a single command like this:\n\n```\nstty -echo -icanon \u0026\u0026 nc localhost 1357\n```\n\nTo undo the changes to your terminal do\n\n```\nstty +echo +icanon\n```\n\n## Graal compatibility\n\nTrikl contains enough type hints to prevent Clojure's type reflection, which\nmakes it compatible with GraalVM. This means you can compile your project to\nnative binaries that boot instantly. Great for tooling!\n\n## License\n\nCopyright \u0026copy; 2018 Arne Brasseur\n\nLicensed under the term of the Mozilla Public License 2.0, see LICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaisland%2Ftrikl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flambdaisland%2Ftrikl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaisland%2Ftrikl/lists"}