commit 06938a96282296d02ed1c6d38b4c863d4a89ff06
parent 856bbdeb6317e8c28b46b828f2357c50f5d0f785
Author: lash <dev@holbrook.no>
Date: Mon, 3 Apr 2023 09:11:44 +0100
Add menu browser choices handling
Diffstat:
8 files changed, 173 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
@@ -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:
diff --git a/go/resource/resource.go b/go/resource/resource.go
@@ -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 {
diff --git a/go/vm/debug.go b/go/vm/debug.go
@@ -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 {
diff --git a/go/vm/debug_test.go b/go/vm/debug_test.go
@@ -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) {
diff --git a/go/vm/opcodes.go b/go/vm/opcodes.go
@@ -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",
}
)
diff --git a/go/vm/runner.go b/go/vm/runner.go
@@ -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 {
diff --git a/go/vm/runner_test.go b/go/vm/runner_test.go
@@ -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)
+ }
+}
diff --git a/go/vm/vm.go b/go/vm/vm.go
@@ -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")