Merge pull request #5663 from urbit/m/drum-sessions

term: extended session support, other improvements
This commit is contained in:
Zach Alberico 2023-01-10 11:20:06 -08:00 committed by GitHub
commit f0c70e041d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1673 additions and 1108 deletions

View File

@ -10,9 +10,9 @@
:::: :: ::::
:: :: ::
=> |% :: external structures
+$ id @tasession :: session id
+$ id sole-id :: session id
+$ house :: all state
$: %8
$: %9
egg=@u :: command count
hoc=(map id session) :: conversations
acl=(set ship) :: remote access whitelist
@ -820,12 +820,23 @@
=/ poz=vase (dy-sore p.cig)
=/ kev=vase
=/ kuv=(unit vase) (slew 7 som)
?: =(~ q.cig)
(fall kuv !>(~))
=/ soz=(list [var=term vax=vase])
%~ tap by
%- ~(run by q.cig)
|=(val=(unit dojo-source) ?~(val !>([~ ~]) (dy-vase p.u.val)))
:: if the generator takes a named argument "drum-session",
:: then if a value isn't already supplied, we set it to the session
:: that this dojo instance is being run in.
:: (dojo is, indeed, quite coupled with drum.)
::
=? soz
?& ?=(^ kuv)
(slab %both %drum-session p.u.kuv)
!(~(has by q.cig) %drum-session)
==
[[%drum-session !>(ses.id)] soz] ::TODO does the who matter?
?: =(~ soz)
(fall kuv !>(~))
~| keyword-arg-failure+~(key by q.cig)
%+ slap
(with-faces kuv+(need kuv) rep+(with-faces soz) ~)
@ -1018,13 +1029,14 @@
|= =card:agent:gall
^+ +>
=? card ?=(%pass -.card)
card(p [id p.card])
^- card:agent:gall
card(p [(scot %p who.id) ses.id p.card])
%_(+> moz [card moz])
::
++ he-diff :: emit update
|= fec=sole-effect
^+ +>
(he-card %give %fact ~[/sole/[id]] %sole-effect !>(fec))
(he-card %give %fact ~[(id-to-path:sole id)] %sole-effect !>(fec))
::
++ he-stop :: abort work
^+ .
@ -1532,21 +1544,47 @@
::
++ on-load
|= ole=vase
^- (quip card:agent:gall _..on-init)
|^ =+ old=!<(house-any ole)
=? old ?=(%5 -.old)
^- house-any
^- house-6
(house-5-to-6 old)
=? old ?=(?(%6 %7) -.old)
(house-6-7-to-8 +.old)
?> ?=(%8 -.old)
`..on-init(state old)
=^ caz old
?. ?=(%8 -.old) [~ old]
(house-8-to-9 old)
?> ?=(%9 -.old)
[caz ..on-init(state old)]
::
+$ house-any $%(house house-7 house-6 house-5)
+$ house-any $%(house house-8 house-7 house-6 house-5)
::
+$ id-8 @tasession
+$ house-8
$: %8
egg=@u
hoc=(map id-8 session)
acl=(set ship)
==
++ house-8-to-9
|= old=house-8
^- (quip card:agent:gall house)
:- %+ turn ~(tap in ~(key by hoc.old))
|= id=@ta
^- card:agent:gall
[%give %kick ~[/sole/[id]] ~]
=- [%9 egg.old - acl.old]
%- ~(gas by *(map sole-id session))
%+ murn ~(tap by hoc.old)
|= [id=@ta s=session]
(bind (upgrade-id:sole id) (late s))
::
+$ house-7 [%7 house-6-7]
+$ house-6 [%6 house-6-7]
+$ house-6-7
$: egg=@u :: command count
hoc=(map id session-6) :: conversations
hoc=(map id-8 session-6) :: conversations
acl=(set ship) :: remote access whitelist
== ::
+$ session-6 :: per conversation
@ -1573,9 +1611,10 @@
old(poy ~, -.dir [our.hid %base ud+0])
::
+$ house-5
[%5 egg=@u hoc=(map id session)]
[%5 egg=@u hoc=(map id-8 session-6)]
++ house-5-to-6
|= old=house-5
^- house-6
[%6 egg.old hoc.old *(set ship)]
--
::
@ -1591,7 +1630,8 @@
he-abet:(~(he-type he hid id.act ~ (~(got by hoc) id.act)) act)
::
%lens-command
=+ !<([=id =command:lens] vase)
=+ !<([ses=@ta =command:lens] vase)
=/ =id [our.hid ses]
he-abet:(~(he-lens he hid id ~ (~(got by hoc) id)) command)
::
%allow-remote-login
@ -1629,8 +1669,7 @@
?> ?| (team:title our.hid src.hid)
(~(has in acl) src.hid)
==
?> ?=([%sole @ ~] path)
=/ id i.t.path
=/ =id (need (path-to-id:sole path))
=? hoc (~(has by hoc) id)
~& [%dojo-peer-replaced id]
(~(del by hoc) id)
@ -1642,7 +1681,7 @@
++ on-leave
|= =path
?> ?=([%sole *] path)
=. hoc (~(del by hoc) t.path)
=. hoc (~(del by hoc) (need (path-to-id:sole path)))
[~ ..on-init]
::
++ on-peek
@ -1651,13 +1690,15 @@
::
++ on-agent
|= [=wire =sign:agent:gall]
?> ?=([@ @ *] wire)
=/ =session (~(got by hoc) i.wire)
=/ he-full ~(. he hid i.wire ~ session)
^- (quip card:agent:gall _..on-init)
?> ?=([@ @ @ *] wire)
=/ =id [(slav %p i.wire) i.t.wire]
=/ =session (~(got by hoc) id)
=/ he-full ~(. he hid id ~ session)
=^ moves state
=< he-abet
^+ he
?+ i.t.wire ~|([%dojo-bad-on-agent wire -.sign] !!)
?+ i.t.t.wire ~|([%dojo-bad-on-agent wire -.sign] !!)
%poke (he-unto:he-full t.wire sign)
%wool (he-wool:he-full t.wire sign)
==
@ -1665,14 +1706,16 @@
::
++ on-arvo
|= [=wire =sign-arvo]
?> ?=([@ *] wire)
=/ =session (~(got by hoc) i.wire)
=/ he-full ~(. he hid i.wire ~ session)
^- (quip card:agent:gall _..on-init)
?> ?=([@ @ *] wire)
=/ =id [(slav %p i.wire) i.t.wire]
=/ =session (~(got by hoc) id)
=/ he-full ~(. he hid id ~ session)
=^ moves state
=< he-abet
?+ +<.sign-arvo ~|([%dojo-bad-take +<.sign-arvo] !!)
%writ (he-writ:he-full t.wire +>.sign-arvo)
%http-response (he-http-response:he-full t.wire +>.sign-arvo)
%writ (he-writ:he-full t.t.wire +>.sign-arvo)
%http-response (he-http-response:he-full t.t.wire +>.sign-arvo)
==
[moves ..on-init]
:: if dojo fails unexpectedly, kill whatever each session is working on

View File

@ -44,17 +44,12 @@
~| path
?> ?=([%session @ %view ~] path)
=* ses i.t.path
:~ :: subscribe to the requested session
:: subscribe to the requested session
::
::NOTE multiple views do not result in multiple subscriptions
:: because they go over the same wire/duct
::
(pass-session ses %view ~)
:: tell session to refresh, so new client knows what's on screen
::TODO should client be responsible for this?
::
(pass-session ses %hail ~)
==
[(pass-session ses %view ~)]~
::
++ on-arvo
|= [=wire =sign-arvo]

View File

@ -83,7 +83,8 @@
::
?+ -.source.com
:_ this(job.state (some [eyre-id com]))
[%pass /sole %agent [our.bowl %dojo] %watch /sole/[eyre-id]]~
=/ =path /sole/(scot %p our.bowl)/[eyre-id]
[%pass /sole %agent [our.bowl %dojo] %watch path]~
::
%export
:_ this(job.state (some [eyre-id com]))

View File

@ -43,13 +43,13 @@
++ on-fail on-fail:def
::
++ command-parser
|= sole-id=@ta
|= =sole-id:shoe
^+ |~(nail *(like [? command]))
%+ stag &
(perk %demo %row %table ~)
::
++ tab-list
|= sole-id=@ta
|= =sole-id:shoe
^- (list [@t tank])
:~ ['demo' leaf+"run example command"]
['row' leaf+"print a row"]
@ -57,7 +57,7 @@
==
::
++ on-command
|= [sole-id=@ta =command]
|= [=sole-id:shoe =command]
^- (quip card _this)
=; [to=(list _sole-id) fec=shoe-effect:shoe]
[[%shoe to fec]~ this]
@ -87,7 +87,7 @@
==
::
++ can-connect
|= sole-id=@ta
|= =sole-id:shoe
^- ?
?| =(~zod src.bowl)
(team:title [our src]:bowl)

View File

@ -8,9 +8,11 @@
::
:- %say
|= $: [now=@da eny=@uvJ byk=beak]
[arg=$?([dap=term ~] [who=ship dap=term ~]) ~]
arg=$?([dap=term ~] [who=ship dap=term ~])
drum-session=@ta
==
:- %drum-link
:- drum-session
?~ +.arg
[p.byk dap.arg]
[who.arg dap.arg]

View File

@ -8,9 +8,11 @@
::
:- %say
|= $: [now=@da eny=@uvJ byk=beak]
[arg=$?([dap=term ~] [who=ship dap=term ~]) ~]
arg=$?([dap=term ~] [who=ship dap=term ~])
drum-session=@ta
==
:- %drum-unlink
:- drum-session
?~ +.arg
[p.byk dap.arg]
[who.arg dap.arg]

View File

@ -15,8 +15,7 @@
+$ state-2 [%2 pith-2]
::
+$ pith-5
$: eel=(set gill:gall) :: connect to
bin=(map @ source) :: terminals
$: bin=(map @ source) :: terminals
==
::
+$ pith-4
@ -75,6 +74,7 @@
off=@ud :: window offset
kil=kill :: kill buffer
inx=@ud :: ring index
eel=(set gill:gall) :: connect to
fug=(map gill:gall (unit target)) :: connections
mir=(pair @ud stub) :: mirrored terminal
== ::
@ -105,15 +105,17 @@
:: :: ::
|%
++ en-gill :: gill to wire
|= gyl=gill:gall
|= [ses=@tas gyl=gill:gall]
^- wire
::TODO include session?
[%drum %phat (scot %p p.gyl) q.gyl ~]
[%drum %phat (scot %p p.gyl) q.gyl ?:(=(%$ ses) ~ [ses ~])]
::
++ de-gill :: gill from wire
|= way=wire ^- gill:gall
~| way
?>(?=([@ @ *] way) [(slav %p i.way) i.t.way])
|= way=wire
^- [@tas gill:gall]
~| wire=way
?> ?=([@ @ ?(~ [@ ~])] way)
:- ?~(t.t.way %$ i.t.t.way)
[(slav %p i.way) i.t.way]
--
::
|= [hid=bowl:gall state]
@ -122,24 +124,29 @@
=+ (~(gut by bin) ses *source)
=* dev -
=| moz=(list card:agent:gall)
=| biz=(list dill-blit:dill)
=| biz=(list blit:dill) ::TODO should be per-session
|%
++ this .
++ klr klr:format
+$ state ^state :: proxy
+$ any-state ^any-state :: proxy
++ on-init (poke-link our.hid %dojo)
++ on-init (poke-link %$ our.hid %dojo)
::
++ prep
|= s=@tas
=. ses ses
=. ses s
=. dev (~(gut by bin) ses *source)
this
::
++ open
%+ cork de-gill
|= [s=@tas g=gill:gall]
[g (prep s)]
::
++ diff-sole-effect-phat :: app event
|= [way=wire fec=sole-effect]
=< se-abet =< se-view
=+ gyl=(de-gill way)
=< se-abet
=^ gyl this (open way)
?: (se-aint gyl) +>.$
(se-diff gyl fec)
::
@ -149,7 +156,7 @@
(prep i.t.pax)
~| [%drum-unauthorized our+our.hid src+src.hid] :: ourself
?> (team:title our.hid src.hid) :: or our own moon
=< se-abet =< se-view
=< se-abet
(se-text "[{<src.hid>}, driving {<our.hid>}]")
::
++ poke-dill
@ -158,7 +165,7 @@
::
++ poke-dill-belt :: terminal event
|= bet=dill-belt:dill
=< se-abet =< se-view
=< se-abet
(se-belt bet)
::
++ poke-dill-blit :: terminal output
@ -166,21 +173,22 @@
se-abet:(se-blit-sys bit)
::
++ poke-link :: connect app
|= gyl=gill:gall
=< se-abet =< se-view
(se-link gyl)
|= [ses=@tas gyl=gill:gall]
=< se-abet
(se-link:(prep ses) gyl)
::
++ poke-unlink :: disconnect app
|= gyl=gill:gall
=< se-abet =< se-view
(se-drop:(se-pull gyl) & gyl)
|= [ses=@ta gyl=gill:gall]
=< se-abet
(se-drop:(se-pull:(prep ses) gyl) & gyl)
::
++ poke-exit :: shutdown
|= ~
se-abet:(se-blit-sys `dill-blit:dill`[%qit ~])
::
++ poke-put :: write file
|= [pax=path txt=@]
|= [pax=path arg=$@(@ [@tas @])]
=^ txt +> ?@(arg [arg +>] [+.arg (prep -.arg)])
se-abet:(se-blit-sys [%sav pax txt]) ::
::
++ poke
@ -196,16 +204,16 @@
::
++ on-load
|= [hood-version=@ud old=any-state]
=< se-abet =< se-view
=< se-abet
=? old ?=(%2 -.old) [%4 [eel bin]:old]
=? old ?=(%3 -.old) [%4 [eel bin]:old]
=? old ?=(%4 -.old)
:+ %5 eel.old
|^ (~(run by bin.old) source-4-to-5)
|^ 5+(~(run by bin.old) source-4-to-5)
++ source-4-to-5
|= s=source-4
|= source-4
^- source
s(fug (~(run by fug.s) |=(t=(unit target-4) (bind t target-4-to-5))))
=; fug [edg off kil inx eel.old fug mir]
(~(run by fug) |=(t=(unit target-4) (bind t target-4-to-5)))
::
++ target-4-to-5
|= t=target-4
@ -228,8 +236,8 @@
::
++ reap-phat :: ack connect
|= [way=wire saw=(unit tang)]
=< se-abet =< se-view
=+ gyl=(de-gill way)
=< se-abet
=^ gyl this (open way)
?~ saw
(se-join gyl)
:: Don't print stack trace because we probably just crashed to
@ -239,9 +247,9 @@
::
++ take-coup-phat :: ack poke
|= [way=wire saw=(unit tang)]
=< se-abet =< se-view
=< se-abet
?~ saw +>
=+ gyl=(de-gill way)
=^ gyl this (open way)
?: (se-aint gyl) +>.$
%- se-dump:(se-drop:(se-pull gyl) & gyl)
:_ u.saw
@ -264,8 +272,8 @@
::
++ quit-phat ::
|= way=wire
=< se-abet =< se-view
=+ gyl=(de-gill way)
=< se-abet
=^ gyl this (open way)
~& [%drum-quit src.hid gyl]
(se-drop %| gyl)
:: :: ::
@ -273,14 +281,18 @@
:: :: ::
++ se-abet :: resolve
^- (quip card:agent:gall state)
=. . se-subze:se-adze
=. . se-view:se-subze:se-adze
:_ sat(bin (~(put by bin) ses dev))
^- (list card:agent:gall)
?~ biz (flop moz)
:_ (flop moz)
=/ =dill-blit:dill ?~(t.biz i.biz [%mor (flop biz)])
=/ =blit:dill ?~(t.biz i.biz [%mor (flop biz)])
::TODO remove /drum after dill cleans up
[%give %fact ~[/drum /dill/[ses]] %dill-blit !>(dill-blit)]
::TODO but once we remove it, the empty trailing segment of
:: /dill/[ses] would prevent outsiders from subscribing
:: to the default session...
=/ to=(list path) [/dill/[ses] ?~(ses ~[/drum] ~)]
[%give %fact to %dill-blit !>(blit)]
::
++ se-adze :: update connections
^+ .
@ -306,7 +318,7 @@
=< .(con +>)
|: $:,[[ses=@tas dev=source] con=_.] ^+ con
=+ xeno=se-subze-local:%_(con ses ses, dev dev)
xeno(ses ses.con, dev dev.con, bin (~(put by bin) ses dev.xeno))
xeno(ses ses.con, dev dev.con, bin (~(put by bin.xeno) ses dev.xeno))
::
++ se-subze-local
^+ .
@ -359,7 +371,7 @@
[%cru *] (se-dump:(se-text (trip p.bet)) q.bet)
[%hey *] +>(mir [0 ~]) :: refresh
[%rez *] +>(edg (dec p.bet)) :: resize window
[%yow *] ~&([%no-yow -.bet] +>)
[%yow *] (se-link p.bet)
==
=+ gul=se-agon
?: |(?=(~ gul) (se-aint u.gul))
@ -463,7 +475,7 @@
+>(eel (~(put in eel) gyl))
::
++ se-blit :: give output
|= bil=dill-blit:dill
|= bil=blit:dill
+>(biz [bil biz])
::
++ se-blit-sys :: output to system
@ -519,19 +531,18 @@
::
++ se-poke :: send a poke
|= [gyl=gill:gall par=cage]
(se-emit %pass (en-gill gyl) %agent gyl %poke par)
(se-emit %pass (en-gill ses gyl) %agent gyl %poke par)
::
++ se-peer :: send a peer
|= gyl=gill:gall
~> %slog.0^leaf/"drum: link {<[p q]:gyl>}"
::TODO include session
=/ =path /sole/(cat 3 'drum_' (scot %p our.hid))
=/ =path (id-to-path:sole our.hid ses)
%- se-emit(fug (~(put by fug) gyl ~))
[%pass (en-gill gyl) %agent gyl %watch path]
[%pass (en-gill ses gyl) %agent gyl %watch path]
::
++ se-pull :: cancel subscription
|= gyl=gill:gall
(se-emit %pass (en-gill gyl) %agent gyl %leave ~)
(se-emit %pass (en-gill ses gyl) %agent gyl %leave ~)
::
++ se-tame :: switch connection
|= gyl=gill:gall
@ -556,7 +567,7 @@
^+ +>
(ta-poke %sole-action !>(act))
::
++ ta-id (cat 3 'drum_' (scot %p our.hid)) :: per-ship duct id
++ ta-id [our.hid ses] :: per-ship-session id
::
++ ta-aro :: hear arrow
|= key=?(%d %l %r %u)
@ -631,7 +642,7 @@
%d ?^ buf.say.inp
ta-del
?: =([our.hid %dojo] gyl)
+>(..ta (se-blit qit+~)) :: quit pier
+>(..ta (se-blit-sys %qit ~)) :: quit pier
+>(..ta (se-klin gyl)) :: unlink app
%e +>(pos.inp (lent buf.say.inp))
%f (ta-aro %r)
@ -676,13 +687,12 @@
(ta-hom %del pos.inp)
::
++ ta-hit :: hear click
|= [r=@ud c=@ud]
|= [x=@ud y=@ud]
^+ +>
?. =(0 r) +>
=/ pol=@ud
(lent-char:klr (make:klr cad.pom))
?: (lth c pol) +>.$
+>.$(pos.inp (min (sub c pol) (lent buf.say.inp)))
=? x (lth x pol) pol
+>.$(pos.inp (min (sub x pol) (lent buf.say.inp)))
::
++ ta-erl :: hear local error
|= pos=@ud

View File

@ -21,7 +21,7 @@
%mor [%a (turn p.dib |=(a=dill-blit:dill json(dib a)))]
%hop %+ frond %hop
?@ p.dib (numb p.dib)
(pairs 'r'^(numb r.p.dib) 'c'^(numb c.p.dib) ~)
(pairs 'x'^(numb x.p.dib) 'y'^(numb y.p.dib) ~)
%put (frond -.dib (tape (tufa p.dib)))
?(%bel %clr) (frond %act %s -.dib)
==

View File

@ -1,8 +1,7 @@
::
:::: /hoon/do-claim/womb/mar
:: %drum-put: download into host system
::
/? 310
|_ [path @]
|_ [path $@(@ [@ta @])]
::
++ grad %noun
++ grow
@ -11,6 +10,6 @@
--
++ grab :: convert from
|%
+$ noun [path @] :: clam from %noun
+$ noun [path $@(@ [@ta @])] :: clam from %noun
--
--

View File

@ -1 +1 @@
[%zuse 417]
[%zuse 416]

View File

@ -3,7 +3,7 @@
!:
=> ..part
|%
++ lull %328
++ lull %327
:: :: ::
:::: :: :: (1) models
:: :: ::
@ -1209,16 +1209,17 @@
$% [%aro p=?(%d %l %r %u)] :: arrow key
[%bac ~] :: true backspace
[%del ~] :: true delete
[%hit r=@ud c=@ud] :: mouse click
[%hit x=@ud y=@ud] :: mouse click
[%ret ~] :: return
== ::
+$ blit :: client output
$% [%bel ~] :: make a noise
[%clr ~] :: clear the screen
[%hop p=$@(@ud [r=@ud c=@ud])] :: set cursor col/pos
[%hop p=$@(@ud [x=@ud y=@ud])] :: set cursor col/pos
[%klr p=stub] :: put styled
[%put p=(list @c)] :: put text at cursor
[%mor p=(list blit)] :: multiple blits
[%nel ~] :: newline
[%put p=(list @c)] :: put text at cursor
[%sag p=path q=*] :: save to jamfile
[%sav p=path q=@] :: save to file
[%url p=@t] :: activate url
@ -1233,7 +1234,6 @@
== ::
+$ dill-blit :: arvo output
$% blit :: client output
[%mor p=(list dill-blit)] :: multiple blits
[%qit ~] :: close console
== ::
+$ flog :: sent to %dill

View File

@ -176,13 +176,10 @@
++ from :: receive blit
|= bit=dill-blit
^+ +>
?: ?=(%mor -.bit)
|- ^+ +>.^$
?~ p.bit +>.^$
$(p.bit t.p.bit, +>.^$ ^$(bit i.p.bit))
?: ?=(%qit -.bit)
(dump %logo ~)
(done %blit [bit ~])
::TODO so why is this a (list blit) again?
(done %blit bit ~)
::
++ sponsor
^- ship
@ -212,16 +209,12 @@
++ open
|= gyl=(list gill)
::TODO should allow handlers from non-base desks
=. +> (pass / %g %jolt %base ram)
::TODO maybe ensure :ram is running?
=. +> peer
%+ roll gyl
|= [g=gill _..open]
(send [%yow g])
::
++ shut
::TODO send a %bye blit?
pull(eye.all (~(del by eye.all) ses))
::
++ send :: send action
|= bet=dill-belt
^+ +>
@ -375,7 +368,12 @@
=/ nus
~| [%no-session ses]
(need (ax hen ses))
=^ moz all abet:shut:nus
::NOTE we do deletion from state outside of the core,
:: because +abet would re-insert.
::TODO send a %bye blit? xx
=^ moz all abet:pull:nus
=. dug.all (~(del by dug.all) ses)
=. eye.all (~(del by eye.all) ses)
[moz ..^$]
:: %view opens a subscription to the target session, on the current duct
::

View File

@ -4,7 +4,7 @@
=> ..lull
~% %zuse ..part ~
|%
++ zuse %417
++ zuse %416
:: :: ::
:::: :: :: (2) engines
:: :: ::
@ -3902,29 +3902,24 @@
++ slag :: slag stub
|= [a=@ b=stub]
^- stub
=+ c=(lnts-char b)
=+ i=(brek a c)
?~ i ~
=+ r=(^slag +(p.u.i) b)
?: =(a q.u.i)
r
=+ n=(snag p.u.i b)
:_ r :- p.n
(^slag (sub (snag p.u.i c) (sub q.u.i a)) q.n)
?: =(0 a) b
?~ b ~
=+ c=(lent q.i.b)
?: =(c a) t.b
?: (gth c a)
[[p.i.b (^slag a q.i.b)] t.b]
$(a (sub a c), b t.b)
::
++ scag :: scag stub
|= [a=@ b=stub]
^- stub
=+ c=(lnts-char b)
=+ i=(brek a c)
?~ i b
?: =(a q.u.i)
(^scag +(p.u.i) b)
%+ welp
(^scag p.u.i b)
=+ n=(snag p.u.i b)
:_ ~ :- p.n
(^scag (sub (snag p.u.i c) (sub q.u.i a)) q.n)
?: =(0 a) ~
?~ b ~
=+ c=(lent q.i.b)
?: (gth c a)
[p.i.b (^scag a q.i.b)]~
:- i.b
$(a (sub a c), b t.b)
::
++ swag :: swag stub
|= [[a=@ b=@] c=stub]

View File

@ -13,11 +13,12 @@
%bel b+&
%clr b+&
%hop ?@ p.blit (numb p.blit)
(pairs 'r'^(numb r.p.blit) 'c'^(numb c.p.blit) ~)
(pairs 'x'^(numb x.p.blit) 'y'^(numb y.p.blit) ~)
%put a+(turn p.blit |=(c=@c s+(tuft c)))
%nel b+&
%url s+p.blit
%wyp b+&
%mor a+(turn p.blit ^blit)
::
%sag
%- pairs
@ -73,7 +74,7 @@
:~ aro+(su (perk %d %l %r %u ~))
bac+ul
del+ul
hit+(ot 'r'^ni 'c'^ni ~)
hit+(ot 'x'^ni 'y'^ni ~)
ret+ul
==
::

View File

@ -13,15 +13,15 @@
/- *sole
/+ sole, auto=language-server-complete
|%
+$ state-0
$: %0
soles=(map @ta sole-share)
+$ state-1
$: %1
soles=(map sole-id sole-share)
==
:: $card: standard gall cards plus shoe effects
::
+$ card
$% card:agent:gall
[%shoe sole-ids=(list @ta) effect=shoe-effect] :: ~ sends to all soles
[%shoe sole-ids=(list sole-id) effect=shoe-effect] :: ~ sends to all
==
:: $shoe-effect: easier sole-effects
::
@ -47,30 +47,30 @@
:: if the head of the result is true, instantly run the command
::
++ command-parser
|~ sole-id=@ta
|~ =sole-id
|~(nail *(like [? command-type]))
:: +tab-list: autocomplete options for the session (to match +command-parser)
::
++ tab-list
|~ sole-id=@ta
|~ =sole-id
:: (list [@t tank])
*(list (option:auto tank))
:: +on-command: called when a valid command is run
::
++ on-command
|~ [sole-id=@ta command=command-type]
|~ [=sole-id command=command-type]
*(quip card _^|(..on-init))
::
++ can-connect
|~ sole-id=@ta
|~ =sole-id
*?
::
++ on-connect
|~ sole-id=@ta
|~ =sole-id
*(quip card _^|(..on-init))
::
++ on-disconnect
|~ sole-id=@ta
|~ =sole-id
*(quip card _^|(..on-init))
::
::NOTE standard gall agent arms below, though they may produce %shoe cards
@ -119,27 +119,27 @@
|* [shoe=* command-type=mold]
|_ =bowl:gall
++ command-parser
|= sole-id=@ta
|= =sole-id
(easy *[? command-type])
::
++ tab-list
|= sole-id=@ta
|= =sole-id
~
::
++ on-command
|= [sole-id=@ta command=command-type]
|= [=sole-id command=command-type]
[~ shoe]
::
++ can-connect
|= sole-id=@ta
|= =sole-id
(team:title [our src]:bowl)
::
++ on-connect
|= sole-id=@ta
|= =sole-id
[~ shoe]
::
++ on-disconnect
|= sole-id=@ta
|= =sole-id
[~ shoe]
--
:: +agent: creates wrapper core that handles sole events and calls shoe arms
@ -147,7 +147,7 @@
++ agent
|* command-type=mold
|= =(shoe command-type)
=| state-0
=| state-1
=* state -
^- agent:gall
=>
@ -164,8 +164,7 @@
%+ turn
?^ sole-ids.card sole-ids.card
~(tap in ~(key by soles))
|= sole-id=@ta
/sole/[sole-id]
id-to-path:sole
::
%table
=; fez=(list sole-effect)
@ -202,9 +201,36 @@
?. ?=([%shoe-app ^] q.old-state)
=^ cards shoe (on-load:og old-state)
[(deal cards) this]
=^ old-inner state +:!<([%shoe-app vase state-0] old-state)
=^ cards shoe (on-load:og old-inner)
[(deal cards) this]
|^ =| old-outer=state-any
=^ old-inner old-outer
+:!<([%shoe-app vase state-any] old-state)
:: ~! q.old-state
:: ?+ +>.q.old-state !!
:: [%0 *] +:!<([%shoe-app vase state-0] old-state)
:: [%1 *] +:!<([%shoe-app vase state-1] old-state)
:: ==
=^ caz shoe (on-load:og old-inner)
=^ cuz old-outer
?. ?=(%0 -.old-outer) [~ old-outer]
(state-0-to-1 old-outer)
?> ?=(%1 -.old-outer)
[(weld cuz (deal caz)) this(state old-outer)]
::
+$ state-any $%(state-1 state-0)
+$ state-0 [%0 soles=(map @ta sole-share)]
++ state-0-to-1
|= old=state-0
^- (quip card:agent:gall state-1)
:- %+ turn ~(tap in ~(key by soles.old))
|= id=@ta
^- card:agent:gall
[%give %kick ~[/sole/[id]] ~]
:- %1
%- ~(gas by *(map sole-id sole-share))
%+ murn ~(tap by soles.old)
|= [id=@ta s=sole-share]
(bind (upgrade-id:sole id) (late s))
--
::
++ on-poke
|= [=mark =vase]
@ -326,19 +352,18 @@
++ on-watch
|= =path
^- (quip card:agent:gall agent:gall)
?. ?=([%sole @ ~] path)
?~ sole-id=(path-to-id:sole path)
=^ cards shoe
(on-watch:og path)
[(deal cards) this]
=* sole-id i.t.path
?> (can-connect:og sole-id)
=. soles (~(put by soles) sole-id *sole-share)
?> (can-connect:og u.sole-id)
=. soles (~(put by soles) u.sole-id *sole-share)
=^ cards shoe
(on-connect:og sole-id)
(on-connect:og u.sole-id)
:_ this
%- deal
:_ cards
[%shoe [sole-id]~ %sole %pro & dap.bowl "> "]
[%shoe [u.sole-id]~ %sole %pro & dap.bowl "> "]
::
++ on-leave
|= =path

View File

@ -136,4 +136,28 @@
=+ dat=(transmute [%mor leg] [%ins pos `@c`0])
?> ?=(%ins -.dat)
p.dat
::
::
++ path-to-id
|= =path
^- (unit sole-id)
?. ?=([%sole @ ?(~ [@ ~])] path) ~
?~ who=(slaw %p i.t.path) ~
`[u.who ?~(t.t.path %$ i.t.t.path)]
::
++ id-to-path
|= sole-id
^- path
::TODO this whole "no empty path ending" business feels icky.
:: do we want default session to be ~.~ ?
:: concern here is that outsiders cannot subscribe to the default
:: session, because /sole/~zod/ isn't a valid path...
[%sole (scot %p who) ?~(ses ~ /[ses])]
::
++ upgrade-id
|= old=@ta
^- (unit sole-id)
%+ rush old
%+ cook (late %$)
;~(pfix (jest 'drum_~') fed:ag)
--

