Merge pull request #58 from urbit/bm/docs

Render docs in Sky
This commit is contained in:
Will Hanlen 2024-06-14 07:13:26 +09:00 committed by GitHub
commit b5c180776a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 641 additions and 468 deletions

View File

@ -26,7 +26,7 @@
+$ card $+(card card:agent:gall)
+$ state-0
$+ state-0
$: =loam:dirt:neo :: layer 1
$: =loam:dirt:neo :: layer 1
=farm:neo :: layer 2
::
=town:neo :: subscription
@ -83,7 +83,7 @@
+* this .
run ~(. +> [bowl ~])
def ~(. (default-agent this %|) bowl)
++ on-init
++ on-init
^- (quip card _this)
=^ cards state
abet:boot:run
@ -131,7 +131,7 @@
[cards this]
++ on-peek on-peek:run
--
:: %- mute
:: %- mute
|_ [=bowl:gall cards=(list card)]
:: |aux: auxilliary helpers
+| %aux
@ -150,7 +150,7 @@
:- p/our.bowl
~[n/~ %sys]
:: |do: effect creation
+| %do
+| %do
++ do-watch
|= [=wire =dock =path]
(pass wire %agent dock watch/path)
@ -234,17 +234,17 @@
|= [=pith:neo sag=(unit saga:neo)]
^- card
=/ =wire gall-grow/(pout pith)
=/ =page
=/ =page
?~ sag none/~
neo-feat/(saga:soften u.sag)
(pass wire %grow (pout pith) page)
::
:: ?: =(p.flow
:: ?: =(p.flow
:: |on: event handlers
+| %on
::
++ on-poke
|= [=mark =vase]
|= [=mark =vase]
^+ run
?+ mark ~|(bad-poke-mark/mark !!)
%neo-move =;(f (f !<(_+<.f vase)) on-move)
@ -396,7 +396,7 @@
|= =poem:neo
neo-poem+!>(poem)
::
++ tell
++ tell
%- raise
|= =myth:neo
neo-myth+!>(myth)
@ -417,7 +417,7 @@
^+ run
=/ [gis=(list gift:dirt:neo) lom=loam:dirt:neo fam=farm:neo]
(tell:till epic)
=. loam lom
=. loam lom
=. farm fam
=. run (lazarus gis)
=. run (take:rage gis)
@ -482,7 +482,7 @@
=. mart.u.mal (~(put in mart.u.mal) [care src])
=. town (~(put of:neo town) pith u.mal)
sale
:: XX: search upwards for
:: XX: search upwards for
=| =mall:neo
=. mart.mall (~(put in mart.mall) [care src])
?. =(~ find-deli)
@ -528,7 +528,7 @@
~& missing-roar/pith
sale
=/ [=path dat=(unit page)] dat.u.roar.syn
?~ dat
?~ dat
~& missing-page/pith
sale
%- on-saga
@ -543,7 +543,7 @@
mismatch-saga-sale/[exe.p.u.shop.mall exe.p.p.res]
~
=. sale (put-mall mall)
=/ del
=/ del
~| town/town
~| mall/mall
~| pith/pith
@ -552,7 +552,7 @@
=/ kid (dif:pith:neo pith del)
~& kid/kid
abet:(fetched:~(meat sale del) (dif:pith:neo pith del) res)
:: XX: possibly check that
:: XX: possibly check that
++ find-deli
=| res=(unit pith:neo)
=/ at=pith:neo pith
@ -672,7 +672,7 @@
^+ sale
:: =. sale abet:(new:meat yuga)
=/ lis ~(tap of:neo yuga)
|-
|-
?~ lis
~& done-yuga-town/town
abet:(new:meat yuga)
@ -700,7 +700,7 @@
=/ wir (wire %fetch)
=. run (emit (do-watch-her wir get-ship fetch-path))
sale
++ watch-sync
++ watch-sync
=/ wir (wire %sync)
=. run (emit (do-watch-her (wire %sync) get-ship peer-path))
sale
@ -785,7 +785,7 @@
?: =(until pith)
%self
=/ left (dif:pith:neo pith until)
?: (~(has by (~(kid of:neo tide) pith)) left)
?: (~(has by (~(kid of:neo tide) pith)) left)
%par
%anc
::
@ -807,7 +807,7 @@
$(pith (snoc pith i.nex))
--
++ rage
|%
|%
++ stalk
|= [=hunt:neo =howl:neo]
^+ run
@ -882,7 +882,7 @@
~& resolved/hunt
:: XX: weird shadowing, be careful
=/ =rave:neo (~(gut of:neo riot) pith.hunt *rave:neo)
=. rave
=. rave
(fume-del rave care.hunt halt/~)
=. riot (~(put of:neo riot) pith.hunt rave)
(resolved:stop hunt)
@ -898,7 +898,7 @@
[pith.from [p/our.bowl pith] %poke %rely !>(rely)]
abet:(arvo move)
--
::
::
++ lazarus
|= git=grit:neo
^+ run
@ -978,7 +978,7 @@
run
=/ q (~(got by clog.halt) u.fow)
|-
?: =(~ q)
?: =(~ q)
=. clog.halt (~(del by clog.halt) u.fow)
run
=^ nex=move:neo q ~(get to q)
@ -999,7 +999,7 @@
::
++ pith
^- pith:neo
:- p/our.bowl
:- p/our.bowl
(~(pith press imp/stud) %out)
++ vase
^- ^vase
@ -1075,12 +1075,12 @@
!, *hoon
|= [to=stud:neo in=in]
^- vase
=/ =stud:neo
=/ =stud:neo
~| missing-con/[grab to]
(~(got by con.dive) [grab %$ to])
=/ conv ~(do con stud)
(slym run:conv in)
::
::
++ all-grow
|= grow=stud:neo
^- vase :: of $-(pail grow-type)
@ -1099,17 +1099,17 @@
^- out
~! p.pail
~! grow
=/ =stud:neo
=/ =stud:neo
~| missing-con/[p.pail grow]
(~(got by con.dive) [p.pail %$ grow])
=/ conv ~(do con stud)
!<(out (slam run:conv q.pail))
::
::
::
++ con
|_ =stud:neo
++ do
=/ vax=vase
++ do
=/ vax=vase
q.q:(need fil:(need (need (~(peek till:aux [loam farm]) %x [p/our.bowl pith]))))
~| con-pith/pith
|%
@ -1136,7 +1136,7 @@
|. ^- ?
=/ src=vase ~(get pro grab)
=/ dst=vase ~(get pro grow)
=/ need=type
=/ need=type
=< p
%+ slap (with-faces:ford:neo get-reef src/src dst/dst ~)
!,(*hoon *$-(src dst))
@ -1158,7 +1158,7 @@
!=(~ (~(peek plow:aux loam) p/our.bowl pith))
++ pith (~(pith press pro/stud) %out)
++ exists (exists-file (~(path press pro/stud) %src))
--
--
::
++ press
|_ =post:neo
@ -1168,7 +1168,7 @@
|= =pith:neo
^- [kind:ford:neo post:neo pith:neo]
~| ejecting/pith
=^ =disk:neo pith
=^ =disk:neo pith
?> ?=([%cod *] pith)
(eject:floppy t.pith)
?> ?=([kind:ford:neo tack:neo @ *] pith)
@ -1176,7 +1176,7 @@
=/ =tack:neo i.t.pith
:+ kind [tack ?@(disk i.t.t.pith [i.t.t.pith ship.disk term.disk])]
t.t.t.pith
++ slip
|= [=kind:ford:neo pax=pith:neo]
=/ [@ p=post:neo =pith:neo]
@ -1245,9 +1245,9 @@
++ finalize
=. ripe &
=/ base=pith:neo /cod/std/out/con
=/ cons
=/ cons
~(tap by ~(tar of:neo ~(snip of:neo (~(dip of:neo tide) base))))
|-
|-
?~ cons
=. run gen-grab
gen-grow
@ -1261,7 +1261,7 @@
++ gen-grab
=/ grabs ~(tap in ~(key by by-grab.dive))
~& genning/grabs
|-
|-
?~ grabs
run
=/ =vase (all-grab i.grabs)
@ -1271,7 +1271,7 @@
++ gen-grow
=/ grows ~(tap in ~(key by by-grow.dive))
~& genning-grows/grows
|-
|-
?~ grows
run
=/ =vase (all-grow i.grows)
@ -1333,7 +1333,7 @@
~| ~(key by ~(tar of:neo loam))
~| imports/file(hoon *hoon)
?> built-imports
=^ pre=pith run
=^ pre=pith run
(make-prelude (snip pax) file)
=/ =conf:neo
(~(gas by *conf:neo) [%sut (ours pre)] ~)
@ -1345,7 +1345,7 @@
^+ run
?~ pos
run
=/ pat
=/ pat
(~(path press fil/i.pos) %src)
?: ~(built pro i.pos)
$(pos t.pos)
@ -1360,7 +1360,7 @@
^+ run
?~ pos
run
=/ pat
=/ pat
(~(path press pro/i.pos) %src)
?: ~(built pro i.pos)
$(pos t.pos)
@ -1372,7 +1372,7 @@
^+ run
?~ pos
run
=/ pat
=/ pat
(welp #/cod/grab (stud-to-pith:neo i.pos))
?: !=(~ (~(peek plow:aux loam) p/our.bowl pat))
$(pos t.pos)
@ -1384,7 +1384,7 @@
^+ run
?~ pos
run
=/ pat
=/ pat
(welp #/cod/grow (stud-to-pith:neo i.pos))
?: !=(~ (~(peek plow:aux loam) p/our.bowl pat))
$(pos t.pos)
@ -1400,7 +1400,7 @@
^+ run
?~ lis
run
=/ pat
=/ pat
(~(path press lib/i.lis) %src)
?: ~(built lib i.lis)
$(lis t.lis)
@ -1477,7 +1477,7 @@
++ make-prelude
|= [pax=pith =file:ford:neo]
^- [pith _run]
=/ pre-path=pith
=/ pre-path=pith
(slip:press %pre pax)
[pre-path (make-deps pre-path (file-to-deps file))]
++ write-hoon
@ -1524,9 +1524,9 @@
(emit (do-card #/[p/our.bowl] %make %root ~ ~))
=. run
(emit (do-card #/[p/our.bowl]/sky %make %sky ~ ~))
=. run
=. run
(emit (do-card #/[p/our.bowl]/srv/hawk %make %hawk-eyre ~ ~))
=. run
=. run
(emit (do-card #/[p/our.bowl]/srv/sky %make %sky-eyre ~ ~))
run
++ pess |=(=post:neo (~(pith press post) %out))
@ -1557,7 +1557,7 @@
^- form:neo
|_ [=bowl:neo =aeon:neo =pail:neo]
++ poke
|= pok=pail:neo
|= pok=pail:neo
^- (quip card:neo pail:neo)
`pail
::
@ -1668,7 +1668,7 @@
!?=([%poke %ack *] q.q.init-move)
++ abet
^+ run
?: =([~ ~] block)
?: =([~ ~] block)
=? run can-ack
(emil `(list card)`(do-ack [p p.q]:init-move err.block))
=. run (emil (turn up do-move))
@ -1766,7 +1766,7 @@
$(arvo new-arvo, done (snoc done nex))
++ poke
|= =pail:neo
^+ arvo ::
^+ arvo ::
=^ cards=(list card:neo) arvo
(soft-surf |.(su-abet:(su-poke:surf pail)))
(ingest cards)
@ -1774,7 +1774,7 @@
:: XX: a hack
::
:: this is implicity recursive, and all external dependencies of
:: the children need to be woken up. this also breaks referential
:: the children need to be woken up. this also breaks referential
:: transparency
++ tomb
|= *
@ -1842,7 +1842,7 @@
=/ pit=pith:neo (~(got by conf) term)
=/ res (look care pit)
=/ nam=name:neo (de-pith:name:neo pit)
?~ res
?~ res
?: =(our.bowl ship.nam)
?. required
$(deps t.deps)
@ -1869,7 +1869,7 @@
=/ [=term =pith:neo] i.cew
=/ d=(unit [req=? =quay:neo]) (~(get by band) term)
:: skip extraneous, XX: is correct?
?~ d
?~ d
$(cew t.cew)
=/ [req=? =quay:neo] u.d
=/ =hunt:neo [(get-care:quay:neo quay) pith]
@ -1942,16 +1942,16 @@
|%
++ su-core .
++ su-emil |=(caz=(list card:neo) su-core(cards (welp cards caz)))
++ su-bowl
++ su-bowl
=/ hare [p/our.bowl here]
^- bowl:neo
:* src
our.bowl
hare
hare
our.bowl
hare
hare
now.bowl
eny.bowl
su-deps
su-deps
su-kids
==
++ su-icon
@ -2146,7 +2146,7 @@
++ on-start-peek
|= [src=pith:neo freq=@dr]
^+ run
=/ =peek:gall:neo
=/ =peek:gall:neo
(~(gut by peek.gall.unix) pith [~ ~h24])
=. refresh.peek (min freq refresh.peek)
=/ new=? =(~ src.peek)
@ -2159,7 +2159,7 @@
++ on-stop-peek
|= src=pith:neo
^+ run
=/ =peek:gall:neo
=/ =peek:gall:neo
(~(gut by peek.gall.unix) pith [~ ~h24])
=. src.peek (~(del in src.peek) src)
=. peek.gall.unix
@ -2176,7 +2176,7 @@
++ on-read-peek
=/ =road:neo pith
?> ?=([dude=@ rest=*] road)
=/ pax
=/ pax
%+ welp /(scot %p our.bowl)/[dude.road]/(scot %da now.bowl)
(pout rest.road)
=/ =pail:neo noun/!>(.^(* %gx pax))
@ -2232,7 +2232,7 @@
?> ?=([%behn %wake *] syn)
=/ =time (slav %da da.wir)
=/ timers ~(tap in (~(get ju behn.unix) time))
|-
|-
?~ timers
=. behn.unix (~(del by behn.unix) time)
run
@ -2374,7 +2374,7 @@
=* loop $
=/ =stud:neo p.q.have
?- -.want
%pro
%pro
(puff p.want rol have)
::
%rol
@ -2410,7 +2410,7 @@
::
::?. (~(has by con.fiesta) [p.have want])
:: ~
::
::
::=/ conv run:~(do con (~(got by con.fiesta) [p.have want]))
::`[want (slam conv q.have)]
::
@ -2458,7 +2458,7 @@
leaf
=. leaf (~(put of:neo leaf) i.lst)
$(lst t.lst)
::
++ gas-epic
=| =epic:neo
@ -2597,4 +2597,3 @@
leaf/"{<p.pail>}"
==
--

View File

@ -1,318 +0,0 @@
# Tutorial 1: Counter
One of the simplest shrubs imaginable is a counter that stores one number and takes one poke: increment the number.
By the end of this tutorial you'll understand the structure of a shrub, and how to write a trivial one of your own. This won't explain shrubbery from first principles — you dont need to understand it from first principles — but you'll see how similar a shrub is to a Gall agent and where they differ.
Youll also get a glimpse of how one shrub can accomodate various frontend interfaces. Well make a simple frontend for Sky, a prototype namespace browser.
## Counter in Gall and Shrubbery
Here's the Gall agent we'll reimplement in shrubbery. It stores one number and takes one poke, `%inc`, to increment the number.
```hoon
/+ dbug, default-agent, verb
|%
+$ versioned-state
$% state-0
==
+$ state-0
$: %0
value=@ud
==
+$ counter-action
$% [%inc ~]
==
+$ card card:agent:gall
--
::
%+ verb &
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init on-init:def
++ on-peek on-peek:def
++ on-watch on-watch:def
++ on-arvo on-arvo:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-fail on-fail:def
++ on-save
!>(state)
::
++ on-load
|= old=vase
^- (quip card _this)
:- ~
%= this
state !<(state-0 old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark
(on-poke:def mark vase)
::
%noun
=/ act
!<(counter-action vase)
?+ -.act
(on-poke:def mark vase)
::
%inc
:- ~
%= this
value +(value)
==
==
==
--
```
Here's the same thing in shrubbery.
```hoon
/@ number
/@ counter-diff
^- kook:neo
|%
++ state pro/%number
++ poke (sy %counter-diff ~)
++ kids *kids:neo
++ deps *deps:neo
++ form
^- form:neo
|_ [=bowl:neo =aeon:neo stud:neo state-vase=vase]
+* state !<(number state-vase)
++ init
|= old=(unit pail:neo)
^- (quip card:neo pail:neo)
`(need old)
++ poke
|= [=stud:neo vaz=vase]
^- (quip card:neo pail:neo)
?> =(%counter-diff stud)
=/ act !<(counter-diff vaz)
?> =(-.act %inc) :: XX can we remove this line?
`number/!>(+(state))
--
--
```
Lets set up a fakeship that has `/base/app/neo.hoon`, the prototype “shrub runner”, then go over the code piece by piece.
## Counter shrub, explained
Now lets take a closer look at the counter shrub. Youll find a version of `counter.hoon` with comments in your `/imp` folder.
```hoon
:: /imp/counter.hoon
/@ number
/@ counter-diff
```
These lines import two files from our `/pro` folder: `number.hoon` and `counter-diff.hoon`.
```hoon
:: /pro/number.hoon
,@ud
```
```hoon
:: /pro/counter-diff.hoon
,[%inc ~]
```
The folder structure you have to work with right now is messier than it will be in the final product. This is an artefact of prototyping shrubbery in a Gall agent in the `%base` desk.
The only folders you need to understand for this tutorial are `/pro`, `/imp`, and `/con`.
- `/pro` for protocols. Like `/sur`, this is where your custom types live.
- `/imp` for implementations. Like `/app`, this is where your Gall agent-like shrubs live.
- `/con` for conversions. Like `/mar`, this is where you define rules for transforming nouns in your desk.
Lets look at the rest of the `/imp` file.
```hoon
/@ number
/@ counter-diff
::
:: outer core of a shrub
^- kook:neo
|%
::
:: the state of counter is a %number, just a @ud
++ state
^- curb:neo
[%pro %number]
::
:: the set of pokes that counter
:: takes only contains %counter-diff
++ poke
^- (set stud:neo)
(sy %counter-diff ~)
::
::
:: counter does not "constrain" its children;
:: any shrub can be made below this shrub in the
:: namespace, they can have any state and any kids
++ kids
^- kids:neo
*kids:neo
::
:: counter has no other shrubs as dependencies
++ deps
^- deps:neo
*deps:neo
::
:: inner core of a shrub
++ form
^- form:neo
:: treat this door's sample as boilerplate
|_ [=bowl:neo =aeon:neo stud:neo state-vase=vase]
::
:: de-vase the state; we don't know what it is,
:: in most cases it will be counter's old state
+* state !<(number state-vase)
::
:: +init, like +on-init
++ init
::
:: minimal +init, just returns the
:: initial state passed in on %make
|= old=(unit pail:neo)
^- (quip card:neo pail:neo)
[~ (need old)]
::
:: +poke, like +on-poke
++ poke
::
:: a stud (e.g. %number or %counter-diff) is kind
:: of like a mark, it only gets more complicated
:: than that with types from other desks/ships
|= [=stud:neo vaz=vase]
::
:: return a (list card:neo) and a
:: pail, which is a (pair stud vase)
^- (quip card:neo pail:neo)
::
:: assert that the poke's stud is %counter-diff,
:: which protects counter from evil vases
?> =(%counter-diff stud)
=/ act
!<(counter-diff vaz)
?> =(-.act %inc)
::
:: return no cards and a pail
[~ [%number !>(+(state))]]
--
--
```
Once youve saved `/imp/counter.hoon`, run `|commit %base` and Neo will add it to its state. We can now interact with this shrub in the Dojo.
## Poking the shrub
A `card:neo` is a `(pair pith note)`.
A `pith` is a list of head-tagged cells forming a typed path. This is the location of the shrub to which your card will be sent.
* The path `/examples/counter/one` will be a pith `~[%examples %counter %one]`.
* The path `/~sampel/examples/counter/one` will be a pith `~[[%p ~sampel] %examples %counter %one]`.
* The path `/~sampel/examples/counter/1` will be a pith `~[[%p ~sampel] %examples %counter [%ud 1]]`.
A `note` is one of the four types of command any shrub will accept.
```hoon
+$ note
$% [%make made] :: create a shrub
[%poke =pail] :: poke a shrub
[%tomb cas=(unit case)] :: tombstone a shrub
[%cull ~] :: ???
==
```
If the `pith` doesnt correspond to the location of an existing shrub, youll have to make a shrub there before doing anything else.
Lets `%make` a shrub at path `/examples/counters/one` from the Dojo, giving it an initial state of `0`. Well explain the structure of the `%make` note in more detail in the Diary tutorial.
```
:neo &neo-card [~[[%p our] %examples %counters %one] [%make %counter `[%number !>(0)] ~]]
```
You should see `>> %make /examples/counters/one` in the Dojo if successful.
Now we can now send a `%poke` to the counter shrub at this path.
```
:neo &neo-card [~[[%p our] %examples %counters %one] [%poke [%counter-diff !>([%inc ~])]]]
```
At time of writing there is no easy way to inspect the state of a shrub from the Dojo. Well just have to build a frontend and hope it all just works.
## Counter frontend in Sky
Shrubbery aims to be interface-agnostic. One part of that vision is `/con` files, which make it possible to convert data from one backend type to any frontend type, and one frontend type to any backend type. Here are Counters `/con` files.
### Converting number to HTMX
```hoon
:: /con/number-htmx.hoon
/@ number :: @ud
/- feather-icons
:- [%number %$ %htmx]
|= =number
|= =bowl:neo
^- manx
;div.p3.fc.g2.ac.br2
;h1: Counter
;p: {<number>}
;form
=hx-post "/neo/hawk{(en-tape:pith:neo here.bowl)}?stud=counter-diff"
=hx-target "find .loading"
=hx-swap "outerHTML"
=head "inc"
;button.bd1.br1.p2.b1.hover.loader
;span.loaded: Increment
;span.loading
;+ loading.feather-icons
==
==
==
==
```
This “converts” the `number` type to a `manx`, specifically targeting a frontend that uses the [HTMX](https://htmx.org/) library. You dont need to know HTMX to build shrubbery frontends or to follow the rest of this tutorial. If you want to understand the HTMX above in more detail, see `/con/number-htmx.hoon` for line-by-line comments.
This isnt a 1:1 conversion from one data type to another; were not converting Hoon `number=1` to JSON `{ "number": 1 }`. If a frontend asks for a `number` in the form of HTMX, we return some [Sail](https://docs.urbit.org/language/hoon/guides/sail) that interpolates the `number` in a basic interface consisting of a heading, the number, and one button to send an `%inc` poke to the Counter shrub.
### Converting Node to %counter-diff
```hoon
:: /con/node-counter-diff.hoon
/@ node :: manx
/@ counter-diff :: [%inc ~]
/- manx-utils
:- [%node %$ %counter-diff]
|= =node
^- counter-diff
=/ mu ~(. manx-utils node)
=/ head (?(%inc) (got:mu %head))
[head ~]
```
This is a more straightforward conversion from a dynamic XML node (in this case, HTMX), to a `%counter-diff`. Using the [manx-utils](https://github.com/tinnus-napbus/manx-utils) Hoon library for brevity, we extract the XML nodes `head` attribute (which has been converted to the term `%inc` on its way here) and use that to form the `%counter-diff`, which is `[%inc ~]`. See `/con/node-counter-diff.hoon` for line-by-line comments.
## Testing the Counter in Sky
The Sky homepage shows you one tile for all of the shrubs who are the immediate children of your `/home` shurb, which was made for you upon booting `%neo` for the first time. You wont see a Counter tile there because there is no `/counter` shrub beneath `/home`, so lets make one.
```
:neo &neo-card [~[[%p our] %home %counter] [%make %counter `[%number !>(0)] ~]]
```
If you refresh your browser you should now see a tile labelled “counter”. Click there to see the Counter frontend from the `/con` file and increment the state of the `/counter` shrub.
## Building on the Counter
If you know your way around Gall, you should now be able to make some minor changes to the counter example above. Try the following:
* Initialize the shrub with a default state if the given `(unit vase)` in `+init` is empty.
* Add more pokes like `%dec`, `%add`, and `%sub` on the backend.
* Add those pokes to the frontend interface, with one button per poke.

View File

@ -0,0 +1,423 @@
# Chapter 1: Counter
One of the simplest shrubs imaginable is a counter that stores one number and takes one poke: make the number go up.
By the end of this chapter you'll understand the structure of a shrub, and how to write a trivial one of your own. This won't explain Shrubbery from first principles — you don't neeed to understand it from first principles to work with it — but you'll see how similar a shrub is to a Gall agent, and where they differ.
You'll also get a glimpse of how one shrub can accomodate various frontend interfaces. We'll make a simple HTMX frontend for Sky, a namespace browser and dev environment.
This chapter is the only real "tutorial" in that Counter doesn't currently exist on your ship. You can build Counter yourself following along this guide. The remaining three chapters will discuss shrubs that already exists in your `%neo` desk: Diary, Messenger, and Tasks.
In the Diary tutorial, you'll see how to write and read data to and from the namepsace. In Messenger, you'll see how shrubs can interact via the dependencies system. In the Tasks chapter, we'll look at how a full-featured UI works in the current system.
This chapter is focused on pattern-matching what you know about Gall to the new system.
## Counter in Gall and Shrubbery
Here's the Gall agent you'll reimplement in Shrubbery. It stores one number and takes one poke, `%inc`, to increment the number.
```
/+ dbug, default-agent, verb
|%
+$ versioned-state
$% state-0
==
+$ state-0
$: %0
value=@ud
==
+$ counter-action
$% [%inc ~]
==
+$ card card:agent:gall
--
::
%+ verb &
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init on-init:def
++ on-peek on-peek:def
++ on-watch on-watch:def
++ on-arvo on-arvo:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-fail on-fail:def
++ on-save
!>(state)
::
++ on-load
|= old=vase
^- (quip card _this)
:- ~
%= this
state !<(state-0 old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark
(on-poke:def mark vase)
::
%noun
=/ act
!<(counter-action vase)
?- -.act
%inc
:- ~
%= this
value +(value)
==
==
==
--
```
Here's the same thing in Shrubery.
```
/@ number
/@ counter-diff
^- kook:neo
|%
++ state
^- curb:neo
[%pro %number]
++ poke
^- (set stud:neo)
(sy %counter-diff ~)
++ deps
^- deps:neo
*deps:neo
++ form
^- form:neo
|_ [=bowl:neo =aeon:neo =stud:neo state-vase=vase]
+* state !<(number state-vase)
++ init
|= old=(unit pail:neo)
^- ((list card:neo) pail:neo)
[~ (need old)]
++ poke
|= [=stud:neo vaz=vase]
^- ((list card:neo) pail:neo)
=/ act
!<(counter-diff vaz)
?> =(-.act %inc)
[~ [%number !>(+(state))]]
--
--
```
## Shrub structure
Let's look at the structure of `/imp/counter`.
```
/@ number
/@ counter-diff
```
These lines import two types from our `/pro` folder: `number` and `counter-diff`. To import from `/pro` we use `/@` as a new Ford-style rune.
A shrub is a five-arm `|%` core — called a `kook:neo` — with an inner two-arm core called a `form:neo`. The `kook` defines type information about the shrub, and the inner `form` contains business logic.
At first glance the `kook` might look familiar to Gall developers, but this is all new logic defining 1) what's stored at this node in the namespace 2) what can be stored below this node, and 3) what we expect to be stored at existing nodes we declare as dependencies.
```
:: $kook:neo
|%
::
:: type this value in the namespace
++ state
!!
::
:: type acceptable requests to
:: change this value in the namespace
++ poke
!!
::
:: constrain the state/pokes of the shrubs that
:: can be created under this shrub in the namespace
++ kids
!!
::
:: declare the state/pokes we expect for existing shrubs
:: whose state we will track, and whose state changes we
:: will react to
++ deps
!!
::
:: handle state changes in this shrub,
:: its kids, and its dependencies
++ form
!!
--
```
The `form` is where the Gall agent-like application logic lives. We only need two arms, which are slightly modified versions of `+on-init` and `+on-poke`.
```
:: $form:neo
|_ [=bowl:neo =aeon:neo =stud:neo state-vase=vase]
::
:: like +on-init, run some logic when this shrub is created
:: unlike +on-init, potentially accept some injected initial state
++ init
|= old=(unit pail:neo)
^- ((list card:neo) pail:neo)
!!
::
:: like +on-poke, run some logic when this shrub is poked
++ poke
|= [=stud:neo vaz=vase]
^- ((list card:neo) pail:neo)
!!
--
```
## Counter logic
Now that we understand the shape of a shrub, let's look at the application logic of the Counter shrub. You can copy the following into the relevant files or type it out for yourself.
There are lots of new types here which are flagged with the `:neo` suffix in code and documentation. We'll cover those in detail in the following chapters.
### /pro/number.hoon
```
,@ud
```
### /pro/counter-diff.hoon
```
,[%inc ~]
```
### /imp/counter.hoon
```
/@ number :: import number type
/@ counter-diff :: import counter-diff type
::
:: outer core
^- kook:neo
|%
::
:: the state of counter is a number
++ state
^- curb:neo
[%pro %number]
::
:: the set of pokes counter takes only contains %counter-diff
:: a stud:neo is like a mark
++ poke
^- (set stud:neo)
(sy %counter-diff ~)
::
:: counter has no dependencies
++ deps
^- deps:neo
*deps:neo
::
:: inner core
++ form
^- form:neo
::
:: the sample is populated with context like bowl, version number, and
:: counter's current state
|_ [=bowl:neo =aeon:neo =stud:neo state-vase=vase]
::
:: de-vase counter's state
+* state !<(number state-vase)
::
:: +init, like +on-init
++ init
::
:: return no cards and the initial given state
:: pail:neo is a (pair stud:neo vase),
:: like a cell of a mark and data
|= old=(unit pail:neo)
^- ((list card:neo) pail:neo)
[~ (need old)]
::
:: +poke, like +on-poke
++ poke
|= [=stud:neo vaz=vase]
^- ((list card:neo) pail:neo)
::
:: de-vase the poke
=/ act
!<(counter-diff vaz)
::
:: crash if we're not incrementing
?> =(-.act %inc)
::
:: return no cards, return a (pair stud:neo vase)
:: where the vase contains the incremented state
[~ [%number !>(+(state))]]
--
--
```
Once you've saved `/imp/counter.hoon` and the `/pro` files, run `|commit %base` and %neo will add it to its state. We can now interact with this shrub in the Dojo.
## Poking the shrub
A `card:neo` is a `(pair pith note)`.
A `pith` is a `(list iota)`, and an `iota` is either a `term` or a head-tagged noun. For instance:
* `/examples/counter/one` would be represented as `~[%examples %counter %one]`.
* `/~sampel/examples/counter/one` would be represented as `~[[%p ~sampel] %examples %counter %one]`.
* `/~sampel/examples/counter/1` would be represented as `~[[%p ~sampel] %examples %counter [%ud 1]]`.
(You might also see a `pith` written in this irregular form `#/[p/our.bowl]/examples/counter/one`.)
Data in Shrubbery is stored by `pith`.
A `note` is one of the four types of command any shrub will accept.
```
+$ note
$% [%make made] :: create a shrub
[%poke =pail] :: poke a shrub
[%tomb cas=(unit case)] :: tombstone a case of the shrub
[%cull ~] :: forward delete
==
```
Lets `%make` a shrub at path `/foo/bar` from the Dojo, giving it an initial state of `0`. Well explain the structure of the `%make` note in more detail in the Diary tutorial.
```
:neo &neo-card [~[[%p our] %foo %bar] [%make %counter `[%number !>(0)] ~]]
```
You should see `>> %make /foo/bar` in the Dojo if successful.
Now we can now send a `%poke` to the counter shrub at this path.
```
:neo &neo-card [~[[%p our] %foo %bar] [%poke [%counter-diff !>([%inc ~])]]]
```
## Counter frontend in Sky
Shrubbery aims to be interface-agnostic. One part of that vision is `/con` files, which make it possible to convert data from one type to another. Here are Counters `/con` files.
### /con/number-htmx.hoon
This converts data stored as the `number` protocol (which is just a `@ud`) to the `htmx` protocol. When you open a shrub in Sky, Sky will attempt to convert its data to the `htmx` type (because Sky includes the [HTMX](https://htmx.org/) library in its frontend) using the appropriate `/con` file. In practice, this means that our `/con` file will take in our shrub's state (and bowl) and output some [Sail](https://docs.urbit.org/language/hoon/guides/sail) that interpolates the `number` in a basic interface consisting of a heading, the number itself, and one button to send an `%inc` poke to the Counter shrub.
```
/@ number :: @ud
:: import /lib/feather-icons (see feather-intro.txt)
/- feather-icons
:: declare that this is a conversion from number to HTMX
:- [%number %$ %htmx]
::
:: this gate accepts a number and
:: a gate that accepts a bowl:neo;
:: we'll use bowl:neo to get the
:: here.bowl of the shrub that's using this /con file
|= =number
|= =bowl:neo
::
:: this gate returns a manx, which is what Hoon uses
:: to store dynamic XML nodes; in this case we'll use
:: Sail to specify a manx that expects the HTMX library
:: to be available on the frontend
^- manx
::
:: open a <div class="p3 fc g2 ac br2">
:: these utility classes are specified in feather.css,
:: which this /con file expects on the frontend
;div.p3.fc.g2.ac.br2
:: <h1>Counter</h1>
;h1: Counter
:: <p>{number}</p>
;p: {<number>}
:: open a <form> with HTMX attributes
;form
::
:: hx-post will issue a POST request to the provided
:: url and swap the response into the DOM
=hx-post "/neo/hawk{(en-tape:pith:neo here.bowl)}?stud=counter-diff"
::
:: hx-target specifies the target for hx-post's DOM
:: swap: the element with class "loading"
=hx-target "find .loading"
::
:: hx-swap specifies how the response to hx-post's
:: request will be swapped in relative to the target
=hx-swap "outerHTML"
::
:: here, the head attribute specifies the poke that
:: hx-post will send to the target shrub; look at
:: /con/node-counter-diff.hoon for more on =head
=head "inc"
::
:: below, the classes "loaded", "loader", and
:: "loading" provide loading spinner behavior on
:: sending and receiving this form's POST request
::
:: <button class="bd1 br1 pr b1 hover loader">
;button.bd1.br1.p2.b1.hover.loader
:: <span class="loaded">Increment</span>
;span.loaded: Increment
:: <span class="loading">
;span.loading
:: import +loading sail from /lib/feather-icons
;+ loading.feather-icons
== :: </span>
== :: </button>
== :: </form>
== :: </div>
```
### /con/node-counter-diff.hoon
This is a more straightforward conversion from a dynamic XML node (in this case, HTMX), to a `%counter-diff`. Using a modified version of the [manx-utils](https://github.com/tinnus-napbus/manx-utils) Hoon library for brevity, we extract the XML nodes `head` attribute and use that to form the `%counter-diff`, which is `[%inc ~]`.
```
/@ node :: manx
/@ counter-diff :: [%inc ~]
:: import /lib/manx-utils, which helps us work with XML
/- manx-utils
:: declare this is a conversion from node to counter-diff
:- [%node %$ %counter-diff]
|= =node
^- counter-diff
:: initiate the manx-utils door with node
=/ mu ~(. manx-utils node)
::
:: got:mu gets an attribute from the manx by its name
:: in this case, the =head specified in /con/number-htmx
:: we expect the head from the manx to be %inc,
:: but we could add more terms to that type union...
=/ head (?(%inc) (got:mu %head))
::
:: return the [%inc ~] poke
[head ~]
```
## Testing the Counter in Sky
The Sky homepage shows you one tile for all of the shrubs who are the immediate children of your `/home` shurb, which was made for you upon booting `%neo` for the first time. You wont see a Counter tile there because there is no `/counter` shrub beneath `/home`, so lets make one.
```
:neo &neo-card [~[[%p our] %home %counter] [%make %counter `[%number !>(0)] ~]]
```
If you refresh your browser you should now see a tile labelled “counter”. Click there to see the Counter frontend from the `/con` file and increment the state of the `/counter` shrub.
## Building on the Counter
You should now be able to make some minor changes to the counter example above. Try the following:
- Initialize the shrub with a default state if the given `(unit vase)` in `+init` is empty.
- Add pokes for `%dec`, `%add`, and `%sub`.

View File

@ -1,13 +1,14 @@
# Tutorial 2: Diary
# Chapter 2: Diary
Now that you understand the structure of a shrub, the natural next step is to look at a shrub with kids.
Unlike the Counter example, there is no equivalent way to implement `/imp/diary` in Gall. Were now going to work directly with Urbits programmable, global, referentially transparent namespace — which is a lot of words to say [`+axal`](https://docs.urbit.org/language/hoon/reference/arvo#axal) — as a tool to read and write data.
Unlike the Counter example, there is no equivalent way to implement `/imp/diary` in Gall. Were now going to work directly with Urbits programmable, global, referentially transparent namespace as a tool to read and write data.
## /imp/diary
Diary is an app that lets you write timestamped diary entries that the frontend will show in a chronological feed. Heres a succinct version of the apps backend logic, which clocks in at 34 lines of code.
```hoon
```
:: /imp/diary.hoon
/@ txt :: @t
/@ diary :: name=@t
@ -47,13 +48,14 @@ Diary is an app that lets you write timestamped diary entries that the frontend
Most of this should be legible after the first tutorial. The only new ideas are the `+kids` arm, the use of `bowl`, and `%tomb`.
## +kids
Every shrub is a node in one tree, with one “root shrub” at the top of that tree. Every shrub below the root shrub is either one of its immediate children or one of its descendants.
In its `+kids` arm, every shrub can define constraints for the shrubs below it in the namespace, whether thats constraining the types of their state or the pokes theyll accept from other shrubs.
Lets expand on diarys `+kids` arm.
```hoon
```
:: /imp/diary.hoon
::
:: constrain shrubs below diary in the namespace
@ -97,9 +99,10 @@ The `pish:neo` statically types the paths which well allow to be created bene
(Its worth flagging that `$curb:neo` contains several combinatorial rules about state types. For example `[%pro %txt]` would mean “the state of the child shrub can be any type which is readily convertible into a `%txt` with a conversion we have available in the `/con` folder”. This gives us a clue as to how we could handle state transitions and interoperability over the lifetime of our shrub, but its outside the remit of this tutorial.)
## bowl:neo
Notice that the `src` in `bowl:neo` differs from `bowl:gall`. Heres the new type in full.
```hoon
```
:: /sur/neo.hoon
+$ bowl
$: src=[=ship =pith] :: a request's source ship and shrub
@ -113,11 +116,12 @@ Notice that the `src` in `bowl:neo` differs from `bowl:gall`. Heres the new t
```
## Generating cards, tombstoning shrubs
We covered `card:neo` in the Counter tutorial, but this is the first time were seeing one generated within a shrub. Diary takes two pokes: `%put-entry`, to create a new diary entry, and `%del-entry` to tombstone one.
Heres the `+poke` arm of the Diary shrub, expanded with comments.
```hoon
```
:: /imp/diary.hoon
++ poke
|= [=stud:neo vax=vase]
@ -191,12 +195,13 @@ Diarys state never changes. Its state is just its name, a `@t`. It stores all
The location of this shrub, `here.bowl`, is just a list. So its easy to `+welp` paths like `/<@da>` or `/<@da>/comments` onto the end. In this context, just keep in mind that they need to be `iota`s like `[%da now.bowl]`.
The `%make` card has two mysteries: it initializes diary entries with an empty `%txt` implementation and an empty map of dependencies. Well punt on dependencies until the next tutorial. If you look at `/imp/txt.hoon`, its just a `~`. This is an *elegant short-term workaround* that allows us to shove `%txt`s into the namespace; if the `/imp/<foo>` file is just a `~`, `/app/neo` will notice this and substitute in a shrub that takes the state of the type defined in `/pro/<foo>`. Like everything else in these tutorials this is subject to change!
The `%make` card has two mysteries: it initializes diary entries with an empty `%txt` implementation and an empty map of dependencies. Well punt on dependencies until the next tutorial. If you look at `/imp/txt.hoon`, its just a `~`. This allows us to shove `%txt`s into the namespace; if the `/imp/<foo>` file is just a `~`, `/app/neo` will notice this and substitute in a shrub that takes the state of the type defined in `/pro/<foo>`. Like everything else in these tutorials this is subject to change!
## Diary frontend
Like Counter, the Diary shrub just has two `/con` files to convert to and from an HTMX frontend within the Sky browser.
```hoon
```
:: /con/diary-htmx.hoon
/@ diary :: name=@t
/- feather-icons
@ -280,7 +285,7 @@ Theres only a bit more going on here than in Counters `/con/number-htmx.ho
The Diary frontend is a text box at the top with a list of entries generated from a list of the Diary shrubs child `pith`s. Nowhere is a list of children in the state being passed from the backend to the frontend, like you might see with a Gall agent passing converting a `(list item)` to a JSON array through a mark in the event that a frontend has scried or subscribed on a path, all of which would require painstaking specification by the developer.
```hoon
```
:: /con/node-diary-diff.hoon
/@ node :: manx
/@ diary-diff :: ?([%put-entry id=@da txt=@t] [%del-entry id=@da])
@ -298,5 +303,6 @@ The Diary frontend is a text box at the top with a list of entries generated fro
Just like the `/con/node-counter-diff.hoon` in the Counter tutorial, all this does is extract attributes from an XML node, having been converted into a `manx`, and manually constructs the poke expected by the shrub.
## Building on Diary
* Amend the `+kids` arm so that other apps could create shrubs like `/path/to/diary/<@da>/comments`.
* Diary takes two pokes: `%put-entry` and `%del-entry`, but only one of these is supported on the frontend. Implement a `%del-entry` poke from the frontend.
- Amend the `+kids` arm so that other apps could create shrubs like `/path/to/diary/<@da>/comments`.
- Diary takes two pokes: `%put-entry` and `%del-entry`, but only one of these is supported on the frontend. Implement a `%del-entry` poke from the frontend.

View File

@ -0,0 +1,48 @@
# Introduction
Shrubbery is a new in-development application model for Urbit. This guide will introduce the foundational Shrubbery concepts and get you started writing shrubs of your own. _It is aimed at Urbit developers who have at least been through App Academy, or are otherwise familiar with Gall agents._
At time of writing, Shrubbery's build system lives in a Gall agent called `%neo`. You can install this agent on a comet or moon with `|install ~met %neo`. You can install this desk on a fakeship from the GitHub repo, as detailed below.
## Cloning from GitHub
+ Clone the `urbit/shrub` repo to your machine.
+ Get the latest `urbit` [runtime](https://docs.urbit.org/manual/getting-started/self-hosted/cli).
+ Boot a fakeship with the `--loom 33` flag.
+ `|mount %base`
+ `rsync -avL —delete urbit/shrub/pkg/arvo/* pier/base`
+ `|commit %base`
+ `|start %neo`
This could take a while, youll see `booted %neo` in the Dojo when its complete.
## Directory structure
The only folders you need to understand for these tutorials are `/pro`, `/imp`, and `/con`.
- `/pro` for protocols. Like `/sur`, this is where your type definitions live.
- `/imp` for implementations. Like `/app`, this is where your app logic lives.
- `/con` for conversions. Like `/mar`, this is where you define rules for transforming nouns in your desk.
## Working in Sky
Sky is a UI for your local namespace. Three of the four examples in this guide are pre-loaded in there. The exception is Counter, which you'll build in your code editor and deploy in Sky yourself.
Every tile there represents a shrub located immediately beneath `/[%p our]/home` in the namespace. In the Counter tutorial you'll learn how to make your own shrubs at this location.
## Troubleshooting
If in doubt, `|nuke %neo` and `|start %neo`.
You'll definitely have to do this if...
- You make a change to a `/pro` file.
- You make a change to the `+init` behavior of a shrub that's in %neo's boot sequence (e.g. `/imp/home`).
If you keep restarting %neo, you'll eventually run out of loom. At this point you should delete your ship and boot up another one.
## Upcoming Changes
Everything in these guides is accurate at time of writing, but Shrubbery is a work in progress. Many implementation details are subject to change.
The way Shrubbery handles user interfaces is the most undercooked part of the system that we spend significant time on in these guides. Accessing `bowl:neo` within the `/con` files is how we get data into the UI, this is likely to change.
This guide launched at the June 2024 Lake Summit, and has an expiration date of sometime in Q3.

View File

@ -1,4 +1,5 @@
# Chapter 3: Messenger
The last major aspect of shrubbery that we need to cover is the dependency system. This consists of the `+deps` arm and the `%rely` poke.
By the end of this tutorial youll understand how dependencies work and how to use them. You should also start to see how you can design functionality that involves multiple shrubs interacting across multiple ships.
@ -16,7 +17,8 @@ One motivation behind this design is to split off functionality into simple, reu
We'll skip covering the Messenger frontend. While there are new ideas here, it's very similar to the Tasks tutorial which is the better context for more frontend. This tutorial will focus on several shrubs interoperating to form one tool. Let's look at `/imp/message-sub`, then the whole system that Messenger uses to manage chats.
## /imp/message-sub
```hoon
```
/@ message
^- kook:neo
|%
@ -71,9 +73,10 @@ We'll skip covering the Messenger frontend. While there are new ideas here, it's
```
## The +deps arm
The only part of this system that needs to define its dependencies is `/imp/message-sub`.
```hoon
```
::
:: define dependencies
++ deps
@ -103,7 +106,7 @@ The only part of this system that needs to define its dependencies is `/imp/mess
With regards to the lifecycle of the shrub, the `+deps` arm types the shrubs whose names and locations are passed in as an argument in the `%make` card when this shrub is created by `/imp/groupchat`.
```hoon
```
[%make %message-sub ~ (malt ~[[%pub (snoc host.poke %pub)]])]
```
@ -118,11 +121,12 @@ The last “new” idea here is `%sig`. This is a special case of `stud:neo` whi
If you look at `/imp/sig.hoon`, its just a `~` stub like the `%txt` implementation. `%sig` imps are not special and are not treated differently to any other stub, theyre just a stylistic convention to say that we dont care about the state of the shrub in question; it could be anything, we wont constrain it at all. Theres also a `/pro/sig.hoon` which lets us do the same thing for pokes.
## Handling state changes in our dependencies
Unlike a Gall agent, a shrub does not send out `%facts` to subscribers in the event of changes to its state, all of which has to be manually implemented by that agents developer. Instead, when its state changes `/app/neo` automatically sends a `%rely` poke to shrubs that have declared the shrub as a dependency. The developer of the listener shrub is the only one who has to write the logic to handle this.
In its `+deps` arm, the `/imp/message-sub` shrub declares the type and behaviour of the shrubs it will accept as dependencies. Shrubs that conform to that type, like `/imp/message-pub`, can be passed in through the `%make` card via the `conf` and `/imp/message-sub` will listen to those shrubs for state changes.
```hoon
```
++ deps
%- ~(gas by *deps:neo)
:~ :* %pub
@ -147,9 +151,10 @@ In `+deps`, the `%y` care declares that were only listening for state changes
The other `care:neo` youll commonly see is `%x`, which refers to a single shrub. You wouldnt use this in `+kids`, but you might use it in `+deps`.
### Handling %rely pokes
Lets see how `/imp/message-sub` handles the `%rely` pokes it recevives from dependencies.
```hoon
```
++ poke
::
:: we receive a stud and vase from the publisher shrub when
@ -180,7 +185,7 @@ Lets see how `/imp/message-sub` handles the `%rely` pokes it recevives from d
The above is mostly self-explanatory, but its worth expanding on `stem:neo` and `mode:neo`.
```hoon
```
+$ stem
$~ [*ever %x %stud *vase]
%+ pair ever
@ -199,15 +204,18 @@ Stems tagged with `%x` come with a `pail` with the new state of the dependency.
The `mode` of these kids is either `%add`, `%dif`, or `%del`. If its `%add`, the dependency is telling us its a new kid. If `%dif`, the kid isnt new but its state has changed. If `%del`, its telling us the kid was deleted and giving us its final state.
## Messenger: Overview
There are several shrubs working in tandem here to provide groupchat and DM functionality. Now that we know how `/imp/message-sub` works, let's look at the overall structure.
### /imp/message-pub
The only part of the Messenger backend left to consider is `/imp/message-pub`. This only imports `/pro/txt` and `/pro/message`.
#### kook:neo
`/imp/message-pub` only has one job, and that's to `%make` `%message`s as kids. Shrubs don't know anything about the shrubs above them that they weren't explicitly told in their `%make` card, so `/imp/message-pub` doesn't know or care whether it's being used to publish DMs or groupchat messages.
```hoon
```
++ state [%pro %sig]
++ poke (sy %message %txt ~)
++ kids
@ -223,9 +231,10 @@ The only part of the Messenger backend left to consider is `/imp/message-pub`. T
```
#### +init
Like `/imp/messenger`, `/imp/message-pub` takes no state.
```hoon
```
++ init
|= old=(unit pail:neo)
^- (quip card:neo pail:neo)
@ -233,9 +242,10 @@ Like `/imp/messenger`, `/imp/message-pub` takes no state.
```
#### +poke
All that happens in the `+poke` arm is this shrub creating `%message`s below it in the namespace. The `%txt` is not actually necessary, but as a primitive it might be nice for `/imp/message-pub` to be able to construct messages without the developer having to specify the metadata.
```hoon
```
++ poke
|= [=stud:neo vax=vase]
^- (quip card:neo pail:neo)
@ -256,11 +266,12 @@ All that happens in the `+poke` arm is this shrub creating `%message`s below it
```
### /imp/dm
If you wanted to implement 1-on-1 DMs in your own shrub, you could just `%make` an `/imp/dm`. If that doesn't do waht you need, you could base your own DM functionality on this.
#### kook:neo
```hoon
```
++ state [%pro %ship] :: who I'm chatting with
++ poke (sy %dm-diff ~)
++ kids
@ -279,7 +290,7 @@ If you wanted to implement 1-on-1 DMs in your own shrub, you could just `%make`
A DM shrub only stores one ship, the `@p` of whoever you're chatting with. It only has two kids: `/path/to/this/dm/theirs` and `/path/to/this/dm/mine`. At these two paths it uses the `/imp/message-pub` and `/imp/message-sub` primitives to store the state of `/theirs` and `/mine`.
```hoon
```
$% [%initiate partner=ship provider=pith]
[%invited partner=ship dm=pith]
::
@ -296,9 +307,10 @@ $% [%initiate partner=ship provider=pith]
- `%post`: send a DM.
#### curb:neo
Cells like `[%pro %ship]` and `[%pro %message-pub]` are examples of `$curb:neo`. This is a powerful type that's beyond the remit of these tutorials, but it's worth clarifying what these cells mean.
```hoon
```
+$ curb
$~ [%pro %$]
$% [%or p=(list curb)]
@ -313,9 +325,10 @@ Cells like `[%pro %ship]` and `[%pro %message-pub]` are examples of `$curb:neo`.
In all of the shrubs we've looked at in these tutorials we could replace every `%pro` curb with the likes of `[%only %ship]` and `[%only %message-pub]` and lose none of the functionality we've looked at. The `[%only %ship]` curb just declares that the state is exclusively a `ship`. However, the `[%pro ship]` curb says that the state can be any type *which can be converted to a `ship` through an available `/con` file*. This has implications for interoperability and state transitions we have not yet fully explored.
#### form:neo
When `/imp/dm` is first created with a `%make` card, it needs to be created with some pre-defined state. The intial state it accepts has to be a `%dm-diff`. Taking a poke type as the initial state type is an unusual choice that was done as an experiment, but the result is essentially the same as a Gall agent sending a poke to itself `+on-init`.
```hoon
```
++ init
|= old=(unit pail:neo)
^- (quip card:neo pail:neo)
@ -354,7 +367,7 @@ The `+poke` arm handles `%acked` and `%post` pokes.
DM state is symmetrical: both ships are publishing to each other and subscribed to each other. When we receive an `%acked` poke, we create an `/imp/message-sub` to subscribe to DMs from the "publisher", which is whoever we're going to talk to. When we receive a `%post` poke, we add that new post to our `/pub` shrub and the other ship's `/imp/message-sub` will mirror it in its own state.
```hoon
```
++ poke
|= [=stud:neo vax=vase]
^- (quip card:neo pail:neo)
@ -381,12 +394,14 @@ DM state is symmetrical: both ships are publishing to each other and subscribed
```
### /imp/groupchat
`/imp/groupchat` uses exactly the same primitives as `/imp/dm` for publishing and subscribing to messages. The only difference is that it's negotiating state between several ships using a one-to-many flow, rather than mirroring state between two ships.
#### kook:neo
Like `/imp/dm`, `/imp/groupchat` just defines two kids at `/.../pub` and `/.../sub` to do most of the heavy lifting for it.
```hoon
```
++ state [%pro %groupchat]
++ poke (sy %groupchat-diff ~)
++ kids
@ -405,7 +420,7 @@ Like `/imp/dm`, `/imp/groupchat` just defines two kids at `/.../pub` and `/.../s
The state is a `groupchat`, which is just a set of members, pending members, and a `pith` for the location of this chat in the namespace.
```hoon
```
$: members=(set ship)
pending=(set ship)
host=pith
@ -420,7 +435,7 @@ $: members=(set ship)
- `%post-to-host`: Send a post to the groupchat's host.
- `%host-to-pub`: Send a post from the groupchat's host to their publisher shrub.
```hoon
```
$% [%invite =ship provider=pith]
[%remove =ship]
[%invited host=pith]
@ -431,11 +446,12 @@ $% [%invite =ship provider=pith]
```
#### +init
When it's initialized, `/imp/groupchat` has either been created by a host on their own ship, or it's been created in response to an invitation from the host.
If it has no state, it creates the new chat with `%message-pub` and `%message-sub` providers. If it does have some initial state, it assumes it's being created by a foreign host ship and takes that state to be the chat history. It only needs to create a `%message-sub` to receive new messages from the publisher.
```hoon
```
++ init
|= old=(unit pail:neo)
^- (quip card:neo pail:neo)
@ -467,13 +483,14 @@ If it has no state, it creates the new chat with `%message-pub` and `%message-su
```
#### +poke
In the `+poke` arm we handle `%invite`, `%remove`, `%acked`, and `%post-to-host`, which is mostly a wrapper around `%host-to-pub`.
Even though it doesn't handle messages — just access control — `/imp/groupchat` has some state to manage. Moreso than anything we've seen before, the below should look a lot like a Gall agent.
Now is a good time to address when developers should store data inside a shrub's state vs. storing it as a kid. There's no right answer, but a good rule of thumb would be "would any other shrub care about this piece of data?" If so, it's more readily available at its own node in the namespace; if not, there's no downside to storing it in the shrub's internal state. You don't want to have to handle kids' state change just to do basic bookkeeping.
```hoon
```
++ poke
|= [=stud:neo vax=vase]
^- (quip card:neo pail:neo)
@ -531,14 +548,16 @@ Now is a good time to address when developers should store data inside a shrub's
```
### /imp/messenger
`/imp/messenger` is the top-level interface through which users can create, post in, and manage groupchats and DMs. This is the shrub that corresponds to the main "Messenger" UI within Sky.
This is a nice way to handle groupchats and DMs all in one place, but it's also a requirement of the way this system is built. There's a chicken-and-egg problem with DMs where ~tex can't invite ~mex to a DM chat unless ~mex already has a DM chat (`/imp/dm`) with ~tex in which to receieve that poke, so DMs rely on `/imp/messenger` to negotiate that with the `%new-dm` poke.
#### kook:neo
Messenger has no state. This shrub is just an interface for creating groupchats and DMs, which are its kids. If those kids are `/imp/dm`s they take `%dm-diff`s, and if they're `/imp/groupchat`s they take `%groupchat-diff`s.
```hoon
```
++ state [%pro %sig]
++ poke (sy %dm-diff %groupchat-diff %messenger-diff ~)
++ kids
@ -557,7 +576,7 @@ Messenger has no state. This shrub is just an interface for creating groupchats
Messenger only supports three user actions: creating a new DM, creating a new groupchat, and inviting someone to a groupchat. Everything else is handled by this shrub's kids.
```hoon
```
$% [%new-dm partner=ship]
[%new-groupchat name=@t invites=(set ship)]
[%invite-to-groupchat name=@t invites=(set ship)]
@ -565,9 +584,10 @@ $% [%new-dm partner=ship]
```
#### +init
Messenger does nothing much on `+init`. Here's `%sig` again, this time head-tagging an empty `pail:neo`. This shrub stores no state, so we can't inject some when we initialize it.
```hoon
```
++ init
|= old=(unit pail:neo)
^- (quip card:neo pail:neo)
@ -575,9 +595,10 @@ Messenger does nothing much on `+init`. Here's `%sig` again, this time head-tagg
```
#### +poke
`/imp/messenger` takes `%messenger-diff`s, but it also takes `%dm-diff`s and `%groupchat-diff`s and, if they're invites to those chats, `%make`s the chat at the right location in the namespace.
```hoon
```
++ poke
|= [=stud:neo vax=vase]
^- (quip card:neo pail:neo)

View File

@ -1,27 +1,18 @@
# Tutorial 4: Tasks
# Chapter 4: Tasks
Lets take a look at the Tasks shrub. From within the Sky frontend, you can create, edit, reorder, and delete nested tasks and subtasks. Checking off all subtasks will mark the parent as complete, which involves some interaction between parent and child shrubs.
In this lesson well see how shrubs keep their parents informed of state changes using the `%gift` poke. This is the most complex UI weve looked at yet, so well also focus on the `/con` files.
## /imp/task.hoon
Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some tasks that may or may not have other tasks as children.
```hoon
/@ task :: [text=cord done=? order=(list path)]
::
:: $task-diff
:: $%  [%new =task prepend=?]
:: [%edit text=cord done=?]
:: [%oust =pith]
:: [%reorder order=(list pith)]
:: ==
```
/@ task
/@ task-diff
=>
::
:: helper core
|%
::
:: check if all kids are completed
++ check-kids
|= =bowl:neo
^- ?
@ -35,7 +26,6 @@ Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some
q.pail:(need (~(get by ~(tar of:neo kids.bowl)) pith))
done.task
::
:: assign a unique numerical ID to a new subtask
++ assign-name
|= =bowl:neo
^- @ud
@ -53,33 +43,20 @@ Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some
?~ name-missing +(last-name)
(rear name-missing)
--
::
:: outer core
^- kook:neo
|%
::
:: state is a %task
++ state pro/%task
::
:: we accept %task-diff and %gift pokes
++ poke (sy %task-diff %gift ~)
::
:: we define one generation of
:: kids at /path/to/this/task/<@ud>
++ kids
:+ ~ %y
%- ~(gas by *lads:neo)
:~ :- [|/%ud |]
[pro/%task (sy %task-diff %gift ~)]
==
++ deps *deps:neo
++ deps *deps:neo
++ form
::
:: inner core
^- form:neo
:: XX refactor sample to same as other tutorials
|_ [=bowl:neo =aeon:neo stud:neo state-vase=vase]
:: XX refactor this to default +init from counter
++ init
|= pal=(unit pail:neo)
^- (quip card:neo pail:neo)
@ -90,9 +67,9 @@ Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some
=/ this !<(task state-vase)
?+ stud !!
%gift
?: (check-kids bowl)
[~ task/!>(this(done %.y))]
[~ task/!>(this(done %.n))]
::check if all kid tasks are done
=/ dun (check-kids bowl)
[~ task/!>(this(done dun, kids-done dun))]
::
%task-diff
=/ diff !<(task-diff vax)
@ -104,6 +81,7 @@ Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some
[~[ud/name] order.this]
(snoc order.this `pith`[ud/name ~])
=. done.this |
=. kids-done.this |
:_ task/!>(this)
:~ :- (welp here.bowl ~[ud/name])
[%make %task `task/!>(task.diff) ~]
@ -115,7 +93,7 @@ Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some
!>
%= this
text text.diff
done ?: (check-kids bowl)
done ?: kids-done.this
done.diff
%.n
==
@ -136,18 +114,19 @@ Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some
```
## The %gift poke
In the `+poke` arm we declare this shrub takes a `%gift` as well as a `%task-diff`, but we dont have to import `%gift`. This is a special poke like `%rely` that `/app/neo` gives us when another shrubs state changes.
Our shrub receives a `%gift` poke every time the state of one of its descendants changes: only kids in the `%y` case, all descendants in the `%z` case.
```hoon
```
++ state [%pro %task]
++ poke (sy %task-diff %gift ~)
```
Then we can handle the poke like any other. In this case, when `/imp/task` receives word that one of its kids state has changed, it checks to see if all of its subtasks (kids) are completed, then updates its own state accordingly.
```hoon
```
?+ stud !!
%gift
:: check if all kid tasks are done
@ -157,10 +136,12 @@ Then we can handle the poke like any other. In this case, when `/imp/task` recei
```
## Frontend
Lets look at the Tasks frontend in detail.
### Converting tasks to HTMX
```hoon
```
/@ task :: [text=cord done=? kids-done=? order=(list pith)]
:: import /lib/feather-icons
/- feather-icons
@ -250,12 +231,14 @@ Lets look at the Tasks frontend in detail.
// tell the user why a clicked checkbox
// can't be marked as checked
document.getElementById("alert").addEventListener("click", function(e){
if (document.getElementById("alert").hasAttribute("readonly")){
e.preventDefault();
alert("Subtasks are not completed");
}
});
document.querySelectorAll(".alert").forEach(function(element) {
element.addEventListener('click', function(e) {
if (element.hasAttribute("readonly")){
e.preventDefault();
alert("Subtasks are not completed");
}
})
})
'''
== :: </script>
::
@ -310,7 +293,7 @@ Lets look at the Tasks frontend in detail.
:: sends %edit poke on input change
|= [=pith =idea:neo]
:: extracts information from idea:neo to task
=/ =pail:neo pail.idea
=/ =pail:neo q.saga.idea
=/ t=task !<(task q.pail)
:: converts pith to tape
=/ pt (pith-tape (welp here.bowl pith))
@ -337,6 +320,7 @@ Lets look at the Tasks frontend in detail.
;+
:: defines class attribute with class names
=/ class [%class "p2 br1 border done s3"]
=/ class-alert [%class "p2 br1 border done s3 alert"]
::
:: checkbox logic:
:: - if task is toggled, checkbox will
@ -371,10 +355,10 @@ Lets look at the Tasks frontend in detail.
:: the rest of manx data
m(a.g [class a.g.m])
::
:: assigns readonly, id and class
:: attributes to checkbox; id will trigger
:: assigns readonly and class
:: attributes to checkbox; 'alert' class will trigger
:: alert script functionality
m(a.g [[%readonly ""] [%id "alert"] class a.g.m])
m(a.g [[%readonly ""] class-alert a.g.m])
::
:: assigning checked and class attributes
:: to the rest of manx data
@ -525,7 +509,8 @@ Lets look at the Tasks frontend in detail.
```
### Converting %task-diffs to HTMX
```hoon
```
/@ task-diff
/- feather-icons
:- [%task-diff %$ %htmx]
@ -545,7 +530,8 @@ Lets look at the Tasks frontend in detail.
```
### Converting HTMX to %task-diffs
```hoon
```
/@ node :: manx
::
:: $task-diff
@ -576,8 +562,8 @@ Lets look at the Tasks frontend in detail.
=/ prepend (vol:mu "prepend")
?: =(prepend 'prepend')
:: construct the task-diff
[head [text | ~] &]
[head [text | ~] |]
[head [text | & ~] &]
[head [text | & ~] |]
::
%edit
:: extract text attribute from XML node

View File

@ -1,6 +1,10 @@
/@ home
/* feather-intro
/* diary
/* counter
/* tasks
/* messenger
/* introduction
^- kook:neo
|%
++ state pro/%home
@ -30,12 +34,16 @@
[#/[p/our.bowl]/home/files %make %folder ~ ~]
[#/[p/our.bowl]/home/planner %make %planner ~ ~]
[#/[p/our.bowl]/home/messenger %make %messenger ~ ~]
::
::
[#/[p/our.bowl]/home/docs %make %folder `folder/!>([%introduction %tutorials %guides ~]) ~]
::
[#/[p/our.bowl]/home/docs/introduction %make %sail `sail/!>([introduction 'prose p-page mw-page ma' ~]) ~]
::
[#/[p/our.bowl]/home/docs/tutorials %make %folder `folder/!>([%counter %diary %messenger %tasks ~]) ~]
[#/[p/our.bowl]/home/docs/tutorials/counter %make %sail `sail/!>([counter 'prose p-page mw-page ma' ~]) ~]
[#/[p/our.bowl]/home/docs/tutorials/diary %make %sail `sail/!>([diary 'prose p-page mw-page ma' ~]) ~]
::
[#/[p/our.bowl]/home/docs/tutorials/messenger %make %sail `sail/!>([messenger 'prose p-page mw-page ma' ~]) ~]
[#/[p/our.bowl]/home/docs/tutorials/tasks %make %sail `sail/!>([tasks 'prose p-page mw-page ma' ~]) ~]
::
[#/[p/our.bowl]/home/docs/guides %make %folder `folder/!>([%feather ~]) ~]
[#/[p/our.bowl]/home/docs/guides/feather %make %sail `sail/!>([feather-intro 'prose p-page mw-page ma' ~]) ~]
==