Add menu browser choices handling

This commit is contained in:
lash 2023-04-03 09:11:44 +01:00
parent 856bbdeb63
commit 06938a9628
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
8 changed files with 173 additions and 22 deletions

View File

@ -18,8 +18,10 @@ The VM defines the following opcode symbols:
* `MOVE <symbol>` - Create a new execution frame, invalidating all previous `MAP` calls. More detailed: After a `MOVE` call, a `BACK` call will return to the same execution frame, with the same symbols available, but all `MAP` calls will have to be repeated.
* `HALT` - Stop execution. The remaining bytecode (typically, the routing code for the node) is returned to the invoking function.
* `INCMP <arg> <symbol>` - Compare registered input to `arg`. If match, it has the same side-effects as `MOVE`. In addition, any consecutive `INCMP` matches will be ignored until `HALT` is called.
* `MSIZE <num>` - Set max display size of menu part to `num` bytes.
* `MSIZE <max> <min>` - Set min and max display size of menu part to `num` bytes.
* `MOUT <choice> <display>` - Add menu display entry. Each entry should have a matching `INCMP` whose `arg` matches `choice`. `display` is a descriptive text of the menu item.
* `MSEP <choice> <display> <direction>` - Define how to display a menu separator, when browsing menus that are too long. Direction `0` is forward, `>0` is backward. If a `>0` value is not defined, no previous step will be available.
### External code
@ -53,6 +55,25 @@ The signal flag arguments should only set a single flag to be tested. If more th
First 8 flags are reserved and used for internal VM operations.
### Avoid duplicate menu items
The vm execution should overwrite duplicate `MOUT` directives with the last definition between `HALT` instructions.
The assembler should detect duplicate `INCMP` and `MOUT` (or menu batch code) selectors, and fail to compile. `MSEP` should be included in duplication detection.
## Menus
A menu has both a display and a input processing part. They are on either side of a `HALT` instruction.
To assist with menu creation, a few batch operation symbols have been made available for use with the assembly language.
* `DOWN <choice> <display> <symbol>` descend to next frame
* `UP <choice> <display>` return to the previous frame
* `NEXT <choice> <display>` include pagination advance
* `PREVIOUS <choice> <display>` include pagination return. If `NEXT` has not been defined this will not be rendered.
## Rendering
The fixed-size output is generated using a templating language, and a combination of one or more _max size_ properties, and an optional _sink_ property that will attempt to consume all remaining capacity of the rendered template.
@ -68,6 +89,17 @@ For example, in this example
The renderer may use up to `256 - 120 - 5 - 12 = 119` bytes from the _sink_ when rendering the output.
### Menu rendering
The menu is appended to the template output.
A max size can be set for the menu, which will count towards the space available for the _template sink_.
Menus too long for a single screen should be browseable through separate screens. How the browse choice is displayed is defined using the `MSEP` definition. The browse choice counts towards the menu size capacity.
When browsing additional menu pages, the template output should not be included.
### Multipage support
Multipage outputs, like listings, are handled using the _sink_ output constraints:

View File

