Introduction
In this walkthrough we will go in-depth on how to build command line interface (CLI)
applications in Urbit using the shoe library.
There are three CLI apps that currently ship with urbit - %dojo, %chat-cli,
and %shoe. You should be familiar with the former two, the latter is an
example app that shows off how the shoe library works that we will be looking
at closely. These are all Gall apps, and their source can be found in the app/
folder of your %base desk.
In the shoe library we take a closer look at the shoe library and its
cores and how they are utilized in CLI apps. Then in the sole
library we look at what shoe effects ultimately break down
into. Finally in %shoe app walkthrough we explore
the functionality of the %shoe app and then go through the code line-by-line.
This tutorial can be
considered to be an application equivalent of the Hoon school
lesson on sole and %ask
generators, which only covers the bare minimum necessary to write generators
that take user input.
The shoe library
Here we describe how sessions are identified, the specialized cards that Gall agents
with the shoe library are able to utilize, and the different cores of /lib/shoe.hoon and their purpose.
Session identifiers
An app using the shoe library will automatically track sessions by their sole-id=@ta.
These are opaque identifiers generated by the connecting client. An app using the shoe
library may be connected to by a local or remote ship in order to send commands,
and each of these connections is assigned a unique @ta that identifies the
ship and which session on that ship if there are multiple.
%shoe cards
Gall agents with the shoe library are able to utilize %shoe cards. These
additions to the standard set of cards have the following shape:
[%shoe sole-ids=(list @ta) effect=shoe-effect]`
sole-ids is the list of session ids that the following effect is
emitted to. An empty sole-ids sends the effect to all connected sessions.
shoe-effects, for now, are always of the shape [%sole effect=sole-effect], where
sole-effects are basic console events such as displaying text, changing the
prompt, beeping, etc. These are described in the section on the sole library.
For example, a %shoe card that causes all connected sessions to beep would
be [%shoe ~ %sole %bel ~].
shoe core
An iron (contravariant) door that defines an interface for Gall agents utilizing
the shoe library. Use this core whenever you want to receive input from the user and run a command. The input will get
put through the parser (+command-parser) and results in a noun of
command-type that the underlying application specifies, which shoe then feeds back into the underlying app as an +on-command callback.
In addition to the ten arms that all Gall core apps possess, +shoe defines and expects a few
more, tailored to common CLI logic. Thus you will need to wrap the shoe:shoe core using the agent:shoe function to obtain a standard
10-arm Gall agent core. See the shoe example app
walkthrough for how to do this.
The additional arms are described below. The Hoon code shows their expected type signature. As we'll see later, the command-type can differ per application. Note also that most of these take a session identifier as an argument. This lets applications provide different users (at potentially different "places" within the application) with different affordances.
+command-parser
++ command-parser
|~ sole-id=@ta
|~(nail *(like [? command-type]))
Input parser for a specific command-line session. Will be run on whatever the user tries to input into the command prompt, and won't let them type anything that doesn't parse. If the head of the result is true, instantly run the command. If it's false, require the user to press return.
+tab-list
++ tab-list
|~ sole-id=@ta
*(list (option:auto tank))
Autocomplete options for the command-line session (to match +command-parser).
+on-command
++ on-command
|~ [sole-id=@ta command=command-type]
*(quip card _this)
Called when a valid command is run.
+can-connect
++ can-connect
|~ sole-id=@ta
*?
Called to determine whether a session may be opened or connected to. For example, you may only want the local ship to be able to connect.
+on-connect
++ on-connect
|~ sole-id=@ta
*(quip card _^|(..on-init))
Called when a session is opened or connected to.
+on-disconnect
++ on-disconnect
|~ sole-id=@ta
*(quip card _^|(..on-init))
Called when a previously made session gets disconnected from.
default core
This core contains the bare minimum implementation of the additional shoe arms
beyond the 10 standard Gall app ams. It is used
analogously to how the default-agent core is used for regular Gall apps.
agent core
This is a function for wrapping a shoe core, which has too many
arms to be a valid Gall agent core. This turns it into a standard Gall agent core by
integrating the additional arms into the standard ones.
The sole library
shoe apps may create specialized cards of the [%shoe (list @ta) shoe-effect] shape, where shoe-effect currently just wrap sole-effects, i.e. instructions for displaying text and producing other effects in the console.
The list of possible sole-effects can be found in /sur/sole.hoon. A few
commonly used ones are as follows.
[%txt tape]is used to display a line of text.[%bel ~]is used to emit a beep.[%pro sole-prompt]is used to set the prompt.[%mor (list sole-effect)]is used to emit multiple effects.
For example, a sole-effect that beeps and displays This is some text. would
be structured as
[%mor [%txt "This is some text."] [%bel ~] ~]
%shoe app walkthrough
Here we explore the capabilities of the %shoe example app and then go through
the code, explaining what each line does.
Playing with %shoe
First let's test the functionality of %shoe so we know what we're getting
into.
Start two fake ships, one named ~zod and the other can have any name - we will
go with ~nus. Fake ships run locally are able to see each other, and our
intention is to connect their %shoe apps.
On each fake ship start %shoe by entering |start %shoe into dojo. This will
automatically
change the prompt to ~zod:shoe> and ~nus:shoe>. Type demo and watch the following appear:
~zod ran the command
~zod:shoe>~zod ran the command should be displayed in bold green text, signifying that
the command originated locally.
Now we will connect the sessions. Switch ~zod back to dojo with Ctrl-X and enter |link ~nus %shoe. If this succeeds you will see the following.
>=
; ~nus is your neighbor
[linked to [p=~nus q=%shoe]]Now ~zod will have two %shoe sessions running - one local one on ~zod and
one remote one on ~nus, which you can access by pressing Ctrl-X until you see
~nus:shoe> from ~zod's console. On the other hand, you should not see
~zod:shoe> on ~nus's side, since you have not connected ~nus to ~zod's
%shoe app. When you enter demo from ~nus:shoe> on
~zod's console you will again see ~zod ran the command, but this time it
should be in the ordinary font used by the console, signifying that the command
is originating from a remote session. Contrast this with entering demo from
~nus:shoe> in ~nus's console, which will display ~nus ran the command in
bold green text.
Now try to link to ~zod's %shoe session from ~nus by switching to the dojo
on ~nus and entering |link ~zod %shoe. You should see
>=
[unlinked from [p=~zod q=%shoe]]and if you press Ctrl-X you will not get a ~zod:shoe> prompt. This is
because the example app is set up to always allow ~zod to connect (as well as
subject moons if the ship happens to be a planet) but not ~nus, so this
message means that ~nus failed to connect to ~zod's %shoe session.
%shoe's code
:: shoe: example usage of /lib/shoe
::
:: the app supports one command: "demo".
:: running this command renders some text on all sole clients.
::
/+ shoe, verb, dbug, default-agent
/+ is the Ford rune which imports libraries from the /lib directory into
the subject.
shoeis theshoelibrary.verbis a library used to print what a Gall agent is doing.dbugis a library of debugging tools.default-agentcontains a Gall agent core with minimal implementations of required Gall arms.
|%
+$ state-0 [%0 ~]
+$ command ~
::
+$ card card:shoe
--
The types used by the app.
state-0 stores the state of the app, which is null as there is no state to
keep track of. It is good practice to include a version number anyways,
in case the app is made stateful at a later time.
command is typically a set of tagged union types that represent the possible
commands that can be entered by the user. Since this app only supports one
command, it is unnecessary for it to have any associated data, thus the command
is represented by ~.
In a non-trivial context, a command is commonly given by [%name data], where %name is the identifier for the type of command and data is
a type or list of types that contain data needed to execute the command. See
app/chat-cli.hoon for examples of commands, such as [%say letter:store] and
[%delete path]. This is not required though, and you could use something like
[chat-room=@t =action].
card is either an ordinary Gall agent card or a %shoe card, which takes
the shape [%shoe sole-ids=(list @ta) effect=shoe-effect]. A %shoe card is
sent to all soles listed in sole-ids, imaking them run the sole-effect
specified by effect (i.e. printing some text). Here we can
reference card:shoe because of /+ shoe at the beginning of the app.
=| state-0
=* state -
::
Add the bunt value of state-0 to the head of the subject, then give it the
macro state. The - here is a lark expression referring to the head of the
subject. This allows us to use state to refer to the state elsewhere in the
code no matter what version we're using, while also getting direct access to the
contents of state (if it had any).
%+ verb |
%- agent:dbug
^- agent:gall
%- (agent:shoe command)
^- (shoe:shoe command)
The casts here are just reminders of what is being produced. So let's focus on
what the % runes are doing, from bottom to top. We call (agent:shoe command)
on what follows (i.e. the rest of the app),
producing a standard Gall agent core. Then we call wrap the Gall agent core with
agent:dbug, endowing it with additional arms useful for debugging, and then
wrap again with verb.
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
des ~(. (default:shoe this command) bowl)
This is boilerplate Gall agent core code. We set this to be a macro for the
subject, which is the Gall agent core itself. We set def and des to be
macros for initialized default-agent and default:shoe doors respectively.
Next we implement all of the arms required for a shoe agent. Starting with
the standard Gall arms:
++ on-init on-init:def
++ on-save !>(state)
++ on-load
|= old=vase
^- (quip card _this)
[~ this]
::
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
These are minimalist Gall app arm implementations using the default behavior found in def.
Here begins the implementation of the additional arms required by the
(shoe:shoe command) interface.
++ command-parser
|= sole-id=@ta
^+ |~(nail *(like [? command]))
(cold [& ~] (jest 'demo'))
+command-parser is of central importance - it is what is used to parse user
input and transform it into commands for the app to execute. Writing a proper
command parser requires understanding of the Hoon parsing functions found in the
standard library. How to do so may be found in the parsing tutorial. For now, it is sufficient to know that this arm matches the text "demo" and
produces a [? command]-shaped noun in response. Note how the & signifies that the command will be run as soon as it has been entered, without waiting for the user to press return.
++ tab-list
|= sole-id=@ta
^- (list [@t tank])
:~ ['demo' leaf+"run example command"]
==
+tab-list is pretty much plug-n-play. For each command you want to be tab
completed, add an entry to the list begun by :~ of the form [%command leaf+"description"]. Now whenever the user types a partial command and presses
tab, the console will display the list of commmands that match the partial
command as well as the descriptions given here.
Thus here we have that starting to type demo and pressing tab will result in
the following output in the console:
demo run example command
~zod:shoe> demowith the remainder of demo now added to the input line.
Next we have +on-command, which is called whenever +command-parser
recognizes that demo has been entered by a user.
++ on-command
|= [sole-id=@ta =command]
^- (quip card _this)
This is a gate that takes in the sole-id corresponding to the session and the
command noun parsed by +command-parser and returns a list of cards and
_this, which is our shoe agent core including its state.
=- [[%shoe ~ %sole -]~ this]
This creates a cell of a %shoe card that triggers a sole-effect given by the head of
the subject -, then the Gall agent core this - i.e. the return result of
this gate. The use of the =- rune means that what follows this
expression is actually run first, which puts the desired sole-effect into the
head of the subject.
=/ =tape "{(scow %p src.bowl)} ran the command"
We define the tape that we want to be printed.
?. =(src our):bowl
[%txt tape]
[%klr [[`%br ~ `%g] [(crip tape)]~]~]
We cannot just produce the tape we want printed, - it needs to fit the
sole-effect type. This tells us that if the
origin of the command is not our ship to just print it normally with the %txt
sole-effect. Otherwise we use %klr, which prints it stylistically (here it
makes the text green and bold).
The following allows either ~zod, or the host ship and its moons, to connect to
this app's command line interface using |link.
++ can-connect
|= sole-id=@ta
^- ?
?| =(~zod src.bowl)
(team:title [our src]:bowl)
==
We use the minimal implementations for the final two shoe arms, since we don't
want to do anything special when users connect or disconnect.
++ on-connect on-connect:des
++ on-disconnect on-disconnect:des
--
This concludes our review of the code of the %shoe app. To continue learning
how to build your own CLI app, we recommend checking out /app/chat-cli.hoon.