Add menu browser choices handling
This commit is contained in:
parent
856bbdeb63
commit
06938a9628
34
README.md
34
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.
|
* `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.
|
* `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.
|
* `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.
|
* `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
|
### 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.
|
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
|
## 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.
|
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.
|
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 support
|
||||||
|
|
||||||
Multipage outputs, like listings, are handled using the _sink_ output constraints:
|
Multipage outputs, like listings, are handled using the _sink_ output constraints:
|
||||||
|
@ -13,8 +13,9 @@ type EntryFunc func(ctx context.Context) (string, error)
|
|||||||
type Resource interface {
|
type Resource interface {
|
||||||
GetTemplate(sym string) (string, error) // Get the template for a given symbol.
|
GetTemplate(sym string) (string, error) // Get the template for a given symbol.
|
||||||
GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol.
|
GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol.
|
||||||
PutMenu(string, string) error // Add a menu item
|
PutMenu(string, string) error // Add a menu item.
|
||||||
ShiftMenu() (string, string, error)
|
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.
|
RenderTemplate(sym string, values map[string]string) (string, error) // Render the given data map using the template of the symbol.
|
||||||
RenderMenu() (string, error)
|
RenderMenu() (string, error)
|
||||||
FuncFor(sym string) (EntryFunc, error) // Resolve symbol code point for.
|
FuncFor(sym string) (EntryFunc, error) // Resolve symbol code point for.
|
||||||
@ -22,6 +23,18 @@ type Resource interface {
|
|||||||
|
|
||||||
type MenuResource struct {
|
type MenuResource struct {
|
||||||
menu [][2]string
|
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 {
|
func(m *MenuResource) PutMenu(selector string, title string) error {
|
||||||
@ -45,9 +58,6 @@ func(m *MenuResource) RenderMenu() (string, error) {
|
|||||||
l := len(r)
|
l := len(r)
|
||||||
choice, title, err := m.ShiftMenu()
|
choice, title, err := m.ShiftMenu()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//if l == 0 { // TODO: replace with EOF
|
|
||||||
// return "", err
|
|
||||||
//}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if l > 0 {
|
if l > 0 {
|
||||||
|
@ -97,6 +97,20 @@ func ToString(b []byte) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
s = fmt.Sprintf("%s %s \"%s\"", s, r, v)
|
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"
|
s += "\n"
|
||||||
if len(b) == 0 {
|
if len(b) == 0 {
|
||||||
|
@ -90,6 +90,46 @@ func TestToString(t *testing.T) {
|
|||||||
if r != expect {
|
if r != expect {
|
||||||
t.Fatalf("expected:\n\t%v\ngot:\n\t%v", expect, r)
|
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) {
|
func TestToStringMultiple(t *testing.T) {
|
||||||
|
@ -17,7 +17,9 @@ const (
|
|||||||
INCMP = 8
|
INCMP = 8
|
||||||
MSIZE = 9
|
MSIZE = 9
|
||||||
MOUT = 10
|
MOUT = 10
|
||||||
_MAX = 10
|
MNEXT = 11
|
||||||
|
MPREV = 12
|
||||||
|
_MAX = 12
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -33,5 +35,7 @@ var (
|
|||||||
INCMP: "INCMP",
|
INCMP: "INCMP",
|
||||||
MSIZE: "MSIZE",
|
MSIZE: "MSIZE",
|
||||||
MOUT: "MOUT",
|
MOUT: "MOUT",
|
||||||
|
MNEXT: "MNEXT",
|
||||||
|
MPREV: "MPREV",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -44,6 +44,10 @@ func Run(b []byte, st *state.State, rs resource.Resource, ctx context.Context) (
|
|||||||
b, err = RunMSize(b, st, rs, ctx)
|
b, err = RunMSize(b, st, rs, ctx)
|
||||||
case MOUT:
|
case MOUT:
|
||||||
b, err = RunMOut(b, st, rs, ctx)
|
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:
|
case HALT:
|
||||||
b, err = RunHalt(b, st, rs, ctx)
|
b, err = RunHalt(b, st, rs, ctx)
|
||||||
return b, err
|
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
|
// RunLoad executes the LOAD opcode
|
||||||
func RunLoad(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
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)
|
sym, sz, b, err := ParseLoad(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return b, err
|
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
|
// RunLoad executes the RELOAD opcode
|
||||||
func RunReload(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
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)
|
sym, b, err := ParseReload(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return b, err
|
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
|
// RunLoad executes the MOVE opcode
|
||||||
func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
func RunMove(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
||||||
sym, b, err := ParseMove(b)
|
sym, b, err := ParseMove(b)
|
||||||
// head, tail, err := instructionSplit(b)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return b, err
|
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
|
// RunIncmp executes the INCMP opcode
|
||||||
func RunInCmp(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
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)
|
sym, target, b, err := ParseInCmp(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return b, err
|
return b, err
|
||||||
@ -209,9 +198,30 @@ func RunHalt(b []byte, st *state.State, rs resource.Resource, ctx context.Contex
|
|||||||
|
|
||||||
// RunMSize
|
// RunMSize
|
||||||
func RunMSize(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
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
|
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) {
|
func RunMOut(b []byte, st *state.State, rs resource.Resource, ctx context.Context) ([]byte, error) {
|
||||||
choice, title, b, err := ParseMOut(b)
|
choice, title, b, err := ParseMOut(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -317,3 +317,36 @@ func TestRunMenu(t *testing.T) {
|
|||||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -47,6 +47,14 @@ func ParseInCmp(b []byte) (string, string, []byte, error) {
|
|||||||
return parseTwoSym(b)
|
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) {
|
func ParseMSize(b []byte) (uint32, []byte, error) {
|
||||||
if len(b) < 1 {
|
if len(b) < 1 {
|
||||||
return 0, b, fmt.Errorf("zero-length argument")
|
return 0, b, fmt.Errorf("zero-length argument")
|
||||||
|
Loading…
Reference in New Issue
Block a user