@ -13,8 +13,9 @@ type EntryFunc func(ctx context.Context) (string, error)
type Resource interface {
GetTemplate(sym string) (string, error) // Get the template for a given symbol.
GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol.
PutMenu(string, string) error // Add a menu item
ShiftMenu() (string, string, error)
PutMenu(string, string) error // Add a menu item.
ShiftMenu() (string, string, error) // Remove and return the first menu item in list.
SetMenuBrowse(string, string, bool) error // Set menu browser display details.
RenderTemplate(sym string, values map[string]string) (string, error) // Render the given data map using the template of the symbol.
RenderMenu() (string, error)
FuncFor(sym string) (EntryFunc, error) // Resolve symbol code point for.
@ -22,6 +23,18 @@ type Resource interface {
type MenuResource struct {
menu [][2]string
next [2]string
prev [2]string
}
func(m *MenuResource) SetMenuBrowse(selector string, title string, back bool) error {
entry := [2]string{selector, title}
if back {
m.prev = entry
} else {
m.next = entry
}
return nil
}
func(m *MenuResource) PutMenu(selector string, title string) error {
@ -45,9 +58,6 @@ func(m *MenuResource) RenderMenu() (string, error) {
l := len(r)
choice, title, err := m.ShiftMenu()
if err != nil {
//if l == 0 { // TODO: replace with EOF
// return "", err
//}
break
}
if l > 0 {

View File

@ -97,6 +97,20 @@ func ToString(b []byte) (string, error) {
return "", err
}
s = fmt.Sprintf("%s %s \"%s\"", s, r, v)
case MNEXT:
r, v, bb, err := ParseMNext(b)
b = bb
if err != nil {
return "", err
}
s = fmt.Sprintf("%s %s \"%s\"", s, r, v)
case MPREV:
r, v, bb, err := ParseMPrev(b)
b = bb
if err != nil {
return "", err
}
s = fmt.Sprintf("%s %s \"%s\"", s, r, v)
}
s += "\n"
if len(b) == 0 {

View File

@ -90,6 +90,46 @@ func TestToString(t *testing.T) {
if r != expect {
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
}
b = NewLine(nil, MNEXT, []string{"11", "nextmenu"}, nil, nil)
r, err = ToString(b)
if err != nil {
t.Fatal(err)
}
expect = "MNEXT 11 \"nextmenu\"\n"
if r != expect {
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
}
b = NewLine(nil, MPREV, []string{"222", "previous menu item"}, nil, nil)
r, err = ToString(b)
if err != nil {
t.Fatal(err)
}
expect = "MPREV 222 \"previous menu item\"\n"
if r != expect {
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
}
b = NewLine(nil, MOUT, []string{"1", "foo"}, nil, nil)
r, err = ToString(b)
if err != nil {
t.Fatal(err)
}
expect = "MOUT 1 \"foo\"\n"
if r != expect {
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
}
b = NewLine(nil, MSIZE, nil, nil, []uint8{0x42})
r, err = ToString(b)
if err != nil {
t.Fatal(err)
}
expect = "MSIZE 66\n"
if r != expect {
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
}
}
func TestToStringMultiple(t *testing.T) {

View File

@ -17,7 +17,9 @@ const (
INCMP = 8
MSIZE = 9
MOUT = 10
_MAX = 10
MNEXT = 11
MPREV = 12
_MAX = 12
)
var (
@ -33,5 +35,7 @@ var (
INCMP: "INCMP",
MSIZE: "MSIZE",
MOUT: "MOUT",
MNEXT: "MNEXT",
MPREV: "MPREV",
}
)

View File

@ -44,6 +44,10 @@ func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) (
b, err = RunMSize(b, st, rs, ctx)
case MOUT:
b, err = RunMOut(b, st, rs, ctx)
case MNEXT:
b, err = RunMNext(b, st, rs, ctx)
case MPREV:
b, err = RunMPrev(b, st, rs, ctx)
case HALT:
b, err = RunHalt(b, st, rs, ctx)
return b, err
@ -105,15 +109,6 @@ func RunCroak(b []byte, st *state.State, rs resource.Resource, ctx context.Conte
// RunLoad executes the LOAD opcode
func RunLoad(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
// head, tail, err := instructionSplit(b)
// if err != nil {
// return b, err
// }
// if !st.Check(head) {
// return b, fmt.Errorf("key %v already loaded", head)
// }
// sz := uint16(tail[0])
// tail = tail[1:]
sym, sz, b, err := ParseLoad(b)
if err != nil {
return b, err
@ -129,10 +124,6 @@ func RunLoad(b []byte, st *state.State, rs resource.Resource, ctx context.Contex
// RunLoad executes the RELOAD opcode
func RunReload(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
// head, tail, err := instructionSplit(b)
// if err != nil {
// return b, err
// }
sym, b, err := ParseReload(b)
if err != nil {
return b, err
@ -149,7 +140,6 @@ func RunReload(b []byte, st *state.State, rs resource.Resource, ctx context.Cont
// RunLoad executes the MOVE opcode
func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
sym, b, err := ParseMove(b)
// head, tail, err := instructionSplit(b)
if err != nil {
return b, err
}
@ -165,7 +155,6 @@ func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Contex
// RunIncmp executes the INCMP opcode
func RunInCmp(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
//head, tail, err := instructionSplit(b)
sym, target, b, err := ParseInCmp(b)
if err != nil {
return b, err
@ -209,9 +198,30 @@ func RunHalt(b []byte, st *state.State, rs resource.Resource, ctx context.Contex
// RunMSize
func RunMSize(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
log.Printf("WARNING MSIZE not yet implemented")
return b, nil
}
// RunMSize
func RunMNext(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
selector, display, b, err := ParseMNext(b)
if err != nil {
return b, err
}
err = rs.SetMenuBrowse(selector, display, false)
return b, err
}
// RunMSize
func RunMPrev(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
selector, display, b, err := ParseMPrev(b)
if err != nil {
return b, err
}
err = rs.SetMenuBrowse(selector, display, false)
return b, err
}
func RunMOut(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
choice, title, b, err := ParseMOut(b)
if err != nil {

View File

@ -317,3 +317,36 @@ func TestRunMenu(t *testing.T) {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
}
func TestRunMenuBrowse(t *testing.T) {
log.Printf("This test is incomplete, it must check the output of a menu browser once one is implemented. For now it only checks whether it can execute the runner endpoints for the instrucitons.")
st := state.NewState(5)
rs := TestResource{}
var err error
b := NewLine(nil, MOVE, []string{"foo"}, nil, nil)
b = NewLine(b, MNEXT, []string{"11", "two"}, nil, nil)
b = NewLine(b, MPREV, []string{"22", "two"}, nil, nil)
b = NewLine(b, MOUT, []string{"0", "one"}, nil, nil)
b = NewLine(b, MOUT, []string{"1", "two"}, nil, nil)
b, err = Run(b, &st, &rs, context.TODO())
if err != nil {
t.Error(err)
}
l := len(b)
if l != 0 {
t.Errorf("expected empty remainder, got length %v: %v", l, b)
}
r, err := rs.RenderMenu()
if err != nil {
t.Fatal(err)
}
expect := "0:one\n1:two"
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
}

View File

@ -47,6 +47,14 @@ func ParseInCmp(b []byte) (string, string, []byte, error) {
return parseTwoSym(b)
}
func ParseMPrev(b []byte) (string, string, []byte, error) {
return parseTwoSym(b)
}
func ParseMNext(b []byte) (string, string, []byte, error) {
return parseTwoSym(b)
}
func ParseMSize(b []byte) (uint32, []byte, error) {
if len(b) < 1 {
return 0, b, fmt.Errorf("zero-length argument")