#!/bin/sh
stty cbreak
cat >/tmp/_roo.forth << 'EOF'

# Roo: A Block Editor for RETRO

This implements a block editor for RETRO. It provides a visual interface,
inspired by VIBE and REM and allows for extending the environment by
simply writing new words that handle specific keys. It also requires a
network connection and the *Tuporo* Gopher server which provides the
actual block store.

Some architectural notes:

- the blocks are stored on a server, not locally
- editing is modal (ala *vi*)
- editor commands are just normal words in the dictionary
- this uses ANSI escape sequences and so requires a traditional terminal
  or terminal emulator
- dvorak based key bindings


## Configuration

So getting started, some configuration settings for the server side:

~~~
:SERVER (-sn)  'forthworks.com #8008 ;
~~~

`SERVER` returns the server url and port.

Next, create a buffer to store the currently loaded block. With the
server-side storage I don't need to keep more than the current block
in memory.

A block is 1024 bytes; this includes one additional to use a terminator.
Doing this allows the block to be passed to `s:evaluate` as a string.

~~~
'Block d:create
  #1025 allot
~~~

I also define a variable, `Current-Block`, which holds the number of
the currently loaded block.

~~~
'Current-Block var
~~~


## Server Communication

With that done, it's now time for a word to load a block from the
server.

Roo requires an associated gopher server (tuporo). This is a special
server that provides access to Forth blocks across a network. The
selectors we are interested in are:

    /r/<block#>

Which returns a raw block (1024 bytes), and:

    /s/<block#>/text

Which copies the text into the specified block.

So first, define words to construct the selectors:

~~~
:selector<get>  (-s)  @Current-Block '/r/%n s:format ;
:selector<set>  (-s)  &Block @Current-Block '/s/%n/%s s:format ;
~~~

And then words to actually talk to the server:

~~~
:load-block     (-)   &Block SERVER selector<get> gopher:get drop ;
:save-block     (-)   here   SERVER selector<set> gopher:get drop ;
~~~

All done :)


## Modes

The `Mode` variable will be used to track the current mode. I have
chosen to implement two modes: command ($C) and insert ($I).

Command mode will be used for all non-entry related options, including
(but not limited to) cursor movement, block navigation, and code
evaluation.

So with two modes I only need one variable to track which mode is
active, and a single word to switch back and forth between them.

~~~
$C 'Mode var<n>
:toggle-mode (-)  @Mode $C eq? [ $I ] [ $C ] choose !Mode ;
~~~


## Cursor & Positioning

I need a way to keep track of where in the block the user currently is.
So two variables: one for the row and one for the column:

~~~
'Cursor-Row var
'Cursor-Col var
~~~

To ensure that the cursor stays within the block, I am implementing a
`constrain` word to limit the range of the cursor. Thanks to `v:limit`
this is really easy.

~~~
:constrain (-)
  &Cursor-Row #0 #15 v:limit
  &Cursor-Col #0 #63 v:limit ;
~~~

And then the words to adjust the cursor positioning:

~~~
:cursor-left   (-)  &Cursor-Col v:dec constrain ;
:cursor-right  (-)  &Cursor-Col v:inc constrain ;
:cursor-up     (-)  &Cursor-Row v:dec constrain ;
:cursor-down   (-)  &Cursor-Row v:inc constrain ;
~~~

The other bit related to the cursor is a word to decide the offset into
the block. This will be used to aid in entering text.

~~~
:cursor-position  (-n)  @Cursor-Row #64 * @Cursor-Col + ;
~~~

The last bit here is `insert-character` which inserts a character to
`cursor-position` in the `Block` and moves the cursor to the right.

~~~
:insert-character (c-) cursor-position &Block + store cursor-right ;
~~~


## Keyboard Handling

Handling of keys is essential to using Roo. I chose to use a method that
I borrowed from Sam Falvo II's VIBE editor and leverage the dictionary
for key handlers.

In Roo a key handler is a word in the `roo:` namespace. A word like:

    roo:c:a

