From 3120681b2bd5abc59645dbee6b4fac01cd55ce4d Mon Sep 17 00:00:00 2001 From: fang Date: Sun, 20 Feb 2022 15:53:53 -0600 Subject: [PATCH 01/54] sole: properly support multiple sessions We update the sole protocol to more cleanly support multiple sessions. Primarily, the "sole id" is updated to be a [@p @ta] instead of a @ta, and it is now generated based off the connected dill session, rather than statically. This change ripples out to applications that support the sole protocol: the subscription path becomes /sole/[ship]/[session] (as opposed to /sole/[per-ship-constant]), and %sole-action pokes include the new id as well. For shoe agents, this means (at the very least) updating the function signatures of the shoe arms. /lib/sole has been updated to include helper functions for parsing a sole-id from a subscription path, and turning a sole-id into its corresponding path. It also has a function to aid in migrating old sole-ids. Existing sole agents are made to kick any known open sessions, forcing a resubscribe by drum, so that they may use exclusively the new format going forward. Third-party agents are recommended to do the same. Note that some functionality, such as |link, still operates exclusively on the default session. Improvements in this area to follow soon. --- pkg/arvo/app/dojo.hoon | 61 ++++++++++++++++++------- pkg/arvo/app/shoe.hoon | 8 ++-- pkg/arvo/lib/hood/drum.hoon | 41 ++++++++++------- pkg/base-dev/lib/shoe.hoon | 81 +++++++++++++++++++++------------ pkg/base-dev/lib/sole.hoon | 24 ++++++++++ pkg/base-dev/sur/sole.hoon | 3 +- pkg/landscape/app/chat-cli.hoon | 55 ++++++++++++++++------ 7 files changed, 193 insertions(+), 80 deletions(-) diff --git a/pkg/arvo/app/dojo.hoon b/pkg/arvo/app/dojo.hoon index 938d9f25ab..c8fbd49d75 100644 --- a/pkg/arvo/app/dojo.hoon +++ b/pkg/arvo/app/dojo.hoon @@ -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 @@ -1019,13 +1019,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 ^+ . @@ -1533,21 +1534,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 @@ -1574,9 +1601,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)] -- :: @@ -1630,8 +1658,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) @@ -1643,7 +1670,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 @@ -1652,13 +1679,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) == diff --git a/pkg/arvo/app/shoe.hoon b/pkg/arvo/app/shoe.hoon index 4b0538ad34..0e827f57c4 100644 --- a/pkg/arvo/app/shoe.hoon +++ b/pkg/arvo/app/shoe.hoon @@ -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) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index c50f5a818b..67abf801a8 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -105,14 +105,15 @@ :: :: :: |% ++ 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) [(slav %p i.way) i.t.way]) + |= way=wire ^- [@tas gill:gall] + ?> ?=([@ @ ?(~ [@ ~])] way) + :- ?~(t.t.way %$ i.t.t.way) + [(slav %p i.way) i.t.way] -- :: |= [hid=bowl:gall state] @@ -135,10 +136,15 @@ =. 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) + =^ gyl this (open way) ?: (se-aint gyl) +>.$ (se-diff gyl fec) :: @@ -228,7 +234,7 @@ ++ reap-phat :: ack connect |= [way=wire saw=(unit tang)] =< se-abet =< se-view - =+ gyl=(de-gill way) + =^ gyl this (open way) ?~ saw (se-join gyl) :: Don't print stack trace because we probably just crashed to @@ -240,7 +246,7 @@ |= [way=wire saw=(unit tang)] =< se-abet =< se-view ?~ saw +> - =+ gyl=(de-gill way) + =^ gyl this (open way) ?: (se-aint gyl) +>.$ %- se-dump:(se-drop:(se-pull gyl) & gyl) :_ u.saw @@ -264,7 +270,7 @@ ++ quit-phat :: |= way=wire =< se-abet =< se-view - =+ gyl=(de-gill way) + =^ gyl this (open way) ~& [%drum-quit src.hid gyl] (se-drop %| gyl) :: :: :: @@ -279,7 +285,11 @@ :_ (flop moz) =/ =dill-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 !>(dill-blit)] :: ++ se-adze :: update connections ^+ . @@ -518,19 +528,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 @@ -555,7 +564,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) diff --git a/pkg/base-dev/lib/shoe.hoon b/pkg/base-dev/lib/shoe.hoon index cd4ab18db5..856a4d7822 100644 --- a/pkg/base-dev/lib/shoe.hoon +++ b/pkg/base-dev/lib/shoe.hoon @@ -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 diff --git a/pkg/base-dev/lib/sole.hoon b/pkg/base-dev/lib/sole.hoon index 66684668a6..ad6b749627 100644 --- a/pkg/base-dev/lib/sole.hoon +++ b/pkg/base-dev/lib/sole.hoon @@ -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) -- diff --git a/pkg/base-dev/sur/sole.hoon b/pkg/base-dev/sur/sole.hoon index e942bccb87..f8141f701e 100644 --- a/pkg/base-dev/sur/sole.hoon +++ b/pkg/base-dev/sur/sole.hoon @@ -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 diff --git a/pkg/landscape/app/chat-cli.hoon b/pkg/landscape/app/chat-cli.hoon index 4398cc7e81..a5aa5dee2c 100644 --- a/pkg/landscape/app/chat-cli.hoon +++ b/pkg/landscape/app/chat-cli.hoon @@ -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 - ?: %- ~(has by wex.bowl) - [/graph-store our-self %graph-store] - ~ - ~[connect] + ;: welp + cards-1 + cards-2 + :: + ?: %- ~(has by wex.bowl) + [/graph-store our-self %graph-store] + ~ + ~[connect] + == :: +connect: connect to the graph-store :: ++ connect From da47cfb08e8e9044790633a76892c6f0794af611 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 21 Feb 2022 12:11:40 -0600 Subject: [PATCH 02/54] drum: correctly initialize session in +prep --- pkg/arvo/lib/hood/drum.hoon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 67abf801a8..1448d649e7 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -132,7 +132,7 @@ :: ++ prep |= s=@tas - =. ses ses + =. ses s =. dev (~(gut by bin) ses *source) this :: From bb11c74278b6619652b34093a77d77d91ab2058e Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 28 Feb 2022 16:24:41 -0600 Subject: [PATCH 03/54] drum: don't drop state during se-subze Previously, it was putting new session state into the old map, preserving only the state of the last session in the map. --- pkg/arvo/lib/hood/drum.hoon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 1448d649e7..0f1aa6ce49 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -315,7 +315,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 ^+ . From bf97b8da38a84e68534faae1c2be96f645dd93d2 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 28 Feb 2022 16:27:29 -0600 Subject: [PATCH 04/54] drum: move +se-view call into +se-abet We practically always do se-abet:se-view anyway. --- pkg/arvo/lib/hood/drum.hoon | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 0f1aa6ce49..3019b88268 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -143,7 +143,7 @@ :: ++ diff-sole-effect-phat :: app event |= [way=wire fec=sole-effect] - =< se-abet =< se-view + =< se-abet =^ gyl this (open way) ?: (se-aint gyl) +>.$ (se-diff gyl fec) @@ -154,7 +154,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 "[{}, driving {}]") :: ++ poke-dill @@ -163,7 +163,7 @@ :: ++ poke-dill-belt :: terminal event |= bet=dill-belt:dill - =< se-abet =< se-view + =< se-abet (se-belt bet) :: ++ poke-dill-blit :: terminal output @@ -172,12 +172,12 @@ :: ++ poke-link :: connect app |= gyl=gill:gall - =< se-abet =< se-view + =< se-abet (se-link gyl) :: ++ poke-unlink :: disconnect app |= gyl=gill:gall - =< se-abet =< se-view + =< se-abet (se-drop:(se-pull gyl) & gyl) :: ++ poke-exit :: shutdown @@ -201,7 +201,7 @@ :: ++ 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) @@ -233,7 +233,7 @@ :: ++ reap-phat :: ack connect |= [way=wire saw=(unit tang)] - =< se-abet =< se-view + =< se-abet =^ gyl this (open way) ?~ saw (se-join gyl) @@ -244,7 +244,7 @@ :: ++ take-coup-phat :: ack poke |= [way=wire saw=(unit tang)] - =< se-abet =< se-view + =< se-abet ?~ saw +> =^ gyl this (open way) ?: (se-aint gyl) +>.$ @@ -269,7 +269,7 @@ :: ++ quit-phat :: |= way=wire - =< se-abet =< se-view + =< se-abet =^ gyl this (open way) ~& [%drum-quit src.hid gyl] (se-drop %| gyl) @@ -278,7 +278,7 @@ :: :: :: ++ 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) From 41a063867b52ebfc87d97b837afacf913c263e32 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 28 Feb 2022 16:41:49 -0600 Subject: [PATCH 05/54] dojo: auto-fill generator's named drum-session arg If a generator has a named argument named "drum-session", and the caller doesn't specify a value for it, dojo auto-fills it with the current drum session identifier. --- pkg/arvo/app/dojo.hoon | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/arvo/app/dojo.hoon b/pkg/arvo/app/dojo.hoon index c8fbd49d75..bf0c24dd47 100644 --- a/pkg/arvo/app/dojo.hoon +++ b/pkg/arvo/app/dojo.hoon @@ -821,12 +821,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) ~) From f6f2fcfcac5ace02e7d451efad9d790aba1003e1 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 28 Feb 2022 16:45:56 -0600 Subject: [PATCH 06/54] drum: make eel per-session This allows us to have different apps connected to different sessions. --- pkg/arvo/lib/hood/drum.hoon | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 3019b88268..27792b56de 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -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 == :: @@ -205,12 +205,12 @@ =? 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 From e53cb4a205c212e121d1c815047711ee1c775c51 Mon Sep 17 00:00:00 2001 From: fang Date: Mon, 28 Feb 2022 16:47:49 -0600 Subject: [PATCH 07/54] drum: make |un/link work for non-default sessions --- pkg/arvo/gen/hood/link.hoon | 4 +++- pkg/arvo/gen/hood/unlink.hoon | 4 +++- pkg/arvo/lib/hood/drum.hoon | 2 +- pkg/arvo/mar/drum-put.hoon | 7 +++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/arvo/gen/hood/link.hoon b/pkg/arvo/gen/hood/link.hoon index 5fe183279b..5e14126a1f 100644 --- a/pkg/arvo/gen/hood/link.hoon +++ b/pkg/arvo/gen/hood/link.hoon @@ -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] diff --git a/pkg/arvo/gen/hood/unlink.hoon b/pkg/arvo/gen/hood/unlink.hoon index d7bc509b00..fc6e1fb012 100644 --- a/pkg/arvo/gen/hood/unlink.hoon +++ b/pkg/arvo/gen/hood/unlink.hoon @@ -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] diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 27792b56de..f62143e21d 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -128,7 +128,7 @@ ++ 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 diff --git a/pkg/arvo/mar/drum-put.hoon b/pkg/arvo/mar/drum-put.hoon index e6094158ed..8fa3773c01 100644 --- a/pkg/arvo/mar/drum-put.hoon +++ b/pkg/arvo/mar/drum-put.hoon @@ -1,8 +1,7 @@ +:: %drum-put: download into host system :: -:::: /hoon/do-claim/womb/mar - :: /? 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 -- -- From 998f7d081a4a88b2fe61d6fbda029274c4e3b5b4 Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 2 Mar 2022 17:24:54 -0600 Subject: [PATCH 08/54] dill: fix %shut session deletion +abet would re-insert the session into state, so we just pull the deletion logic outside of the main core. --- pkg/arvo/sys/vane/dill.hoon | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/arvo/sys/vane/dill.hoon b/pkg/arvo/sys/vane/dill.hoon index b7c81afc7b..e2c1e4d2e0 100644 --- a/pkg/arvo/sys/vane/dill.hoon +++ b/pkg/arvo/sys/vane/dill.hoon @@ -219,10 +219,6 @@ |= [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 ^+ +> @@ -384,7 +380,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 :: From d98611a04bb3f5aeb87bc154cc9485a72aef3402 Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 2 Mar 2022 17:34:19 -0600 Subject: [PATCH 09/54] webterm: support multiple sessions Fully implements webterm support for multiple dill terminal sessions. Remaining work includes styling, session creation safety (name-wise), and general cleanup. Co-authored-by: tomholford Co-authored-by: liam-fitzgerald --- pkg/interface/webterm/.eslintrc.js | 7 + pkg/interface/webterm/.nvmrc | 1 + pkg/interface/webterm/Buffer.tsx | 276 ++++++++++++++++++ pkg/interface/webterm/Tab.tsx | 44 +++ pkg/interface/webterm/Tabs.tsx | 36 +++ pkg/interface/webterm/app.tsx | 416 +++------------------------- pkg/interface/webterm/constants.ts | 1 + pkg/interface/webterm/index.html | 38 +++ pkg/interface/webterm/lib/blit.ts | 73 +++++ pkg/interface/webterm/lib/stye.ts | 60 ++++ pkg/interface/webterm/lib/theme.ts | 21 ++ pkg/interface/webterm/state.ts | 12 +- pkg/interface/webterm/tsconfig.json | 26 ++ pkg/npm/api/term/lib.ts | 4 +- pkg/npm/api/term/types.ts | 4 +- 15 files changed, 626 insertions(+), 393 deletions(-) create mode 100644 pkg/interface/webterm/.eslintrc.js create mode 100644 pkg/interface/webterm/.nvmrc create mode 100644 pkg/interface/webterm/Buffer.tsx create mode 100644 pkg/interface/webterm/Tab.tsx create mode 100644 pkg/interface/webterm/Tabs.tsx create mode 100644 pkg/interface/webterm/constants.ts create mode 100644 pkg/interface/webterm/lib/blit.ts create mode 100644 pkg/interface/webterm/lib/stye.ts create mode 100644 pkg/interface/webterm/lib/theme.ts create mode 100644 pkg/interface/webterm/tsconfig.json diff --git a/pkg/interface/webterm/.eslintrc.js b/pkg/interface/webterm/.eslintrc.js new file mode 100644 index 0000000000..66d65c2914 --- /dev/null +++ b/pkg/interface/webterm/.eslintrc.js @@ -0,0 +1,7 @@ +const config = { + "rules": { + "spaced-comment": false, + } +} + +export default config; diff --git a/pkg/interface/webterm/.nvmrc b/pkg/interface/webterm/.nvmrc new file mode 100644 index 0000000000..0b77208aee --- /dev/null +++ b/pkg/interface/webterm/.nvmrc @@ -0,0 +1 @@ +16.14.0 \ No newline at end of file diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx new file mode 100644 index 0000000000..d33b2e9ec2 --- /dev/null +++ b/pkg/interface/webterm/Buffer.tsx @@ -0,0 +1,276 @@ +import { Terminal, ITerminalOptions } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +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 { useDark } from './join'; +import { showBlit, csi, showSlog } from './lib/blit'; + +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 +}; + +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; +}; + +const onResize = (session: Session) => () => { + //TODO debounce, if it ever becomes a problem + //TODO test that we only send this to the selected session, + // and that we *do* send it on-selected-change if necessary. + session?.fit.fit(); +}; + +const onInput = (name: string, session: Session, e: string) => { + if (!session) { + return; + } + const term = session.term; + const belts = readInput(term, e); + belts.map((b) => { + api.poke(pokeBelt(name, b)); + }); +}; + +interface BufferProps { + name: string, + selected: boolean, +} + +export default function Buffer({ name, selected }: BufferProps) { + const container = useRef(null); + const dark = useDark(); + + const session: Session = useTermState(s => s.sessions[name]); + + const initSession = useCallback(async (name: string, dark: boolean) => { + console.log('setting up', name); + + // set up xterm terminal + // + const term = new Terminal(termConfig); + term.setOption('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 }; + + // set up event handlers + // + term.onData(e => onInput(name, ses, e)); + term.onBinary(e => onInput(name, ses, e)); + term.onResize((e) => { + api.poke(pokeTask(name, { blew: { w: e.cols, h: e.rows } })); + }); + + // open subscription + // + await api.subscribe({ app: 'herm', path: '/session/'+name+'/view', + event: (e) => { + showBlit(ses.term, e); + if (e.bel && !selected) { + useTermState.getState().set(state => { + state.sessions[name].hasBell = true; + }); + } + //TODO should handle %bye on this higher level though, for deletion + }, + quit: () => { // quit + // TODO show user a message + console.error('oops quit, pls handle'); + } + }); + + useTermState.getState().set((state) => { + state.sessions[name] = ses; + }); + }, []); + + // init session + useEffect(() => { + if(session) { + return; + } + + initSession(name, dark); + }, [name]); + + // on selected change, maybe setup the term, or put it into the container + // + const setContainer = useCallback((containerRef: HTMLDivElement | null) => { + let newContainer = containerRef || container.current; + if(session && newContainer) { + container.current = newContainer; + console.log('newcont', newContainer); + // session.term.open(newContainer); + } else { + console.log('kaboom', session); + } + }, [session]); + + // on-init, open slogstream and fetch existing sessions + // + useEffect(() => { + + window.addEventListener('resize', onResize(session)); + + return () => { + // TODO clean up subs? + window.removeEventListener('resize', onResize(session)); + }; + }, []); + + // on dark mode change, change terminals' theme + // + useEffect(() => { + const theme = makeTheme(dark); + if (session) { + session.term.setOption('theme', theme); + } + if (container.current) { + container.current.style.backgroundColor = theme.background || ''; + } + }, [dark]); + + useEffect(() => { + if (session && selected && !session.term.isOpen) { + session!.term.open(container.current); + session!.fit.fit(); + session!.term.focus(); + session!.term.isOpen = true; + } + }, [selected, session]); + + return ( + !session && !selected ? +

Loading...

+ : + + + + + ); +} diff --git a/pkg/interface/webterm/Tab.tsx b/pkg/interface/webterm/Tab.tsx new file mode 100644 index 0000000000..0c3f148319 --- /dev/null +++ b/pkg/interface/webterm/Tab.tsx @@ -0,0 +1,44 @@ +import { DEFAULT_SESSION } from './constants'; +import React, { useCallback } from 'react'; +import useTermState from './state'; +import { style } from 'styled-system'; +import api from './api'; +import { pokeTask } from '@urbit/api/term'; + +export const Tab = ( { session, name } ) => { + + const isSelected = useTermState().selected === name; + + const onClick = () => { + console.log('click!', name); + useTermState.getState().set((state) => { + state.selected = name; + state.sessions[name].hasBell = false; + }); + useTermState.getState().sessions[name]?.term?.focus(); + } + + const onDelete = (e) => { + e.stopPropagation(); + api.poke(pokeTask(name, { shut: null })); + useTermState.getState().set(state => { + if (state.selected === name) { + state.selected = DEFAULT_SESSION; + } + state.names = state.names.filter(n => n !== name); + delete state.sessions[name]; + }); + //TODO clean up the subscription + } + + return ( + + ); +}; diff --git a/pkg/interface/webterm/Tabs.tsx b/pkg/interface/webterm/Tabs.tsx new file mode 100644 index 0000000000..37d64eed5c --- /dev/null +++ b/pkg/interface/webterm/Tabs.tsx @@ -0,0 +1,36 @@ +import { pokeTask } from '@urbit/api/term'; +import api from './api'; +import React from 'react'; +import useTermState from './state'; +import { Tab } from './Tab'; + +export const Tabs = () => { + const { sessions, names } = useTermState(); + + const onAddClick = () => { + const name = prompt('please entew a session name uwu'); + if (!name) { + return; + } + //TODO name must be @ta + api.poke(pokeTask(name, { open: { term: 'hood', apps: [{ who: '~'+(window as any).ship, app: 'dojo' }] } })); + useTermState.getState().set(state => { + state.names = [name, ...state.names].sort(); + state.selected = name; + state.sessions[name] = null; + }); + + + } + + return ( +
+ { names.map((n, i) => { + return ( + + ); + })} + +
+ ); +}; diff --git a/pkg/interface/webterm/app.tsx b/pkg/interface/webterm/app.tsx index 8b0dd65a65..d7c782f296 100644 --- a/pkg/interface/webterm/app.tsx +++ b/pkg/interface/webterm/app.tsx @@ -1,292 +1,42 @@ /* eslint-disable max-lines */ import React, { - useEffect, - useRef, - useCallback + useCallback, useEffect } from 'react'; -import useTermState from './state'; +import useTermState, { Sessions } 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 { Reset, _dark, _light } from '@tlon/indigo-react'; import 'xterm/css/xterm.css'; import { - Belt, Blit, Stye, Stub, Tint, Deco, - pokeTask, pokeBelt + scrySessions } from '@urbit/api/term'; -import bel from './lib/bel'; import { ThemeProvider } from 'styled-components'; +import { Tabs } from './Tabs'; +import Buffer from './Buffer'; +import { DEFAULT_SESSION } from './constants'; +import { showSlog } from './lib/blit'; 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 - }; -}; - -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 -}; - -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(null); - // TODO allow switching of selected - const { sessions, selected, slogstream, set } = useTermState(); - const session = sessions[selected]; + const { names, selected } = useTermState(); const dark = useDark(); + const initSessions = useCallback(async () => { + const response = await api.scry(scrySessions()); + + useTermState.getState().set((state) => { + state.names = response.sort(); + }); + }, []); + const setupSlog = useCallback(() => { console.log('slog: setting up...'); let available = false; @@ -298,9 +48,10 @@ export default function TermApp(props: TermAppProps) { }; slog.onmessage = (e) => { - const session = useTermState.getState().sessions['']; + const session = useTermState.getState().sessions[DEFAULT_SESSION]; if (!session) { - console.log('default session mia!', 'slog:', slog); + console.log('slog: default session mia!', 'msg:', e.data); + console.log(Object.keys(useTermState.getState().sessions), session); return; } showSlog(session.term, e.data); @@ -319,131 +70,26 @@ export default function TermApp(props: TermAppProps) { } }; - set((state) => { + useTermState.getState().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]); + initSessions(); + setupSlog(); + }, []); return ( <> - - - - + +
+ {names.map(name => { + return ; + })} +
); diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts new file mode 100644 index 0000000000..23cbd34975 --- /dev/null +++ b/pkg/interface/webterm/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_SESSION = ''; diff --git a/pkg/interface/webterm/index.html b/pkg/interface/webterm/index.html index 5eaf05c35b..ed7f861119 100644 --- a/pkg/interface/webterm/index.html +++ b/pkg/interface/webterm/index.html @@ -27,6 +27,44 @@ margin: 0; padding: 0; } + + .buffer-container { + height: calc(100% - 33px); + } + + div.tabs { + height: 33px; + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + padding: 5px 0; + border-bottom: 1px solid black; + } + + div.tabs > * { + margin-left: 5px; + margin-right: 5px; + border: solid 1px black; + padding: 7px; + cursor: pointer; + } + + div.tab { + text-align: center; + /* display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; */ + } + + div.tabs > div.selected > a.session-name { + font-weight: bold; + } + + div.tabs > a.delete-session { + padding: 5px; + } + diff --git a/pkg/interface/webterm/lib/blit.ts b/pkg/interface/webterm/lib/blit.ts new file mode 100644 index 0000000000..5343284333 --- /dev/null +++ b/pkg/interface/webterm/lib/blit.ts @@ -0,0 +1,73 @@ +import { Terminal } from 'xterm'; +import { saveAs } from 'file-saver'; +import { Blit, Stub } 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 ('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); +}; + +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')); +}; diff --git a/pkg/interface/webterm/lib/stye.ts b/pkg/interface/webterm/lib/stye.ts new file mode 100644 index 0000000000..bc2d99b5ea --- /dev/null +++ b/pkg/interface/webterm/lib/stye.ts @@ -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'; +}; diff --git a/pkg/interface/webterm/lib/theme.ts b/pkg/interface/webterm/lib/theme.ts new file mode 100644 index 0000000000..6044f1c30a --- /dev/null +++ b/pkg/interface/webterm/lib/theme.ts @@ -0,0 +1,21 @@ +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 + }; +}; diff --git a/pkg/interface/webterm/state.ts b/pkg/interface/webterm/state.ts index 253e7854f8..2392db880e 100644 --- a/pkg/interface/webterm/state.ts +++ b/pkg/interface/webterm/state.ts @@ -3,18 +3,22 @@ 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 } | 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, +} const useTermState = create((set, get) => ({ sessions: {} as Sessions, + names: [''], selected: '', // empty string is default session slogstream: null, theme: 'auto', diff --git a/pkg/interface/webterm/tsconfig.json b/pkg/interface/webterm/tsconfig.json new file mode 100644 index 0000000000..9a8642966d --- /dev/null +++ b/pkg/interface/webterm/tsconfig.json @@ -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" + ] +} diff --git a/pkg/npm/api/term/lib.ts b/pkg/npm/api/term/lib.ts index 3d04240994..67e14175f5 100644 --- a/pkg/npm/api/term/lib.ts +++ b/pkg/npm/api/term/lib.ts @@ -1,5 +1,5 @@ -import { Scry } from '../http-api/src' -import { Poke } from '../http-api/src/types'; +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 => ({ diff --git a/pkg/npm/api/term/types.ts b/pkg/npm/api/term/types.ts index 502df692fc..df4767cffb 100644 --- a/pkg/npm/api/term/types.ts +++ b/pkg/npm/api/term/types.ts @@ -53,8 +53,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 From e41263c6532637e641caa51fc23765e3154deb8a Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 2 Mar 2022 22:49:31 -0600 Subject: [PATCH 10/54] dojo: add missing nu-sole-id compatability Forgotten code. --- pkg/arvo/app/dojo.hoon | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/arvo/app/dojo.hoon b/pkg/arvo/app/dojo.hoon index bf0c24dd47..691b30df10 100644 --- a/pkg/arvo/app/dojo.hoon +++ b/pkg/arvo/app/dojo.hoon @@ -1706,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 From 25a1c79aa3f4dd9fc2ce4ee129bb888274129e50 Mon Sep 17 00:00:00 2001 From: fang Date: Wed, 2 Mar 2022 23:25:38 -0600 Subject: [PATCH 11/54] drum: properly hook up new |link More uncommitted code. --- pkg/arvo/lib/hood/drum.hoon | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index f62143e21d..001e073248 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -171,21 +171,22 @@ se-abet:(se-blit-sys bit) :: ++ poke-link :: connect app - |= gyl=gill:gall + |= [ses=@tas gyl=gill:gall] =< se-abet - (se-link gyl) + (se-link:(prep ses) gyl) :: ++ poke-unlink :: disconnect app - |= gyl=gill:gall + |= [ses=@ta gyl=gill:gall] =< se-abet - (se-drop:(se-pull gyl) & gyl) + (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 @@ -368,7 +369,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)) From 1a50957950fa9bd972d5e5c00c64428c4f8f9319 Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 3 Mar 2022 16:58:48 -0600 Subject: [PATCH 12/54] ux: session ID input validation When creating a new session, validate that it meets the following conditions: - must start with an alphabetical - can be composed of alphanumerics with hyphens - can be length 1 or longer - cannot begin or end with a hyphen --- pkg/interface/webterm/Tabs.tsx | 11 +++++------ pkg/interface/webterm/constants.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/interface/webterm/Tabs.tsx b/pkg/interface/webterm/Tabs.tsx index 37d64eed5c..bf3d32b64b 100644 --- a/pkg/interface/webterm/Tabs.tsx +++ b/pkg/interface/webterm/Tabs.tsx @@ -3,29 +3,28 @@ import api from './api'; import React from 'react'; import useTermState from './state'; import { Tab } from './Tab'; +import { SESSION_ID_REGEX } from './constants'; export const Tabs = () => { const { sessions, names } = useTermState(); const onAddClick = () => { const name = prompt('please entew a session name uwu'); - if (!name) { + if (!name || !SESSION_ID_REGEX.test(name) || names.includes(name)) { + console.log('invalid session name:', name); return; } - //TODO name must be @ta - api.poke(pokeTask(name, { open: { term: 'hood', apps: [{ who: '~'+(window as any).ship, app: 'dojo' }] } })); + api.poke(pokeTask(name, { open: { term: 'hood', apps: [{ who: '~' + (window as any).ship, app: 'dojo' }] } })); useTermState.getState().set(state => { state.names = [name, ...state.names].sort(); state.selected = name; state.sessions[name] = null; }); - - } return (
- { names.map((n, i) => { + {names.map((n, i) => { return ( ); diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index 23cbd34975..7e173ac64c 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -1 +1,11 @@ export const DEFAULT_SESSION = ''; + +/** + * Session ID validity: + * + * - must start with an alphabetical + * - can be composed of alphanumerics with hyphens + * - can be length 1 or longer + * - cannot begin or end with a hyphen + */ +export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d\-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; From fe1ece47d83a35d446722c7bfcc4181468c59a57 Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 3 Mar 2022 17:09:38 -0600 Subject: [PATCH 13/54] api: clean up subscriptions on deletion of session On subscribe, track the subcription ID in the Session state. On deletion, unsubscribe using the same ID. --- pkg/interface/webterm/Buffer.tsx | 6 +++--- pkg/interface/webterm/Tab.tsx | 26 +++++++++++++++++++------- pkg/interface/webterm/state.ts | 7 ++++++- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index d33b2e9ec2..ef65c592bd 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -162,7 +162,7 @@ export default function Buffer({ name, selected }: BufferProps) { // term.write(csi('?9h')); - const ses: Session = { term, fit, hasBell: false }; + const ses: Session = { term, fit, hasBell: false, subscriptionId: null }; // set up event handlers // @@ -174,7 +174,7 @@ export default function Buffer({ name, selected }: BufferProps) { // open subscription // - await api.subscribe({ app: 'herm', path: '/session/'+name+'/view', + ses.subscriptionId = await api.subscribe({ app: 'herm', path: '/session/'+name+'/view', event: (e) => { showBlit(ses.term, e); if (e.bel && !selected) { @@ -189,7 +189,7 @@ export default function Buffer({ name, selected }: BufferProps) { console.error('oops quit, pls handle'); } }); - + useTermState.getState().set((state) => { state.sessions[name] = ses; }); diff --git a/pkg/interface/webterm/Tab.tsx b/pkg/interface/webterm/Tab.tsx index 0c3f148319..f55627f6d7 100644 --- a/pkg/interface/webterm/Tab.tsx +++ b/pkg/interface/webterm/Tab.tsx @@ -1,16 +1,20 @@ import { DEFAULT_SESSION } from './constants'; import React, { useCallback } from 'react'; -import useTermState from './state'; +import useTermState, { Session } from './state'; import { style } from 'styled-system'; import api from './api'; import { pokeTask } from '@urbit/api/term'; -export const Tab = ( { session, name } ) => { +interface TabProps { + session: Session; + name: string; +} + +export const Tab = ( { session, name }: TabProps ) => { const isSelected = useTermState().selected === name; const onClick = () => { - console.log('click!', name); useTermState.getState().set((state) => { state.selected = name; state.sessions[name].hasBell = false; @@ -18,9 +22,18 @@ export const Tab = ( { session, name } ) => { useTermState.getState().sessions[name]?.term?.focus(); } - const onDelete = (e) => { + const onDelete = useCallback(async (e) => { e.stopPropagation(); - api.poke(pokeTask(name, { shut: null })); + + // 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; @@ -28,8 +41,7 @@ export const Tab = ( { session, name } ) => { state.names = state.names.filter(n => n !== name); delete state.sessions[name]; }); - //TODO clean up the subscription - } + }, [session]); return (
diff --git a/pkg/interface/webterm/state.ts b/pkg/interface/webterm/state.ts index 2392db880e..b6ca56633a 100644 --- a/pkg/interface/webterm/state.ts +++ b/pkg/interface/webterm/state.ts @@ -3,7 +3,12 @@ import { FitAddon } from 'xterm-addon-fit'; import create from 'zustand'; import produce from 'immer'; -export type Session = { term: Terminal, fit: FitAddon, hasBell: boolean } | null; +export type Session = { + term: Terminal, + fit: FitAddon, + hasBell: boolean, + subscriptionId: number | null, +} | null; export type Sessions = { [id: string]: Session; } export interface TermState { From 438e6d4df983aec7ab1fe1e5ff5a8e089900200e Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 3 Mar 2022 17:10:54 -0600 Subject: [PATCH 14/54] ui: style tabs also rename join --> useDark; clean up extraneous logging statements --- pkg/interface/webterm/{app.tsx => App.tsx} | 10 ++--- pkg/interface/webterm/Buffer.tsx | 11 ++--- pkg/interface/webterm/Tab.tsx | 2 +- pkg/interface/webterm/Tabs.tsx | 2 +- pkg/interface/webterm/index.html | 43 ++++++++++++++----- .../webterm/{join.ts => lib/useDark.ts} | 8 ++-- 6 files changed, 45 insertions(+), 31 deletions(-) rename pkg/interface/webterm/{app.tsx => App.tsx} (91%) rename pkg/interface/webterm/{join.ts => lib/useDark.ts} (75%) diff --git a/pkg/interface/webterm/app.tsx b/pkg/interface/webterm/App.tsx similarity index 91% rename from pkg/interface/webterm/app.tsx rename to pkg/interface/webterm/App.tsx index d7c782f296..9ad1d61858 100644 --- a/pkg/interface/webterm/app.tsx +++ b/pkg/interface/webterm/App.tsx @@ -1,13 +1,12 @@ -/* eslint-disable max-lines */ import React, { useCallback, useEffect } from 'react'; -import useTermState, { Sessions } from './state'; -import { useDark } from './join'; +import useTermState from './state'; +import { useDark } from './lib/useDark'; import api from './api'; -import { Reset, _dark, _light } from '@tlon/indigo-react'; +import { _dark, _light } from '@tlon/indigo-react'; import 'xterm/css/xterm.css'; @@ -83,11 +82,10 @@ export default function TermApp(props: TermAppProps) { return ( <> -
{names.map(name => { - return ; + return ; })}
diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index ef65c592bd..8ef69e2e94 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -12,8 +12,7 @@ import useTermState from './state'; import React from 'react'; import { Box, Col } from '@tlon/indigo-react'; import { makeTheme } from './lib/theme'; -import { useDark } from './join'; -import { showBlit, csi, showSlog } from './lib/blit'; +import { showBlit, csi } from './lib/blit'; const termConfig: ITerminalOptions = { logLevel: 'warn', @@ -138,11 +137,11 @@ const onInput = (name: string, session: Session, e: string) => { interface BufferProps { name: string, selected: boolean, + dark: boolean, } -export default function Buffer({ name, selected }: BufferProps) { +export default function Buffer({ name, selected, dark }: BufferProps) { const container = useRef(null); - const dark = useDark(); const session: Session = useTermState(s => s.sessions[name]); @@ -210,10 +209,6 @@ export default function Buffer({ name, selected }: BufferProps) { let newContainer = containerRef || container.current; if(session && newContainer) { container.current = newContainer; - console.log('newcont', newContainer); - // session.term.open(newContainer); - } else { - console.log('kaboom', session); } }, [session]); diff --git a/pkg/interface/webterm/Tab.tsx b/pkg/interface/webterm/Tab.tsx index f55627f6d7..0813292bde 100644 --- a/pkg/interface/webterm/Tab.tsx +++ b/pkg/interface/webterm/Tab.tsx @@ -44,7 +44,7 @@ export const Tab = ( { session, name }: TabProps ) => { }, [session]); return ( -
+ ); }; diff --git a/pkg/interface/webterm/index.html b/pkg/interface/webterm/index.html index ed7f861119..e2f4a18c27 100644 --- a/pkg/interface/webterm/index.html +++ b/pkg/interface/webterm/index.html @@ -29,32 +29,38 @@ } .buffer-container { - height: calc(100% - 33px); + height: calc(100% - 40px); } div.tabs { - height: 33px; + height: 40px; display: flex; flex-flow: row nowrap; justify-content: flex-start; - padding: 5px 0; + 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: 7px; + padding: 10px; cursor: pointer; } - div.tab { - text-align: center; - /* display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: space-between; */ + 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 { @@ -65,6 +71,23 @@ padding: 5px; } + @media (prefers-color-scheme: dark) { + 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, button.tab { + background-color: rgb(26, 26, 26); + color: rgba(255, 255, 255, 0.9); + border-color: rgba(255, 255, 255, 0.9); + } + + div.tabs > div.selected { + border-bottom: black solid 1px; + } + } diff --git a/pkg/interface/webterm/join.ts b/pkg/interface/webterm/lib/useDark.ts similarity index 75% rename from pkg/interface/webterm/join.ts rename to pkg/interface/webterm/lib/useDark.ts index 957abbd3d1..cd96254033 100644 --- a/pkg/interface/webterm/join.ts +++ b/pkg/interface/webterm/lib/useDark.ts @@ -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); From 87ac253b8d9dd6873129c83a4268623c040c2271 Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 3 Mar 2022 18:11:37 -0600 Subject: [PATCH 15/54] ux: default terminal sets correct theme onload also, increase size of Tab click target --- pkg/interface/webterm/Buffer.tsx | 5 +++-- pkg/interface/webterm/Tab.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index 8ef69e2e94..dca9d8d716 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -13,6 +13,7 @@ import React from 'react'; import { Box, Col } from '@tlon/indigo-react'; import { makeTheme } from './lib/theme'; import { showBlit, csi } from './lib/blit'; +import { DEFAULT_SESSION } from './constants'; const termConfig: ITerminalOptions = { logLevel: 'warn', @@ -146,7 +147,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { const session: Session = useTermState(s => s.sessions[name]); const initSession = useCallback(async (name: string, dark: boolean) => { - console.log('setting up', name); + console.log('setting up', name === DEFAULT_SESSION ? 'default' : name); // set up xterm terminal // @@ -234,7 +235,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { if (container.current) { container.current.style.backgroundColor = theme.background || ''; } - }, [dark]); + }, [session, dark]); useEffect(() => { if (session && selected && !session.term.isOpen) { diff --git a/pkg/interface/webterm/Tab.tsx b/pkg/interface/webterm/Tab.tsx index 0813292bde..2fac14813e 100644 --- a/pkg/interface/webterm/Tab.tsx +++ b/pkg/interface/webterm/Tab.tsx @@ -44,8 +44,8 @@ export const Tab = ( { session, name }: TabProps ) => { }, [session]); return ( -
- +
+ {session?.hasBell ? '🔔 ' : ''} {name === DEFAULT_SESSION ? 'default' : name} {' '} From 8906d1c17d581ef809bdd21049f255d42b1ca53e Mon Sep 17 00:00:00 2001 From: fang Date: Sat, 5 Mar 2022 18:17:48 -0600 Subject: [PATCH 16/54] dill: move %mor case into $blit This lets us send a single blit around, instead of sending facts for every individual blit in a draw event. --- pkg/arvo/lib/hood/drum.hoon | 10 +++++----- pkg/arvo/sys/lull.hoon | 5 +++-- pkg/arvo/sys/vane/dill.hoon | 7 ++----- pkg/base-dev/lib/dill.hoon | 1 + pkg/interface/webterm/lib/blit.ts | 4 +++- pkg/npm/api/term/types.ts | 3 ++- pkg/urbit/vere/io/term.c | 12 +++++++++++- 7 files changed, 27 insertions(+), 15 deletions(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 001e073248..5399edeeab 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -122,7 +122,7 @@ =+ (~(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 @@ -284,13 +284,13 @@ ^- (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 ::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 !>(dill-blit)] + [%give %fact to %dill-blit !>(blit)] :: ++ se-adze :: update connections ^+ . @@ -473,7 +473,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 @@ -640,7 +640,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) diff --git a/pkg/arvo/sys/lull.hoon b/pkg/arvo/sys/lull.hoon index d89745182d..32a1de2c52 100644 --- a/pkg/arvo/sys/lull.hoon +++ b/pkg/arvo/sys/lull.hoon @@ -1088,8 +1088,9 @@ [%clr ~] :: clear the screen [%hop p=$@(@ud [r=@ud c=@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 @@ -1099,12 +1100,12 @@ $% belt :: client input [%cru p=@tas q=(list tank)] :: echo error [%hey ~] :: refresh + ::TODO inconsistent with %hit and %hop [%rez p=@ud q=@ud] :: resize, cols, rows [%yow p=gill:gall] :: connect to app == :: +$ dill-blit :: arvo output $% blit :: client output - [%mor p=(list dill-blit)] :: multiple blits [%qit ~] :: close console == :: +$ flog :: sent to %dill diff --git a/pkg/arvo/sys/vane/dill.hoon b/pkg/arvo/sys/vane/dill.hoon index e2c1e4d2e0..afb827c32d 100644 --- a/pkg/arvo/sys/vane/dill.hoon +++ b/pkg/arvo/sys/vane/dill.hoon @@ -178,13 +178,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 diff --git a/pkg/base-dev/lib/dill.hoon b/pkg/base-dev/lib/dill.hoon index d4cecdf142..d3592c07d7 100644 --- a/pkg/base-dev/lib/dill.hoon +++ b/pkg/base-dev/lib/dill.hoon @@ -18,6 +18,7 @@ %nel b+& %url s+p.blit %wyp b+& + %mor a+(turn p.blit ^blit) :: %sag %- pairs diff --git a/pkg/interface/webterm/lib/blit.ts b/pkg/interface/webterm/lib/blit.ts index 5343284333..5285675f13 100644 --- a/pkg/interface/webterm/lib/blit.ts +++ b/pkg/interface/webterm/lib/blit.ts @@ -10,7 +10,9 @@ export const csi = (cmd: string, ...args: number[]) => { export const showBlit = (term: Terminal, blit: Blit) => { let out = ''; - if ('bel' in blit) { + 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(); diff --git a/pkg/npm/api/term/types.ts b/pkg/npm/api/term/types.ts index df4767cffb..362973f31b 100644 --- a/pkg/npm/api/term/types.ts +++ b/pkg/npm/api/term/types.ts @@ -27,8 +27,9 @@ export type Blit = | { clr: null } // clear the screen | { hop: number | { r: number, c: 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 diff --git a/pkg/urbit/vere/io/term.c b/pkg/urbit/vere/io/term.c index e2b6376ca3..ba5775cbd0 100644 --- a/pkg/urbit/vere/io/term.c +++ b/pkg/urbit/vere/io/term.c @@ -1353,7 +1353,17 @@ _term_ef_blit(u3_utty* uty_u, _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; From 8d9a59bfe4f67f7eeeb144c4580b7713e8d1d4ff Mon Sep 17 00:00:00 2001 From: tomholford Date: Wed, 9 Mar 2022 16:14:06 -0600 Subject: [PATCH 17/54] devex: eslint config --- pkg/interface/webterm/.eslintrc.js | 26 +- pkg/interface/webterm/package-lock.json | 715 ++++++++++++++++-------- pkg/interface/webterm/package.json | 9 +- 3 files changed, 496 insertions(+), 254 deletions(-) diff --git a/pkg/interface/webterm/.eslintrc.js b/pkg/interface/webterm/.eslintrc.js index 66d65c2914..8f36013edf 100644 --- a/pkg/interface/webterm/.eslintrc.js +++ b/pkg/interface/webterm/.eslintrc.js @@ -1,7 +1,23 @@ -const config = { +module.exports = exports = { "rules": { - "spaced-comment": false, - } + "spaced-comment": 0, + }, + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:react/recommended", + ], + "settings": { + "react": { + "version": "detect" + }, + "import/resolver": { + typescript: {} // this loads /tsconfig.json to eslint + }, + }, + "env": { + "browser": true, + "es6": true + }, + "plugins": ["import", "react-hooks"] } - -export default config; diff --git a/pkg/interface/webterm/package-lock.json b/pkg/interface/webterm/package-lock.json index 2e42a0ff01..902930d988 100644 --- a/pkg/interface/webterm/package-lock.json +++ b/pkg/interface/webterm/package-lock.json @@ -40,7 +40,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", @@ -49,7 +49,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", @@ -3006,6 +3008,12 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.14.172", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", @@ -3867,16 +3875,16 @@ "dev": true }, "node_modules/array-includes": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", - "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", + "es-abstract": "^1.19.1", "get-intrinsic": "^1.1.1", - "is-string": "^1.0.5" + "is-string": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -3912,16 +3920,15 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", - "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "node_modules/array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "function-bind": "^1.1.1" + "es-abstract": "^1.19.0" }, "engines": { "node": ">= 0.4" @@ -6464,9 +6471,9 @@ } }, "node_modules/es-abstract": { - "version": "1.18.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.6.tgz", - "integrity": "sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", @@ -6480,7 +6487,9 @@ "is-callable": "^1.2.4", "is-negative-zero": "^2.0.1", "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", "is-string": "^1.0.7", + "is-weakref": "^1.0.1", "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", @@ -6623,34 +6632,171 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-react": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz", - "integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", "dev": true, "dependencies": { - "array-includes": "^3.1.3", - "array.prototype.flatmap": "^1.2.4", - "doctrine": "^2.1.0", - "estraverse": "^5.2.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.4", - "object.fromentries": "^2.0.4", - "object.values": "^1.1.4", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "string.prototype.matchall": "^4.0.5" + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz", + "integrity": "sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.1", + "glob": "^7.1.7", + "is-glob": "^4.0.1", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.9.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7" + "eslint": "*", + "eslint-plugin-import": "*" } }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { + "node_modules/eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.25.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", + "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.2", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.12.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", @@ -6662,26 +6808,22 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", + "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", "dev": true, "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "node_modules/eslint-scope": { @@ -9250,9 +9392,9 @@ } }, "node_modules/is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -9355,9 +9497,9 @@ } }, "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" @@ -9487,6 +9629,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -9544,6 +9695,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -11786,19 +11949,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsx-ast-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", - "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.2", - "object.assign": "^4.1.2" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -12805,9 +12955,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -13375,38 +13525,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.entries": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", - "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", - "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.getownpropertydescriptors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", @@ -13437,14 +13555,14 @@ } }, "node_modules/object.values": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", - "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "es-abstract": "^1.19.1" }, "engines": { "node": ">= 0.4" @@ -14107,13 +14225,13 @@ } }, "node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "node_modules/proxy-addr": { @@ -15877,25 +15995,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz", - "integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -16501,6 +16600,27 @@ "node": ">=6" } }, + "node_modules/tsconfig-paths": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.13.0.tgz", + "integrity": "sha512-nWuffZppoaYK0vQ1SQmkSsQzJoHA4s6uzdb2waRpD806x9yfq153AdVsWz4je2qZcW+pENrMQXbGQ3sMCkXuhw==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -20825,6 +20945,12 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/lodash": { "version": "4.14.172", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", @@ -21538,16 +21664,16 @@ "dev": true }, "array-includes": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", - "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", + "es-abstract": "^1.19.1", "get-intrinsic": "^1.1.1", - "is-string": "^1.0.5" + "is-string": "^1.0.7" } }, "array-union": { @@ -21568,16 +21694,15 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "array.prototype.flatmap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", - "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", "dev": true, "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "function-bind": "^1.1.1" + "es-abstract": "^1.19.0" } }, "asn1.js": { @@ -23681,9 +23806,9 @@ } }, "es-abstract": { - "version": "1.18.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.6.tgz", - "integrity": "sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -23697,7 +23822,9 @@ "is-callable": "^1.2.4", "is-negative-zero": "^2.0.1", "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", "is-string": "^1.0.7", + "is-weakref": "^1.0.1", "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", @@ -24021,27 +24148,140 @@ } } }, - "eslint-plugin-react": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz", - "integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==", + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", "dev": true, "requires": { - "array-includes": "^3.1.3", - "array.prototype.flatmap": "^1.2.4", - "doctrine": "^2.1.0", - "estraverse": "^5.2.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.4", - "object.fromentries": "^2.0.4", - "object.values": "^1.1.4", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "string.prototype.matchall": "^4.0.5" + "debug": "^3.2.7", + "resolve": "^1.20.0" }, "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-import-resolver-typescript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz", + "integrity": "sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==", + "dev": true, + "requires": { + "debug": "^4.3.1", + "glob": "^7.1.7", + "is-glob": "^4.0.1", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.9.0" + } + }, + "eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.25.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", + "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.2", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -24051,24 +24291,21 @@ "esutils": "^2.0.2" } }, - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true - }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } } } }, + "eslint-plugin-react-hooks": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", + "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -25889,9 +26126,9 @@ } }, "is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", "dev": true, "requires": { "has": "^1.0.3" @@ -25960,9 +26197,9 @@ "dev": true }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -26050,6 +26287,12 @@ "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", "dev": true }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -26086,6 +26329,15 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -27772,16 +28024,6 @@ "minimist": "^1.2.0" } }, - "jsx-ast-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", - "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", - "dev": true, - "requires": { - "array-includes": "^3.1.2", - "object.assign": "^4.1.2" - } - }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -28547,9 +28789,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -29031,29 +29273,6 @@ "object-keys": "^1.1.1" } }, - "object.entries": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", - "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" - } - }, - "object.fromentries": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", - "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has": "^1.0.3" - } - }, "object.getownpropertydescriptors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", @@ -29075,14 +29294,14 @@ } }, "object.values": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", - "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "es-abstract": "^1.19.1" } }, "obuf": { @@ -29614,13 +29833,13 @@ } }, "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "proxy-addr": { @@ -31082,22 +31301,6 @@ } } }, - "string.prototype.matchall": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz", - "integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - } - }, "string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -31573,6 +31776,26 @@ } } }, + "tsconfig-paths": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.13.0.tgz", + "integrity": "sha512-nWuffZppoaYK0vQ1SQmkSsQzJoHA4s6uzdb2waRpD806x9yfq153AdVsWz4je2qZcW+pENrMQXbGQ3sMCkXuhw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", diff --git a/pkg/interface/webterm/package.json b/pkg/interface/webterm/package.json index e859b48253..856b29eede 100644 --- a/pkg/interface/webterm/package.json +++ b/pkg/interface/webterm/package.json @@ -36,7 +36,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 +45,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 +60,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", From e7e5c6340977abf15831f07831ac24e1ad69edd3 Mon Sep 17 00:00:00 2001 From: tomholford Date: Wed, 9 Mar 2022 16:15:24 -0600 Subject: [PATCH 18/54] ux: support `agent!session-name` syntax When adding a session, using this special syntax will create a new session for the indicated agent. E.g., `book!my-session` opens a new session for the %book agent. --- pkg/interface/webterm/Tabs.tsx | 21 ++------ pkg/interface/webterm/constants.ts | 15 ++++++ pkg/interface/webterm/lib/useAddSession.ts | 59 ++++++++++++++++++++++ 3 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 pkg/interface/webterm/lib/useAddSession.ts diff --git a/pkg/interface/webterm/Tabs.tsx b/pkg/interface/webterm/Tabs.tsx index e98c38d3f4..09751537a9 100644 --- a/pkg/interface/webterm/Tabs.tsx +++ b/pkg/interface/webterm/Tabs.tsx @@ -1,26 +1,11 @@ -import { pokeTask } from '@urbit/api/term'; -import api from './api'; import React from 'react'; import useTermState from './state'; import { Tab } from './Tab'; -import { SESSION_ID_REGEX } from './constants'; +import { useAddSession } from './lib/useAddSession'; export const Tabs = () => { const { sessions, names } = useTermState(); - - const onAddClick = () => { - const name = prompt('please entew a session name uwu'); - if (!name || !SESSION_ID_REGEX.test(name) || names.includes(name)) { - console.log('invalid session name:', name); - return; - } - api.poke(pokeTask(name, { open: { term: 'hood', apps: [{ who: '~' + (window as any).ship, app: 'dojo' }] } })); - useTermState.getState().set(state => { - state.names = [name, ...state.names].sort(); - state.selected = name; - state.sessions[name] = null; - }); - } + const { addSession } = useAddSession(); return (
@@ -29,7 +14,7 @@ export const Tabs = () => { ); })} - +
); }; diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index 7e173ac64c..cd80b9aa22 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -9,3 +9,18 @@ export const DEFAULT_SESSION = ''; * - cannot begin or end with a hyphen */ export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d\-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; + +/** + * 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]{4})\!(([a-z]{1}[a-z\d\-]*[a-z\d]{1}$)|(^[a-z]{1}))/; diff --git a/pkg/interface/webterm/lib/useAddSession.ts b/pkg/interface/webterm/lib/useAddSession.ts new file mode 100644 index 0000000000..5f06d5c8d0 --- /dev/null +++ b/pkg/interface/webterm/lib/useAddSession.ts @@ -0,0 +1,59 @@ +import { 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 = 'hood'; // default agent + let sessionName: string; + + const userInput = prompt('please entew a session name uwu'); + // user canceled or did not enter a value + if (!userInput) { + return; + } + + // check for custom agent session syntax + if (AGENT_SESSION_REGEX.test(userInput)) { + const match = AGENT_SESSION_REGEX.exec(userInput); + if (!match) { + 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) { + return; + } + sessionName = match[1]; + } else { + return; + } + + // avoid nil or duplicate sessions + if(!sessionName || names.includes(sessionName)) { + return; + } + + try { + 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 + }; +}; From ee492e6f8309cbba3d77dbd7fd258764f5d99679 Mon Sep 17 00:00:00 2001 From: tomholford Date: Wed, 9 Mar 2022 16:25:10 -0600 Subject: [PATCH 19/54] devex: cleaning up lint issues --- pkg/interface/webterm/App.tsx | 12 ++++-------- pkg/interface/webterm/Buffer.tsx | 19 +++++++++---------- pkg/interface/webterm/Tab.tsx | 6 ++---- pkg/interface/webterm/constants.ts | 14 +++++++------- pkg/interface/webterm/lib/useDark.ts | 2 +- pkg/interface/webterm/state.ts | 6 ++++-- 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/pkg/interface/webterm/App.tsx b/pkg/interface/webterm/App.tsx index 9ad1d61858..9ce3b0c00d 100644 --- a/pkg/interface/webterm/App.tsx +++ b/pkg/interface/webterm/App.tsx @@ -20,11 +20,7 @@ import Buffer from './Buffer'; import { DEFAULT_SESSION } from './constants'; import { showSlog } from './lib/blit'; -type TermAppProps = { - ship: string; -} - -export default function TermApp(props: TermAppProps) { +export default function TermApp() { const { names, selected } = useTermState(); const dark = useDark(); @@ -41,7 +37,7 @@ export default function TermApp(props: TermAppProps) { let available = false; const slog = new EventSource('/~_~/slog', { withCredentials: true }); - slog.onopen = (e) => { + slog.onopen = () => { console.log('slog: opened stream'); available = true; }; @@ -84,8 +80,8 @@ export default function TermApp(props: TermAppProps) {
- {names.map(name => { - return ; + {names.map((name) => { + return ; })}
diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index dca9d8d716..063019ed26 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -62,7 +62,7 @@ const readInput = (term: Terminal, e: string): Belt[] => { } else if (13 === c) { belts.push({ ret: null }); } else if (c <= 26) { - let k = String.fromCharCode(96 + c); + const k = String.fromCharCode(96 + c); //NOTE prevent remote shut-downs if ('d' !== k) { belts.push({ mod: { mod: 'ctl', key: k } }); @@ -178,7 +178,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { event: (e) => { showBlit(ses.term, e); if (e.bel && !selected) { - useTermState.getState().set(state => { + useTermState.getState().set((state) => { state.sessions[name].hasBell = true; }); } @@ -189,7 +189,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { console.error('oops quit, pls handle'); } }); - + useTermState.getState().set((state) => { state.sessions[name] = ses; }); @@ -207,7 +207,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { // on selected change, maybe setup the term, or put it into the container // const setContainer = useCallback((containerRef: HTMLDivElement | null) => { - let newContainer = containerRef || container.current; + const newContainer = containerRef || container.current; if(session && newContainer) { container.current = newContainer; } @@ -216,7 +216,6 @@ export default function Buffer({ name, selected, dark }: BufferProps) { // on-init, open slogstream and fetch existing sessions // useEffect(() => { - window.addEventListener('resize', onResize(session)); return () => { @@ -239,10 +238,10 @@ export default function Buffer({ name, selected, dark }: BufferProps) { useEffect(() => { if (session && selected && !session.term.isOpen) { - session!.term.open(container.current); - session!.fit.fit(); - session!.term.focus(); - session!.term.isOpen = true; + session.term.open(container.current); + session.fit.fit(); + session.term.focus(); + session.term.isOpen = true; } }, [selected, session]); @@ -256,7 +255,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { bg='white' fontFamily='mono' overflow='hidden' - style={selected ? {} : {display: 'none'}} + style={selected ? {} : { display: 'none' }} > { - const isSelected = useTermState().selected === name; const onClick = () => { @@ -20,7 +18,7 @@ export const Tab = ( { session, name }: TabProps ) => { state.sessions[name].hasBell = false; }); useTermState.getState().sessions[name]?.term?.focus(); - } + }; const onDelete = useCallback(async (e) => { e.stopPropagation(); @@ -34,7 +32,7 @@ export const Tab = ( { session, name }: TabProps ) => { await api.poke(pokeTask(name, { shut: null })); // remove from zustand - useTermState.getState().set(state => { + useTermState.getState().set((state) => { if (state.selected === name) { state.selected = DEFAULT_SESSION; } diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index cd80b9aa22..61c11b13c8 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -2,25 +2,25 @@ export const DEFAULT_SESSION = ''; /** * Session ID validity: - * + * * - must start with an alphabetical * - can be composed of alphanumerics with hyphens * - can be length 1 or longer * - cannot begin or end with a hyphen */ -export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d\-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; +export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; /** * 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 + * + * Note that the second capture group after the ! is composed of the session ID * regex above. */ -export const AGENT_SESSION_REGEX = /^([a-z]{4})\!(([a-z]{1}[a-z\d\-]*[a-z\d]{1}$)|(^[a-z]{1}))/; +export const AGENT_SESSION_REGEX = /^([a-z]{4})!(([a-z]{1}[a-z\d-]*[a-z\d]{1}$)|(^[a-z]{1}))/; diff --git a/pkg/interface/webterm/lib/useDark.ts b/pkg/interface/webterm/lib/useDark.ts index cd96254033..ddd9c10aea 100644 --- a/pkg/interface/webterm/lib/useDark.ts +++ b/pkg/interface/webterm/lib/useDark.ts @@ -14,7 +14,7 @@ export function useDark() { return () => { themeWatcher.removeEventListener('change', update); - } + }; }, []); const theme = useTermState(s => s.theme); diff --git a/pkg/interface/webterm/state.ts b/pkg/interface/webterm/state.ts index b6ca56633a..e481e36e5b 100644 --- a/pkg/interface/webterm/state.ts +++ b/pkg/interface/webterm/state.ts @@ -3,8 +3,8 @@ import { FitAddon } from 'xterm-addon-fit'; import create from 'zustand'; import produce from 'immer'; -export type Session = { - term: Terminal, +export type Session = { + term: Terminal, fit: FitAddon, hasBell: boolean, subscriptionId: number | null, @@ -21,12 +21,14 @@ export interface TermState { set: any, } +// eslint-disable-next-line no-unused-vars const useTermState = create((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)); } From 200b504c4e19ac76eada25fa48844d0dc2b9ce9c Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 10 Mar 2022 00:03:27 -0600 Subject: [PATCH 20/54] api: resubscribe after clog --- pkg/interface/webterm/Buffer.tsx | 37 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index 063019ed26..eecadb9f13 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -174,21 +174,34 @@ export default function Buffer({ name, selected, dark }: BufferProps) { // open subscription // - ses.subscriptionId = await api.subscribe({ app: 'herm', path: '/session/'+name+'/view', - event: (e) => { - showBlit(ses.term, e); - if (e.bel && !selected) { + const initSubscription = async () => { + const subscriptionId = await api.subscribe({ + app: 'herm', path: '/session/' + name + '/view', + event: (e) => { + showBlit(ses.term, e); + if (e.bel && !selected) { + 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('oops quit, reconnecting...'); + const newSubscriptionId = await initSubscription(); useTermState.getState().set((state) => { - state.sessions[name].hasBell = true; + state.sessions[name].subscriptionId = newSubscriptionId; }); } - //TODO should handle %bye on this higher level though, for deletion - }, - quit: () => { // quit - // TODO show user a message - console.error('oops quit, pls handle'); - } - }); + }); + + return subscriptionId; + }; + + ses.subscriptionId = await initSubscription(); useTermState.getState().set((state) => { state.sessions[name] = ses; From bf0f4e97c999256d1fcd2deaa12e452265a8bd93 Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 10 Mar 2022 22:54:30 -0600 Subject: [PATCH 21/54] api: exponential backoff when resubscribing Use the new `lib/retry` to attempt to reconnect when clogged. If unsucessful after 5 attempts, stop retrying and log an error. --- pkg/interface/webterm/Buffer.tsx | 15 +++++++++--- pkg/interface/webterm/constants.ts | 2 +- pkg/interface/webterm/lib/retry.ts | 39 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 pkg/interface/webterm/lib/retry.ts diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index eecadb9f13..f24d08bd49 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -14,6 +14,7 @@ import { Box, Col } from '@tlon/indigo-react'; import { makeTheme } from './lib/theme'; import { showBlit, csi } from './lib/blit'; import { DEFAULT_SESSION } from './constants'; +import { retry } from './lib/retry'; const termConfig: ITerminalOptions = { logLevel: 'warn', @@ -191,10 +192,16 @@ export default function Buffer({ name, selected, dark }: BufferProps) { }, quit: async () => { // quit console.error('oops quit, reconnecting...'); - const newSubscriptionId = await initSubscription(); - useTermState.getState().set((state) => { - state.sessions[name].subscriptionId = newSubscriptionId; - }); + 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); + } } }); diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index 61c11b13c8..a96349bc14 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -11,7 +11,7 @@ export const DEFAULT_SESSION = ''; export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; /** - * Open a session with a given agent using `[agent]![session_name] + * Open a session with a given agent using `[agent]![session_name]` * * For example: * ``` diff --git a/pkg/interface/webterm/lib/retry.ts b/pkg/interface/webterm/lib/retry.ts new file mode 100644 index 0000000000..dea78b0b2b --- /dev/null +++ b/pkg/interface/webterm/lib/retry.ts @@ -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); +} From 586c2da857b7ce56e55c73bcd235cfa8c0e69427 Mon Sep 17 00:00:00 2001 From: fang Date: Tue, 15 Mar 2022 01:24:16 +0100 Subject: [PATCH 22/54] webterm: improve session creation regexes Trailing dashes are, in fact, allowed. As are numerics in the agent name. --- pkg/interface/webterm/constants.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index a96349bc14..deaf08db6f 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -6,9 +6,8 @@ export const DEFAULT_SESSION = ''; * - must start with an alphabetical * - can be composed of alphanumerics with hyphens * - can be length 1 or longer - * - cannot begin or end with a hyphen */ -export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; +export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d-]*$)/; /** * Open a session with a given agent using `[agent]![session_name]` @@ -23,4 +22,4 @@ export const SESSION_ID_REGEX = /(^[a-z]{1}[a-z\d-]*[a-z\d]{1}$)|(^[a-z]{1}$)/; * Note that the second capture group after the ! is composed of the session ID * regex above. */ -export const AGENT_SESSION_REGEX = /^([a-z]{4})!(([a-z]{1}[a-z\d-]*[a-z\d]{1}$)|(^[a-z]{1}))/; +export const AGENT_SESSION_REGEX = /^([a-z]{1}[a-z\d-]*)!([a-z]{1}[a-z\d-]*$)/; From 0d2c135959e1c634f8c69c4a45390005c0fb8d89 Mon Sep 17 00:00:00 2001 From: fang Date: Tue, 15 Mar 2022 01:34:19 +0100 Subject: [PATCH 23/54] webterm: small cleanup, comments Also includes a more-sane prompt() description. --- pkg/interface/webterm/Buffer.tsx | 2 +- pkg/interface/webterm/constants.ts | 1 + pkg/interface/webterm/lib/useAddSession.ts | 11 ++++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index f24d08bd49..067fd0cf9e 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -191,7 +191,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { console.log(`subscription error, id ${id}:`, e); }, quit: async () => { // quit - console.error('oops quit, reconnecting...'); + console.error('quit, reconnecting...'); try { const newSubscriptionId = await retry(initSubscription, () => { console.log('attempting to reconnect ...'); diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index deaf08db6f..66d4b0eccf 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -1,4 +1,5 @@ export const DEFAULT_SESSION = ''; +export const DEFAULT_HANDLER = 'hood'; /** * Session ID validity: diff --git a/pkg/interface/webterm/lib/useAddSession.ts b/pkg/interface/webterm/lib/useAddSession.ts index 5f06d5c8d0..f25c13b2b8 100644 --- a/pkg/interface/webterm/lib/useAddSession.ts +++ b/pkg/interface/webterm/lib/useAddSession.ts @@ -1,4 +1,8 @@ -import { AGENT_SESSION_REGEX, SESSION_ID_REGEX } from '../constants'; +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'; @@ -8,10 +12,10 @@ export const useAddSession = () => { const { names } = useTermState(); const addSession = useCallback(async () => { - let agent = 'hood'; // default agent + let agent = DEFAULT_HANDLER; let sessionName: string; - const userInput = prompt('please entew a session name uwu'); + const userInput = prompt('Please enter an alpha-numeric session name.'); // user canceled or did not enter a value if (!userInput) { return; @@ -42,6 +46,7 @@ export const useAddSession = () => { } 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(); From 6256a0a6647fd20b4b03c57eaa4c8a2cded3c0cd Mon Sep 17 00:00:00 2001 From: tomholford Date: Tue, 15 Mar 2022 11:16:44 -0600 Subject: [PATCH 24/54] ux: inform user when session input is invalid Show a helpful error message via `alert` instead of failing silently. --- pkg/interface/webterm/lib/useAddSession.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/interface/webterm/lib/useAddSession.ts b/pkg/interface/webterm/lib/useAddSession.ts index f25c13b2b8..b740ebcc44 100644 --- a/pkg/interface/webterm/lib/useAddSession.ts +++ b/pkg/interface/webterm/lib/useAddSession.ts @@ -18,6 +18,7 @@ export const useAddSession = () => { const userInput = prompt('Please enter an alpha-numeric session name.'); // user canceled or did not enter a value if (!userInput) { + alert('A valid name is required to create a new session'); return; } @@ -25,6 +26,7 @@ export const useAddSession = () => { 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]; @@ -33,15 +35,18 @@ export const useAddSession = () => { } 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; } - // avoid nil or duplicate sessions - if(!sessionName || names.includes(sessionName)) { + // prevent duplicate sessions + if(names.includes(sessionName)) { + alert(`Session name must be unique ("${sessionName}" already in use)`); return; } From 01de5a06b0aebe797570e4f137ee2b3e1eb24c18 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 25 Mar 2022 13:45:29 +0100 Subject: [PATCH 25/54] term: consistently use x/y coordinate ordering %rez has always used "width & height". Certainly, "x & y" is more standard than "row & column". As such, we settle on making %hop and %hit respect the more natural ordering. This change is safe because these interfaces haven't made it to livenet yet. --- pkg/arvo/lib/hood/drum.hoon | 8 ++++---- pkg/arvo/mar/dill/blit.hoon | 2 +- pkg/arvo/sys/lull.hoon | 5 ++--- pkg/base-dev/lib/dill.hoon | 4 ++-- pkg/interface/webterm/Buffer.tsx | 2 +- pkg/interface/webterm/lib/blit.ts | 2 +- pkg/npm/api/term/types.ts | 4 ++-- pkg/urbit/vere/io/term.c | 8 +++++--- 8 files changed, 18 insertions(+), 17 deletions(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 5399edeeab..39ceb90dbd 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -685,13 +685,13 @@ (ta-hom %del pos.inp) :: ++ ta-hit :: hear click - |= [r=@ud c=@ud] + |= [x=@ud y=@ud] ^+ +> - ?. =(0 r) +> + ?. =(0 y) +> =/ pol=@ud (lent-char:klr (make:klr cad.pom)) - ?: (lth c pol) +>.$ - +>.$(pos.inp (min (sub c pol) (lent buf.say.inp))) + ?: (lth x pol) +>.$ + +>.$(pos.inp (min (sub x pol) (lent buf.say.inp))) :: ++ ta-erl :: hear local error |= pos=@ud diff --git a/pkg/arvo/mar/dill/blit.hoon b/pkg/arvo/mar/dill/blit.hoon index 5ec9b08cbe..3ba85e3dcf 100644 --- a/pkg/arvo/mar/dill/blit.hoon +++ b/pkg/arvo/mar/dill/blit.hoon @@ -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) == diff --git a/pkg/arvo/sys/lull.hoon b/pkg/arvo/sys/lull.hoon index 32a1de2c52..3caace4f64 100644 --- a/pkg/arvo/sys/lull.hoon +++ b/pkg/arvo/sys/lull.hoon @@ -1080,13 +1080,13 @@ $% [%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 [%mor p=(list blit)] :: multiple blits [%nel ~] :: newline @@ -1100,7 +1100,6 @@ $% belt :: client input [%cru p=@tas q=(list tank)] :: echo error [%hey ~] :: refresh - ::TODO inconsistent with %hit and %hop [%rez p=@ud q=@ud] :: resize, cols, rows [%yow p=gill:gall] :: connect to app == :: diff --git a/pkg/base-dev/lib/dill.hoon b/pkg/base-dev/lib/dill.hoon index d3592c07d7..41608b2635 100644 --- a/pkg/base-dev/lib/dill.hoon +++ b/pkg/base-dev/lib/dill.hoon @@ -13,7 +13,7 @@ %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 @@ -74,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 == :: diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index 067fd0cf9e..67b8f03c62 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -90,7 +90,7 @@ const readInput = (term: Terminal, e: string): Belt[] => { 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 } }); + belts.push({ hit: { y: term.rows - r, x: c - 1 } }); } e = e.slice(3); break; diff --git a/pkg/interface/webterm/lib/blit.ts b/pkg/interface/webterm/lib/blit.ts index 5285675f13..cd25de168f 100644 --- a/pkg/interface/webterm/lib/blit.ts +++ b/pkg/interface/webterm/lib/blit.ts @@ -21,7 +21,7 @@ export const showBlit = (term: Terminal, blit: 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('H', term.rows - blit.hop.y, blit.hop.x + 1); } out += csi('s'); // save cursor position } else if ('put' in blit) { diff --git a/pkg/npm/api/term/types.ts b/pkg/npm/api/term/types.ts index 362973f31b..2f352a7281 100644 --- a/pkg/npm/api/term/types.ts +++ b/pkg/npm/api/term/types.ts @@ -25,7 +25,7 @@ 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 | { mor: Blit[] } // multiple blits | { nel: null } // newline @@ -43,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 = diff --git a/pkg/urbit/vere/io/term.c b/pkg/urbit/vere/io/term.c index ba5775cbd0..387e939ea8 100644 --- a/pkg/urbit/vere/io/term.c +++ b/pkg/urbit/vere/io/term.c @@ -405,7 +405,7 @@ _term_it_show_blank(u3_utty* uty_u) * 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; @@ -698,7 +698,9 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y) c3_y row_y = cay_y - 32; // only acknowledge button 1 presses within our 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_belt(uty_u, u3nt(c3__hit, + tat_u->esc.col_y - 1, + tat_u->siz.row_l - row_y)); } tat_u->esc.mou = c3n; tat_u->esc.ton_y = tat_u->esc.col_y = 0; @@ -1334,7 +1336,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, 0); } else { _term_it_move_cursor(uty_u, u3h(pos), u3t(pos)); From 890e5f1e9b2b1a1128dd81c5492fd3b73315badc Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 25 Mar 2022 14:41:53 +0100 Subject: [PATCH 26/54] webterm: do not warn on session creation cancel --- pkg/interface/webterm/lib/useAddSession.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/interface/webterm/lib/useAddSession.ts b/pkg/interface/webterm/lib/useAddSession.ts index b740ebcc44..25a5fc0c29 100644 --- a/pkg/interface/webterm/lib/useAddSession.ts +++ b/pkg/interface/webterm/lib/useAddSession.ts @@ -17,8 +17,7 @@ export const useAddSession = () => { const userInput = prompt('Please enter an alpha-numeric session name.'); // user canceled or did not enter a value - if (!userInput) { - alert('A valid name is required to create a new session'); + if (null === userInput) { return; } From 8da6c20f700772af7c65825d1a1cf9f5b3179407 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 25 Mar 2022 15:08:50 +0100 Subject: [PATCH 27/54] herm: stop sending %hail on-connect Client will probably want to send a %blew first anyway. By not doing any screen refreshed in herm, we avoid doing unnecessary redraws on-connect. --- pkg/arvo/app/herm.hoon | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pkg/arvo/app/herm.hoon b/pkg/arvo/app/herm.hoon index 0d7f45a65b..aefe6c8df7 100644 --- a/pkg/arvo/app/herm.hoon +++ b/pkg/arvo/app/herm.hoon @@ -44,17 +44,12 @@ ~| path ?> ?=([%session @ %view ~] path) =* ses i.t.path - :~ :: 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 ~) - == + :: 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 ~)]~ :: ++ on-arvo |= [=wire =sign-arvo] From 60ed368bc4752b7244eb7d8bf6074f5b26699fa9 Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 25 Mar 2022 15:38:56 +0100 Subject: [PATCH 28/54] webterm: fix bell icon in tabs Presumably due to how js non-objects work in closures, the selected prop we were reading out whenever a blit came in was stale. Also, it was possible that a bell was hiding inside a %mor blit, so we add a small helper for checking properly. --- pkg/interface/webterm/Buffer.tsx | 5 +++-- pkg/interface/webterm/lib/blit.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index 67b8f03c62..8d5ae2ce76 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -12,7 +12,7 @@ import useTermState from './state'; import React from 'react'; import { Box, Col } from '@tlon/indigo-react'; import { makeTheme } from './lib/theme'; -import { showBlit, csi } from './lib/blit'; +import { showBlit, csi, hasBell } from './lib/blit'; import { DEFAULT_SESSION } from './constants'; import { retry } from './lib/retry'; @@ -180,7 +180,8 @@ export default function Buffer({ name, selected, dark }: BufferProps) { app: 'herm', path: '/session/' + name + '/view', event: (e) => { showBlit(ses.term, e); - if (e.bel && !selected) { + //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; }); diff --git a/pkg/interface/webterm/lib/blit.ts b/pkg/interface/webterm/lib/blit.ts index cd25de168f..e98f8fe750 100644 --- a/pkg/interface/webterm/lib/blit.ts +++ b/pkg/interface/webterm/lib/blit.ts @@ -73,3 +73,13 @@ export const showSlog = (term: Terminal, slog: string) => { + 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; + } +} From 531fd61ace550154ff8dfb1e2d8f1ebc96477a4f Mon Sep 17 00:00:00 2001 From: tomholford Date: Fri, 1 Apr 2022 06:59:39 -0700 Subject: [PATCH 29/54] ux: add info modal Add a new tab to the top bar that shows an alert with a brief usage guide. --- pkg/interface/webterm/App.tsx | 6 ++++- pkg/interface/webterm/InfoButton.tsx | 19 ++++++++++++++++ pkg/interface/webterm/Tabs.tsx | 8 ++++++- pkg/interface/webterm/index.html | 34 +++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 pkg/interface/webterm/InfoButton.tsx diff --git a/pkg/interface/webterm/App.tsx b/pkg/interface/webterm/App.tsx index 9ce3b0c00d..c8dee297cd 100644 --- a/pkg/interface/webterm/App.tsx +++ b/pkg/interface/webterm/App.tsx @@ -19,6 +19,7 @@ import { Tabs } from './Tabs'; import Buffer from './Buffer'; import { DEFAULT_SESSION } from './constants'; import { showSlog } from './lib/blit'; +import { InfoButton } from './InfoButton'; export default function TermApp() { const { names, selected } = useTermState(); @@ -78,7 +79,10 @@ export default function TermApp() { return ( <> - +
+ + +
{names.map((name) => { return ; diff --git a/pkg/interface/webterm/InfoButton.tsx b/pkg/interface/webterm/InfoButton.tsx new file mode 100644 index 0000000000..4ec3a3347e --- /dev/null +++ b/pkg/interface/webterm/InfoButton.tsx @@ -0,0 +1,19 @@ +import React, { useCallback } from 'react'; +import { Icon } from '@tlon/indigo-react'; + +export const InfoButton = () => { + const onInfoClick = useCallback(() => { + alert('To select text in the terminal, hold down the alt key.'); + }, []); + + return ( + <> + + + ); +}; diff --git a/pkg/interface/webterm/Tabs.tsx b/pkg/interface/webterm/Tabs.tsx index 09751537a9..3b0cd0a0f2 100644 --- a/pkg/interface/webterm/Tabs.tsx +++ b/pkg/interface/webterm/Tabs.tsx @@ -2,6 +2,7 @@ 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(); @@ -14,7 +15,12 @@ export const Tabs = () => { ); })} - +
); }; diff --git a/pkg/interface/webterm/index.html b/pkg/interface/webterm/index.html index e2f4a18c27..1ab8707324 100644 --- a/pkg/interface/webterm/index.html +++ b/pkg/interface/webterm/index.html @@ -32,7 +32,19 @@ height: calc(100% - 40px); } + 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; @@ -71,6 +83,15 @@ 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) { div.tabs { background-color: rgb(26, 26, 26); @@ -78,14 +99,25 @@ border-bottom-color: rgba(255, 255, 255, 0.9); } - div.tab, button.tab { + 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: black solid 1px; + } + + button.info-btn { + border-bottom: solid rgba(255, 255, 255, 0.9) 1px; + background: rgb(26, 26, 26); } } From c5b07f520379614e183285cd437763bf4146ab2a Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 1 Apr 2022 22:15:34 +0200 Subject: [PATCH 30/54] term: batch simple input events into a single %txt By accumulating %txt events until we reach a more complex event or reach the end of the input buffer, we can significantly reduce the "overhead" of pasting text into the terminal. Instead of an event for each character, we now inject up to a buffer's worth of characters (currently, 123 bytes) at a time. This makes pasting process much faster. Incidentally, the behavior for pasting text with syntax errors into the dojo may be a little bit surprising: after every buffer boundary, the dojo will complain about the syntax error, moving the cursor to its location, and causing the remainder of the text to be inserted in that position. This may result in garbled-looking input in some cases. This ux problem should be resolved on dojo's end, perhaps by highlighting syntax errors with color, instead of the cursor. Alleviates most of the need for #5687. --- pkg/urbit/include/vere/vere.h | 9 ++++--- pkg/urbit/vere/io/term.c | 47 +++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/pkg/urbit/include/vere/vere.h b/pkg/urbit/include/vere/vere.h index 023635f35c..3d7c0b0308 100644 --- a/pkg/urbit/include/vere/vere.h +++ b/pkg/urbit/include/vere/vere.h @@ -158,10 +158,11 @@ c3_y col_y; // column coordinate } esc; - struct { - c3_y syb_y[5]; // utf8 code buffer - c3_w len_w; // present length - c3_w wid_w; // total width + 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 { diff --git a/pkg/urbit/vere/io/term.c b/pkg/urbit/vere/io/term.c index 387e939ea8..f4039bf8b3 100644 --- a/pkg/urbit/vere/io/term.c +++ b/pkg/urbit/vere/io/term.c @@ -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 @@ -636,9 +637,28 @@ _term_io_belt(u3_utty* uty_u, u3_noun blb) } } -/* _term_io_suck_char(): process a single character. +/* _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) { u3_utat* tat_u = &uty_u->tat_u; @@ -652,10 +672,10 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y) _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; } @@ -667,13 +687,13 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y) // 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; @@ -698,7 +718,7 @@ _term_io_suck_char(u3_utty* uty_u, c3_y cay_y) c3_y row_y = cay_y - 32; // only acknowledge button 1 presses within our window if ( 1 != tat_u->esc.ton_y && row_y <= tat_u->siz.row_l ) { - _term_io_belt(uty_u, u3nt(c3__hit, + _term_io_spit(uty_u, u3nt(c3__hit, tat_u->esc.col_y - 1, tat_u->siz.row_l - row_y)); } @@ -720,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; @@ -799,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); } } } From 064b15e5a01e3f466f33bd8bc87a00a9d838834c Mon Sep 17 00:00:00 2001 From: fang Date: Sun, 3 Apr 2022 21:38:09 +0200 Subject: [PATCH 31/54] term: move coordinate origin to top left Having the origin at the top left instead of the bottom left is more conventional and ergonomic. The only thing this complicates is prompt-specific logic, where we care about the coordinates of the bottom-most line on the screen. For that reason, the bulk of the changes here are in vere, where we treat the bottom-most line specially, drawing the spinner onto it. Webterm is likewise updated to account for the new coordinate system. Drum now opts to accept clicks anywhere on the screen, and does its best to move the cursor as close to the clicked location as possible (within the confines of the prompt). --- pkg/arvo/lib/hood/drum.hoon | 3 +-- pkg/interface/webterm/Buffer.tsx | 2 +- pkg/interface/webterm/lib/blit.ts | 2 +- pkg/urbit/vere/io/term.c | 29 ++++++++++++++--------------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 39ceb90dbd..1914b69fa3 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -687,10 +687,9 @@ ++ ta-hit :: hear click |= [x=@ud y=@ud] ^+ +> - ?. =(0 y) +> =/ pol=@ud (lent-char:klr (make:klr cad.pom)) - ?: (lth x pol) +>.$ + =? x (lth x pol) pol +>.$(pos.inp (min (sub x pol) (lent buf.say.inp))) :: ++ ta-erl :: hear local error diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index 8d5ae2ce76..da4a8e8a6f 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -90,7 +90,7 @@ const readInput = (term: Terminal, e: string): Belt[] => { if (1 === m) { const c = e.charCodeAt(2) - 32; const r = e.charCodeAt(3) - 32; - belts.push({ hit: { y: term.rows - r, x: c - 1 } }); + belts.push({ hit: { y: r - 1, x: c - 1 } }); } e = e.slice(3); break; diff --git a/pkg/interface/webterm/lib/blit.ts b/pkg/interface/webterm/lib/blit.ts index e98f8fe750..a34e75900b 100644 --- a/pkg/interface/webterm/lib/blit.ts +++ b/pkg/interface/webterm/lib/blit.ts @@ -21,7 +21,7 @@ export const showBlit = (term: Terminal, blit: Blit) => { if (typeof blit.hop === 'number') { out += csi('H', term.rows, blit.hop + 1); } else { - out += csi('H', term.rows - blit.hop.y, blit.hop.x + 1); + out += csi('H', blit.hop.y + 1, blit.hop.x + 1); } out += csi('s'); // save cursor position } else if ('put' in blit) { diff --git a/pkg/urbit/vere/io/term.c b/pkg/urbit/vere/io/term.c index f4039bf8b3..a653a6bcb0 100644 --- a/pkg/urbit/vere/io/term.c +++ b/pkg/urbit/vere/io/term.c @@ -385,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); } } @@ -401,7 +401,7 @@ _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. */ @@ -413,7 +413,7 @@ _term_it_move_cursor(u3_utty* uty_u, c3_w col_w, c3_w row_w) 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; @@ -473,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 @@ -490,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); } @@ -513,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 @@ -716,11 +717,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_spit(uty_u, u3nt(c3__hit, - tat_u->esc.col_y - 1, - tat_u->siz.row_l - row_y)); + _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; @@ -907,8 +906,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; @@ -1357,7 +1356,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, pos, 0); + _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)); @@ -1369,7 +1368,7 @@ _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: { From d3f9d7621799a57844469d097dfed2941bd0bd62 Mon Sep 17 00:00:00 2001 From: fang Date: Sun, 3 Apr 2022 21:44:05 +0200 Subject: [PATCH 32/54] term: move cursor to end before exit When urbit exits, the host shell it was running in takes over, often re-prints its own prompt. Here, we move the cursor to the bottom of the screen right before exiting, so that any subsequent output doesn't destroy whatever we had on-screen when we closed. --- pkg/urbit/vere/io/term.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/urbit/vere/io/term.c b/pkg/urbit/vere/io/term.c index a653a6bcb0..7b3f7720e2 100644 --- a/pkg/urbit/vere/io/term.c +++ b/pkg/urbit/vere/io/term.c @@ -1762,6 +1762,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)); From 50d120e3c166410f9d1646b17a537b8e93653f18 Mon Sep 17 00:00:00 2001 From: tomholford Date: Mon, 11 Apr 2022 12:05:25 -0700 Subject: [PATCH 33/54] devex: address PR feedback - move App#initSessions definition outside function component closure - enhance useAddSessions performance --- pkg/interface/webterm/App.tsx | 16 ++++++++-------- pkg/interface/webterm/Tabs.tsx | 2 +- pkg/interface/webterm/lib/useAddSession.ts | 4 +--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pkg/interface/webterm/App.tsx b/pkg/interface/webterm/App.tsx index c8dee297cd..211066b74e 100644 --- a/pkg/interface/webterm/App.tsx +++ b/pkg/interface/webterm/App.tsx @@ -21,18 +21,18 @@ 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 initSessions = useCallback(async () => { - const response = await api.scry(scrySessions()); - - useTermState.getState().set((state) => { - state.names = response.sort(); - }); - }, []); - const setupSlog = useCallback(() => { console.log('slog: setting up...'); let available = false; diff --git a/pkg/interface/webterm/Tabs.tsx b/pkg/interface/webterm/Tabs.tsx index 3b0cd0a0f2..ed583635a3 100644 --- a/pkg/interface/webterm/Tabs.tsx +++ b/pkg/interface/webterm/Tabs.tsx @@ -6,7 +6,7 @@ import { Icon } from '@tlon/indigo-react'; export const Tabs = () => { const { sessions, names } = useTermState(); - const { addSession } = useAddSession(); + const addSession = useAddSession(); return (
diff --git a/pkg/interface/webterm/lib/useAddSession.ts b/pkg/interface/webterm/lib/useAddSession.ts index 25a5fc0c29..c5556c14b3 100644 --- a/pkg/interface/webterm/lib/useAddSession.ts +++ b/pkg/interface/webterm/lib/useAddSession.ts @@ -62,7 +62,5 @@ export const useAddSession = () => { } }, [names]); - return { - addSession - }; + return addSession; }; From 349033fb121aa39b77e8846b974f3a64115f6192 Mon Sep 17 00:00:00 2001 From: tomholford Date: Wed, 13 Apr 2022 08:47:30 -0700 Subject: [PATCH 34/54] ux: detect OS for hotkey instructions Also, ensure changes from this PR are included in the session branch: https://github.com/urbit/urbit/pull/5529 --- pkg/interface/webterm/Buffer.tsx | 4 ++- pkg/interface/webterm/InfoButton.tsx | 9 +++-- pkg/interface/webterm/lib/theme.ts | 3 +- pkg/interface/webterm/lib/useDetectOS.ts | 44 ++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 pkg/interface/webterm/lib/useDetectOS.ts diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index da4a8e8a6f..e0ce5e1435 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -33,7 +33,9 @@ const termConfig: ITerminalOptions = { bellSound: bel, // // allows text selection by holding modifier (option, or shift) - macOptionClickForcesSelection: true + macOptionClickForcesSelection: true, + // prevent insertion of simulated arrow keys on-altclick + altClickMovesCursor: false }; const readInput = (term: Terminal, e: string): Belt[] => { diff --git a/pkg/interface/webterm/InfoButton.tsx b/pkg/interface/webterm/InfoButton.tsx index 4ec3a3347e..b0ba22953e 100644 --- a/pkg/interface/webterm/InfoButton.tsx +++ b/pkg/interface/webterm/InfoButton.tsx @@ -1,10 +1,15 @@ 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(() => { - alert('To select text in the terminal, hold down the alt key.'); - }, []); + const key = isMacOS ? 'alt' : 'shift'; + + alert(`To select text in the terminal, hold down the ${key} key.`); + }, [isMacOS]); return ( <> diff --git a/pkg/interface/webterm/lib/theme.ts b/pkg/interface/webterm/lib/theme.ts index 6044f1c30a..9b97806d42 100644 --- a/pkg/interface/webterm/lib/theme.ts +++ b/pkg/interface/webterm/lib/theme.ts @@ -16,6 +16,7 @@ export const makeTheme = (dark: boolean): ITheme => { foreground: fg, background: bg, brightBlack: '#7f7f7f', // NOTE slogs - cursor: fg + cursor: fg, + selection: fg }; }; diff --git a/pkg/interface/webterm/lib/useDetectOS.ts b/pkg/interface/webterm/lib/useDetectOS.ts new file mode 100644 index 0000000000..7b8f915f9f --- /dev/null +++ b/pkg/interface/webterm/lib/useDetectOS.ts @@ -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 + }; +}; From 37ce741a77898d0bdd99fff497699e622bea6146 Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 14 Apr 2022 04:52:43 -0700 Subject: [PATCH 35/54] deps: replace deprecated xterm#setOption See: https://github.com/xtermjs/xterm.js/blob/4.15.0/typings/xterm.d.ts#L1053 --- pkg/interface/webterm/Buffer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index e0ce5e1435..f18f0ab6e0 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -155,7 +155,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { // set up xterm terminal // const term = new Terminal(termConfig); - term.setOption('theme', makeTheme(dark)); + term.options.theme = makeTheme(dark); const fit = new FitAddon(); term.loadAddon(fit); fit.fit(); @@ -252,7 +252,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { useEffect(() => { const theme = makeTheme(dark); if (session) { - session.term.setOption('theme', theme); + session.term.options.theme = theme; } if (container.current) { container.current.style.backgroundColor = theme.background || ''; From dfded5e592a7e4f27272733c455896f4709fecba Mon Sep 17 00:00:00 2001 From: tomholford Date: Thu, 14 Apr 2022 07:13:09 -0700 Subject: [PATCH 36/54] ux: refactor resize behavior - debounced resize event listener - new Buffer#onSelect: resize, focus, and pokes `herm` with updated rows / cols - simplify container ref implementation (no need for a callback ref), remove isOpen hack - add lodash for debounce - Tab#onClick no longer handles focus (it's now handled by Buffer#onSelect) --- pkg/interface/webterm/Buffer.tsx | 87 +++++++++++++------------ pkg/interface/webterm/Tab.tsx | 1 - pkg/interface/webterm/constants.ts | 1 + pkg/interface/webterm/package-lock.json | 1 + pkg/interface/webterm/package.json | 1 + 5 files changed, 50 insertions(+), 41 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index f18f0ab6e0..4fb21b7757 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -1,5 +1,6 @@ import { Terminal, ITerminalOptions } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; +import { debounce } from 'lodash'; import bel from './lib/bel'; import api from './api'; @@ -13,7 +14,7 @@ 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 } from './constants'; +import { DEFAULT_SESSION, RESIZE_DEBOUNCE_MS } from './constants'; import { retry } from './lib/retry'; const termConfig: ITerminalOptions = { @@ -120,11 +121,10 @@ const readInput = (term: Terminal, e: string): Belt[] => { return belts; }; -const onResize = (session: Session) => () => { - //TODO debounce, if it ever becomes a problem - //TODO test that we only send this to the selected session, - // and that we *do* send it on-selected-change if necessary. - session?.fit.fit(); +const onResize = async (name: string, session: Session) => { + if (session) { + session.fit.fit(); + } }; const onInput = (name: string, session: Session, e: string) => { @@ -145,7 +145,7 @@ interface BufferProps { } export default function Buffer({ name, selected, dark }: BufferProps) { - const container = useRef(null); + const containerRef = useRef(null); const session: Session = useTermState(s => s.sessions[name]); @@ -165,7 +165,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) { // term.write(csi('?9h')); - const ses: Session = { term, fit, hasBell: false, subscriptionId: null }; + const ses: Session = { term, fit, hasBell: false, subscriptionId: null }; // set up event handlers // @@ -218,6 +218,15 @@ export default function Buffer({ name, selected, dark }: BufferProps) { }); }, []); + const onSelect = useCallback(async () => { + if (session && selected) { + session.fit.fit(); + session.term.focus(); + await api.poke(pokeTask(name, { blew: { w: session.term.cols, h: session.term.rows } })); + } + }, [session, selected]); + + // Effects // init session useEffect(() => { if(session) { @@ -227,25 +236,27 @@ export default function Buffer({ name, selected, dark }: BufferProps) { initSession(name, dark); }, [name]); - // on selected change, maybe setup the term, or put it into the container - // - const setContainer = useCallback((containerRef: HTMLDivElement | null) => { - const newContainer = containerRef || container.current; - if(session && newContainer) { - container.current = newContainer; + // attach to DOM when ref is available + useEffect(() => { + if(session && containerRef.current && !session.term.element) { + session.term.open(containerRef.current); } - }, [session]); + }, [session, containerRef]); - // on-init, open slogstream and fetch existing sessions + // on-init, open slogstream and fetch existing sessions // useEffect(() => { - window.addEventListener('resize', onResize(session)); + if(!session) { + return; + } + + const debouncedResize = debounce(() => onResize(name, session), RESIZE_DEBOUNCE_MS); + window.addEventListener('resize', debouncedResize); return () => { - // TODO clean up subs? - window.removeEventListener('resize', onResize(session)); + window.removeEventListener('resize', debouncedResize); }; - }, []); + }, [session]); // on dark mode change, change terminals' theme // @@ -254,24 +265,20 @@ export default function Buffer({ name, selected, dark }: BufferProps) { if (session) { session.term.options.theme = theme; } - if (container.current) { - container.current.style.backgroundColor = theme.background || ''; + if (containerRef.current) { + containerRef.current.style.backgroundColor = theme.background || ''; } }, [session, dark]); + // On select, resize, focus, and poke herm with updated cols and rows useEffect(() => { - if (session && selected && !session.term.isOpen) { - session.term.open(container.current); - session.fit.fit(); - session.term.focus(); - session.term.isOpen = true; - } - }, [selected, session]); + onSelect(); + }, [onSelect]); return ( !session && !selected ?

Loading...

- : + : - - - + + + ); } diff --git a/pkg/interface/webterm/Tab.tsx b/pkg/interface/webterm/Tab.tsx index 209d408e53..6f15e9f59d 100644 --- a/pkg/interface/webterm/Tab.tsx +++ b/pkg/interface/webterm/Tab.tsx @@ -17,7 +17,6 @@ export const Tab = ( { session, name }: TabProps ) => { state.selected = name; state.sessions[name].hasBell = false; }); - useTermState.getState().sessions[name]?.term?.focus(); }; const onDelete = useCallback(async (e) => { diff --git a/pkg/interface/webterm/constants.ts b/pkg/interface/webterm/constants.ts index 66d4b0eccf..549f4bd94d 100644 --- a/pkg/interface/webterm/constants.ts +++ b/pkg/interface/webterm/constants.ts @@ -1,5 +1,6 @@ export const DEFAULT_SESSION = ''; export const DEFAULT_HANDLER = 'hood'; +export const RESIZE_DEBOUNCE_MS = 100; /** * Session ID validity: diff --git a/pkg/interface/webterm/package-lock.json b/pkg/interface/webterm/package-lock.json index 902930d988..4e617d5f30 100644 --- a/pkg/interface/webterm/package-lock.json +++ b/pkg/interface/webterm/package-lock.json @@ -15,6 +15,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", diff --git a/pkg/interface/webterm/package.json b/pkg/interface/webterm/package.json index 856b29eede..0e58cc6080 100644 --- a/pkg/interface/webterm/package.json +++ b/pkg/interface/webterm/package.json @@ -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", From 65f9f904c73becdc0e7c656b8a5c217f0a6a5c6c Mon Sep 17 00:00:00 2001 From: fang Date: Fri, 15 Apr 2022 18:02:42 +0200 Subject: [PATCH 37/54] zuse: rewrite klr:format's +scag and +slag The previous implementation was counting the full length of the stub unnecessarily. Doing a single "dumb" traversal is ~40% faster. --- pkg/arvo/sys/zuse.hoon | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/pkg/arvo/sys/zuse.hoon b/pkg/arvo/sys/zuse.hoon index 1d465267ef..a1895c0eed 100644 --- a/pkg/arvo/sys/zuse.hoon +++ b/pkg/arvo/sys/zuse.hoon @@ -3788,29 +3788,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] From 2d3e803704af38e761f014ff612d8097494ee2f8 Mon Sep 17 00:00:00 2001 From: tomholford Date: Tue, 19 Apr 2022 03:41:40 -0700 Subject: [PATCH 38/54] devex: improved resize behavior - only resize when necessary (check the container's height) - refactor CSS: use position relative / absolute to stack Buffers instead of display:none; this affects the calcuations used by fit() - fix dark mode styles, tweak viewport height (100vh --> 99vh) to prevent overflow scroller --- pkg/interface/webterm/Buffer.tsx | 27 +++++++++++++++++++-------- pkg/interface/webterm/constants.ts | 1 + pkg/interface/webterm/index.html | 14 ++++++++++++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/pkg/interface/webterm/Buffer.tsx b/pkg/interface/webterm/Buffer.tsx index 4fb21b7757..3e1be2bb34 100644 --- a/pkg/interface/webterm/Buffer.tsx +++ b/pkg/interface/webterm/Buffer.tsx @@ -14,7 +14,7 @@ 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 } from './constants'; +import { DEFAULT_SESSION, RESIZE_DEBOUNCE_MS, RESIZE_THRESHOLD_PX } from './constants'; import { retry } from './lib/retry'; const termConfig: ITerminalOptions = { @@ -124,6 +124,7 @@ const readInput = (term: Terminal, e: string): Belt[] => { const onResize = async (name: string, session: Session) => { if (session) { session.fit.fit(); + api.poke(pokeTask(name, { blew: { w: session.term.cols, h: session.term.rows } })); } }; @@ -171,9 +172,6 @@ export default function Buffer({ name, selected, dark }: BufferProps) { // term.onData(e => onInput(name, ses, e)); term.onBinary(e => onInput(name, ses, e)); - term.onResize((e) => { - api.poke(pokeTask(name, { blew: { w: e.cols, h: e.rows } })); - }); // open subscription // @@ -218,11 +216,22 @@ export default function Buffer({ name, selected, dark }: BufferProps) { }); }, []); + 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) { + if (session && selected && shouldResize()) { session.fit.fit(); - session.term.focus(); await api.poke(pokeTask(name, { blew: { w: session.term.cols, h: session.term.rows } })); + session.term.focus(); } }, [session, selected]); @@ -243,13 +252,14 @@ export default function Buffer({ name, selected, dark }: BufferProps) { } }, [session, containerRef]); - // on-init, open slogstream and fetch existing sessions + // 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); @@ -285,7 +295,8 @@ export default function Buffer({ name, selected, dark }: BufferProps) { bg='white' fontFamily='mono' overflow='hidden' - style={selected ? {} : { display: 'none' }} + className="terminal-container" + style={selected ? { zIndex: 999 } : {}} >