Skip to content

goldfeld/i9n

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

i9n

A ClojureScript library for creating terminal-based, curses interfaces. Being built off Node.js allows applications made with i9n to be snappy, light and mostly instant to boot up. Don’t worry much if you’re not into node, most of the little beast is half-hidden behind bars.

This project’s main goal is to simultaneously allow simple interfaces to be done declaratively–hopefully within the grasp of someone who isn’t a programmer (though don’t ask me why this someone would want a terminal UI)–and provide a simple, solid backbone for complex curses applications like file managers, music players, ascii-based games and even text editors, all the while remaining a lib and not a framework, by having little enough opinion on the rest of your code except for the use of core.async.

The way i9n (standing for interaction) lays out interfaces is through a navigation abstraction, composed of screens–think like pages in a website, though any given screen can be highly interactive much like a Single Page Application in web dev. There’s a special declarative ease reserved for list-based navigation, because it’s such a common use case (e.g. menus), but screens can contain any number of UI widgets and interactions in addition to lists.

These widgets, interactions and the navigation hierarchy are all represented declaratively by plain Clojure data structures like vectors and maps, though of course “declarative” ends whenever you plug in functions to trigger custom actions (which you are very much encouraged to do all the time.) Here’s a full interface’s navigation hierarchy:

[["my german vocab" ["review words" :review
                     "add new word" :add
                     "quit" i9n-os/exit]]
 [:review "review" ["ich" (fn [] "I")
                    "bin" (fn [] "am")
                    "praktisch" (fn [] "practical")
                    "perfekt" (constantly "perfect")]]
 [:add "add word" "not implemented (or really necessary) yet."]]

The example above fully describes a flashcard (memorization) program which lets you achieve a four-word strong German vocabulary and form a full sentence. Of course, a more expansive vocabulary should probably be backed by reading word/translation pairs from a text file. The first screen, entitled “my german vocab”, is where the application will start, and is implicitly given the id :root. The others have ids :review and :add which we use as keyword actions on the root menu screen to link them up. Actions can also be arbitrary functions, as shown by the especially useful functions that return a string, which declaratively generates an action to add that string as an alert-like message beside the menu option selected (in these cases the German words you’re memorizing.)

Interfaces in i9n are meant to be asynchronous, lazy and responsive through the use of core.async and an input channel where you can put messages. This channel then serves as the backbone of the navigation’s loop, as every operation message (adding and modifying screens, moving from screen to screen, among others) is processed sequentially and synchronously, even as they’re accepted asynchronously. This guarantees, for instance, that you can send an operation message to add a new screen, then another message to go to this new screen, and be sure the screen exists in the navigation’s hierarchy.

Usage

Navigation

Operations

:next

Takes as first argument either the id of the screen to go next, or the full nav-entry–that is, a vector with three elements: the id, the title and a body. In this latter case, the nav-entry is added to the hierarchy (or updated, if the id already exists.)

:next messages are the standard way to navigate the interface, because it saves a link so you can go back to the previous screen. It also optionally takes the list position in the current screen (default to 0 if not supplied.), so that when going back you end up in the position you left, which is very natural and even expected behavior in navigation screens.

The third–also optional–argument is the list position to go to in the next screen, defaulting to 0 as well. Going to a specific position might be useful e.g. you’re jumping to some search result.

:hop

Like :next, except it doesn’t save a link to go back to, and so it doesn’t take :next’s current position argument, instead taking an optional position to go to as second and last argument. This also makes it useful for hopping to another position within the same screen.

Note that when using :next the new link created overwrites the former link, since there can only be one meaning when you press back. Here there’s no overwriting, so if you hop to a random screen, you might be able to back into a former connection that was made to it when :next was used. This is precisely what happens in the action of going back itself, which uses :hop for its internal implementation. This allows you to go next many screens and then be able to go all the way back.

:set

Like :hop, except that it must always take a full nav-entry as first argument, never a screen id. A :set can be useful for setting the interface to a temporary screen with a dummy id (say, :temp or really anything, though not nil) that won’t go into the hierarchy.

In reality I was lying and :hop is not used internally to implement the action of going back screens. It’s :set that’s used, because when a link is saved, the whole nav-entry is copied there, which allows local, temporary state to be preserved for when you go back. So this operation can be used for similar effect.

