go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit 7e01f2725d9c90b00f0bb5e624cb70d5e635e581
parent 6a1611a0c87098f2e627843de9f319ad6e6e8bca
Author: lash <dev@holbrook.no>
Date:   Sat,  8 Apr 2023 23:35:13 +0100

WIP Factor page, sizer, menu to render package, cachemap to page

Diffstat:
Mgo/cache/cache.go | 131+++++++++++++++++++++----------------------------------------------------------
Mgo/cache/cache_test.go | 82-------------------------------------------------------------------------------
Dgo/menu/menu.go | 132-------------------------------------------------------------------------------
Dgo/menu/menu_test.go | 94-------------------------------------------------------------------------------
Ago/render/menu.go | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago/render/menu_test.go | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago/render/page.go | 289++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago/render/size.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago/render/size_test.go | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgo/resource/fs.go | 2+-
Mgo/resource/render.go | 72++++++++++++++++++++++++++++++++++++------------------------------------
Mgo/resource/resource.go | 134+++----------------------------------------------------------------------------
Mgo/resource/resource_test.go | 2+-
Dgo/resource/size.go | 98-------------------------------------------------------------------------------
Dgo/resource/size_test.go | 121-------------------------------------------------------------------------------
Mgo/resource/state.go | 15---------------
Mgo/resource/state_test.go | 87-------------------------------------------------------------------------------
17 files changed, 886 insertions(+), 894 deletions(-)