View File

@ -20,7 +20,9 @@
|= jon=^json ^- sole-action
%- need %. jon
=> [dejs-soft:format ..sole-action]
|^ (ot id+so dat+(fo %ret (of det+change tab+ni ~)) ~)
|^ (ot id+id dat+(fo %ret (of det+change tab+ni ~)) ~)
++ id
(ot who+(su ;~(pfix sig fed:ag)) ses+so ~)
++ fo
|* [a=term b=fist]
|=(c=json ?.(=([%s a] c) (b c) (some [a ~])))

View File

@ -3,8 +3,9 @@
::
^?
|%
+$ sole-id [who=@p ses=@ta]
+$ sole-action :: sole to app
$: id=@ta :: duct id
$: id=sole-id :: session id
$= dat
$% :: [%abo ~] :: reset interaction
[%det sole-change] :: command line edit

View File

@ -1 +1 @@
[%zuse 417]
[%zuse 416]

View File

@ -1 +1 @@
[%zuse 417]
[%zuse 416]

View File

@ -0,0 +1,23 @@
module.exports = exports = {
"rules": {
"spaced-comment": 0,
},
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:react/recommended",
],
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
typescript: {} // this loads <rootdir>/tsconfig.json to eslint
},
},
"env": {
"browser": true,
"es6": true
},
"plugins": ["import", "react-hooks"]
}