Will implement a handler called when 'a' is typed in command mode. And

    roo:i:`

Would implement a handler for the '`' key in insert mode.

In command mode keys not matching a handler are ignored. For words that
do match up to a control word, the word will be called. In insert mode,
any keys not mapped to a word will be inserted into the block at the
current position.

My default keymap will be (subject to change!):

    `    Switch modes
    h    Cursor left
    t    Cursor down
    n    Cursor up
    s    Cursor right
    H    Previous block
    S    Next block
    e    Evaluate block
    q    Quit

Getting started, I define a word to take a character and pack it into a
string. It then tries to find this in the dictionary.

~~~
:handler-for (c-d)
  @Mode $C eq? [ 'roo:c:_ ]
               [ 'roo:i:_ ] choose [ #6 + store ] sip d:lookup ;
~~~

With that, I can implement another helper: `call<dt>`, which will take
the dictionary token returned by `handler-for` and call the xt for the
word.

~~~
:call<dt>  (d-)  d:xt fetch call ;
~~~

The final piece is the top level key handler. This has the following
jobs:

- try to find a handler for the key
  - if mode is $C and the handler is valid, call the handler
  - if mode is $I and the handler is invalid, insert the key into the
    block
  - if mode is $I and the handler is valid, call the handler

~~~
:handle-key (c-)
  dup handler-for
  @Mode $I -eq? [ nip 0; call<dt> ]
                [ dup n:zero? [ drop insert-character ]
                              [ nip call<dt>          ] choose ] choose ;
~~~

Having finished this, it's trivial to define the majority of the basic
commands:

~~~
:roo:c:H &Current-Block v:dec load-block ;
:roo:c:S &Current-Block v:inc load-block ;
:roo:c:h cursor-left ;
:roo:c:t cursor-down ;
:roo:c:n cursor-up ;
:roo:c:s cursor-right ;
:roo:c:e &Block s:evaluate ;
:roo:c:` toggle-mode ;
~~~

I only define one command in input mode, to switch back to command mode:

~~~
:roo:i:` toggle-mode save-block ;
~~~

Note that this calls `save-block` to update the remote block storage.
This is the only place I call `save-block`.

One last word is a handler to allow the editor to be closed cleanly.
This also has a variable, `Completed`, which will be used to decide
if editing is finished.

~~~
'Completed var
:roo:c:q &Completed v:on ;
~~~


## Display

The block display is kept minimalistic. Each line is bounded by a single
vertical bar (|) on the right edge, and there is a separatator line at
the bottom to indicate the base of the block. To the left of this is a
single number, indicating the current block number. This is followed by
the mode indicator.

I also display the current stack contents below the block.

The display looks like:

    (blank)                                                         |
    :roo:c:+ (nn-m) + ;                                             |
    :roo:c:1 (-n)  #1 ;    :roo:c:2 (-n)  #2 ;                      |
    :roo:c:4 (-n)  #4 ;    :roo:c:3 (-n)  #3 ;                      |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
                                                                    |
    ----------------------------------------------------------------+ 29C
    1 2 <3>

The cursor display will be platform specific.

~~~
:position-cursor (-)
  @Cursor-Col @Cursor-Row [ n:inc ] bi@
  ASCII:ESC '%c[%n;%nH s:format s:put ;

:clear-display (-)
  ASCII:ESC c:put '[2J s:put
  ASCII:ESC c:put '[H s:put ;

:display-block (-)
  clear-display
  &Block #16 [ #64 [ fetch-next c:put ] times $| c:put nl ] times drop
  #64 [ $- c:put ] times $+ c:put sp @Current-Block n:put @Mode c:put nl
  dump-stack position-cursor ;
~~~


## The Final Piece

All that's left is a single top level loop to tie it all together.

~~~
:edit
  &Completed v:off
  #0 !Current-Block load-block
  [ display-block c:get handle-key @Completed ] until ;

edit
~~~



EOF
retro /tmp/_roo.forth
rm -f /tmp/_roo.forth
stty -cbreak