:fix

Updates nav-entries as surgically as you need, taking care to apply your fixes both in the hierarchy (i.e. persist the changes) and in any temporary state such as the user’s current screen. It can be used to replace, add or remove an option or part of it. Takes the id of the screen to change as first argument, the place (where to change) as second, and what to change as rest arguments.

place syntax

The most basic place designation is an integer (equal or greater than zero–with a negative number the fix operation will be ignored), which signifies a fix starting from that index. That means if you give it 4 as the place and then supply four following arguments in the :fix operation, the option starting at index 4 as well as the one starting at index 6 will be changed–or added if those indexes didn’t exist.

The only way to change the title of an entry, as opposed to the options of its body, is to use :title as the place. The table below features other keywords accepted as a place. Note that option means a pair of elements (label and action) from the body’s vector, so option 3 would start at index 6 and end at index 7.

keywordplace meaning
:lastLast option’s label, i.e. last even index
:last-actionLast option’s action, i.e. very last index
:popRemoves the last option, takes no argument after the place
:appendAdds after the last option
:prependAdds before the first option

The append and prepend keywords above are useful in that they always add new stuff, but what about adding to the middle of the list? The next table shows vector-based place syntax, allowing you to do that and other hopefully helpful things.

syntaxplace meaning
[:insert n]Adds before the start of option n
[:insert-after & xs]Adds after option matching any of elements xs
[:insert-before & xs]Adds before option matching any of elements xs
[:action n]Changes action(s) of option(s) starting from n
[:label n]Changes label(s) of option(s) starting from n
[:action-find & xs]Like :action but from first matching any of xs
[:label-find & xs]Like :label but from first matching any of xs
[:from & xs]Replaces from first option matching any of xs
[:after n & xs]Like :from but starts n options after
[:before n & xs]Like :from but starts n options before
[:shrink n start]Remove n options from option start
[:remove n & xs]Remove n options from first matching any of xs

When there are several needles xs from which to find a matching result in the haystack, each needle is first searched over the whole haystack before trying the next one. Trying needles is less dangerous than it sounds.

:put

Takes all the same arguments as :fix, but doesn’t persist the fix into the hierarchy. Thus, the fix is only applied to temporary state nav-entries, such as the one representing the user’s current screen. If there is no such temporary state target where to apply the fix, nothing is done by the :put operation.

:select

Example: (a/put! in [:select 2])

Select option at index n in whatever screen is the current. Accepts :last as an index.

:dirty

Example: (a/put! in [:dirty :screen1 :screen4])

Takes the id(s) of the screen(s) to be made dirty. Dirtying is exclusively for the library user, to facilitate his keeping track of which parts of the hierarchy will have to be lazily recomputed, if at all, when they’re finally accessed–this is coupled with passing a :flush channel inside the cfg parameter’s :watches property, on which to listen to flush messages.

Even though dirtying could be managed externally by the user, building it into the navigation loop takes care of a few things for you:

  1. a flush notification is sent out when a dirty screen is finally accessed, after first clearing the screen’s dirty status;
  2. you can send in :stub messages, which are just like :add messages, except that the screen is created dirty, which means you lazily create just a stub, and wait for a flush message to finish building the screen only when it’s first needed.
:state

Example: (a/put! in [:state :user-setting1 :foo :user-setting2 :bar])

Use for any application-specific state that you need to keep between screens–globally in fact, stored within the nav object as a map under the :state key.

Built-in user-facing facilities may interact with state to make the use of state easier and more high-level than sending :state messages; see enlightened.os.navigation/pick-option for example.

:column

Allows setting properties of columnar data in a given screen. Takes the id of the screen as first argument and the integer index of the column as second. The rest of the arguments should consist of pairs of keyword and value (keyword arguments). All arguments are optional. The keywords accepted are:

keywordarguments typedescription
:widthintAlways keep col width at this character length.
:sort(fn [a b])Sort table by this col, using supplied fn.

License

Copyright © 2014 Vic Goldfeld

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

About

Fast cljs + nodejs terminal (curses) declarative UIs

Resources

License

Stars

Watchers

Forks

Packages

No packages published