View File

@ -0,0 +1 @@
16.14.0

View File

@ -0,0 +1,94 @@
import React, {
useCallback, useEffect
} from 'react';
import useTermState from './state';
import { useDark } from './lib/useDark';
import api from './api';
import { _dark, _light } from '@tlon/indigo-react';
import 'xterm/css/xterm.css';
import {
scrySessions
} from '@urbit/api/term';
import { ThemeProvider } from 'styled-components';
import { Tabs } from './Tabs';
import Buffer from './Buffer';
import { DEFAULT_SESSION } from './constants';
import { showSlog } from './lib/blit';
import { InfoButton } from './InfoButton';
const initSessions = async () => {
const response = await api.scry(scrySessions());
useTermState.getState().set((state) => {
state.names = response.sort();
});
};
export default function TermApp() {
const { names, selected } = useTermState();
const dark = useDark();
const setupSlog = useCallback(() => {
console.log('slog: setting up...');
let available = false;
const slog = new EventSource('/~_~/slog', { withCredentials: true });
slog.onopen = () => {
console.log('slog: opened stream');
available = true;
};
slog.onmessage = (e) => {
const session = useTermState.getState().sessions[DEFAULT_SESSION];
if (!session) {
console.log('slog: default session mia!', 'msg:', e.data);
console.log(Object.keys(useTermState.getState().sessions), session);
return;
}
showSlog(session.term, e.data);
};
slog.onerror = (e) => {
console.error('slog: eventsource error:', e);
if (available) {
window.setTimeout(() => {
if (slog.readyState !== EventSource.CLOSED) {
return;
}
console.log('slog: reconnecting...');
setupSlog();
}, 10000);
}
};
useTermState.getState().set((state) => {
state.slogstream = slog;
});
}, []);
useEffect(() => {
initSessions();
setupSlog();
}, []);
return (
<>
<ThemeProvider theme={dark ? _dark : _light}>
<div className="header">
<Tabs />
<InfoButton />
</div>
<div className="buffer-container">
{names.map((name) => {
return <Buffer key={name} name={name} selected={name === selected} dark={dark} />;
})}
</div>
</ThemeProvider>
</>
);
}

View File

@ -0,0 +1,350 @@
import { Terminal, ITerminalOptions } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { debounce } from 'lodash';
import bel from './lib/bel';
import api from './api';
import {
Belt, pokeTask, pokeBelt
} from '@urbit/api/term';
import { Session } from './state';
import { useCallback, useEffect, useRef } from 'react';
import useTermState from './state';
import React from 'react';
import { Box, Col } from '@tlon/indigo-react';
import { makeTheme } from './lib/theme';
import { showBlit, csi, hasBell } from './lib/blit';
import { DEFAULT_SESSION, RESIZE_DEBOUNCE_MS, RESIZE_THRESHOLD_PX } from './constants';
import { retry } from './lib/retry';
const termConfig: ITerminalOptions = {
logLevel: 'warn',
//
convertEol: true,
//
rows: 24,
cols: 80,
scrollback: 10000,
//
fontFamily: '"Source Code Pro", "Roboto mono", "Courier New", monospace',
fontWeight: 400,
// NOTE theme colors configured dynamically
//
bellStyle: 'sound',
bellSound: bel,
//
// allows text selection by holding modifier (option, or shift)
macOptionClickForcesSelection: true,
// prevent insertion of simulated arrow keys on-altclick
altClickMovesCursor: false
};
const readInput = (term: Terminal, e: string): Belt[] => {
const belts: Belt[] = [];
let strap = '';
while (e.length > 0) {
let c = e.charCodeAt(0);
// text input
//
if (c >= 32 && c !== 127) {
strap += e[0];
e = e.slice(1); //TODO revisit wrt (list @c) & unicode characters
continue;
} else if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
// special keys/characters
//
if (0 === c) {
term.write('\x07'); // bel
} else if (8 === c || 127 === c) {
belts.push({ bac: null });
} else if (13 === c) {
belts.push({ ret: null });
} else if (c <= 26) {
const k = String.fromCharCode(96 + c);
//NOTE prevent remote shut-downs
if ('d' !== k) {
belts.push({ mod: { mod: 'ctl', key: k } });
}
}
// escape sequences
//
if (27 === c) { // ESC
e = e.slice(1);
c = e.charCodeAt(0);
if (91 === c || 79 === c) { // [ or O
e = e.slice(1);
c = e.charCodeAt(0);
/* eslint-disable max-statements-per-line */
switch (c) {
case 65: belts.push({ aro: 'u' }); break;
case 66: belts.push({ aro: 'd' }); break;
case 67: belts.push({ aro: 'r' }); break;
case 68: belts.push({ aro: 'l' }); break;
//
case 77: {
const m = e.charCodeAt(1) - 31;
if (1 === m) {
const c = e.charCodeAt(2) - 32;
const r = e.charCodeAt(3) - 32;
belts.push({ hit: { y: r - 1, x: c - 1 } });
}
e = e.slice(3);
break;
}
//
default: term.write('\x07'); break; // bel
}
} else if (c >= 97 && c <= 122) { // a <= c <= z
belts.push({ mod: { mod: 'met', key: e[0] } });
} else if (c === 46) { // .
belts.push({ mod: { mod: 'met', key: '.' } });
} else if (c === 8 || c === 127) {
belts.push({ mod: { mod: 'met', key: { bac: null } } });
} else {
term.write('\x07'); break; // bel
}
}
e = e.slice(1);
}
if ('' !== strap) {
if (1 === strap.length) {
belts.push(strap);
} else {
belts.push({ txt: strap.split('') });
}
strap = '';
}
return belts;
};
const onResize = async (name: string, session: Session) => {
if (session) {
session.fit.fit();
useTermState.getState().set((state) => {
state.sessions[name].pending++;
});
api.poke(pokeTask(name, { blew: { w: session.term.cols, h: session.term.rows } })).then(() => {
useTermState.getState().set((state) => {
state.sessions[name].pending--;
});
});
}
};
const onInput = (name: string, session: Session, e: string) => {
if (!session) {
return;
}
const term = session.term;
const belts = readInput(term, e);
belts.forEach((b) => {
useTermState.getState().set((state) => {
state.sessions[name].pending++;
});
api.poke(pokeBelt(name, b)).then(() => {
useTermState.getState().set((state) => {
state.sessions[name].pending--;
});
});
});
};
interface BufferProps {
name: string,
selected: boolean,
dark: boolean,
}
export default function Buffer({ name, selected, dark }: BufferProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const session: Session = useTermState(s => s.sessions[name]);
const initSession = useCallback(async (name: string, dark: boolean) => {
console.log('setting up', name === DEFAULT_SESSION ? 'default' : name);
// set up xterm terminal
//
const term = new Terminal(termConfig);
term.options.theme = makeTheme(dark);
const fit = new FitAddon();
term.loadAddon(fit);
fit.fit();
term.focus();
// start mouse reporting
//
term.write(csi('?9h'));
const ses: Session = {
term,
fit,
hasBell: false,
pending: 0,
subscriptionId: null
};
// set up event handlers
//
term.attachCustomKeyEventHandler((e: KeyboardEvent) => {
//NOTE ctrl+shift keypresses never make it into term.onData somehow,
// so we handle them specially ourselves.
// we may be able to remove this once xterm.js fixes #3382 & co.
if (e.shiftKey
&& e.ctrlKey
&& e.type === 'keydown'
&& e.key.length === 1
) {
api.poke(pokeBelt(name, { mod: { mod: 'ctl', key: e.key } }));
return false;
}
return true;
});
term.onData(e => onInput(name, ses, e));
term.onBinary(e => onInput(name, ses, e));
// open subscription
//
const initSubscription = async () => {
const subscriptionId = await api.subscribe({
app: 'herm', path: '/session/' + name + '/view',
event: (e) => {
showBlit(ses.term, e);
//NOTE getting selected from state because selected prop is stale
if (hasBell(e) && (useTermState.getState().selected !== name)) {
useTermState.getState().set((state) => {
state.sessions[name].hasBell = true;
});
}
//TODO should handle %bye on this higher level though, for deletion
},
err: (e, id) => {
console.log(`subscription error, id ${id}:`, e);
},
quit: async () => { // quit
console.error('quit, reconnecting...');
try {
const newSubscriptionId = await retry(initSubscription, () => {
console.log('attempting to reconnect ...');
}, 5);
useTermState.getState().set((state) => {
state.sessions[name].subscriptionId = newSubscriptionId;
});
} catch (error) {
console.log('unable to reconnect', error);
}
}
});
return subscriptionId;
};
ses.subscriptionId = await initSubscription();
useTermState.getState().set((state) => {
state.sessions[name] = ses;
});
}, []);
const shouldResize = useCallback(() => {
if(!session) {
return false;
}
const containerHeight = document.querySelector('.buffer-container')?.clientHeight || Infinity;
const terminalHeight = session.term.element?.clientHeight || 0;
return (containerHeight - terminalHeight) >= RESIZE_THRESHOLD_PX;
}, [session]);
const onSelect = useCallback(async () => {
if (session && selected && shouldResize()) {
session.fit.fit();
await api.poke(pokeTask(name, { blew: { w: session.term.cols, h: session.term.rows } }));
session.term.focus();
}
}, [session?.term, selected]);
// Effects
// init session
useEffect(() => {
if(session) {
return;
}
initSession(name, dark);
}, [name]);
// attach to DOM when ref is available
useEffect(() => {
if(session && containerRef.current && !session.term.element) {
session.term.open(containerRef.current);
}
}, [session, containerRef]);
// initialize resize listeners
//
useEffect(() => {
if(!session) {
return;
}
// TODO: use ResizeObserver for improved performance?
const debouncedResize = debounce(() => onResize(name, session), RESIZE_DEBOUNCE_MS);
window.addEventListener('resize', debouncedResize);
return () => {
window.removeEventListener('resize', debouncedResize);
};
}, [session]);
// on dark mode change, change terminals' theme
//
useEffect(() => {
const theme = makeTheme(dark);
if (session) {
session.term.options.theme = theme;
}
if (containerRef.current) {
containerRef.current.style.backgroundColor = theme.background || '';
}
}, [session, dark]);
// On select, resize, focus, and poke herm with updated cols and rows
useEffect(() => {
onSelect();
}, [onSelect]);
return (
!session && !selected ?
<p>Loading...</p>
:
<Box
width='100%'
height='100%'
bg='white'
fontFamily='mono'
overflow='hidden'
className="terminal-container"
style={selected ? { zIndex: 999 } : {}}
>
<Col
width='100%'
height='100%'
minHeight='0'
px={['0', '2']}
pb={['0', '2']}
ref={containerRef}
>
</Col>
</Box>
);
}

View File

@ -0,0 +1,24 @@
import React, { useCallback } from 'react';
import { Icon } from '@tlon/indigo-react';
import { useDetectOS } from './lib/useDetectOS';
export const InfoButton = () => {
const { isMacOS } = useDetectOS();
const onInfoClick = useCallback(() => {
const key = isMacOS ? 'alt' : 'shift';
alert(`To select text in the terminal, hold down the ${key} key.`);
}, [isMacOS]);
return (
<>
<button className="info-btn" onClick={onInfoClick}>
<Icon
icon="Info"
size="18px"
/>
</button>
</>
);
};

View File

@ -0,0 +1,55 @@
import useIsMounted from './lib/useIsMounted';
import React from 'react';
import { useEffect, useState } from 'react';
const DELAY_MS = 1000;
const FRAME_MS = 250;
const CHARS = '|/-\\';
const Spinner = () => {
const [index, setIndex] = useState(0);
const [intervalTimer, setIntervalTimer] = useState<ReturnType<typeof setInterval> | undefined>();
const isMounted = useIsMounted();
useEffect(() => {
setIntervalTimer(
setInterval(() => {
if (isMounted()) {
setIndex(idx => idx === CHARS.length - 1 ? 0 : idx + 1);
}
}, FRAME_MS)
);
return () => {
if (intervalTimer) {
clearInterval(intervalTimer);
}
};
}, []);
return <span>&nbsp;{CHARS[index]}</span>;
};
export const DelayedSpinner = () => {
const [showSpinner, setShowSpinner] = useState(false);
const [delayTimer, setDelayTimer] = useState<ReturnType<typeof setTimeout> | undefined>();
const isMounted = useIsMounted();
useEffect(() => {
setDelayTimer(
setTimeout(() => {
if (isMounted()) {
setShowSpinner(true);
}
}, DELAY_MS)
);
return () => {
if (delayTimer) {
clearTimeout(delayTimer);
}
};
}, []);
return showSpinner ? <Spinner /> : null;
};

View File

