herm: userspace dill proxy

Listens to the default dill session and passes its %blits on to
subscribers. Passes any %belt pokes it gets into dill.

Updates webdojo to make use of it, which is the primary motivation for
herm's existence.
This commit is contained in:
fang 2020-10-24 01:25:44 +02:00
parent 83d46dae88
commit 185b553c99
No known key found for this signature in database
GPG Key ID: EB035760C1BBA972
10 changed files with 303 additions and 140 deletions

61
pkg/arvo/app/herm.hoon Normal file
View File

@ -0,0 +1,61 @@
:: herm: stand-in for term.c with http interface
::
/+ default-agent, dbug, verb
=, able:jael
|%
+$ state-0 [%0 ~]
--
::
=| state-0
=* state -
%+ verb |
%- agent:dbug
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card:agent:gall _this)
:_ this
[%pass /herm/1 %arvo %d %view [//term/1]~]~
::
++ on-save !>([%0 ~])
++ on-load
|= old=vase
^- (quip card:agent:gall _this)
[~ this(state [%0 ~])]
::
++ on-watch
|= =path
^- (quip card:agent:gall _this)
?. ?=([%herm ~] path) !!
[~ this]
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card:agent:gall _this)
?. =(/herm/1 wire) !!
?. ?=([%d %blit *] sign-arvo)
~| [%unexpected-sign [- +<]:sign-arvo]
!!
:: ~& [dap.bowl %blit (turn p.sign-arvo head)]
:_ this
%+ turn p.sign-arvo
|= =blit:dill
[%give %fact [/herm]~ %blit !>(blit)]
::
++ on-poke
|= [=mark =vase]
^- (quip card:agent:gall _this)
?. ?=(%belt mark)
~| [%unexpected-mark mark]
!!
:_ this
[%pass /herm/1 %arvo %d %belt !<(belt:dill vase)]~
::
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--

32
pkg/arvo/mar/belt.hoon Normal file
View File

@ -0,0 +1,32 @@
:: belt: runtime belt structure
::
~% %mar-belt ..is ~
|_ =belt:dill
++ grad %noun
:: +grab: convert from
::
++ grab
~% %belt-grab ..grab ~
|%
++ noun belt:dill
++ json
~/ %mar-belt-json
^- $-(^json belt:dill)
=, dejs:format
%- of
:~ aro+(su (perk %d %l %r %u ~))
bac+ul
ctl+`$-(json @c)`so
del+ul
met+`$-(json @c)`so
ret+ul
txt+`$-(json (list @c))`sa
==
--
:: +grow: convert to
::
++ grow
|%
++ noun belt
--
--

48
pkg/arvo/mar/blit.hoon Normal file
View File

@ -0,0 +1,48 @@
:: blit: runtime blit structure
::
~% %mar-blit ..is ~
|_ =blit:dill
++ grad %noun
:: +grab: convert from
::
++ grab
|%
++ noun blit:dill
--
:: +grow: convert to
::
++ grow
~% %blit-grow ..grow ~
|%
++ noun blit
++ json
^- ^json
=, enjs:format
%+ frond -.blit
?- -.blit
%bel b+&
%clr b+&
%hop (numb p.blit)
%lin s+(crip (tufa p.blit))
%mor b+&
%sag (pairs 'path'^(path p.blit) 'file'^s+(jam q.blit) ~)
%sav (pairs 'path'^(path p.blit) 'file'^s+q.blit ~)
%url s+p.blit
::
%klr
:- %a
%+ turn p.blit
|= [=stye text=(list @c)]
%- pairs
:~ 'text'^s+(crip (tufa text))
::
:- 'stye'
%- pairs
:~ 'back'^[?~(. ~ s+.)]:p.q.stye
'fore'^[?~(. ~ s+.)]:q.q.stye
'deco'^a+(turn ~(tap in p.stye) |=(d=deco ?~(d ~ s+d)))
==
==
==
--
--

View File

@ -29,8 +29,8 @@ export default class Api {
});
}
soto(data) {
return this.action('dojo', 'sole-action', { id: this.dojoId, dat: data });
belt(belt) {
return this.action('herm', 'belt', belt);
}
action(appl, mark, data) {

View File

@ -72,14 +72,13 @@ export default class DojoApp extends Component {
cursor: 'text'
}}
>
<History commandLog={this.state.txt} />
<History log={this.state.lines.slice(0, -1)} />
<Input
ship={this.props.ship}
cursor={this.state.cursor}
prompt={this.state.prompt}
input={this.state.input}
api={this.api}
store={this.store}
line={this.state.lines.slice(-1)[0]}
/>
</div>
</div>

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react';
import Line from './line';
export class History extends Component {
constructor(props) {
super(props);
@ -12,14 +14,8 @@ export class History extends Component {
style={{ resize: 'none' }}
>
<div style={{ marginTop: 'auto' }}>
{this.props.commandLog.map((text, index) => {
return (
<p className="mono" key={index}
style={{ overflowWrap: 'break-word', whiteSpace: 'pre' }}
>
{text}
</p>
);
{this.props.log.map((line, i) => {
return <Line key={i} index={i} line={line} />;
})}
</div>
</div>

View File

@ -10,6 +10,7 @@ export class Input extends Component {
type: 'Sending to Dojo'
};
this.keyPress = this.keyPress.bind(this);
this.click = this.click.bind(this);
this.inputRef = React.createRef();
}
@ -24,64 +25,70 @@ export class Input extends Component {
}
keyPress(e) {
let key = e.key;
// let paste event pass
if ((e.getModifierState('Control') || event.getModifierState('Meta'))
&& e.key === 'v') {
&& e.key === 'v') {
return;
}
let belt = null;
if (key === 'ArrowLeft') belt = {aro: 'l'};
else if (key === 'ArrowRight') belt = {aro: 'r'};
else if (key === 'ArrowUp') belt = {aro: 'u'};
else if (key === 'ArrowDown') belt = {aro: 'd'};
else if (key === 'Backspace') belt = {bac: null};
else if (key === 'Delete') belt = {del: null};
else if (key === 'Tab') belt = {ctl: 'i'};
else if (key === 'Enter') belt = {ret: null};
else if (key.length === 1) belt = {txt: key};
else belt = null;
if (belt && e.getModifierState('Control')) {
if (belt.txt !== undefined) belt = {ctl: belt.txt};
} else
if (belt &&
(e.getModifierState('Meta') || e.getModifierState('Alt'))) {
if (belt.bac !== undefined) belt = {met: 'bac'};
}
if (belt !== null) {
this.props.api.belt(belt);
}
//TODO handle paste
e.preventDefault();
const allowedKeys = [
'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab'
];
}
if ((e.key.length > 1) && (!(allowedKeys.includes(e.key)))) {
return;
}
paste(e) {
const clipboardData = e.clipboardData || window.clipboardData;
this.props.api.belt({ txt: clipboardData.getData('Text') });
e.preventDefault();
}
// submit on enter
if (e.key === 'Enter') {
this.setState({ awaiting: true, type: 'Sending to Dojo' });
this.props.api.soto('ret').then(() => {
this.setState({ awaiting: false });
});
} else if ((e.key === 'Backspace') && (this.props.cursor > 0)) {
this.props.store.doEdit({ del: this.props.cursor - 1 });
return this.props.store.setState({ cursor: this.props.cursor - 1 });
} else if (e.key === 'Backspace') {
return;
} else if (e.key.startsWith('Arrow')) {
if (e.key === 'ArrowLeft') {
if (this.props.cursor > 0) {
this.props.store.setState({ cursor: this.props.cursor - 1 });
}
} else if (e.key === 'ArrowRight') {
if (this.props.cursor < this.props.input.length) {
this.props.store.setState({ cursor: this.props.cursor + 1 });
}
}
}
// tab completion
else if (e.key === 'Tab') {
this.setState({ awaiting: true, type: 'Getting suggestions' });
this.props.api.soto({ tab: this.props.cursor }).then(() => {
this.setState({ awaiting: false });
});
}
// capture and transmit most characters
else {
this.props.store.doEdit({ ins: { cha: e.key, at: this.props.cursor } });
this.props.store.setState({ cursor: this.props.cursor + 1 });
}
click(e) {
// prevent desynced cursor movement
e.preventDefault();
e.target.setSelectionRange(this.props.cursor, this.props.cursor);
}
render() {
const line = this.props.line;
let prompt = 'connecting...';
if (line) {
if (line.lin) {
prompt = line.lin;
}
//TODO render prompt style
else if (line.klr) {
prompt = line.klr.reduce((l, p) => (l + p), '');
}
}
return (
<div className="flex flex-row flex-grow-1 relative">
<div className="flex-shrink-0"><span class="dn-s">{cite(this.props.ship)}:</span>dojo
</div>
<div className="flex-shrink-0"></div>
<span id="prompt">
{this.props.prompt}
</span>
@ -95,22 +102,13 @@ export class Input extends Component {
className="mono ml1 flex-auto dib w-100"
id="dojo"
cursor={this.props.cursor}
onClick={e => this.props.store.setState({ cursor: e.target.selectionEnd })}
onKeyDown={this.keyPress}
onPaste={(e) => {
const clipboardData = e.clipboardData || window.clipboardData;
const paste = Array.from(clipboardData.getData('Text'));
paste.reduce(async (previous, next) => {
await previous;
this.setState({ cursor: this.props.cursor + 1 });
return this.props.store.doEdit({ ins: { cha: next, at: this.props.cursor } });
}, Promise.resolve());
e.preventDefault();
}}
onClick={this.click}
onPaste={this.paste}
ref={this.inputRef}
defaultValue={this.props.input}
defaultValue="connecting..."
value={prompt}
/>
<Spinner awaiting={this.state.awaiting} text={`${this.state.type}...`} classes="absolute right-0 bottom-0 inter pa ba pa2 b--gray1-d" />
</div>
);
}

View File

@ -0,0 +1,66 @@
import React, { Component, useMemo } from 'react';
import { Box, Text } from '@tlon/indigo-react';
export default function Line({index, line}) {
const out = useMemo(() => {
let text = '';
if (line.lin) {
text = line.lin;
}
else if (line.klr) {
text = line.klr.map((part, i) => {
let prop = part.stye.deco.reduce((prop, deco) => {
switch (deco) {
case null: return prop;
case 'br': return {bold: true, ...prop};
case 'bl': return {blink: true, ...prop}; //TODO
case 'un': return {textDecoration: 'underline', ...prop}; //TODO fixme
default: console.log('weird deco', deco); return prop;
}
}, {});
switch (part.stye.fore) {
case null: break;
case 'r': prop.color = 'red'; break;
case 'g': prop.color = 'green'; break;
case 'b': prop.color = 'blue'; break;
case 'c': prop.color = 'cyan'; break;
case 'm': prop.color = 'purple'; break;
case 'y': prop.color = 'yellow'; break;
case 'k': prop.color = 'black'; break;
case 'w': prop.color = 'white'; break;
default: console.log('weird fore', part.stye.fore);
}
switch (part.stye.back) {
case null: break;
case 'r': prop.backgroundColor = 'red'; break;
case 'g': prop.backgroundColor = 'green'; break;
case 'b': prop.backgroundColor = 'blue'; break;
case 'c': prop.backgroundColor = 'cyan'; break;
case 'm': prop.backgroundColor = 'purple'; break;
case 'y': prop.backgroundColor = 'yellow'; break;
case 'k': prop.backgroundColor = 'black'; break;
case 'w': prop.backgroundColor = 'white'; break;
default: console.log('weird back', part.stye.back);
}
if (Object.keys(prop).length === 0)
{
return part.text;
} else {
return (<Text mono fontSize='inherit' key={index+'-'+i} {...prop}>{part.text}</Text>);
}
});
}
// render line
//
return (
<Text mono display='block' fontSize='14px'
style={{ overflowWrap: 'break-word', whiteSpace: 'pre' }}
>
{text}
</Text>
);
}, [line, index]);
return out;
}

View File

@ -2,92 +2,55 @@ import Share from './components/lib/sole';
export default class Store {
constructor() {
this.state = this.initialState();
this.sync = this.sync.bind(this);
this.print = this.print.bind(this);
this.buffer = new Share();
}
initialState() {
return {
txt: [],
prompt: '',
lines: [''],
cursor: 0,
input: ''
};
}
clear() {
this.handleEvent({
data: { clear: true }
});
this.setState(this.initialState())
}
handleEvent(data) {
// recursive handler
if (data.data) {
var dojoReply = data.data;
} else {
var dojoReply = data;
}
const blit = data.data;
if (dojoReply.clear) {
this.setState(this.initialState(), (() => {
return;
}));
}
// %mor sole-effects are nested, so throw back to handler
if (dojoReply.map) {
return dojoReply.map(reply => this.handleEvent(reply));
}
switch (Object.keys(dojoReply)[0]) {
case 'txt':
return this.print(dojoReply.txt);
case 'tab':
this.print(dojoReply.tab.match + ' ' + dojoReply.tab.info);
return;
case 'tan':
return dojoReply.tan.split('\n').map(this.print);
case 'pro':
return this.setState({ prompt: dojoReply.pro.cad });
case 'hop':
return this.setState({ cursor: dojoReply.hop });
case 'det':
this.buffer.receive(dojoReply.det);
return this.sync(dojoReply.det.ted);
case 'act':
switch (dojoReply.act) {
case 'clr': return this.setState({ txt: [] });
case 'nex': return this.setState({
input: '',
cursor: 0
});
}
switch (Object.keys(blit)[0]) {
case 'bel':
let beep = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
beep.play();
break;
default: console.log(dojoReply);
case 'clr':
this.setState({ lines: [''] });
break;
case 'hop':
this.setState({ cursor: blit.hop });
break;
case 'lin':
this.state.lines[this.state.lines.length - 1] = blit;
this.setState({ lines: this.state.lines });
break;
case 'klr':
this.state.lines[this.state.lines.length - 1] = blit;
this.setState({ lines: this.state.lines });
break;
case 'mor':
this.state.lines.push('');
this.setState({ lines: this.state.lines });
break;
//TODO file downloads for %sag and %sav
case 'url':
//TODO too invasive? just print as <a>?
window.open(blit.url);
break;
default: console.log('weird blit', blit);
}
}
doEdit(ted) {
const detSend = this.buffer.transmit(ted);
this.sync(ted);
return this.api.soto({ det: detSend });
}
print(txt) {
const textLog = this.state.txt;
textLog.push(txt);
return this.setState({ txt: textLog });
}
sync(ted) {
return this.setState({
input: this.buffer.buf,
cursor: this.buffer.transpose(ted, this.state.cursor)
});
}
setStateHandler(setState) {
this.setState = setState;
}

View File

@ -72,7 +72,7 @@ export default class Subscription {
}
firstRound() {
this.subscribe('/sole/' + this.api.dojoId, 'dojo');
this.subscribe('/herm', 'herm');
}
handleEvent(diff) {