diff --git a/go/cache/cache.go b/go/cache/cache.go @@ -9,11 +9,10 @@ type Cache struct { CacheSize uint32 // Total allowed cumulative size of values (not code) in cache CacheUseSize uint32 // Currently used bytes by all values (not code) in cache Cache []map[string]string // All loaded cache items - CacheMap map[string]string // Mapped - menuSize uint16 // Max size of menu - outputSize uint32 // Max size of output + //CacheMap map[string]string // Mapped + //outputSize uint32 // Max size of output sizes map[string]uint16 // Size limits for all loaded symbols. - sink *string + //sink *string } // NewCache creates a new ready-to-use cache object @@ -22,7 +21,7 @@ func NewCache() *Cache { Cache: []map[string]string{make(map[string]string)}, sizes: make(map[string]uint16), } - ca.resetCurrent() + //ca.resetCurrent() return ca } @@ -32,12 +31,6 @@ func(ca *Cache) WithCacheSize(cacheSize uint32) *Cache { return ca } -// WithCacheSize applies a cumulative cache size limitation for all cached items. -func(ca *Cache) WithOutputSize(outputSize uint32) *Cache { - ca.outputSize = outputSize - return ca -} - // Add adds a cache value under a cache symbol key. // // Also stores the size limitation of for key for later updates. @@ -72,6 +65,15 @@ func(ca *Cache) Add(key string, value string, sizeLimit uint16) error { return nil } +// ReservedSize returns the maximum byte size available for the given symbol. +func(ca *Cache) ReservedSize(key string) (uint16, error) { + v, ok := ca.sizes[key] + if !ok { + return 0, fmt.Errorf("unknown symbol: %s", key) + } + return v, nil +} + // Update sets a new value for an existing key. // // Uses the size limitation from when the key was added. @@ -95,9 +97,9 @@ func(ca *Cache) Update(key string, value string) error { r := ca.Cache[checkFrame][key] l := uint32(len(r)) ca.Cache[checkFrame][key] = "" - if ca.CacheMap[key] != "" { - ca.CacheMap[key] = value - } + //if ca.CacheMap[key] != "" { + // ca.CacheMap[key] = value + //} ca.CacheUseSize -= l sz := ca.checkCapacity(value) if sz == 0 { @@ -117,58 +119,6 @@ func(ca *Cache) Get() (map[string]string, error) { return ca.Cache[len(ca.Cache)-1], nil } -func(ca *Cache) Sizes() (map[string]uint16, error) { - if len(ca.Cache) == 0 { - return nil, fmt.Errorf("get at top frame") - } - sizes := make(map[string]uint16) - var haveSink bool - for k, _ := range ca.CacheMap { - l, ok := ca.sizes[k] - if !ok { - panic(fmt.Sprintf("missing size for %v", k)) - } - if l == 0 { - if haveSink { - panic(fmt.Sprintf("duplicate sink for %v", k)) - } - haveSink = true - } - sizes[k] = l - } - return sizes, nil -} - -// Map marks the given key for retrieval. -// -// After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations. -// -// Only one symbol with no size limitation may be mapped at the current level. -func(ca *Cache) Map(key string) error { - m, err := ca.Get() - if err != nil { - return err - } - l := ca.sizes[key] - if l == 0 { - if ca.sink != nil { - return fmt.Errorf("sink already set to symbol '%v'", *ca.sink) - } - ca.sink = &key - } - ca.CacheMap[key] = m[key] - return nil -} - -// Fails if key is not mapped. -func(ca *Cache) Val(key string) (string, error) { - r := ca.CacheMap[key] - if len(r) == 0 { - return "", fmt.Errorf("key %v not mapped", key) - } - return r, nil -} - // Reset flushes all state contents below the top level. func(ca *Cache) Reset() { if len(ca.Cache) == 0 { @@ -179,36 +129,11 @@ func(ca *Cache) Reset() { return } -// Size returns size used by values and menu, and remaining size available -func(ca *Cache) Usage() (uint32, uint32) { - var l int - var c uint16 - for k, v := range ca.CacheMap { - l += len(v) - c += ca.sizes[k] - } - r := uint32(l) - r += uint32(ca.menuSize) - return r, uint32(c)-r -} - -// return 0-indexed frame number where key is defined. -1 if not defined -func(ca *Cache) frameOf(key string) int { - for i, m := range ca.Cache { - for k, _ := range m { - if k == key { - return i - } - } - } - return -1 -} - // Push adds a new level to the cache. func (ca *Cache) Push() error { m := make(map[string]string) ca.Cache = append(ca.Cache, m) - ca.resetCurrent() + //ca.resetCurrent() return nil } @@ -228,7 +153,7 @@ func (ca *Cache) Pop() error { log.Printf("free frame %v key %v value size %v", l, k, sz) } ca.Cache = ca.Cache[:l] - ca.resetCurrent() + //ca.resetCurrent() return nil } @@ -238,10 +163,10 @@ func(ca *Cache) Check(key string) bool { } // flush relveant properties for level change -func(ca *Cache) resetCurrent() { - ca.sink = nil - ca.CacheMap = make(map[string]string) -} +//func(ca *Cache) resetCurrent() { +// ca.sink = nil +// ca.CacheMap = make(map[string]string) +//} // bytes that will be added to cache use size for string // returns 0 if capacity would be exceeded @@ -255,3 +180,15 @@ func(ca *Cache) checkCapacity(v string) uint32 { } return sz } + +// return 0-indexed frame number where key is defined. -1 if not defined +func(ca *Cache) frameOf(key string) int { + for i, m := range ca.Cache { + for k, _ := range m { + if k == key { + return i + } + } + } + return -1 +} diff --git a/go/cache/cache_test.go b/go/cache/cache_test.go @@ -4,7 +4,6 @@ import ( "testing" ) - func TestNewCache(t *testing.T) { ca := NewCache() if ca.CacheSize != 0 { @@ -34,7 +33,6 @@ func TestStateCacheUse(t *testing.T) { } } - func TestStateDownUp(t *testing.T) { ca := NewCache() err := ca.Push() @@ -104,83 +102,3 @@ func TestCacheLoadDup(t *testing.T) { } } -func TestCacheCurrentSize(t *testing.T) { - ca := NewCache() - err := ca.Push() - if err != nil { - t.Error(err) - } - err = ca.Add("foo", "inky", 0) - if err != nil { - t.Error(err) - } - err = ca.Push() - err = ca.Add("bar", "pinky", 10) - if err != nil { - t.Error(err) - } - err = ca.Map("bar") - if err != nil { - t.Error(err) - } - err = ca.Add("baz", "tinkywinkydipsylalapoo", 51) - if err != nil { - t.Error(err) - } - err = ca.Map("baz") - if err != nil { - t.Error(err) - } - - l, c := ca.Usage() - if l != 27 { - t.Errorf("expected actual length 27, got %v", l) - } - if c != 34 { - t.Errorf("expected remaining length 34, got %v", c) - } -} - -func TestStateMapSink(t *testing.T) { - ca := NewCache() - ca.Push() - err := ca.Add("foo", "bar", 0) - if err != nil { - t.Error(err) - } - ca.Push() - err = ca.Add("bar", "xyzzy", 6) - if err != nil { - t.Error(err) - } - err = ca.Add("baz", "bazbaz", 18) - if err != nil { - t.Error(err) - } - err = ca.Add("xyzzy", "plugh", 0) - if err != nil { - t.Error(err) - } - err = ca.Map("foo") - if err != nil { - t.Error(err) - } - err = ca.Map("xyzzy") - if err == nil { - t.Errorf("Expected fail on duplicate sink") - } - err = ca.Map("baz") - if err != nil { - t.Error(err) - } - ca.Push() - err = ca.Map("foo") - if err != nil { - t.Error(err) - } - ca.Pop() - err = ca.Map("foo") - if err != nil { - t.Error(err) - } -} diff --git a/go/menu/menu.go b/go/menu/menu.go @@ -1,132 +0,0 @@ -package menu - -import ( - "fmt" - "log" -) - -// BrowseConfig defines the availability and display parameters for page browsing. -type BrowseConfig struct { - NextAvailable bool - NextSelector string - NextTitle string - PreviousAvailable bool - PreviousSelector string - PreviousTitle string -} - -// Default browse settings for convenience. -func DefaultBrowseConfig() BrowseConfig { - return BrowseConfig{ - NextAvailable: true, - NextSelector: "11", - NextTitle: "next", - PreviousAvailable: true, - PreviousSelector: "22", - PreviousTitle: "previous", - } -} - -type Menu struct { - menu [][2]string - browse BrowseConfig - pageCount uint16 - canNext bool - canPrevious bool -} - -// NewMenu creates a new Menu with an explicit page count. -func NewMenu() *Menu { - return &Menu{} -} - -// WithBrowseConfig defines the criteria for page browsing. -func(m *Menu) WithPageCount(pageCount uint16) *Menu { - m.pageCount = pageCount - return m -} - -// WithBrowseConfig defines the criteria for page browsing. -func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu { - m.browse = cfg - - return m -} - -// Put adds a menu option to the menu rendering. -func(m *Menu) Put(selector string, title string) error { - m.menu = append(m.menu, [2]string{selector, title}) - log.Printf("menu %v", m.menu) - return nil -} - -// Render returns the full current state of the menu as a string. -// -// After this has been executed, the state of the menu will be empty. -func(m *Menu) Render(idx uint16) (string, error) { - err := m.applyPage(idx) - if err != nil { - return "", err - } - - r := "" - for true { - l := len(r) - choice, title, err := m.shiftMenu() - if err != nil { - break - } - if l > 0 { - r += "\n" - } - r += fmt.Sprintf("%s:%s", choice, title) - } - return r, nil -} - -// add available browse options. -func(m *Menu) applyPage(idx uint16) error { - if m.pageCount == 0 { - return nil - } else if idx >= m.pageCount { - return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount) - } - if m.browse.NextAvailable { - m.canNext = true - } - if m.browse.PreviousAvailable { - m.canPrevious = true - } - if idx == m.pageCount - 1 { - m.canNext = false - } - if idx == 0 { - m.canPrevious = false - } - if m.canNext { - err := m.Put(m.browse.NextSelector, m.browse.NextTitle) - if err != nil { - return err - } - } - if m.canPrevious { - err := m.Put(m.browse.PreviousSelector, m.browse.PreviousTitle) - if err != nil { - return err - } - } - return nil -} - -// removes and returns the first of remaining menu options. -// fails if menu is empty. -func(m *Menu) shiftMenu() (string, string, error) { - if len(m.menu) == 0 { - return "", "", fmt.Errorf("menu is empty") - } - r := m.menu[0] - m.menu = m.menu[1:] - return r[0], r[1], nil -} - - diff --git a/go/menu/menu_test.go b/go/menu/menu_test.go @@ -1,94 +0,0 @@ -package menu - -import ( - "testing" -) - -func TestMenuInit(t *testing.T) { - m := NewMenu() - err := m.Put("1", "foo") - if err != nil { - t.Fatal(err) - } - err = m.Put("2", "bar") - if err != nil { - t.Fatal(err) - } - r, err := m.Render(0) - if err != nil { - t.Fatal(err) - } - expect := `1:foo -2:bar` - if r != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) - } -} - -func TestMenuBrowse(t *testing.T) { - cfg := DefaultBrowseConfig() - m := NewMenu().WithPageCount(3).WithBrowseConfig(cfg) - err := m.Put("1", "foo") - if err != nil { - t.Fatal(err) - } - err = m.Put("2", "bar") - if err != nil { - t.Fatal(err) - } - - r, err := m.Render(0) - if err != nil { - t.Fatal(err) - } - expect := `1:foo -2:bar -11:next` - if r != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) - } - - err = m.Put("1", "foo") - if err != nil { - t.Fatal(err) - } - err = m.Put("2", "bar") - if err != nil { - t.Fatal(err) - } - r, err = m.Render(1) - if err != nil { - t.Fatal(err) - } - expect = `1:foo -2:bar -11:next -22:previous` - if r != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) - } - - err = m.Put("1", "foo") - if err != nil { - t.Fatal(err) - } - err = m.Put("2", "bar") - if err != nil { - t.Fatal(err) - } - r, err = m.Render(2) - if err != nil { - t.Fatal(err) - } - expect = `1:foo -2:bar -22:previous` - if r != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) - } - - _, err = m.Render(3) - if err == nil { - t.Fatalf("expected render fail") - } -} diff --git a/go/render/menu.go b/go/render/menu.go @@ -0,0 +1,154 @@ +package render + +import ( + "fmt" +) + +// BrowseConfig defines the availability and display parameters for page browsing. +type BrowseConfig struct { + NextAvailable bool + NextSelector string + NextTitle string + PreviousAvailable bool + PreviousSelector string + PreviousTitle string +} + +// Default browse settings for convenience. +func DefaultBrowseConfig() BrowseConfig { + return BrowseConfig{ + NextAvailable: true, + NextSelector: "11", + NextTitle: "next", + PreviousAvailable: true, + PreviousSelector: "22", + PreviousTitle: "previous", + } +} + +type Menu struct { + menu [][2]string + browse BrowseConfig + pageCount uint16 + canNext bool + canPrevious bool + outputSize uint16 +} + +// NewMenu creates a new Menu with an explicit page count. +func NewMenu() *Menu { + return &Menu{} +} + +// WithBrowseConfig defines the criteria for page browsing. +func(m *Menu) WithPageCount(pageCount uint16) *Menu { + m.pageCount = pageCount + return m +} + +// WithSize defines the maximum byte size of the rendered menu. +func(m *Menu) WithOutputSize(outputSize uint16) *Menu { + m.outputSize = outputSize + return m +} + +// WithBrowseConfig defines the criteria for page browsing. +func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu { + m.browse = cfg + return m +} + +// Put adds a menu option to the menu rendering. +func(m *Menu) Put(selector string, title string) error { + m.menu = append(m.menu, [2]string{selector, title}) + return nil +} + +// Render returns the full current state of the menu as a string. +// +// After this has been executed, the state of the menu will be empty. +func(m *Menu) Render(idx uint16) (string, error) { + var menuCopy [][2]string + for _, v := range m.menu { + menuCopy = append(menuCopy, v) + } + + err := m.applyPage(idx) + if err != nil { + return "", err + } + + r := "" + for true { + l := len(r) + choice, title, err := m.shiftMenu() + if err != nil { + break + } + if l > 0 { + r += "\n" + } + r += fmt.Sprintf("%s:%s", choice, title) + } + m.menu = menuCopy + return r, nil +} + +// add available browse options. +func(m *Menu) applyPage(idx uint16) error { + if m.pageCount == 0 { + if idx > 0 { + return fmt.Errorf("index %v > 0 for non-paged menu", idx) + } + return nil + } else if idx >= m.pageCount { + return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount) + } + + m.reset() + + if idx == m.pageCount - 1 { + m.canNext = false + } + if idx == 0 { + m.canPrevious = false + } + + if m.canNext { + err := m.Put(m.browse.NextSelector, m.browse.NextTitle) + if err != nil { + return err + } + } + if m.canPrevious { + err := m.Put(m.browse.PreviousSelector, m.browse.PreviousTitle) + if err != nil { + return err + } + } + return nil +} + +// removes and returns the first of remaining menu options. +// fails if menu is empty. +func(m *Menu) shiftMenu() (string, string, error) { + if len(m.menu) == 0 { + return "", "", fmt.Errorf("menu is empty") + } + r := m.menu[0] + m.menu = m.menu[1:] + return r[0], r[1], nil +} + +func(m *Menu) reset() { + if m.browse.NextAvailable { + m.canNext = true + } + if m.browse.PreviousAvailable { + m.canPrevious = true + } +} + +func(m *Menu) ReservedSize() uint16 { + return m.outputSize +} diff --git a/go/render/menu_test.go b/go/render/menu_test.go @@ -0,0 +1,84 @@ +package render + +import ( + "testing" +) + +func TestMenuInit(t *testing.T) { + m := NewMenu() + err := m.Put("1", "foo") + if err != nil { + t.Fatal(err) + } + err = m.Put("2", "bar") + if err != nil { + t.Fatal(err) + } + r, err := m.Render(0) + if err != nil { + t.Fatal(err) + } + expect := `1:foo +2:bar` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + + r, err = m.Render(1) + if err == nil { + t.Fatalf("expected render fail") + } + +} + +func TestMenuBrowse(t *testing.T) { + cfg := DefaultBrowseConfig() + m := NewMenu().WithPageCount(3).WithBrowseConfig(cfg) + err := m.Put("1", "foo") + if err != nil { + t.Fatal(err) + } + err = m.Put("2", "bar") + if err != nil { + t.Fatal(err) + } + + r, err := m.Render(0) + if err != nil { + t.Fatal(err) + } + expect := `1:foo +2:bar +11:next` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + + r, err = m.Render(1) + if err != nil { + t.Fatal(err) + } + expect = `1:foo +2:bar +11:next +22:previous` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + + r, err = m.Render(2) + if err != nil { + t.Fatal(err) + } + expect = `1:foo +2:bar +22:previous` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + + _, err = m.Render(3) + if err == nil { + t.Fatalf("expected render fail") + } +} diff --git a/go/render/page.go b/go/render/page.go @@ -0,0 +1,289 @@ +package render + +import ( + "bytes" + "fmt" + "log" + "strings" + "text/template" + + "git.defalsify.org/festive/cache" + "git.defalsify.org/festive/resource" +) + +type Page struct { + cacheMap map[string]string // Mapped + cache *cache.Cache + resource resource.Resource + menu *Menu + sink *string + sinkSize uint16 + sizer *Sizer + sinkProcessed bool +} + +func NewPage(cache *cache.Cache, rs resource.Resource) *Page { + return &Page{ + cache: cache, + cacheMap: make(map[string]string), + resource: rs, + } +} + +func(pg *Page) WithMenu(menu *Menu) *Page { + pg.menu = menu + if pg.sizer != nil { + pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize()) + } + return pg +} + +func(pg *Page) WithSizer(sizer *Sizer) *Page { + pg.sizer = sizer + if pg.menu != nil { + pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize()) + } + return pg +} + +// Size returns size used by values and menu, and remaining size available +func(pg *Page) Usage() (uint32, uint32, error) { + var l int + var c uint16 + for k, v := range pg.cacheMap { + l += len(v) + sz, err := pg.cache.ReservedSize(k) + if err != nil { + return 0, 0, err + } + c += sz + } + r := uint32(l) + if pg.menu != nil { + r += uint32(pg.menu.ReservedSize()) + } + return r, uint32(c)-r, nil +} + +// Map marks the given key for retrieval. +// +// After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations. +// +// Only one symbol with no size limitation may be mapped at the current level. +func(pg *Page) Map(key string) error { + m, err := pg.cache.Get() + if err != nil { + return err + } + l, err := pg.cache.ReservedSize(key) + if err != nil { + return err + } + if l == 0 { + if pg.sink != nil { + return fmt.Errorf("sink already set to symbol '%v'", *pg.sink) + } + pg.sink = &key + } + pg.cacheMap[key] = m[key] + if pg.sizer != nil { + err := pg.sizer.Set(key, l) + if err != nil { + return err + } + } + return nil +} + +// Fails if key is not mapped. +func(pg *Page) Val(key string) (string, error) { + r := pg.cacheMap[key] + if len(r) == 0 { + return "", fmt.Errorf("key %v not mapped", key) + } + return r, nil +} + +// Moved from cache, MAP should hook to this object +func(pg *Page) Sizes() (map[string]uint16, error) { + sizes := make(map[string]uint16) + var haveSink bool + for k, _ := range pg.cacheMap { + l, err := pg.cache.ReservedSize(k) + if err != nil { + return nil, err + } + if l == 0 { + if haveSink { + panic(fmt.Sprintf("duplicate sink for %v", k)) + } + haveSink = true + } + pg.sinkSize = l + } + return sizes, nil +} + +// DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate. +func(pg *Page) RenderTemplate(sym string, values map[string]string, idx uint16) (string, error) { + tpl, err := pg.resource.GetTemplate(sym) + if err != nil { + return "", err + } + if pg.sizer != nil { + values, err = pg.sizer.GetAt(values, idx) + if err != nil { + return "", err + } + } else if idx > 0 { + return "", fmt.Errorf("sizer needed for indexed render") + } + log.Printf("render for index: %v", idx) + + tp, err := template.New("tester").Option("missingkey=error").Parse(tpl) + if err != nil { + return "", err + } + + b := bytes.NewBuffer([]byte{}) + err = tp.Execute(b, values) + if err != nil { + return "", err + } + return b.String(), err +} + +// render menu and all syms except sink, split sink into display chunks +func(pg *Page) prepare(sym string, values map[string]string, idx uint16) (map[string]string, error) { + var sink string + var sinkValues []string + noSinkValues := make(map[string]string) + for k, v := range values { + sz, err := pg.cache.ReservedSize(k) + if err != nil { + return nil, err + } + if sz == 0 { + sink = k + sinkValues = strings.Split(v, "\n") + v = "" + log.Printf("found sink %s with field count %v", k, len(sinkValues)) + } + noSinkValues[k] = v + } + + if sink == "" { + log.Printf("no sink found for sym %s", sym) + return values, nil + } + + pg.sizer.AddCursor(0) + s, err := pg.render(sym, noSinkValues, 0) + if err != nil { + return nil, err + } + + remaining, ok := pg.sizer.Check(s) + if !ok { + return nil, fmt.Errorf("capacity exceeded") + } + + log.Printf("%v bytes available for sink split", remaining) + + l := 0 + var count uint16 + tb := strings.Builder{} + rb := strings.Builder{} + + for i, v := range sinkValues { + log.Printf("processing sinkvalue %v: %s", i, v) + l += len(v) + if uint32(l) > remaining { + if tb.Len() == 0 { + return nil, fmt.Errorf("capacity insufficient for sink field %v", i) + } + rb.WriteString(tb.String()) + rb.WriteRune('\n') + c := uint32(rb.Len()) + pg.sizer.AddCursor(c) + tb.Reset() + l = 0 + count += 1 + } + if tb.Len() > 0 { + tb.WriteByte(byte(0x00)) + } + tb.WriteString(v) + } + + if tb.Len() > 0 { + rb.WriteString(tb.String()) + count += 1 + } + + r := rb.String() + r = strings.TrimRight(r, "\n") + + noSinkValues[sink] = r + + if pg.menu != nil { + pg.menu = pg.menu.WithPageCount(count) + } + + for i, v := range strings.Split(r, "\n") { + log.Printf("nosinkvalue %v: %s", i, v) + } + + return noSinkValues, nil +} + +func(pg *Page) render(sym string, values map[string]string, idx uint16) (string, error) { + var ok bool + r := "" + s, err := pg.RenderTemplate(sym, values, idx) + if err != nil { + return "", err + } + log.Printf("rendered %v bytes for template", len(s)) + r += s + if pg.sizer != nil { + _, ok = pg.sizer.Check(r) + if !ok { + return "", fmt.Errorf("limit exceeded: %v", pg.sizer) + } + } + s, err = pg.menu.Render(idx) + if err != nil { + return "", err + } + log.Printf("rendered %v bytes for menu", len(s)) + r += "\n" + s + if pg.sizer != nil { + _, ok = pg.sizer.Check(r) + if !ok { + return "", fmt.Errorf("limit exceeded: %v", pg.sizer) + } + } + return r, nil +} + +func(pg *Page) Render(sym string, values map[string]string, idx uint16) (string, error) { + var err error + + values, err = pg.prepare(sym, values, idx) + if err != nil { + return "", err + } + + log.Printf("nosink %v", values) + return pg.render(sym, values, idx) +} + +func(pg *Page) Reset() { + pg.sink = nil + pg.sinkSize = 0 + pg.sinkProcessed = false + pg.cacheMap = make(map[string]string) +} + + diff --git a/go/render/size.go b/go/render/size.go @@ -0,0 +1,102 @@ +package render + +import ( + "bytes" + "fmt" + "log" + "strings" +) + +type Sizer struct { + outputSize uint32 + menuSize uint16 + memberSizes map[string]uint16 + totalMemberSize uint32 + crsrs []uint32 + sink string +} + +func NewSizer(outputSize uint32) *Sizer { + return &Sizer{ + outputSize: outputSize, + memberSizes: make(map[string]uint16), + } +} + +func(szr *Sizer) WithMenuSize(menuSize uint16) *Sizer { + szr.menuSize = menuSize + return szr +} + +func(szr *Sizer) Set(key string, size uint16) error { + var ok bool + _, ok = szr.memberSizes[key] + if ok { + return fmt.Errorf("already have key %s", key) + } + szr.memberSizes[key] = size + if size == 0 { + szr.sink = key + } + szr.totalMemberSize += uint32(size) + return nil +} + +func(szr *Sizer) Check(s string) (uint32, bool) { + log.Printf("sizercheck %s", s) + l := uint32(len(s)) + if szr.outputSize > 0 { + if l > szr.outputSize { + log.Printf("sizer check fails with length %v: %s", l, szr) + return 0, false + } + l = szr.outputSize - l + } + return l, true +} + +func(szr *Sizer) String() string { + var diff uint32 + if szr.outputSize > 0 { + diff = szr.outputSize - szr.totalMemberSize - uint32(szr.menuSize) + } + return fmt.Sprintf("output: %v, member: %v, menu: %v, diff: %v", szr.outputSize, szr.totalMemberSize, szr.menuSize, diff) +} + +func(szr *Sizer) Size(s string) (uint16, error) { + r, ok := szr.memberSizes[s] + if !ok { + return 0, fmt.Errorf("unknown member: %s", s) + } + return r, nil +} + +func(szr *Sizer) AddCursor(c uint32) { + log.Printf("added cursor: %v", c) + szr.crsrs = append(szr.crsrs, c) +} + +func(szr *Sizer) GetAt(values map[string]string, idx uint16) (map[string]string, error) { + if szr.sink == "" { + return values, nil + } + outValues := make(map[string]string) + for k, v := range values { + if szr.sink == k { + if idx >= uint16(len(szr.crsrs)) { + return nil, fmt.Errorf("no more values in index") + } + c := szr.crsrs[idx] + v = v[c:] + nl := strings.Index(v, "\n") + log.Printf("k %v v %v c %v nl %v", k, v, c, nl) + if nl > 0 { + v = v[:nl] + } + b := bytes.ReplaceAll([]byte(v), []byte{0x00}, []byte{0x0a}) + v = string(b) + } + outValues[k] = v + } + return outValues, nil +} diff --git a/go/render/size_test.go b/go/render/size_test.go @@ -0,0 +1,181 @@ +package render + +import ( + "context" + "fmt" + "testing" + + "git.defalsify.org/festive/state" + "git.defalsify.org/festive/resource" + "git.defalsify.org/festive/cache" +) + +type TestSizeResource struct { + *resource.MenuResource +} + +func getTemplate(sym string) (string, error) { + var tpl string + switch sym { + case "small": + tpl = "one {{.foo}} two {{.bar}} three {{.baz}}" + case "toobig": + tpl = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus in mattis lorem. Aliquam erat volutpat. Ut vitae metus." + case "pages": + tpl = "one {{.foo}} two {{.bar}} three {{.baz}}\n{{.xyzzy}}" + } + return tpl, nil +} + +func funcFor(sym string) (resource.EntryFunc, error) { + switch sym { + case "foo": + return getFoo, nil + case "bar": + return getBar, nil + case "baz": + return getBaz, nil + case "xyzzy": + return getXyzzy, nil + } + return nil, fmt.Errorf("unknown func: %s", sym) +} + +func getFoo(ctx context.Context) (string, error) { + return "inky", nil +} + +func getBar(ctx context.Context) (string, error) { + return "pinky", nil +} + +func getBaz(ctx context.Context) (string, error) { + return "blinky", nil +} + +func getXyzzy(ctx context.Context) (string, error) { + return "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", nil +} + +func TestSizeCheck(t *testing.T) { + szr := NewSizer(16) + l, ok := szr.Check("foobar") + if !ok { + t.Fatalf("expected ok") + } + if l != 10 { + t.Fatalf("expected 10, got %v", l) + } + + l, ok = szr.Check("inkypinkyblinkyclyde") + if ok { + t.Fatalf("expected not ok") + } + if l != 0 { + t.Fatalf("expected 0, got %v", l) + } +} + +func TestSizeLimit(t *testing.T) { + st := state.NewState(0) + ca := cache.NewCache() + mn := NewMenu().WithOutputSize(32) + mrs := resource.NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) + rs := TestSizeResource{ + mrs, + } + szr := NewSizer(128) + pg := NewPage(ca, rs).WithMenu(mn).WithSizer(szr) + ca.Push() + st.Down("test") + ca.Add("foo", "inky", 4) + ca.Add("bar", "pinky", 10) + ca.Add("baz", "blinky", 0) + pg.Map("foo") + pg.Map("bar") + pg.Map("baz") + + mn.Put("1", "foo the foo") + mn.Put("2", "go to bar") + + vals, err := ca.Get() + if err != nil { + t.Fatal(err) + } + + _, err = pg.Render("small", vals, 0) + if err != nil { + t.Fatal(err) + } + + mn.Put("1", "foo the foo") + mn.Put("2", "go to bar") + + _, err = pg.Render("toobig", vals, 0) + if err == nil { + t.Fatalf("expected size exceeded") + } +} + +func TestSizePages(t *testing.T) { + st := state.NewState(0) + ca := cache.NewCache() + mn := NewMenu().WithOutputSize(32) + mrs := resource.NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) + rs := TestSizeResource{ + mrs, + } + szr := NewSizer(128) + pg := NewPage(ca, rs).WithSizer(szr).WithMenu(mn) + ca.Push() + st.Down("test") + ca.Add("foo", "inky", 4) + ca.Add("bar", "pinky", 10) + ca.Add("baz", "blinky", 20) + ca.Add("xyzzy", "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", 0) + pg.Map("foo") + pg.Map("bar") + pg.Map("baz") + pg.Map("xyzzy") + + vals, err := ca.Get() + if err != nil { + t.Fatal(err) + } + + mn.Put("1", "foo the foo") + mn.Put("2", "go to bar") + + r, err := pg.Render("pages", vals, 0) + if err != nil { + t.Fatal(err) + } + + expect := `one inky two pinky three blinky +inky pinky +blinky clyde sue +tinkywinky dipsy +lala poo +1:foo the foo +2:go to bar` + + + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + r, err = pg.Render("pages", vals, 1) + if err != nil { + t.Fatal(err) + } + + expect = `one inky two pinky three blinky +one two three four five six seven +eight nine ten +eleven twelve +1:foo the foo +2:go to bar` + if r != expect { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) + } + +} diff --git a/go/resource/fs.go b/go/resource/fs.go @@ -23,7 +23,7 @@ func NewFsResource(path string) (FsResource) { } } -func(fs FsResource) GetTemplate(sym string, sizer *Sizer) (string, error) { +func(fs FsResource) GetTemplate(sym string) (string, error) { fp := path.Join(fs.Path, sym) r, err := ioutil.ReadFile(fp) s := string(r) diff --git a/go/resource/render.go b/go/resource/render.go @@ -1,40 +1,40 @@ package resource -import ( - "bytes" - "fmt" - "log" - "text/template" -) +//import ( +// "bytes" +// "fmt" +// "log" +// "text/template" +//) -// DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate. -func DefaultRenderTemplate(r Resource, sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) { - v, err := r.GetTemplate(sym, nil) - if err != nil { - return "", err - } - - if sizer != nil { - values, err = sizer.GetAt(values, idx) - } else if idx > 0 { - return "", fmt.Errorf("sizer needed for indexed render") - } - log.Printf("render for index: %v", idx) - - if err != nil { - return "", err - } - - tp, err := template.New("tester").Option("missingkey=error").Parse(v) - if err != nil { - return "", err - } - - b := bytes.NewBuffer([]byte{}) - err = tp.Execute(b, values) - if err != nil { - return "", err - } - return b.String(), err -} +//// DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate. +//func DefaultRenderTemplate(r Resource, sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) { +// v, err := r.GetTemplate(sym, nil) +// if err != nil { +// return "", err +// } +// +// if sizer != nil { +// values, err = sizer.GetAt(values, idx) +// } else if idx > 0 { +// return "", fmt.Errorf("sizer needed for indexed render") +// } +// log.Printf("render for index: %v", idx) +// +// if err != nil { +// return "", err +// } +// +// tp, err := template.New("tester").Option("missingkey=error").Parse(v) +// if err != nil { +// return "", err +// } +// +// b := bytes.NewBuffer([]byte{}) +// err = tp.Execute(b, values) +// if err != nil { +// return "", err +// } +// return b.String(), err +//} diff --git a/go/resource/resource.go b/go/resource/resource.go @@ -2,23 +2,18 @@ package resource import ( "context" - "fmt" - "log" - "strings" ) // EntryFunc is a function signature for retrieving value for a key type EntryFunc func(ctx context.Context) (string, error) type CodeFunc func(sym string) ([]byte, error) -type TemplateFunc func(sym string, sizer *Sizer) (string, error) +type TemplateFunc func(sym string) (string, error) type FuncForFunc func(sym string) (EntryFunc, error) // Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries. type Resource interface { - GetTemplate(sym string, sizer *Sizer) (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. - RenderTemplate(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) // Render the given data map using the template of the symbol. - Render(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) // Render full output. FuncFor(sym string) (EntryFunc, error) // Resolve symbol content point for. } @@ -52,87 +47,6 @@ func(m *MenuResource) WithTemplateGetter(templateGetter TemplateFunc) *MenuResou return m } -// render menu and all syms except sink, split sink into display chunks -func(m *MenuResource) prepare(sym string, values map[string]string, idx uint16, sizer *Sizer) (map[string]string, error) { - var sink string - var sinkValues []string - noSinkValues := make(map[string]string) - for k, v := range values { - sz, err := sizer.Size(k) - if err != nil { - return nil, err - } - if sz == 0 { - sink = k - sinkValues = strings.Split(v, "\n") - v = "" - log.Printf("found sink %s with field count %v", k, len(sinkValues)) - } - noSinkValues[k] = v - } - - if sink == "" { - log.Printf("no sink found for sym %s", sym) - return values, nil - } - - s, err := m.render(sym, noSinkValues, 0, nil) - if err != nil { - return nil, err - } - - remaining, ok := sizer.Check(s) - if !ok { - return nil, fmt.Errorf("capacity exceeded") - } - - log.Printf("%v bytes available for sink split", remaining) - - l := 0 - tb := strings.Builder{} - rb := strings.Builder{} - - sizer.AddCursor(0) - for i, v := range sinkValues { - log.Printf("processing sinkvalue %v: %s", i, v) - l += len(v) - if uint32(l) > remaining { - if tb.Len() == 0 { - return nil, fmt.Errorf("capacity insufficient for sink field %v", i) - } - rb.WriteString(tb.String()) - rb.WriteRune('\n') - c := uint32(rb.Len()) - sizer.AddCursor(c) - tb.Reset() - l = 0 - } - if tb.Len() > 0 { - tb.WriteByte(byte(0x00)) - } - tb.WriteString(v) - } - - if tb.Len() > 0 { - rb.WriteString(tb.String()) - } - - r := rb.String() - r = strings.TrimRight(r, "\n") - - noSinkValues[sink] = r - - for i, v := range strings.Split(r, "\n") { - log.Printf("nosinkvalue %v: %s", i, v) - } - - return noSinkValues, nil -} - -func(m *MenuResource) RenderTemplate(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) { - return DefaultRenderTemplate(m, sym, values, idx, sizer) -} - func(m *MenuResource) FuncFor(sym string) (EntryFunc, error) { return m.funcFunc(sym) } @@ -141,47 +55,7 @@ func(m *MenuResource) GetCode(sym string) ([]byte, error) { return m.codeFunc(sym) } -func(m *MenuResource) GetTemplate(sym string, sizer *Sizer) (string, error) { - return m.templateFunc(sym, sizer) +func(m *MenuResource) GetTemplate(sym string) (string, error) { + return m.templateFunc(sym) } -func(m *MenuResource) render(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) { - var ok bool - r := "" - s, err := m.RenderTemplate(sym, values, idx, sizer) - if err != nil { - return "", err - } - log.Printf("rendered %v bytes for template", len(s)) - r += s - if sizer != nil { - _, ok = sizer.Check(r) - if !ok { - return "", fmt.Errorf("limit exceeded: %v", sizer) - } - } - s, err = m.RenderMenu(idx) - if err != nil { - return "", err - } - log.Printf("rendered %v bytes for menu", len(s)) - r += s - if sizer != nil { - _, ok = sizer.Check(r) - if !ok { - return "", fmt.Errorf("limit exceeded: %v", sizer) - } - } - return r, nil -} - -func(m *MenuResource) Render(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) { - var err error - - values, err = m.prepare(sym, values, idx, sizer) - if err != nil { - return "", err - } - - return m.render(sym, values, idx, sizer) -} diff --git a/go/resource/resource_test.go b/go/resource/resource_test.go @@ -10,7 +10,7 @@ type TestSizeResource struct { *MenuResource } -func getTemplate(sym string, sizer *Sizer) (string, error) { +func getTemplate(sym string) (string, error) { var tpl string switch sym { case "small": diff --git a/go/resource/size.go b/go/resource/size.go @@ -1,98 +0,0 @@ -package resource - -import ( - "bytes" - "fmt" - "log" - "strings" - - "git.defalsify.org/festive/state" -) - -type Sizer struct { - outputSize uint32 - menuSize uint16 - memberSizes map[string]uint16 - totalMemberSize uint32 - crsrs []uint32 - sink string -} - -func SizerFromState(st *state.State) (Sizer, error){ - sz := Sizer{ - outputSize: st.GetOutputSize(), - menuSize: st.GetMenuSize(), - memberSizes: make(map[string]uint16), - } - - sizes, err := st.Sizes() - if err != nil { - return sz, err - } - for k, v := range sizes { - if v == 0 { - sz.sink = k - } - sz.memberSizes[k] = v - sz.totalMemberSize += uint32(v) - } - return sz, nil -} - -func(szr *Sizer) Check(s string) (uint32, bool) { - l := uint32(len(s)) - if szr.outputSize > 0 { - if l > szr.outputSize { - log.Printf("sizer check fails with length %v: %s", l, szr) - return 0, false - } - l = szr.outputSize - l - } - return l, true -} - -func(szr *Sizer) String() string { - var diff uint32 - if szr.outputSize > 0 { - diff = szr.outputSize - szr.totalMemberSize - uint32(szr.menuSize) - } - return fmt.Sprintf("output: %v, member: %v, menu: %v, diff: %v", szr.outputSize, szr.totalMemberSize, szr.menuSize, diff) -} - -func(szr *Sizer) Size(s string) (uint16, error) { - r, ok := szr.memberSizes[s] - if !ok { - return 0, fmt.Errorf("unknown member: %s", s) - } - return r, nil -} - -func(szr *Sizer) AddCursor(c uint32) { - log.Printf("added cursor: %v", c) - szr.crsrs = append(szr.crsrs, c) -} - -func(szr *Sizer) GetAt(values map[string]string, idx uint16) (map[string]string, error) { - if szr.sink == "" { - return values, nil - } - outValues := make(map[string]string) - for k, v := range values { - if szr.sink == k { - if idx >= uint16(len(szr.crsrs)) { - return nil, fmt.Errorf("no more values in index") - } - c := szr.crsrs[idx] - v = v[c:] - nl := strings.Index(v, "\n") - log.Printf("k %v v %v c %v nl %v", k, v, c, nl) - if nl > 0 { - v = v[:nl] - } - b := bytes.ReplaceAll([]byte(v), []byte{0x00}, []byte{0x0a}) - v = string(b) - } - outValues[k] = v - } - return outValues, nil -} diff --git a/go/resource/size_test.go b/go/resource/size_test.go @@ -1,121 +0,0 @@ -package resource - -import ( - "testing" - - "git.defalsify.org/festive/state" -) - -func TestSizeLimit(t *testing.T) { - st := state.NewState(0).WithOutputSize(128) - mrs := NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) - rs := TestSizeResource{ - mrs, - } - st.Down("test") - st.Add("foo", "inky", 4) - st.Add("bar", "pinky", 10) - st.Add("baz", "blinky", 0) - st.Map("foo") - st.Map("bar") - st.Map("baz") - st.SetMenuSize(32) - szr, err := SizerFromState(&st) - if err != nil { - t.Fatal(err) - } - - rs.PutMenu("1", "foo the foo") - rs.PutMenu("2", "go to bar") - - tpl, err := rs.GetTemplate("small", &szr) - if err != nil { - t.Fatal(err) - } - - vals, err := st.Get() - if err != nil { - t.Fatal(err) - } - _ = tpl - - _, err = rs.Render("small", vals, 0, &szr) - if err != nil { - t.Fatal(err) - } - - rs.PutMenu("1", "foo the foo") - rs.PutMenu("2", "go to bar") - - _, err = rs.Render("toobig", vals, 0, &szr) - if err == nil { - t.Fatalf("expected size exceeded") - } -} - -func TestSizePages(t *testing.T) { - st := state.NewState(0).WithOutputSize(128) - mrs := NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) - rs := TestSizeResource{ - mrs, - } - st.Down("test") - st.Add("foo", "inky", 4) - st.Add("bar", "pinky", 10) - st.Add("baz", "blinky", 20) - st.Add("xyzzy", "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", 0) - st.Map("foo") - st.Map("bar") - st.Map("baz") - st.Map("xyzzy") - st.SetMenuSize(32) - szr, err := SizerFromState(&st) - if err != nil { - t.Fatal(err) - } - - vals, err := st.Get() - if err != nil { - t.Fatal(err) - } - - rs.PutMenu("1", "foo the foo") - rs.PutMenu("2", "go to bar") - - r, err := rs.Render("pages", vals, 0, &szr) - if err != nil { - t.Fatal(err) - } - - expect := `one inky two pinky three blinky -inky pinky -blinky clyde sue -tinkywinky dipsy -lala poo` - - - if r != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) - } - - rs.PutMenu("1", "foo the foo") - rs.PutMenu("2", "go to bar") - - szr, err = SizerFromState(&st) - if err != nil { - t.Fatal(err) - } - r, err = rs.Render("pages", vals, 1, &szr) - if err != nil { - t.Fatal(err) - } - - expect = `one inky two pinky three blinky -one two three four five six seven -eight nine ten -eleven twelve` - if r != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) - } - -} diff --git a/go/resource/state.go b/go/resource/state.go @@ -27,18 +27,3 @@ func(sr *StateResource) WithState(st *state.State) *StateResource { sr.st = st return sr } - -func(sr *StateResource) SetMenuBrowse(selector string, title string, back bool) error { - var err error - next, prev := sr.st.Sides() - - if back { - if prev { - err = sr.Resource.SetMenuBrowse(selector, title, true) - } - } else if next { - err = sr.Resource.SetMenuBrowse(selector, title, false) - - } - return err -} diff --git a/go/resource/state_test.go b/go/resource/state_test.go @@ -12,90 +12,3 @@ func TestStateResourceInit(t *testing.T) { _ = ToStateResource(rs).WithState(&st) _ = NewStateResource(&st) } - -func TestStateBrowseNoSink(t *testing.T) { - st := state.NewState(0) - st.Down("root") - - rs := NewStateResource(&st) - rs.PutMenu("1", "foo") - rs.PutMenu("2", "bar") - err := rs.SetMenuBrowse("11", "next", false) - if err != nil { - t.Fatal(err) - } - err = rs.SetMenuBrowse("22", "prev", true) - if err != nil { - t.Fatal(err) - } - s, err := rs.RenderMenu(0) - if err != nil { - t.Fatal(err) - } - - expect := `1:foo -2:bar` - if s != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, s) - } -} - - -func TestStateBrowseSink(t *testing.T) { - st := state.NewState(0) - st.Down("root") - - rs := NewStateResource(&st) - rs.PutMenu("1", "foo") - rs.PutMenu("2", "bar") - err := rs.SetMenuBrowse("11", "next", false) - if err != nil { - t.Fatal(err) - } - err = rs.SetMenuBrowse("22", "prev", true) - if err != nil { - t.Fatal(err) - } - s, err := rs.RenderMenu(0) - if err != nil { - t.Fatal(err) - } - - expect := `1:foo -2:bar -11:next` - if s != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, s) - } - - idx, err := st.Next() - if err != nil { - t.Fatal(err) - } - if idx != 1 { - t.Fatalf("expected idx 1, got %v", idx) - } - rs = NewStateResource(&st) - rs.PutMenu("1", "foo") - rs.PutMenu("2", "bar") - err = rs.SetMenuBrowse("11", "next", false) - if err != nil { - t.Fatal(err) - } - err = rs.SetMenuBrowse("22", "prev", true) - if err != nil { - t.Fatal(err) - } - s, err = rs.RenderMenu(idx) - if err != nil { - t.Fatal(err) - } - - expect = `1:foo -2:bar -11:next -22:prev` - if s != expect { - t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, s) - } -}