@ -0,0 +1,55 @@
import { DEFAULT_SESSION } from './constants';
import React, { useCallback, useEffect } from 'react';
import useTermState, { Session } from './state';
import api from './api';
import { pokeTask } from '@urbit/api/term';
import { DelayedSpinner as Spinner } from './Spinner';
interface TabProps {
session: Session;
name: string;
}
export const Tab = ( { session, name }: TabProps ) => {
const isSelected = useTermState().selected === name;
const onClick = () => {
useTermState.getState().set((state) => {
state.selected = name;
state.sessions[name].hasBell = false;
});
};
const onDelete = useCallback(async (e) => {
e.stopPropagation();
// clean up subscription
if(session && session.subscriptionId) {
await api.unsubscribe(session.subscriptionId);
}
// DELETE
await api.poke(pokeTask(name, { shut: null }));
// remove from zustand
useTermState.getState().set((state) => {
if (state.selected === name) {
state.selected = DEFAULT_SESSION;
}
state.names = state.names.filter(n => n !== name);
delete state.sessions[name];
});
}, [session]);
return (
<div className={'tab ' + (isSelected ? 'selected' : '')} onClick={onClick}>
<a className='session-name'>
{session?.hasBell ? '🔔 ' : ''}
{name === DEFAULT_SESSION ? 'default' : name}
{session && session.pending > 0 ? <Spinner /> : null}
{' '}
</a>
{name === DEFAULT_SESSION ? null : <a className="delete-session" onClick={onDelete}>x</a>}
</div>
);
};

View File

@ -0,0 +1,26 @@
import React from 'react';
import useTermState from './state';
import { Tab } from './Tab';
import { useAddSession } from './lib/useAddSession';
import { Icon } from '@tlon/indigo-react';
export const Tabs = () => {
const { sessions, names } = useTermState();
const addSession = useAddSession();
return (
<div className="tabs">
{names.map((n, i) => {
return (
<Tab session={sessions[n]} name={n} key={i} />
);
})}
<button className="tab" onClick={addSession}>
<Icon
icon="Plus"
size="18px"
/>
</button>
</div>
);
};

View File

@ -1,454 +0,0 @@
/* eslint-disable max-lines */
import React, {
useEffect,
useRef,
useCallback
} from 'react';
import useTermState from './state';
import { useDark } from './join';
import api from './api';
import { Terminal, ITerminalOptions, ITheme } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { saveAs } from 'file-saver';
import { Box, Col, Reset, _dark, _light } from '@tlon/indigo-react';
import 'xterm/css/xterm.css';
import {
Belt, Blit, Stye, Stub, Tint, Deco,
pokeTask, pokeBelt
} from '@urbit/api/term';
import bel from './lib/bel';
import { ThemeProvider } from 'styled-components';
type TermAppProps = {
ship: string;
}
const makeTheme = (dark: boolean): ITheme => {
let fg, bg: string;
if (dark) {
fg = 'white';
bg = 'rgb(26,26,26)';
} else {
fg = 'black';
bg = 'white';
}
// TODO indigo colors.
// we can't pluck these from ThemeContext because they have transparency.
// technically xterm supports transparency, but it degrades performance.
return {
foreground: fg,
background: bg,
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg,
cursorAccent: bg,
selection: fg
};
};
const termConfig: ITerminalOptions = {
logLevel: 'warn',
//
convertEol: true,
//
rows: 24,
cols: 80,
scrollback: 10000,
//
fontFamily: '"Source Code Pro", "Roboto mono", "Courier New", monospace',
fontWeight: 400,
// NOTE theme colors configured dynamically
//
bellStyle: 'sound',
bellSound: bel,
//
// allows text selection by holding modifier (option, or shift)
macOptionClickForcesSelection: true,
// prevent insertion of simulated arrow keys on-altclick
altClickMovesCursor: false
};
const csi = (cmd: string, ...args: number[]) => {
return '\x1b[' + args.join(';') + cmd;
};
const tint = (t: Tint) => {
switch (t) {
case null: return '9';
case 'k': return '0';
case 'r': return '1';
case 'g': return '2';
case 'y': return '3';
case 'b': return '4';
case 'm': return '5';
case 'c': return '6';
case 'w': return '7';
default: return `8;2;${t.r%256};${t.g%256};${t.b%256}`;
}
};
const stye = (s: Stye) => {
let out = '';
// text decorations
//
if (s.deco.length > 0) {
out += s.deco.reduce((decs: number[], deco: Deco) => {
/* eslint-disable max-statements-per-line */
switch (deco) {
case null: decs.push(0); return decs;
case 'br': decs.push(1); return decs;
case 'un': decs.push(4); return decs;
case 'bl': decs.push(5); return decs;
default: console.log('weird deco', deco); return decs;
}
}, []).join(';');
}
// background color
//
if (s.back !== null) {
if (out !== '') {
out += ';';
}
out += '4';
out += tint(s.back);
}
// foreground color
//
if (s.fore !== null) {
if (out !== '') {
out += ';';
}
out += '3';
out += tint(s.fore);
}
if (out === '') {
return out;
}
return '\x1b[' + out + 'm';
};
const showBlit = (term: Terminal, blit: Blit) => {
let out = '';
if ('bel' in blit) {
out += '\x07';
} else if ('clr' in blit) {
term.clear();
out += csi('u');
} else if ('hop' in blit) {
if (typeof blit.hop === 'number') {
out += csi('H', term.rows, blit.hop + 1);
} else {
out += csi('H', term.rows - blit.hop.r, blit.hop.c + 1);
}
out += csi('s'); // save cursor position
} else if ('put' in blit) {
out += blit.put.join('');
out += csi('u');
} else if ('klr' in blit) {
out += blit.klr.reduce((lin: string, p: Stub) => {
lin += stye(p.stye);
lin += p.text.join('');
lin += csi('m', 0);
return lin;
}, '');
out += csi('u');
} else if ('nel' in blit) {
out += '\n';
} else if ('sag' in blit || 'sav' in blit) {
const sav = ('sag' in blit) ? blit.sag : blit.sav;
const name = sav.path.split('/').slice(-2).join('.');
const buff = Buffer.from(sav.file, 'base64');
const blob = new Blob([buff], { type: 'application/octet-stream' });
saveAs(blob, name);
} else if ('url' in blit) {
window.open(blit.url);
} else if ('wyp' in blit) {
out += '\r' + csi('K');
out += csi('u');
//
} else {
console.log('weird blit', blit);
}
term.write(out);
};
// NOTE should generally only be passed the default terminal session
const showSlog = (term: Terminal, slog: string) => {
// set scroll region to exclude the bottom line,
// scroll up one line,
// move cursor to start of the newly created whitespace,
// set text to grey,
// print the slog,
// restore color, scroll region, and cursor.
//
term.write(csi('r', 1, term.rows - 1)
+ csi('S', 1)
+ csi('H', term.rows - 1, 1)
+ csi('m', 90)
+ slog
+ csi('m', 0)
+ csi('r')
+ csi('u'));
};
const readInput = (term: Terminal, e: string): Belt[] => {
const belts: Belt[] = [];
let strap = '';
while (e.length > 0) {
let c = e.charCodeAt(0);
// text input
//
if (c >= 32 && c !== 127) {
strap += e[0];
e = e.slice(1);
continue;
} else if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
// special keys/characters
//
if (0 === c) {
term.write('\x07'); // bel
} else if (8 === c || 127 === c) {
belts.push({ bac: null });
} else if (13 === c) {
belts.push({ ret: null });
} else if (c <= 26) {
let k = String.fromCharCode(96 + c);
//NOTE prevent remote shut-downs
if ('d' !== k) {
belts.push({ mod: { mod: 'ctl', key: k } });
}
}
// escape sequences
//
if (27 === c) { // ESC
e = e.slice(1);
c = e.charCodeAt(0);
if (91 === c || 79 === c) { // [ or O
e = e.slice(1);
c = e.charCodeAt(0);
/* eslint-disable max-statements-per-line */
switch (c) {
case 65: belts.push({ aro: 'u' }); break;
case 66: belts.push({ aro: 'd' }); break;
case 67: belts.push({ aro: 'r' }); break;
case 68: belts.push({ aro: 'l' }); break;
//
case 77: {
const m = e.charCodeAt(1) - 31;
if (1 === m) {
const c = e.charCodeAt(2) - 32;
const r = e.charCodeAt(3) - 32;
belts.push({ hit: { r: term.rows - r, c: c - 1 } });
}
e = e.slice(3);
break;
}
//
default: term.write('\x07'); break; // bel
}
} else if (c >= 97 && c <= 122) { // a <= c <= z
belts.push({ mod: { mod: 'met', key: e[0] } });
} else if (c === 46) { // .
belts.push({ mod: { mod: 'met', key: '.' } });
} else if (c === 8 || c === 127) {
belts.push({ mod: { mod: 'met', key: { bac: null } } });
} else {
term.write('\x07'); break; // bel
}
}
e = e.slice(1);
}
if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
return belts;
};
export default function TermApp(props: TermAppProps) {
const container = useRef<HTMLDivElement>(null);
// TODO allow switching of selected
const { sessions, selected, slogstream, set } = useTermState();
const session = sessions[selected];
const dark = useDark();
const setupSlog = useCallback(() => {
console.log('slog: setting up...');
let available = false;
const slog = new EventSource('/~_~/slog', { withCredentials: true });
slog.onopen = (e) => {
console.log('slog: opened stream');
available = true;
};
slog.onmessage = (e) => {
const session = useTermState.getState().sessions[''];
if (!session) {
console.log('default session mia!', 'slog:', slog);
return;
}
showSlog(session.term, e.data);
};
slog.onerror = (e) => {
console.error('slog: eventsource error:', e);
if (available) {
window.setTimeout(() => {
if (slog.readyState !== EventSource.CLOSED) {
return;
}
console.log('slog: reconnecting...');
setupSlog();
}, 10000);
}
};
set((state) => {
state.slogstream = slog;
});
}, [sessions]);
const onInput = useCallback((ses: string, e: string) => {
const term = useTermState.getState().sessions[ses].term;
const belts = readInput(term, e);
belts.map((b) => {
api.poke(pokeBelt(ses, b));
});
}, [sessions]);
const onResize = useCallback(() => {
// TODO debounce, if it ever becomes a problem
session?.fit.fit();
}, [session]);
// on-init, open slogstream
//
useEffect(() => {
if (!slogstream) {
setupSlog();
}
window.addEventListener('resize', onResize);
return () => {
// TODO clean up subs?
window.removeEventListener('resize', onResize);
};
}, [onResize, setupSlog]);
// on dark mode change, change terminals' theme
//
useEffect(() => {
const theme = makeTheme(dark);
for (const ses in sessions) {
sessions[ses].term.setOption('theme', theme);
}
if (container.current) {
container.current.style.backgroundColor = theme.background || '';
}
}, [dark, sessions]);
// on selected change, maybe setup the term, or put it into the container
//
useEffect(() => {
let ses = session;
// initialize terminal
//
if (!ses) {
// set up terminal
//
const term = new Terminal(termConfig);
term.setOption('theme', makeTheme(dark));
const fit = new FitAddon();
term.loadAddon(fit);
// start mouse reporting
//
term.write(csi('?9h'));
// set up event handlers
//
term.onData(e => onInput(selected, e));
term.onBinary(e => onInput(selected, e));
term.onResize((e) => {
api.poke(pokeTask(selected, { blew: { w: e.cols, h: e.rows } }));
});
ses = { term, fit };
// open subscription
//
api.subscribe({ app: 'herm', path: '/session/'+selected+'/view',
event: (e) => {
const ses = useTermState.getState().sessions[selected];
if (!ses) {
console.log('on blit: no such session', selected, sessions, useTermState.getState().sessions);
return;
}
showBlit(ses.term, e);
},
quit: () => { // quit
// TODO show user a message
}
});
}
if (container.current && !container.current.contains(ses.term.element || null)) {
ses.term.open(container.current);
ses.fit.fit();
ses.term.focus();
}
set((state) => {
state.sessions[selected] = ses;
});
return () => {
// TODO unload term from container
// but term.dispose is too powerful? maybe just empty the container?
};
}, [set, session, container]);
return (
<>
<ThemeProvider theme={dark ? _dark : _light}>
<Reset />
<Box
width='100%'
height='100%'
bg='white'
fontFamily='mono'
overflow='hidden'
>
<Col
width='100%'
height='100%'
minHeight='0'
px={['0','2']}
pb={['0','2']}
ref={container}
>
</Col>
</Box>
</ThemeProvider>
</>
);
}

View File

@ -0,0 +1,28 @@
export const DEFAULT_SESSION = '';
export const DEFAULT_HANDLER = 'hood';
export const RESIZE_DEBOUNCE_MS = 100;
export const RESIZE_THRESHOLD_PX = 15;
/**
* Session ID validity:
*
* - must start with an alphabetical
* - can be composed of alphanumerics with hyphens
* - can be length 1 or longer
*/
export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d-]*$)/;
/**
* Open a session with a given agent using `[agent]![session_name]`
*
* For example:
* ```
* book!my-session
* ```
*
* This will create a new session in webterm for the `%book` agent.
*
* Note that the second capture group after the ! is composed of the session ID
* regex above.
*/
export const AGENT_SESSION_REGEX = /^([a-z]{1}[a-z\d-]*)!([a-z]{1}[a-z\d-]*$)/;

View File

@ -23,10 +23,113 @@
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
<style>
body, #root {
height: 100vh;
height: 99vh; /* prevent scrollbar on outer frame */
margin: 0;
padding: 0;
}
.buffer-container {
height: calc(100% - 40px);
position: relative;
}
.terminal-container {
position: absolute;
top: 0;
}
div.header {
display: grid;
grid-template-areas: "tabs info";
grid-template-columns: auto min-content;
grid-template-rows: auto;
}
div.info {
grid-area: info;
}
div.tabs {
grid-area: tabs;
height: 40px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
padding: 5px 5px 0 5px;
border-bottom: 1px solid black;
background-color: white;
}
div.tabs > * {
margin-left: 5px;
margin-right: 5px;
border: solid 1px black;
padding: 10px;
cursor: pointer;
}
div.tab, button.tab {
margin-bottom: -1px; /** To overlay the selected tab on the tabs container bottom border */
border-top-left-radius: 5px;
border-top-right-radius: 5px;
font-family: monospace;
font-size: 14px;
line-height: 18px;
}
div.tabs > div.selected {
border-bottom: white solid 1px;
}
div.tabs > div.selected > a.session-name {
font-weight: bold;
}
div.tabs > a.delete-session {
padding: 5px;
}
button.info-btn {
border: none;
border-bottom: solid black 1px;
background: transparent;
padding: 10px;
line-height: 10px;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
html {
background-color: rgb(26,26,26);
}
div.tabs {
background-color: rgb(26, 26, 26);
color: rgba(255, 255, 255, 0.9);
border-bottom-color: rgba(255, 255, 255, 0.9);
}
div.tab {
background-color: rgb(26, 26, 26);
color: rgba(255, 255, 255, 0.9);
border-color: rgba(255, 255, 255, 0.9);
}
button.tab {
background-color: rgb(42, 42, 42);
color: rgba(255, 255, 255, 0.9);
border-color: rgba(255, 255, 255, 0.9);
}
div.tabs > div.selected {
border-bottom: rgb(26,26,26) solid 1px;
}
button.info-btn {
border-bottom: solid rgba(255, 255, 255, 0.9) 1px;
background: rgb(26, 26, 26);
}
}
</style>
</head>
<body>

View File

@ -0,0 +1,129 @@
import { Terminal } from 'xterm';
import { saveAs } from 'file-saver';
import { Blit, Stub, Stye } from '@urbit/api/term';
import { stye } from '../lib/stye';
export const csi = (cmd: string, ...args: number[]) => {
return '\x1b[' + args.join(';') + cmd;
};
export const showBlit = (term: Terminal, blit: Blit) => {
let out = '';
if ('mor' in blit) {
return blit.mor.map(b => showBlit(term, b));
} else if ('bel' in blit) {
out += '\x07';
} else if ('clr' in blit) {
term.clear();
out += csi('u');
} else if ('hop' in blit) {
if (typeof blit.hop === 'number') {
out += csi('H', term.rows, blit.hop + 1);
} else {
out += csi('H', blit.hop.y + 1, blit.hop.x + 1);
}
out += csi('s'); // save cursor position
} else if ('put' in blit) {
out += blit.put.join('');
out += csi('u');
} else if ('klr' in blit) {
out += blit.klr.reduce((lin: string, p: Stub) => {
lin += stye(p.stye);
lin += p.text.join('');
lin += csi('m', 0);
return lin;
}, '');
out += csi('u');
} else if ('nel' in blit) {
out += '\n';
} else if ('sag' in blit || 'sav' in blit) {
const sav = ('sag' in blit) ? blit.sag : blit.sav;
const name = sav.path.split('/').slice(-2).join('.');
const buff = Buffer.from(sav.file, 'base64');
const blob = new Blob([buff], { type: 'application/octet-stream' });
saveAs(blob, name);
} else if ('url' in blit) {
window.open(blit.url);
} else if ('wyp' in blit) {
out += '\r' + csi('K');
out += csi('u');
//
} else {
console.log('weird blit', blit);
}
term.write(out);
};
export const showSlog = (term: Terminal, slog: string) => {
// set scroll region to exclude the bottom line,
// scroll up one line,
// move cursor to start of the newly created whitespace,
// set text to grey,
// print the slog,
// restore color, scroll region, and cursor.
//
term.write(csi('r', 1, term.rows - 1)
+ csi('S', 1)
+ csi('H', term.rows - 1, 1)
+ csi('m', 90)
+ slog
+ csi('m', 0)
+ csi('r')
+ csi('u'));
};
export const hasBell = (blit: Blit) => {
if ('bel' in blit) {
return true;
} else if ('mor' in blit) {
return blit.mor.some(hasBell);
} else {
return false;
}
};
// debug rendering
//NOTE doesn't behave nicely in the presence of eob %nel blits,
// because those aren't idempotent
const blotStye: Stye = { deco: [], back: { r: 255, g: 0, b: 255 }, fore: 'k' };
const blitToBlot = (blit: Blit): Blit => {
if ('mor' in blit) {
return { mor: blit.mor.map(blitToBlot) };
} else if ('put' in blit) {
return { klr: [{ text: blit.put, stye: blotStye }] };
} else if ('klr' in blit) {
return { klr: blit.klr.map((s: Stub) => {
return { text: s.text, stye: blotStye };
}) };
} else {
return blit;
}
};
const queue: {term: Terminal, blit: Blit}[] = [];
const renderFromQueue = () => {
const next = queue.shift();
if (!next) {
return;
}
showBlit(next.term, next.blit);
if (0 === queue.length) {
return;
}
setTimeout(renderFromQueue, 200);
};
export const showBlitDebug = (term: Terminal, blit: Blit) => {
const blot = blitToBlot(blit);
if (0 === queue.length) {
showBlit(term, blot);
queue.push({ term, blit });
setTimeout(renderFromQueue, 200);
} else {
queue.push({ term, blit: blot });
queue.push({ term, blit });
}
};

View File

@ -0,0 +1,39 @@
/**
* Wait for the given milliseconds
* @param {number} milliseconds The given time to wait
* @returns {Promise} A fulfiled promise after the given time has passed
*/
function waitFor(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
/**
* Execute a promise and retry with exponential backoff
* based on the maximum retry attempts it can perform
* @param {Promise} promise promise to be executed
* @param {function} onRetry callback executed on every retry
* @param {number} maxRetries The maximum number of retries to be attempted
* @returns {Promise} The result of the given promise passed in
*/
export function retry(promise, onRetry, maxRetries) {
async function retryWithBackoff(retries) {
try {
if (retries > 0) {
const timeToWait = 2 ** retries * 100;
console.log(`waiting for ${timeToWait}ms...`);
await waitFor(timeToWait);
}
return await promise();
} catch (e) {
if (retries < maxRetries) {
onRetry();
return retryWithBackoff(retries + 1);
} else {
console.warn('Max retries reached. Bubbling the error up');
throw e;
}
}
}
return retryWithBackoff(0);
}

View File

@ -0,0 +1,60 @@
import { Deco, Stye, Tint } from '@urbit/api/term';
const tint = (t: Tint) => {
switch (t) {
case null: return '9';
case 'k': return '0';
case 'r': return '1';
case 'g': return '2';
case 'y': return '3';
case 'b': return '4';
case 'm': return '5';
case 'c': return '6';
case 'w': return '7';
default: return `8;2;${t.r%256};${t.g%256};${t.b%256}`;
}
};
export const stye = (s: Stye) => {
let out = '';
// text decorations
//
if (s.deco.length > 0) {
out += s.deco.reduce((decs: number[], deco: Deco) => {
/* eslint-disable max-statements-per-line */
switch (deco) {
case null: decs.push(0); return decs;
case 'br': decs.push(1); return decs;
case 'un': decs.push(4); return decs;
case 'bl': decs.push(5); return decs;
default: console.log('weird deco', deco); return decs;
}
}, []).join(';');
}
// background color
//
if (s.back !== null) {
if (out !== '') {
out += ';';
}
out += '4';
out += tint(s.back);
}
// foreground color
//
if (s.fore !== null) {
if (out !== '') {
out += ';';
}
out += '3';
out += tint(s.fore);
}
if (out === '') {
return out;
}
return '\x1b[' + out + 'm';
};

View File

@ -0,0 +1,23 @@
import { ITheme } from 'xterm';
export const makeTheme = (dark: boolean): ITheme => {
let fg, bg: string;
if (dark) {
fg = 'white';
bg = 'rgb(26,26,26)';
} else {
fg = 'black';
bg = 'white';
}
// TODO indigo colors.
// we can't pluck these from ThemeContext because they have transparency.
// technically xterm supports transparency, but it degrades performance.
return {
foreground: fg,
background: bg,
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg,
cursorAccent: bg,
selection: fg
};
};

View File

@ -0,0 +1,66 @@
import {
DEFAULT_HANDLER,
AGENT_SESSION_REGEX,
SESSION_ID_REGEX
} from '../constants';
import useTermState from '../state';
import api from '../api';
import { pokeTask } from '@urbit/api/term';
import { useCallback } from 'react';
export const useAddSession = () => {
const { names } = useTermState();
const addSession = useCallback(async () => {
let agent = DEFAULT_HANDLER;
let sessionName: string;
const userInput = prompt('Please enter an alpha-numeric session name.');
// user canceled or did not enter a value
if (null === userInput) {
return;
}
// check for custom agent session syntax
if (AGENT_SESSION_REGEX.test(userInput)) {
const match = AGENT_SESSION_REGEX.exec(userInput);
if (!match) {
alert('Invalid format. Valid syntax: agent!session-name');
return;
}
agent = match[1];
sessionName = match[2];
// else, use the default session creation regex
} else if (SESSION_ID_REGEX.test(userInput)) {
const match = SESSION_ID_REGEX.exec(userInput);
if (!match) {
alert('Invalid format. Valid syntax: session-name');
return;
}
sessionName = match[1];
} else {
alert('Invalid format. Valid syntax: session-name');
return;
}
// prevent duplicate sessions
if(names.includes(sessionName)) {
alert(`Session name must be unique ("${sessionName}" already in use)`);
return;
}
try {
//TODO eventually, customizable app pre-linking?
await api.poke(pokeTask(sessionName, { open: { term: agent, apps: [{ who: '~' + (window as any).ship, app: 'dojo' }] } }));
useTermState.getState().set((state) => {
state.names = [sessionName, ...state.names].sort();
state.selected = sessionName;
state.sessions[sessionName] = null;
});
} catch (error) {
console.log('unable to create session:', error);
}
}, [names]);
return addSession;
};

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { useTheme } from './settings';
import useTermState from './state';
import useTermState from '../state';
export function useDark() {
const [osDark, setOsDark] = useState(false);
@ -11,12 +10,11 @@ export function useDark() {
setOsDark(e.matches);
};
setOsDark(themeWatcher.matches);
themeWatcher.addListener(update);
themeWatcher.addEventListener('change', update);
return () => {
themeWatcher.removeListener(update);
}
themeWatcher.removeEventListener('change', update);
};
}, []);
const theme = useTermState(s => s.theme);

View File

@ -0,0 +1,44 @@
/* eslint-disable no-useless-escape */
// Regex patterns inspired by:
// https://github.com/faisalman/ua-parser-js/blob/master/src/ua-parser.js
const LINUX = [
/\b(joli|palm)\b ?(?:os)?\/?([\w\.]*)/i,
/(mint)[\/\(\) ]?(\w*)/i,
/(mageia|vectorlinux)[; ]/i,
/([kxln]?ubuntu|debian|suse|opensuse|gentoo|arch(?= linux)|slackware|fedora|mandriva|centos|pclinuxos|red ?hat|zenwalk|linpus|raspbian|plan 9|minix|risc os|contiki|deepin|manjaro|elementary os|sabayon|linspire)(?: gnu\/linux)?(?: enterprise)?(?:[- ]linux)?(?:-gnu)?[-\/ ]?(?!chrom|package)([-\w\.]*)/i,
/(hurd|linux) ?([\w\.]*)/i,
/(gnu) ?([\w\.]*)/i,
/\b([-frentopcghs]{0,5}bsd|dragonfly)[\/ ]?(?!amd|[ix346]{1,2}86)([\w\.]*)/i,
/(haiku) (\w+)/i,
/(sunos) ?([\w\.\d]*)/i,
/((?:open)?solaris)[-\/ ]?([\w\.]*)/i,
/(aix) ((\d)(?=\.|\)| )[\w\.])*/i,
/\b(beos|os\/2|amigaos|morphos|openvms|fuchsia|hp-ux)/i,
/(unix) ?([\w\.]*)/i
];
const MAC_OS = [
/(mac os x) ?([\w\. ]*)/i,
/(macintosh|mac_powerpc\b)(?!.+haiku)/i
];
const WINDOWS = [
/microsoft (windows) (vista|xp)/i,
/(windows) nt 6\.2; (arm)/i,
/(windows (?:phone(?: os)?|mobile))[\/ ]?([\d\.\w ]*)/i,
/(windows)[\/ ]?([ntce\d\. ]+\w)(?!.+xbox)/i
];
export const useDetectOS = () => {
const userAgent = navigator.userAgent;
const isLinux = LINUX.some(regex => regex.test(userAgent));
const isMacOS = MAC_OS.some(regex => regex.test(userAgent));
const isWindows = WINDOWS.some(regex => regex.test(userAgent));
return {
isLinux,
isMacOS,
isWindows
};
};

View File

@ -0,0 +1,17 @@
import { useCallback, useEffect, useRef } from 'react';
function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return useCallback(() => isMounted.current, []);
}
export default useIsMounted;

Binary file not shown.

View File

@ -11,6 +11,7 @@
"@urbit/api": "^1.1.1",
"@urbit/http-api": "^1.2.1",
"file-saver": "^2.0.5",
"lodash": "^4.17.21",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-router-dom": "^5.2.0",
@ -36,7 +37,7 @@
"@types/styled-system": "^5.1.10",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.24.0",
"@urbit/eslint-config": "^1.0.0",
"@urbit/eslint-config": "^1.0.3",
"@welldone-software/why-did-you-render": "^6.1.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
@ -45,7 +46,9 @@
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint-plugin-react": "^7.22.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^4.5.1",
"husky": "^6.0.0",
@ -58,7 +61,8 @@
"webpack-dev-server": "^3.11.2"
},
"scripts": {
"lint": "eslint ./src/**/*.{ts,tsx}",
"lint": "eslint ./**/*.{ts,tsx}",
"lint-fix": "eslint --fix ./**/*.{ts,tsx}",
"lint-file": "eslint",
"tsc": "tsc",
"tsc:watch": "tsc --watch",

View File

@ -3,21 +3,33 @@ import { FitAddon } from 'xterm-addon-fit';
import create from 'zustand';
import produce from 'immer';
type Session = { term: Terminal, fit: FitAddon };
type Sessions = { [id: string]: Session; }
export type Session = {
term: Terminal,
fit: FitAddon,
hasBell: boolean,
pending: number,
subscriptionId: number | null,
} | null;
export type Sessions = { [id: string]: Session; }
export interface TermState {
sessions: Sessions,
names: string[],
selected: string,
slogstream: null | EventSource,
theme: 'auto' | 'light' | 'dark'
};
theme: 'auto' | 'light' | 'dark',
//TODO: figure out the type
set: any,
}
// eslint-disable-next-line no-unused-vars
const useTermState = create<TermState>((set, get) => ({
sessions: {} as Sessions,
names: [''],
selected: '', // empty string is default session
slogstream: null,
theme: 'auto',
// eslint-disable-next-line no-unused-vars
set: (f: (draft: TermState) => void) => {
set(produce(f));
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": false,
"noImplicitReturns": false,
"moduleResolution": "node",
"esModuleInterop": true,
"noUnusedLocals": false,
"noImplicitAny": false,
"noEmit": true,
"target": "ESNext",
"module": "ESNext",
"strict": false,
"strictNullChecks": true,
"jsx": "react",
"baseUrl": "."
},
"include": [
"**/*"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -16,15 +16,15 @@
+$ card card:shoe
::
+$ versioned-state
$% state-3
$% state-4
state-3
state-2
state-1
state-0
==
::
+$ state-3
$: %3
::TODO support multiple sessions
+$ state-4
$: %4
sessions=(map sole-id session) :: sole sessions
bound=(map resource glyph) :: bound resource glyphs
binds=(jug glyph resource) :: resource glyph lookup
@ -33,7 +33,17 @@
timez=(pair ? @ud) :: timezone adjustment
==
::
+$ sole-id @ta
+$ state-3
$: %3
sessions=(map @ta session)
bound=(map resource glyph)
binds=(jug glyph resource)
settings=(set term)
width=@ud
timez=(pair ? @ud)
==
::
+$ sole-id sole-id:shoe
+$ session
$: viewing=(set resource) :: connected graphs
history=(list uid:post) :: scrollback pointers
@ -115,7 +125,7 @@
== ::
::
--
=| state-3
=| state-4
=* state -
::
%- agent:dbug
@ -258,14 +268,14 @@
settings width timez
==
::
=^ cards u.old
=^ cards-1 u.old
?. ?=(%2 -.u.old) [~ u.old]
:- :~ [%pass /chat-store %agent [our-self %chat-store] %leave ~]
[%pass /invites %agent [our.bowl %invite-store] %leave ~]
==
^- state-3
:- %3
:* %+ ~(put in *(map sole-id session))
:* %+ ~(put in *(map @ta session))
(cat 3 'drum_' (scot %p our.bowl))
:* ~ ~ 0
::
@ -290,14 +300,29 @@
timez.u.old
==
::
?> ?=(%3 -.u.old)
=^ cards-2 u.old
?. ?=(%3 -.u.old) [~ u.old]
:- %+ turn ~(tap in ~(key by sessions.u.old))
|= id=@ta
^- card:agent:gall
[%give %kick ~[/sole/[id]] ~]
=- u.old(- %4, sessions -)
%- ~(gas by *(map sole-id session))
%+ murn ~(tap by sessions.u.old)
|= [id=@ta s=session]
(bind (upgrade-id:sole:shoe id) (late s))
::
?> ?=(%4 -.u.old)
:_ u.old
%+ welp
cards
;: welp
cards-1
cards-2
::
?: %- ~(has by wex.bowl)
[/graph-store our-self %graph-store]
~
~[connect]
==
:: +connect: connect to the graph-store
::
++ connect

View File

@ -1 +1 @@
[%zuse 417]
[%zuse 416]

View File

@ -1,4 +1,5 @@
import { Poke } from '../lib'
import { Scry } from '../../http-api/src'
import { Poke } from '../../http-api/src/types';
import { Belt, Task, SessionTask } from './types';
export const pokeTask = (session: string, task: Task): Poke<SessionTask> => ({

View File

@ -25,10 +25,11 @@ export type Stub = {
export type Blit =
| { bel: null } // make a noise
| { clr: null } // clear the screen
| { hop: number | { r: number, c: number } } // set cursor col/pos
| { hop: number | { x: number, y: number } } // set cursor col/pos
| { klr: Stub[] } // put styled
| { put: string[] } // put text at cursor
| { mor: Blit[] } // multiple blits
| { nel: null } // newline
| { put: string[] } // put text at cursor
| { sag: { path: string, file: string } } // save to jamfile
| { sav: { path: string, file: string } } // save to file
| { url: string } // activate url
@ -42,7 +43,7 @@ export type Bolt =
| { aro: 'd' | 'l' | 'r' | 'u' }
| { bac: null }
| { del: null }
| { hit: { r: number, c: number } }
| { hit: { x: number, y: number } }
| { ret: null }
export type Belt =
@ -53,8 +54,8 @@ export type Belt =
export type Task =
| { belt: Belt }
| { blew: { w: number, h: number } }
| { flow: { term: string, apps: Array<{ who: string, app: string }> } }
| { hail: null }
| { hook: null }
| { open: { term: string, apps: Array<{ who: string, app: string }> } }
| { shut: null }
export type SessionTask = { session: string } & Task

View File

@ -572,7 +572,7 @@ _setup_cert_store()
{
BIO* cbio = BIO_new_mem_buf(include_ca_bundle_crt, include_ca_bundle_crt_len);
if ( !cbio || !(_cert_store = PEM_X509_INFO_read_bio(cbio, NULL, NULL, NULL)) ) {
u3l_log("boot: failed to decode embedded CA certificates\r\n");
u3l_log("boot: failed to decode embedded CA certificates");
exit(1);
}
@ -1267,7 +1267,7 @@ _cw_eval(c3_i argc, c3_c* argv[])
}
u3s_cue_xeno_done(sil_u);
if ( c3n == u3v_boot_lite(pil) ) {
u3l_log("lite: boot failed\r\n");
u3l_log("lite: boot failed");
exit(1);
}
}
@ -1858,7 +1858,7 @@ _cw_vere(c3_i argc, c3_c* argv[])
// initialize curl
//
if ( 0 != curl_global_init(CURL_GLOBAL_DEFAULT) ) {
u3l_log("boot: curl initialization failed\r\n");
u3l_log("boot: curl initialization failed");
exit(1);
}
@ -1888,11 +1888,11 @@ _cw_vere(c3_i argc, c3_c* argv[])
if ( u3_king_vere(pac_c, ver_c, arc_c, dir_c, 0) ) {
u3l_log("vere: download failed\r\n");
u3l_log("vere: download failed");
exit(1);
}
u3l_log("vere: download succeeded\r\n");
u3l_log("vere: download succeeded");
}
/* _cw_vile(): generatoe/print keyfile
@ -2230,7 +2230,7 @@ main(c3_i argc,
{
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
if ( NULL == (u3_Host.cev_u = CreateEvent(&sa, FALSE, FALSE, NULL)) ) {
u3l_log("boot: failed to create Ctrl-C event: %d\r\n", GetLastError());
u3l_log("boot: failed to create Ctrl-C event: %d", GetLastError());
exit(1);
}
}

View File

@ -156,12 +156,14 @@
c3_o mou; // M (for mouse event) received
c3_y ton_y; // mouse button
c3_y col_y; // column coordinate
c3_y seq_y; // vt sequence
} esc;
struct {
struct { // input buffering
c3_y syb_y[5]; // utf8 code buffer
c3_w len_w; // present length
c3_w wid_w; // total width
u3_noun imp; // %txt input buffer
} fut;
struct {

View File

@ -1174,13 +1174,8 @@ u3e_live(c3_o nuu_o, c3_c* dir_c)
/* If the images were empty, we are logically booting.
*/
<<<<<<< HEAD
if ( !nor_w && !sou_w ) {
u3l_log("live: logical boot");
=======
if ( (0 == u3P.nor_u.pgs_w) && (0 == u3P.sou_u.pgs_w) ) {
u3l_log("live: logical boot\r\n");
>>>>>>> next/arvo
u3l_log("live: logical boot");
nuu_o = c3y;
}
else {

View File

@ -33,7 +33,7 @@ u3_weak
u3l_punt(const char* name, u3_weak pro)
{
if ( u3_none == pro ) {
u3l_log("%s-punt\r\n", name);
u3l_log("%s-punt", name);
}
return pro;
}

View File

@ -1772,7 +1772,7 @@ _cm_crypto()
if ( 0 == CRYPTO_set_mem_functions(&_cm_malloc_ssl,
&_cm_realloc_ssl,
&_cm_free_ssl) ) {
u3l_log("%s\r\n", "openssl initialization failed");
u3l_log("%s", "openssl initialization failed");
abort();
}
@ -1815,7 +1815,7 @@ u3m_init(size_t len_i)
|| (len_i < (1 << (u3a_page + 2)))
|| (len_i > u3a_bytes) )
{
u3l_log("loom: bad size: %zu\r\n", len_i);
u3l_log("loom: bad size: %zu", len_i);
exit(1);
}

View File

@ -198,7 +198,7 @@ u3v_lily(u3_noun fot, u3_noun txt, c3_l* tid_l)
(c3n == u3r_safe_word(q_uco, &wad_w)) ||
(wad_w & 0x80000000) )
{
u3l_log("strange lily %s\n", u3r_string(txt));
u3l_log("strange lily %s", u3r_string(txt));
u3z(txt); u3z(uco); return c3n;
}
else {

View File

@ -610,7 +610,7 @@ u3_disk_acquire(c3_c* pax_c)
if ( NULL != (loq_u = c3_fopen(paf_c, "r")) ) {
if ( 1 != fscanf(loq_u, "%" SCNu32, &pid_w) ) {
u3l_log("lockfile %s is corrupt!\n", paf_c);
u3l_log("lockfile %s is corrupt!", paf_c);
kill(getpid(), SIGTERM);
sleep(1); c3_assert(0);
}
@ -620,13 +620,13 @@ u3_disk_acquire(c3_c* pax_c)
int ret = kill(pid_w, SIGTERM);
if ( -1 == ret && errno == EPERM ) {
u3l_log("disk: permission denied when trying to kill process %d!\n", pid_w);
u3l_log("disk: permission denied when trying to kill process %d!", pid_w);
kill(getpid(), SIGTERM);
sleep(1); c3_assert(0);
}
if ( -1 != ret ) {
u3l_log("disk: stopping process %d, live in %s...\n",
u3l_log("disk: stopping process %d, live in %s...",
pid_w, pax_c);
for ( i_w = 0; i_w < 16; i_w++ ) {
@ -644,10 +644,10 @@ u3_disk_acquire(c3_c* pax_c)
}
}
if ( 16 == i_w ) {
u3l_log("disk: process %d seems unkillable!\n", pid_w);
u3l_log("disk: process %d seems unkillable!", pid_w);
c3_assert(0);
}
u3l_log("disk: stopped old process %u\n", pid_w);
u3l_log("disk: stopped old process %u", pid_w);
}
}
fclose(loq_u);
@ -655,7 +655,7 @@ u3_disk_acquire(c3_c* pax_c)
}
if ( NULL == (loq_u = c3_fopen(paf_c, "w")) ) {
u3l_log("disk: unable to open %s\n", paf_c);
u3l_log("disk: unable to open %s", paf_c);
c3_assert(0);
}
@ -703,7 +703,7 @@ u3_disk_exit(u3_disk* log_u)
if ( (c3y == log_u->ted_o)
&& (0 > uv_cancel(&log_u->req_u)) )
{
// u3l_log("disk: unable to cleanup\r\n");
// u3l_log("disk: unable to cleanup");
return;
}

View File

@ -726,7 +726,7 @@ _ames_ef_send(u3_ames* sam_u, u3_noun lan, u3_noun pac)
//
else if ( 0 == lan_u.por_s ) {
if ( u3C.wag_w & u3o_verbose ) {
u3l_log("ames: inscrutable lane\n");
u3l_log("ames: inscrutable lane");
}
_ames_pact_free(pac_u);
}

View File

@ -333,7 +333,7 @@ _conn_moor_bail(void* ptr_v, ssize_t err_i, const c3_c* err_c)
u3_shan* san_u = can_u->san_u;
if ( err_i != UV_EOF ) {
u3l_log("conn: moor bail %zd %s\n", err_i, err_c);
u3l_log("conn: moor bail %zd %s", err_i, err_c);
if ( _(can_u->liv_o) ) {
_conn_send_noun(can_u, u3nq(0, c3__bail, u3i_word(err_i),
u3i_string(err_c)));
@ -573,7 +573,7 @@ _conn_moor_poke(void* ptr_v, c3_d len_d, c3_y* byt_y)
rud = u3dc("scot", c3__uv, u3k(rid));
tag_c = u3r_string(tag);
rid_c = u3r_string(rud);
u3l_log("conn: %s %s\n", tag_c, rid_c);
u3l_log("conn: %s %s", tag_c, rid_c);
c3_free(tag_c); c3_free(rid_c);
switch (tag) {
@ -690,7 +690,7 @@ _conn_init_sock(u3_shan* san_u)
c3_assert(!ret_i);
ret_i = uv_listen((uv_stream_t*)&san_u->pyp_u, 0, _conn_sock_cb);
c3_assert(!ret_i);
u3l_log("conn: listening on %s\n", pip_c);
u3l_log("conn: listening on %s", pip_c);
#else // _WIN32
// the full socket path is limited to about 108 characters,
@ -702,46 +702,46 @@ _conn_init_sock(u3_shan* san_u)
c3_i err_i;
if ( NULL == getcwd(pax_c, sizeof(pax_c)) ) {
u3l_log("conn: getcwd: %s\n", uv_strerror(errno));
u3l_log("conn: getcwd: %s", uv_strerror(errno));
u3_king_bail();
}
if ( 0 != chdir(u3_Host.dir_c) ) {
u3l_log("conn: chdir: %s\n", uv_strerror(errno));
u3l_log("conn: chdir: %s", uv_strerror(errno));
u3_king_bail();
}
if ( 0 != unlink(URB_SOCK_PATH) && errno != ENOENT ) {
u3l_log("conn: unlink: %s\n", uv_strerror(errno));
u3l_log("conn: unlink: %s", uv_strerror(errno));
goto _conn_sock_err_chdir;
}
if ( 0 != (err_i = uv_pipe_init(u3L, &san_u->pyp_u, 0)) ) {
u3l_log("conn: uv_pipe_init: %s\n", uv_strerror(err_i));
u3l_log("conn: uv_pipe_init: %s", uv_strerror(err_i));
goto _conn_sock_err_chdir;
}
if ( 0 != (err_i = uv_pipe_bind(&san_u->pyp_u, URB_SOCK_PATH)) ) {
u3l_log("conn: uv_pipe_bind: %s\n", uv_strerror(err_i));
u3l_log("conn: uv_pipe_bind: %s", uv_strerror(err_i));
goto _conn_sock_err_chdir;
}
if ( 0 != (err_i = uv_listen((uv_stream_t*)&san_u->pyp_u, 0,
_conn_sock_cb)) ) {
u3l_log("conn: uv_listen: %s\n", uv_strerror(err_i));
u3l_log("conn: uv_listen: %s", uv_strerror(err_i));
goto _conn_sock_err_unlink;
}
if ( 0 != chdir(pax_c) ) {
u3l_log("conn: chdir: %s\n", uv_strerror(errno));
u3l_log("conn: chdir: %s", uv_strerror(errno));
goto _conn_sock_err_close;
}
u3l_log("conn: listening on %s/%s\n", u3_Host.dir_c, URB_SOCK_PATH);
u3l_log("conn: listening on %s/%s", u3_Host.dir_c, URB_SOCK_PATH);
return;
_conn_sock_err_close:
uv_close((uv_handle_t*)&san_u->pyp_u, _conn_close_cb);
_conn_sock_err_unlink:
if ( 0 != unlink(URB_SOCK_PATH) ) {
u3l_log("conn: unlink: %s\n", uv_strerror(errno));
u3l_log("conn: unlink: %s", uv_strerror(errno));
}
_conn_sock_err_chdir:
if ( 0 != chdir(pax_c) ) {
u3l_log("conn: chdir: %s\n", uv_strerror(errno));
u3l_log("conn: chdir: %s", uv_strerror(errno));
}
u3_king_bail();
#endif // _WIN32
@ -764,7 +764,7 @@ _conn_born_news(u3_ovum* egg_u, u3_ovum_news new_e)
static void
_conn_born_bail(u3_ovum* egg_u, u3_noun lud)
{
u3l_log("conn: %%born failure; %%fyrd not supported\n");
u3l_log("conn: %%born failure; %%fyrd not supported");
u3z(lud);
u3_ovum_free(egg_u);
}
@ -820,7 +820,7 @@ _conn_ef_handle(u3_conn* con_u,
}
}
else {
u3l_log("conn: handle-no-coq %" PRIx32 " %" PRIu32 "\n",
u3l_log("conn: handle-no-coq %" PRIx32 " %" PRIu32,
sev_l, coq_l);
}
u3z(rid); u3z(tag); u3z(dat);
@ -866,11 +866,11 @@ _conn_io_exit(u3_auto* car_u)
if ( 0 != unlink(paf_c) ) {
if ( ENOENT != errno ) {
u3l_log("conn: failed to unlink socket: %s\n", uv_strerror(errno));
u3l_log("conn: failed to unlink socket: %s", uv_strerror(errno));
}
}
else {
// u3l_log("conn: unlinked %s\n", paf_c);
// u3l_log("conn: unlinked %s", paf_c);
}
c3_free(paf_c);

View File

@ -147,6 +147,7 @@ u3_term_log_init(void)
uty_u->tat_u.fut.len_w = 0;
uty_u->tat_u.fut.wid_w = 0;
uty_u->tat_u.fut.imp = u3_nul;
}
// default size
@ -384,7 +385,7 @@ _term_it_clear_line(u3_utty* uty_u)
// if we're clearing the bottom line, clear our mirror of it too
//
if ( 0 == uty_u->tat_u.mir.rus_w ) {
if ( uty_u->tat_u.siz.row_l - 1 == uty_u->tat_u.mir.rus_w ) {
_term_it_free_line(uty_u);
}
}
@ -400,19 +401,19 @@ _term_it_show_blank(u3_utty* uty_u)
/* _term_it_move_cursor(): move cursor to row & column
*
* row 0 is at the bottom, col 0 is to the left.
* row 0 is at the top, col 0 is to the left.
* if the given position exceeds the known window size,
* it is clipped to stay within the window.
*/
static void
_term_it_move_cursor(u3_utty* uty_u, c3_w row_w, c3_w col_w)
_term_it_move_cursor(u3_utty* uty_u, c3_w col_w, c3_w row_w)
{
c3_l row_l = uty_u->tat_u.siz.row_l;
c3_l col_l = uty_u->tat_u.siz.col_l;
if ( row_w >= row_l ) { row_w = row_l - 1; }
if ( col_w >= col_l ) { col_w = col_l - 1; }
_term_it_send_csi(uty_u, 'H', 2, row_l - row_w, col_w + 1);
_term_it_send_csi(uty_u, 'H', 2, row_w + 1, col_w + 1);
_term_it_dump_buf(uty_u, &uty_u->ufo_u.suc_u);
uty_u->tat_u.mir.rus_w = row_w;
@ -472,7 +473,7 @@ _term_it_restore_line(u3_utty* uty_u)
{
u3_utat* tat_u = &uty_u->tat_u;
_term_it_send_csi(uty_u, 'H', 2, tat_u->siz.row_l, 0);
_term_it_send_csi(uty_u, 'H', 2, tat_u->siz.row_l, 1);
_term_it_dump_buf(uty_u, &uty_u->ufo_u.cel_u);
_term_it_send_stub(uty_u, u3k(tat_u->mir.lin));
//NOTE send_stub restores cursor position
@ -489,7 +490,8 @@ _term_it_save_stub(u3_utty* uty_u, u3_noun tub)
// keep track of changes to bottom-most line, to aid spinner drawing logic.
// -t mode doesn't need this logic, because it doesn't render the spinner.
//
if ( (0 == tat_u->mir.rus_w) && (c3n == u3_Host.ops_u.tem)) {
if ( ( tat_u->siz.row_l - 1 == tat_u->mir.rus_w ) &&
( c3n == u3_Host.ops_u.tem ) ) {
lin = u3dq("wail:klr:format", lin, tat_u->mir.cus_w, u3k(tub), ' ');
lin = u3do("pact:klr:format", lin);
}
@ -512,8 +514,8 @@ _term_it_show_nel(u3_utty* uty_u)
}
uty_u->tat_u.mir.cus_w = 0;
if ( uty_u->tat_u.mir.rus_w > 0 ) {
uty_u->tat_u.mir.rus_w--;
if ( uty_u->tat_u.mir.rus_w < uty_u->tat_u.siz.row_l - 1 ) {
uty_u->tat_u.mir.rus_w++;
}
else {
// newline at bottom of screen, so bottom line is now empty
@ -617,7 +619,26 @@ _term_io_belt(u3_utty* uty_u, u3_noun blb)
}
}
/* _term_io_spit(): input the buffer (if any), then input the belt (if any)
*/
static void
_term_io_spit(u3_utty* uty_u, u3_noun bet) {
u3_utat* tat_u = &uty_u->tat_u;
if (u3_nul != tat_u->fut.imp) {
_term_io_belt(uty_u, u3nc(c3__txt, u3kb_flop(tat_u->fut.imp)));
tat_u->fut.imp = u3_nul;
}
if (u3_none != bet) {
_term_io_belt(uty_u, bet);
}
}
/* _term_io_suck_char(): process a single character.
*
* Note that this accumulates simple inputs in a buffer, and is not
* guaranteed to flush it fully. After a call (or sequence of calls)
* to this function, please call _term_io_spit(uty_u, u3_none) to
* flush any remainder.
*/
static void
_term_io_suck_char(u3_utty* uty_u, c3_y cay_y)
@ -628,33 +649,53 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y)
//
if ( c3y == tat_u->esc.ape ) {
if ( c3y == tat_u->esc.bra ) {
// vt sequence
//
if ( cay_y == '~' ) {
switch ( tat_u->esc.seq_y ) {
default: {
_term_it_dump_buf(uty_u, &uty_u->ufo_u.bel_u);
break;
}
case '3': _term_io_spit(uty_u, u3nc(c3__del, u3_nul)); break;
}
tat_u->esc.ape = tat_u->esc.bra = c3n;
tat_u->esc.seq_y = 0;
}
else if ( cay_y <= '9' ) {
tat_u->esc.seq_y = cay_y;
}
// xterm sequence
//
else {
switch ( cay_y ) {
default: {
_term_it_dump_buf(uty_u, &uty_u->ufo_u.bel_u);
break;
}
case 'A': _term_io_belt(uty_u, u3nc(c3__aro, 'u')); break;
case 'B': _term_io_belt(uty_u, u3nc(c3__aro, 'd')); break;
case 'C': _term_io_belt(uty_u, u3nc(c3__aro, 'r')); break;
case 'D': _term_io_belt(uty_u, u3nc(c3__aro, 'l')); break;
case 'A': _term_io_spit(uty_u, u3nc(c3__aro, 'u')); break;
case 'B': _term_io_spit(uty_u, u3nc(c3__aro, 'd')); break;
case 'C': _term_io_spit(uty_u, u3nc(c3__aro, 'r')); break;
case 'D': _term_io_spit(uty_u, u3nc(c3__aro, 'l')); break;
//
case 'M': tat_u->esc.mou = c3y; break;
}
tat_u->esc.ape = tat_u->esc.bra = c3n;
}
}
else {
if ( (cay_y >= 'a') && (cay_y <= 'z') ) {
tat_u->esc.ape = c3n;
// XX for backwards compatibility, check kelvin version
// and fallback to [%met @c]
//
_term_io_belt(uty_u, u3nt(c3__mod, c3__met, cay_y));
_term_io_spit(uty_u, u3nt(c3__mod, c3__met, cay_y));
}
else if ( 8 == cay_y || 127 == cay_y ) {
tat_u->esc.ape = c3n;
// XX backwards compatibility [%met @c]
//
_term_io_belt(uty_u, u3nq(c3__mod, c3__met, c3__bac, u3_nul));
_term_io_spit(uty_u, u3nq(c3__mod, c3__met, c3__bac, u3_nul));
}
else if ( ('[' == cay_y) || ('O' == cay_y) ) {
tat_u->esc.bra = c3y;
@ -677,9 +718,9 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y)
}
else {
c3_y row_y = cay_y - 32;
// only acknowledge button 1 presses within our window
// only acknowledge button 1 presses within our known window
if ( 1 != tat_u->esc.ton_y && row_y <= tat_u->siz.row_l ) {
_term_io_belt(uty_u, u3nt(c3__hit, tat_u->siz.row_l - row_y, tat_u->esc.col_y - 1));
_term_io_spit(uty_u, u3nt(c3__hit, tat_u->esc.col_y - 1, row_y - 1));
}
tat_u->esc.mou = c3n;
tat_u->esc.ton_y = tat_u->esc.col_y = 0;
@ -699,28 +740,28 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y)
wug = u3do("taft", huv);
tat_u->fut.len_w = tat_u->fut.wid_w = 0;
_term_io_belt(uty_u, u3nt(c3__txt, wug, u3_nul));
tat_u->fut.imp = u3nc(wug, tat_u->fut.imp);
}
}
// individual characters
//
else {
if ( (cay_y >= 32) && (cay_y < 127) ) { // visual ascii
_term_io_belt(uty_u, u3nt(c3__txt, cay_y, u3_nul));
tat_u->fut.imp = u3nc(cay_y, tat_u->fut.imp);
}
else if ( 0 == cay_y ) { // null
_term_it_dump_buf(uty_u, &uty_u->ufo_u.bel_u);
}
else if ( 8 == cay_y || 127 == cay_y ) { // backspace & delete
_term_io_belt(uty_u, u3nc(c3__bac, u3_nul));
_term_io_spit(uty_u, u3nc(c3__bac, u3_nul));
}
else if ( 10 == cay_y || 13 == cay_y ) { // newline & carriage return
_term_io_belt(uty_u, u3nc(c3__ret, u3_nul));
_term_io_spit(uty_u, u3nc(c3__ret, u3_nul));
}
else if ( cay_y <= 26 ) {
// XX backwards compatibility [%ctl @c]
//
_term_io_belt(uty_u, u3nt(c3__mod, c3__ctl, ('a' + (cay_y - 1))));
_term_io_spit(uty_u, u3nt(c3__mod, c3__ctl, ('a' + (cay_y - 1))));
}
else if ( 27 == cay_y ) {
tat_u->esc.ape = c3y;
@ -778,6 +819,7 @@ _term_suck(u3_utty* uty_u, const c3_y* buf, ssize_t siz_i)
for ( i=0; i < siz_i; i++ ) {
_term_io_suck_char(uty_u, buf[i]);
}
_term_io_spit(uty_u, u3_none);
}
}
}
@ -865,8 +907,8 @@ _term_spin_step(u3_utty* uty_u)
// if we know where the bottom line is, and the cursor is not on it,
// move it to the bottom left
//
if ( tat_u->siz.row_l && tat_u->mir.rus_w > 0 ) {
_term_it_send_csi(uty_u, 'H', 2, tat_u->siz.row_l, 0);
if ( tat_u->siz.row_l && tat_u->mir.rus_w < tat_u->siz.row_l - 1 ) {
_term_it_send_csi(uty_u, 'H', 2, tat_u->siz.row_l, 1);
}
c3_w i_w;
@ -1313,7 +1355,7 @@ _term_ef_blit(u3_utty* uty_u,
case c3__hop: {
u3_noun pos = u3t(blt);
if ( c3y == u3r_ud(pos) ) {
_term_it_move_cursor(uty_u, 0, pos);
_term_it_move_cursor(uty_u, pos, uty_u->tat_u.siz.row_l - 1);
}
else {
_term_it_move_cursor(uty_u, u3h(pos), u3t(pos));
@ -1325,14 +1367,24 @@ _term_ef_blit(u3_utty* uty_u,
} break;
case c3__lin: { //TMP backwards compatibility
_term_it_move_cursor(uty_u, 0, 0);
_term_it_move_cursor(uty_u, 0, uty_u->tat_u.siz.row_l - 1);
_term_it_clear_line(uty_u);
} //
case c3__put: {
_term_it_show_tour(uty_u, u3k(u3t(blt)));
} break;
case c3__mor: //TMP backwards compatibility
case c3__mor: {
if (u3_nul != u3t(blt)) {
u3_noun bis = u3t(blt);
while (u3_nul != bis) {
_term_ef_blit(uty_u, u3k(u3h(bis)));
bis = u3t(bis);
}
break;
}
//TMP fall through to nel for backwards compatibility
}
case c3__nel: {
_term_it_show_nel(uty_u);
} break;
@ -1709,6 +1761,10 @@ _term_io_exit(u3_auto* car_u)
//
_term_it_dump_buf(uty_u, &uty_u->ufo_u.mof_u);
// move cursor to the end
//
_term_it_move_cursor(uty_u, 0, uty_u->tat_u.siz.row_l - 1);
// NB, closed in u3_term_log_exit()
//
uv_read_stop((uv_stream_t*)&(uty_u->pin_u));

View File

@ -293,7 +293,7 @@ _unix_mkdirp(c3_c* pax_c)
while ( fas_c ) {
*fas_c = 0;
if ( 0 != mkdir(pax_c, 0777) && EEXIST != errno ) {
u3l_log("unix: mkdir %s: %s\n", pax_c, strerror(errno));
u3l_log("unix: mkdir %s: %s", pax_c, strerror(errno));
u3m_bail(c3__fail);
}
*fas_c++ = '/';
@ -318,7 +318,7 @@ u3_unix_save(c3_c* pax_c, u3_atom pad)
c3_c* ful_c;
if ( !u3_unix_cane(pax_c) ) {
u3l_log("%s: non-canonical path\n", pax_c);
u3l_log("%s: non-canonical path", pax_c);
u3z(pad); u3m_bail(c3__fail);
}
if ( '/' == *pax_c) {
@ -333,7 +333,7 @@ u3_unix_save(c3_c* pax_c, u3_atom pad)
_unix_mkdirp(ful_c);
fid_i = c3_open(ful_c, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if ( fid_i < 0 ) {
u3l_log("%s: %s\n", ful_c, strerror(errno));
u3l_log("%s: %s", ful_c, strerror(errno));
c3_free(ful_c);
u3z(pad); u3m_bail(c3__fail);
}
@ -347,7 +347,7 @@ u3_unix_save(c3_c* pax_c, u3_atom pad)
c3_free(pad_y);
if ( rit_w != fln_w ) {
u3l_log("%s: %s\n", ful_c, strerror(errno));
u3l_log("%s: %s", ful_c, strerror(errno));
c3_free(ful_c);
u3m_bail(c3__fail);
}

View File

@ -576,13 +576,8 @@ _boothack_doom(void)
u3_noun whu = u3dc("slaw", 'p', u3k(fak));
if ( u3_nul == whu ) {
<<<<<<< HEAD
u3l_log("boot: malformed -F ship %s", u3_Host.ops_u.fak_c);
exit(1);
=======
u3l_log("boot: malformed -F ship %s\r\n", u3_Host.ops_u.fak_c);
u3_king_bail();
>>>>>>> next/arvo
}
bot = u3nc(c3__fake, u3k(u3t(whu)));
@ -979,7 +974,7 @@ _king_save_file(c3_c* url_c, FILE* fil_u)
long cod_i;
if ( !(cul_u = curl_easy_init()) ) {
u3l_log("failed to initialize libcurl\n");
u3l_log("failed to initialize libcurl");
exit(1);
}
@ -993,11 +988,11 @@ _king_save_file(c3_c* url_c, FILE* fil_u)
// XX retry?
//
if ( CURLE_OK != res_i ) {
u3l_log("curl: failed %s: %s\n", url_c, curl_easy_strerror(res_i));
u3l_log("curl: failed %s: %s", url_c, curl_easy_strerror(res_i));
ret_i = -1;
}
if ( 300 <= cod_i ) {
u3l_log("curl: error %s: HTTP %ld\n", url_c, cod_i);
u3l_log("curl: error %s: HTTP %ld", url_c, cod_i);
ret_i = -2;
}
@ -1063,14 +1058,14 @@ _king_init_pace(c3_c* pac_c)
return 0;
}
else {
u3l_log("dock: init pace (%s): open %s\n", pac_c, strerror(errno));
u3l_log("dock: init pace (%s): open %s", pac_c, strerror(errno));
c3_free(bin_c);
return -1;
}
}
if ( _king_write_raw(fid_i, (c3_y*)pac_c, strlen(pac_c)) ) {
u3l_log("dock: init pace (%s): write %s\n", pac_c, strerror(errno));
u3l_log("dock: init pace (%s): write %s", pac_c, strerror(errno));
close(fid_i);
c3_free(bin_c);
return -1;
@ -1078,12 +1073,12 @@ _king_init_pace(c3_c* pac_c)
// XX sync first?
//
else if ( close(fid_i) ) {
u3l_log("dock: init pace (%s): close %s\n", pac_c, strerror(errno));
u3l_log("dock: init pace (%s): close %s", pac_c, strerror(errno));
c3_free(bin_c);
return 1;
}
u3l_log("dock: pace (%s): configured at %s/.bin/pace\r\n",
u3l_log("dock: pace (%s): configured at %s/.bin/pace",
pac_c, u3_Host.dir_c);
return 0;
@ -1143,12 +1138,12 @@ u3_king_vere(c3_c* pac_c, // pace
|| !(fil_u = fdopen(fid_i, "wb")) )
{
if ( EEXIST == errno ) {
u3l_log("already installed\n");
u3l_log("already installed");
c3_free(bin_c);
return 0;
}
else {
u3l_log("unable to open %s: %s\r\n", bin_c, strerror(errno));
u3l_log("unable to open %s: %s", bin_c, strerror(errno));
c3_free(bin_c);
return -1;
}
@ -1159,7 +1154,7 @@ u3_king_vere(c3_c* pac_c, // pace
c3_assert( ret_i > 0 );
if ( (ret_i = _king_save_file(url_c, fil_u)) ) {
u3l_log("unable to save %s to %s: %d\r\n", url_c, bin_c, ret_i);
u3l_log("unable to save %s to %s: %d", url_c, bin_c, ret_i);
c3_free(url_c);
fclose(fil_u);
unlink(bin_c);
@ -1192,7 +1187,7 @@ u3_king_vere(c3_c* pac_c, // pace
}
}
u3l_log("vere: saved to %s\n", bin_c);
u3l_log("vere: saved to %s", bin_c);
c3_free(url_c);
c3_free(bin_c);
@ -1215,13 +1210,13 @@ _king_do_upgrade(c3_c* pac_c, c3_c* ver_c)
arc_c = u3_Host.arc_c;
}
else {
u3l_log("vere: --arch required\r\n");
u3l_log("vere: --arch required");
return;
}
#endif
if ( _king_make_pace(pac_c) ) {
u3l_log("vere: unable to make pace (%s) directory in pier\n", pac_c);
u3l_log("vere: unable to make pace (%s) directory in pier", pac_c);
u3_king_bail();
exit(1);
}
@ -1234,13 +1229,13 @@ _king_do_upgrade(c3_c* pac_c, c3_c* ver_c)
// XX get link option
//
if ( u3_king_vere(pac_c, ver_c, arc_c, dir_c, 1) ) {
u3l_log("vere: upgrade failed\r\n");
u3l_log("vere: upgrade failed");
u3_king_bail();
exit(1);
}
c3_free(dir_c);
u3l_log("vere: upgrade succeeded\r\n");
u3l_log("vere: upgrade succeeded");
// XX print restart instructions
}
@ -1476,7 +1471,7 @@ u3_king_dock(c3_c* pac_c)
// XX get link option
//
if ( _king_copy_vere(pac_c, URBIT_VERSION, arc_c, 1) ) {
u3l_log("vere: binary copy failed\r\n");
u3l_log("vere: binary copy failed");
u3_king_bail();
exit(1);
}
@ -1484,7 +1479,7 @@ u3_king_dock(c3_c* pac_c)
// NB: failure ignored
//
_king_init_pace(pac_c);
u3l_log("vere: binary copy succeeded\r\n");
u3l_log("vere: binary copy succeeded");
// XX print restart instructions
}
}
@ -1514,10 +1509,10 @@ u3_king_done(void)
if ( u3_Host.xit_i ) {
if ( c3y == u3_Host.nex_o ) {
u3l_log("vere: upgrade failed\r\n");
u3l_log("vere: upgrade failed");
}
else if ( c3y == u3_Host.pep_o ) {
u3l_log("vere: prep for upgrade failed\r\n");
u3l_log("vere: prep for upgrade failed");
}
}
else {
@ -1535,15 +1530,15 @@ u3_king_done(void)
switch ( u3_king_next(pac_c, &ver_c) ) {
case -2: {
u3l_log("vere: unable to check for next version\n");
u3l_log("vere: unable to check for next version");
} break;
case -1: {
u3l_log("vere: up to date\n");
u3l_log("vere: up to date");
} break;
case 0: {
u3l_log("vere: next (%%%s): %s\n", pac_c, ver_c);
u3l_log("vere: next (%%%s): %s", pac_c, ver_c);
_king_do_upgrade(pac_c, ver_c);
c3_free(ver_c);
} break;
@ -1554,7 +1549,7 @@ u3_king_done(void)
c3_free(pac_c);
}
else if ( c3y == u3_Host.pep_o ) {
u3l_log("vere: ready for upgrade\n");
u3l_log("vere: ready for upgrade");
}
// copy binary into pier on boot

View File

@ -1056,7 +1056,7 @@ _lord_on_serf_err_cb(uv_stream_t* pyp_u,
uv_read_stop(pyp_u);
if ( siz_i != UV_EOF ) {
u3l_log("lord: serf stderr: %s\r\n", uv_strerror(siz_i));
u3l_log("lord: serf stderr: %s", uv_strerror(siz_i));
}
}

View File

@ -504,7 +504,7 @@ _pier_on_scry_done(void* ptr_v, u3_noun nun)
ext_c = "txt";
}
else {
u3l_log("pier: cannot export cell as %s\n", u3_Host.ops_u.puf_c);
u3l_log("pier: cannot export cell as %s", u3_Host.ops_u.puf_c);
out = u3_none;
}
u3z(puf);
@ -516,13 +516,8 @@ _pier_on_scry_done(void* ptr_v, u3_noun nun)
c3_c fil_c[256];
snprintf(fil_c, 256, "%s.%s", pac_c + 1, ext_c);
<<<<<<< HEAD
u3_walk_save(fil_c, 0, out, pir_u->pax_c, pad);
u3l_log("pier: scry result in %s", fil_c);
=======
u3_unix_save(fil_c, out);
u3l_log("pier: scry result in %s/.urb/put/%s\n", u3_Host.dir_c, fil_c);
>>>>>>> next/arvo
u3l_log("pier: scry result in %s/.urb/put/%s", u3_Host.dir_c, fil_c);
}
}
@ -678,8 +673,8 @@ _pier_wyrd_fail(u3_pier* pir_u, u3_ovum* egg_u, u3_noun lud)
// XX organizing version constants
//
#define VERE_NAME "vere"
#define VERE_ZUSE 417
#define VERE_LULL 328
#define VERE_ZUSE 416
#define VERE_LULL 327
/* _pier_wyrd_aver(): check for %wend effect and version downgrade. RETAIN
*/

View File

@ -1,326 +0,0 @@
/* vere/walk.c
**
*/
#include "all.h"
#include "vere/vere.h"
/* |%
** ++ arch :: fs node
** $% [& p=@uvI q=*] :: file, hash/data
** [| p=(map ,@ta arch)] :: directory
** == ::
** --
*/
#if 0
static u3_noun
_walk_ok(u3_noun nod)
{
u3_noun don = u3n_mung(u3k(u2A->toy.arch), u3k(nod));
if ( c3n == u3_sing(nod, don) ) {
c3_assert(0);
}
u3z(don);
return nod;
}
#endif
/* u3_walk_safe(): load file or 0.
*/
u3_noun
u3_walk_safe(c3_c* pas_c)
{
struct stat buf_b;
c3_i fid_i = open(pas_c, O_RDONLY, 0644);
c3_w fln_w, red_w;
c3_y* pad_y;
if ( (fid_i < 0) || (fstat(fid_i, &buf_b) < 0) ) {
// u3l_log("%s: %s", pas_c, strerror(errno));
return 0;
}
fln_w = buf_b.st_size;
pad_y = c3_malloc(buf_b.st_size);
red_w = read(fid_i, pad_y, fln_w);
close(fid_i);
if ( fln_w != red_w ) {
c3_free(pad_y);
return 0;
}
else {
u3_noun pad = u3i_bytes(fln_w, (c3_y *)pad_y);
c3_free(pad_y);
return pad;
}
}
/* u3_walk_load(): load file or bail.
*/
u3_noun
u3_walk_load(c3_c* pas_c)
{
struct stat buf_b;
c3_i fid_i = open(pas_c, O_RDONLY, 0644);
c3_w fln_w, red_w;
c3_y* pad_y;
if ( (fid_i < 0) || (fstat(fid_i, &buf_b) < 0) ) {
u3l_log("%s: %s", pas_c, strerror(errno));
return u3m_bail(c3__fail);
}
fln_w = buf_b.st_size;
pad_y = c3_malloc(buf_b.st_size);
red_w = read(fid_i, pad_y, fln_w);
close(fid_i);
if ( fln_w != red_w ) {
c3_free(pad_y);
u3l_log("u3_walk_load failed");
return u3m_bail(c3__fail);
}
else {
u3_noun pad = u3i_bytes(fln_w, (c3_y *)pad_y);
c3_free(pad_y);
return pad;
}
}
/* _walk_mkdirp(): recursively make directories in pax at bas_c (RETAIN)
*/
static void
_walk_mkdirp(c3_c* bas_c, u3_noun pax)
{
c3_c* pax_c;
c3_y* waq_y;
c3_w pax_w, fas_w, len_w;
if ( u3_nul == pax ) {
return;
}
pax_w = u3r_met(3, u3h(pax));
fas_w = strlen(bas_c);
len_w = 1 + fas_w + pax_w;
pax_c = c3_malloc(1 + len_w);
strcpy(pax_c, bas_c);
pax_c[fas_w] = '/';
waq_y = (void*)(1 + pax_c + fas_w);
u3r_bytes(0, pax_w, waq_y, u3h(pax));
pax_c[len_w] = '\0';
if ( 0 != mkdir(pax_c, 0755) && EEXIST != errno ) {
u3l_log("error mkdiring %s: %s", pax_c, strerror(errno));
u3m_bail(c3__fail);
}
_walk_mkdirp(pax_c, u3t(pax));
c3_free(pax_c);
}
/* u3_walk_save(): save file or bail.
*/
void
u3_walk_save(c3_c* pas_c, u3_noun tim, u3_atom pad, c3_c* bas_c, u3_noun pax)
{
c3_i fid_i = open(pas_c, O_WRONLY | O_CREAT | O_TRUNC, 0666);
c3_w fln_w, rit_w;
c3_y* pad_y;
if ( fid_i < 0 ) {
if ( ENOENT == errno && u3_nul != pax ) {
_walk_mkdirp(bas_c, pax);
return u3_walk_save(pas_c, tim, pad, 0, u3_nul);
}
u3l_log("%s: %s", pas_c, strerror(errno));
u3m_bail(c3__fail);
}
fln_w = u3r_met(3, pad);
pad_y = c3_malloc(fln_w);
u3r_bytes(0, fln_w, pad_y, pad);
u3z(pad);
u3z(pax);
rit_w = write(fid_i, pad_y, fln_w);
close(fid_i);
c3_free(pad_y);
if ( rit_w != fln_w ) {
u3l_log("%s: %s", pas_c, strerror(errno));
u3m_bail(c3__fail);
}
if ( 0 != tim ) {
struct timeval tim_tv[2];
u3_time_out_tv(&tim_tv[0], u3k(tim));
u3_time_out_tv(&tim_tv[1], tim);
utimes(pas_c, tim_tv);
}
}
/* _walk_in(): inner loop of _walk(), producing map.
*/
static u3_noun
_walk_in(const c3_c* dir_c, c3_w len_w)
{
DIR* dir_d = opendir(dir_c);
u3_noun map = u3_nul;
if ( !dir_d ) {
return u3_nul;
}
else while ( 1 ) {
struct dirent ent_n;
struct dirent* out_n;
if ( u3_readdir_r(dir_d, &ent_n, &out_n) != 0 ) {
u3l_log("%s: %s", dir_c, strerror(errno));
break;
}
else if ( !out_n ) {
break;
}
else if ( !strcmp(out_n->d_name, ".") ||
!strcmp(out_n->d_name, "..") ||
('~' == out_n->d_name[0]) ||
('.' == out_n->d_name[0]) ) // XX restricts some spans
{
continue;
}
else {
c3_c* fil_c = out_n->d_name;
c3_w lef_w = len_w + 1 + strlen(fil_c);
c3_c* pat_c = c3_malloc(lef_w + 1);
struct stat buf_b;
strncpy(pat_c, dir_c, lef_w);
pat_c[len_w] = '/';
strncpy(pat_c + len_w + 1, fil_c, lef_w);
pat_c[lef_w] = '\0';
if ( 0 != stat(pat_c, &buf_b) ) {
c3_free(pat_c);
} else {
u3_noun tim = c3_stat_mtime(&buf_b);
if ( !S_ISDIR(buf_b.st_mode) ) {
c3_c* dot_c = strrchr(fil_c, '.');
c3_c* nam_c = strdup(fil_c);
c3_c* ext_c = strdup(dot_c + 1);
nam_c[dot_c - fil_c] = 0;
{
u3_noun nam = u3i_string(nam_c);
u3_noun ext = u3i_string(ext_c);
u3_noun get = u3kdb_get(u3k(map), u3k(nam));
u3_noun dat = u3_walk_load(pat_c);
u3_noun hax;
if ( !strcmp("noun", ext_c) ) {
dat = u3ke_cue(dat);
}
hax = u3do("sham", u3k(dat));
if ( u3_none == get ) { get = u3_nul; }
get = u3kdb_put(get, ext, u3nt(c3y, hax, dat));
map = u3kdb_put(map, nam, u3nc(c3n, get));
}
c3_free(nam_c);
c3_free(ext_c);
}
else {
u3_noun dir = _walk_in(pat_c, lef_w);
if ( u3_nul != dir ) {
map = u3kdb_put
(map, u3i_string(fil_c), u3nc(c3n, dir));
}
else u3z(tim);
}
c3_free(pat_c);
}
}
}
closedir(dir_d);
return map;
}
/* u3_walk(): traverse `dir_c` to produce an arch, updating `old`.
*/
u3_noun
u3_walk(const c3_c* dir_c, u3_noun old)
{
// XX - obviously, cheaper to update old data.
u3z(old);
{
struct stat buf_b;
if ( 0 != stat(dir_c, &buf_b) ) {
u3l_log("can't stat %s", dir_c);
// return u3m_bail(c3__fail);
c3_assert(0);
}
else {
return u3nc(c3n,
_walk_in(dir_c, strlen(dir_c)));
}
}
}
/* u3_path(): C unix path in computer for file or directory.
*/
c3_c*
u3_path(c3_o fyl, u3_noun pax)
{
c3_w len_w;
c3_c *pas_c;
// measure
//
len_w = strlen(u3_Local);
{
u3_noun wiz = pax;
while ( u3_nul != wiz ) {
len_w += (1 + u3r_met(3, u3h(wiz)));
wiz = u3t(wiz);
}
}
// cut
//
pas_c = c3_malloc(len_w + 1);
strncpy(pas_c, u3_Local, len_w);
pas_c[len_w] = '\0';
{
u3_noun wiz = pax;
c3_c* waq_c = (pas_c + strlen(pas_c));
while ( u3_nul != wiz ) {
c3_w tis_w = u3r_met(3, u3h(wiz));
if ( (c3y == fyl) && (u3_nul == u3t(wiz)) ) {
*waq_c++ = '.';
} else *waq_c++ = '/';
u3r_bytes(0, tis_w, (c3_y*)waq_c, u3h(wiz));
waq_c += tis_w;
wiz = u3t(wiz);
}
*waq_c = 0;
}
u3z(pax);
return pas_c;
}

View File

@ -1,9 +1,9 @@
:~ title+'Terminal'
info+'A web interface to your Urbit\'s command line.'
color+0x2e.4347
glob-http+['https://bootstrap.urbit.org/glob-0v7.1hgb7.euged.6oj3e.cdhdg.rah02.glob' 0v7.1hgb7.euged.6oj3e.cdhdg.rah02]
glob-http+['https://bootstrap.urbit.org/glob-0v5.hvjci.n7c4h.1onl6.34g14.fut7c.glob' 0v5.hvjci.n7c4h.1onl6.34g14.fut7c]
base+'webterm'
version+[1 0 1]
version+[1 1 0]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -1 +1 @@
[%zuse 417]
[%zuse 416]