go-vise

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

commit ac6d8342ee5f47b4e58c71c2737b2ab54e76426b
parent 5f9f83bc5e0b7f8e283c96ddf3332e882959fef3
Author: lash <dev@holbrook.no>
Date:   Tue, 25 Apr 2023 09:28:58 +0100

WIP factor out page split

Diffstat:
Mrender/menu.go | 51++++++++++++++++++++++++++++++++++++++++++---------
Mrender/page.go | 138+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mrender/page_test.go | 4++--
Mrender/size.go | 27++++++++++++++-------------
Mrender/size_test.go | 8++++----
Arender/split.go | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arender/split_test.go | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 456 insertions(+), 88 deletions(-)

diff --git a/render/menu.go b/render/menu.go @@ -44,7 +44,8 @@ type Menu struct { pageCount uint16 // number of pages the menu should represent. canNext bool // availability flag for the "next" browse option. canPrevious bool // availability flag for the "previous" browse option. - outputSize uint16 // maximum size constraint for the menu. + //outputSize uint16 // maximum size constraint for the menu. + sink bool } // NewMenu creates a new Menu with an explicit page count. @@ -58,16 +59,23 @@ func(m *Menu) WithPageCount(pageCount uint16) *Menu { return m } -// WithSize defines the maximum byte size of the rendered menu. -func(m *Menu) WithOutputSize(outputSize uint16) *Menu { - m.outputSize = outputSize +func(m *Menu) WithPages() *Menu { + if m.pageCount == 0 { + m.pageCount = 1 + } return m } +// WithSize defines the maximum byte size of the rendered menu. +//func(m *Menu) WithOutputSize(outputSize uint16) *Menu { +// m.outputSize = outputSize +// return m +//} + // GetOutputSize returns the defined heuristic menu size. -func(m *Menu) GetOutputSize() uint32 { - return uint32(m.outputSize) -} +//func(m *Menu) GetOutputSize() uint32 { +// return uint32(m.outputSize) +//} // WithBrowseConfig defines the criteria for page browsing. func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu { @@ -87,8 +95,33 @@ func(m *Menu) Put(selector string, title string) error { } // ReservedSize returns the maximum render byte size of the menu. -func(m *Menu) ReservedSize() uint16 { - return m.outputSize +//func(m *Menu) ReservedSize() uint16 { +// return m.outputSize +//} + + // mainSize, prevsize, nextsize, nextsize+prevsize +func(m *Menu) Sizes() ([4]uint32, error) { + var menuSizes [4]uint32 + cfg := m.GetBrowseConfig() + tmpm := NewMenu().WithBrowseConfig(cfg) + v, err := tmpm.Render(0) + if err != nil { + return menuSizes, err + } + menuSizes[0] = uint32(len(v)) + tmpm = tmpm.WithPageCount(2) + v, err = tmpm.Render(0) + if err != nil { + return menuSizes, err + } + menuSizes[1] = uint32(len(v)) - menuSizes[0] + v, err = tmpm.Render(1) + if err != nil { + return menuSizes, err + } + menuSizes[2] = uint32(len(v)) - menuSizes[0] + menuSizes[3] = menuSizes[1] + menuSizes[2] + return menuSizes, nil } // Render returns the full current state of the menu as a string. diff --git a/render/page.go b/render/page.go @@ -34,18 +34,18 @@ func NewPage(cache cache.Memory, rs resource.Resource) *Page { // WithMenu sets a menu renderer for the page. func(pg *Page) WithMenu(menu *Menu) *Page { pg.menu = menu - if pg.sizer != nil { - pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize()) - } + //if pg.sizer != nil { + // pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize()) + //} return pg } // WithSizer sets a size constraints definition for the page. func(pg *Page) WithSizer(sizer *Sizer) *Page { pg.sizer = sizer - if pg.menu != nil { - pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize()) - } + //if pg.menu != nil { + //pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize()) + //} return pg } @@ -77,9 +77,9 @@ func(pg *Page) Usage() (uint32, uint32, error) { } r := uint32(l) rsv := uint32(c)-r - if pg.menu != nil { - r += uint32(pg.menu.ReservedSize()) - } + //if pg.menu != nil { + // r += uint32(pg.menu.ReservedSize()) + //} return r, rsv, nil } @@ -206,79 +206,50 @@ func(pg *Page) Reset() { } } - -// render menu and all syms except sink, split sink into display chunks -// TODO: Function too long, split up -func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string, idx uint16) (map[string]string, error) { +// extract sink values to separate array, and set the content of sink in values map to zero-length string. +// +// this allows render of page with emptry content the sink symbol to discover remaining capacity. +func(pg *Page) split(sym string, values map[string]string) (map[string]string, string, []string, error) { var sink string - - if pg.sizer == nil { - return values, nil - } - 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 + return nil, "", nil, err } if sz == 0 { sink = k sinkValues = strings.Split(v, "\n") v = "" - Logg.Infof("found sink", "sym", k, "fields", len(sinkValues)) + Logg.Infof("found sink", "sym", sym, "sink", k) } noSinkValues[k] = v } if sink == "" { Logg.Tracef("no sink found", "sym", sym) - return values, nil - } - - pg.sizer.AddCursor(0) - s, err := pg.render(ctx, sym, noSinkValues, 0) - if err != nil { - return nil, err - } - // remaining includes core menu - remaining, ok := pg.sizer.Check(s) - if !ok { - return nil, fmt.Errorf("capacity exceeded") - } - - var menuSizes [4]uint32 // mainSize, prevsize, nextsize, nextsize+prevsize - if pg.menu != nil { - cfg := pg.menu.GetBrowseConfig() - tmpm := NewMenu().WithBrowseConfig(cfg) - v, err := tmpm.Render(0) - if err != nil { - return nil, err - } - menuSizes[0] = uint32(len(v)) - tmpm = tmpm.WithPageCount(2) - v, err = tmpm.Render(0) - if err != nil { - return nil, err - } - menuSizes[1] = uint32(len(v)) - menuSizes[0] - v, err = tmpm.Render(1) - if err != nil { - return nil, err - } - menuSizes[2] = uint32(len(v)) - menuSizes[0] - menuSizes[3] = menuSizes[1] + menuSizes[2] + return values, "", nil, nil } + return noSinkValues, sink, sinkValues, nil +} - Logg.Debugf("calculated pre-navigation allocation", "bytes", remaining) - +// flatten the sink values array into a paged string. +// +// newlines (within the same page) render are defined by NUL (0x00). +// +// pages are separated by LF (0x0a). +func(pg *Page) joinSink(sinkValues []string, remaining uint32, menuSizes [4]uint32) (string, uint16, error) { l := 0 var count uint16 tb := strings.Builder{} rb := strings.Builder{} + // remaining is remaining less one LF netRemaining := remaining - 1 + + // BUG: this reserves the previous browse before we know we need it if len(sinkValues) > 1 { netRemaining -= menuSizes[1] - 1 } @@ -288,7 +259,7 @@ func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string Logg.Tracef("processing sink", "idx", i, "value", v) if uint32(l) > netRemaining - 1 { if tb.Len() == 0 { - return nil, fmt.Errorf("capacity insufficient for sink field %v", i) + return "", 0, fmt.Errorf("capacity insufficient for sink field %v", i) } rb.WriteString(tb.String()) rb.WriteRune('\n') @@ -315,14 +286,61 @@ func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string r := rb.String() r = strings.TrimRight(r, "\n") + return r, count, nil +} + +// render menu and all syms except sink, split sink into display chunks +func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string, idx uint16) (map[string]string, error) { + if pg.sizer == nil { + return values, nil + } + + // extract sink values + noSinkValues, sink, sinkValues, err := pg.split(sym, values) + + // check if menu is sink aswell, fail if it is. + if pg.menu != nil { + // if pg.menu.ReservedSize() + } + + // pre-render template without sink + // this includes the menu before any browsing options have been added + pg.sizer.AddCursor(0) + s, err := pg.render(ctx, sym, noSinkValues, 0) + if err != nil { + return nil, err + } + + // this is the available bytes left for sink content and browse menu + remaining, ok := pg.sizer.Check(s) + if !ok { + return nil, fmt.Errorf("capacity exceeded") + } - noSinkValues[sink] = r + // pre-calculate the menu sizes for all browse conditions + var menuSizes [4]uint32 + if pg.menu != nil { + menuSizes, err = pg.menu.Sizes() + if err != nil { + return nil, err + } + } + Logg.Debugf("calculated pre-navigation allocation", "bytes", remaining, "menusizes", menuSizes) + + // process sink values array into newline-separated string + sinkString, count, err := pg.joinSink(sinkValues, remaining, menuSizes) + if err != nil { + return nil, err + } + noSinkValues[sink] = sinkString + // update the page count of the menu if pg.menu != nil { pg.menu = pg.menu.WithPageCount(count) } - for i, v := range strings.Split(r, "\n") { + // write all sink values to log. + for i, v := range strings.Split(sinkString, "\n") { Logg.Tracef("nosinkvalue", "idx", i, "value", v) } diff --git a/render/page_test.go b/render/page_test.go @@ -55,7 +55,7 @@ func TestPageCurrentSize(t *testing.T) { t.Errorf("expected remaining length 34, got %v", c) } - mn := NewMenu().WithOutputSize(32) + mn := NewMenu() //.WithOutputSize(32) pg = pg.WithMenu(mn) l, c, err = pg.Usage() if err != nil { @@ -124,7 +124,7 @@ func TestWithError(t *testing.T) { pg := NewPage(ca, rs) ca.Push() - mn := NewMenu().WithOutputSize(32) + mn := NewMenu() //.WithOutputSize(32) err := mn.Put("0", "aiee") if err != nil { t.Fatal(err) diff --git a/render/size.go b/render/size.go @@ -9,7 +9,7 @@ import ( // Sizer splits dynamic contents into individual segments for browseable pages. type Sizer struct { outputSize uint32 // maximum output for a single page. - menuSize uint16 // actual menu size for the dynamic page being sized +// menuSize uint16 // actual menu size for the dynamic page being sized memberSizes map[string]uint16 // individual byte sizes of all content to be rendered by template. totalMemberSize uint32 // total byte size of all content to be rendered by template (sum of memberSizes) crsrs []uint32 // byte offsets in the sink content for browseable pages indices. @@ -25,10 +25,10 @@ func NewSizer(outputSize uint32) *Sizer { } // WithMenuSize sets the size of the menu being used in the rendering context. -func(szr *Sizer) WithMenuSize(menuSize uint16) *Sizer { - szr.menuSize = menuSize - return szr -} +//func(szr *Sizer) WithMenuSize(menuSize uint16) *Sizer { +// szr.menuSize = menuSize +// return szr +//} // Set adds a content symbol in the state it will be used by the renderer. func(szr *Sizer) Set(key string, size uint16) error { @@ -56,11 +56,12 @@ func(szr *Sizer) Check(s string) (uint32, bool) { // String implements the String interface. 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) +// 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) + return fmt.Sprintf("output: %v, member: %v", szr.outputSize, szr.totalMemberSize) } // Size gives the byte size of content for a single symbol. @@ -75,9 +76,9 @@ func(szr *Sizer) Size(s string) (uint16, error) { } // Menusize returns the currently defined menu size. -func(szr *Sizer) MenuSize() uint16 { - return szr.menuSize -} +//func(szr *Sizer) MenuSize() uint16 { +// return szr.menuSize +//} // AddCursor adds a pagination cursor for the paged sink content. func(szr *Sizer) AddCursor(c uint32) { diff --git a/render/size_test.go b/render/size_test.go @@ -129,7 +129,7 @@ func TestSizeCheck(t *testing.T) { func TestSizeLimit(t *testing.T) { st := state.NewState(0) ca := cache.NewCache() - mn := NewMenu().WithOutputSize(32) + mn := NewMenu() //.WithOutputSize(32) //mrs := NewMenuResource() //.WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) //rs := TestSizeResource{ // mrs, @@ -182,7 +182,7 @@ func TestSizeLimit(t *testing.T) { func TestSizePages(t *testing.T) { st := state.NewState(0) ca := cache.NewCache() - mn := NewMenu().WithOutputSize(32) + mn := NewMenu() //.WithOutputSize(32) //mrs := NewMenuResource() //.WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) //rs := TestSizeResource{ // mrs, @@ -243,7 +243,7 @@ func TestManySizes(t *testing.T) { for i := 60; i < 160; i++ { st := state.NewState(0) ca := cache.NewCache() - mn := NewMenu().WithOutputSize(32) + mn := NewMenu() //.WithOutputSize(32) rs := NewTestSizeResource() //.WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) //rs := TestSizeResource{ // mrs, @@ -273,7 +273,7 @@ func TestManySizesMenued(t *testing.T) { for i := 60; i < 160; i++ { st := state.NewState(0) ca := cache.NewCache() - mn := NewMenu().WithOutputSize(32) + mn := NewMenu() //.WithOutputSize(32) rs := NewTestSizeResource() szr := NewSizer(uint32(i)) pg := NewPage(ca, rs).WithSizer(szr).WithMenu(mn) diff --git a/render/split.go b/render/split.go @@ -0,0 +1,207 @@ +package render + +import ( + "fmt" + "log" + "strings" +) + + +func bookmark(values []string) []uint32 { + var c int + var bookmarks []uint32 = []uint32{0} + + for _, v := range values { + c += len(v) + 1 + bookmarks = append(bookmarks, uint32(c)) + } + return bookmarks +} +// +//func paginate(bookmarks []uint32, capacity uint32) ([][]uint32, error) { +// if len(bookmarks) == 0 { +// return nil, fmt.Errorf("empty bookmark array") +// } +// var c uint32 +// var pages [][]uint32 +// var prev uint32 +// +// pages = append(pages, []uint32{}) +// currentPage := 0 +// lookAhead := bookmarks[1:] +// +// for i, v := range lookAhead { +// lc := v - c +// if lc > capacity { +// c = prev +// if lc - c > capacity { +// return nil, fmt.Errorf("value at %v alone exceeds capacity", i) +// } +// currentPage += 1 +// pages = append(pages, []uint32{}) +// } +// pages[currentPage] = append(pages[currentPage], bookmarks[i]) +// prev = v +// } +// +// pages[currentPage] = append(pages[currentPage], bookmarks[len(bookmarks)-1]) +// return pages, nil +//} + +func isLast(cursor uint32, end uint32, capacity uint32) bool { + l := end - cursor + remaining := capacity + return l <= remaining +} + +func paginate(bookmarks []uint32, capacity uint32, nextSize uint32, prevSize uint32) ([][]uint32, error) { + if len(bookmarks) == 0 { + return nil, fmt.Errorf("empty page array") + } + + var pages [][]uint32 + var c uint32 + lastIndex := len(bookmarks) - 1 + last := bookmarks[lastIndex] + var haveMore bool + + if isLast(0, last, capacity) { + pages = append(pages, bookmarks) + return pages, nil + } + + lookAhead := bookmarks[1:] + pages = append(pages, []uint32{}) + var i int + + haveMore = true + for haveMore { + remaining := int(capacity) + if i > 0 { + remaining -= int(prevSize) + } + if remaining < 0 { + return nil, fmt.Errorf("underrun in item %v:%v (%v) index %v prevsize %v remain %v cap %v", bookmarks[i], lookAhead[i], lookAhead[i] - bookmarks[i], i, prevSize, remaining, capacity) + } + if isLast(c, last, uint32(remaining)) { + haveMore = false + } else { + remaining -= int(nextSize) + } + if remaining < 0 { + return nil, fmt.Errorf("underrun in item %v:%v (%v) index %v prevsize %v nextsize %v remain %v cap %v", bookmarks[i], lookAhead[i], lookAhead[i] - bookmarks[i], i, prevSize, nextSize, remaining, capacity) + } + + var z int + currentPage := len(pages) - 1 + for i < lastIndex { + log.Printf("have item %v:%v (%v) index %v prevsize %v nextsize %v remain %v cap %v", bookmarks[i], lookAhead[i], lookAhead[i] - bookmarks[i], i, prevSize, nextSize, remaining, capacity) + + v := lookAhead[i] + delta := int((v - c) + 1) + if z == 0 { + if delta > remaining { + return nil, fmt.Errorf("single value at %v exceeds capacity", i) + } + } + z += delta + if z > remaining { + break + } + pages[currentPage] = append(pages[currentPage], bookmarks[i]) + c = v + i += 1 + } + log.Printf("more %v remaining %v c %v last %v pages %v", haveMore, remaining, c, last, pages) + + if haveMore { + pages = append(pages, []uint32{}) + } + } + + l := len(pages)-1 + pages[l] = append(pages[l], last) + return pages, nil +} + +func explode(values []string, pages [][]uint32) string { + s := strings.Join(values, "") + s += "\n" + sb := strings.Builder{} + + var start uint32 + var end uint32 + var lastPage int + var z uint32 + for i, page := range pages { + for _, c := range page { + if c == 0 { + continue + } + z += 1 + if i != lastPage { + sb.WriteRune('\n') + } else if c > 0 { + sb.WriteByte(byte(0x00)) + } + end = c - z + v := s[start:end] + log.Printf("page %v part %v %v %s", i, start, end, v) + v = s[start:end] + sb.WriteString(v) + start = end + } + lastPage = i + } + r := sb.String() + r = strings.TrimRight(r, "\n") + return r +} + +// if lastCursor <= capacity { +// return pages, nil +// } +// +// var flatPages [][]uint32 +// +// pages = append(pages, []uint32{}) +// for _, v := range bookmarks { +// for _, vv := range v { +// pages[0] = append(pages[0], vv) +// } +// } +// +// var c uint32 +// var prev uint32 +// currentPage := 0 +// +// for i, page := range pages { +// var delta uint32 +// if i == 0 { +// delta = nextSize +// } else if i == len(pages) - 1 { +// delta = prevSize +// } else { +// delta = nextSize + prevSize +// } +// remaining := capacity - delta +// log.Printf("processing page %v", page) +// lookAhead := page[1:] +// +// for j, v := range lookAhead { +// lc := v - c +// log.Printf("currentpage j %v lc %v v %v remain %v", j, lc, v, remaining) +// if lc > remaining { +// c = prev +// if lc - c > remaining { +// return nil, fmt.Errorf("value at page %v idx %v alone exceeds capacity", i, v) +// } +// currentPage += 1 +// page = append(page, []uint32{}) +// } +// page[currentPage] = append(page[currentPage], page[j]) +// prev = v +// } +// } +// return page, nil +//} diff --git a/render/split_test.go b/render/split_test.go @@ -0,0 +1,109 @@ +package render + +import ( +// "bytes" +// "log" + "testing" +) + +func TestSplitBookmark(t *testing.T) { + vals := []string{"inky", "pinky", "blinky", "clyde"} + r := bookmark(vals) + expect := []uint32{0, 5, 11, 18, 24} + for i, v := range expect { + if r[i] != v { + t.Fatalf("expected val %v cursor %v, got %v", i, v, r[i]) + } + } +} + +func TestSplitPaginate(t *testing.T) { + vals := []string{"inky", "pinky", "blinky", "clyde"} + v := bookmark(vals) + r, err := paginate(v, 15, 0, 0) + if err != nil { + t.Fatal(err) + } + if len(r) != 2 { + t.Fatalf("expected bookmark len 2, got %v", len(r)) + } + expect := []uint32{0, 5} + if len(r[0]) != len(expect) { + t.Fatalf("expected page 1 len %v, got %v", len(expect), len(r[0])) + } + for i, v := range expect { + if r[0][i] != v { + t.Fatalf("expected page 1 val %v cursor %v, got %v", i, v, r[0][i]) + } + } + expect = []uint32{11, 18, 24} + if len(r[1]) != len(expect) { + t.Fatalf("expected page 2 len %v, got %v", len(expect), len(r[1])) + } + for i, v := range expect { + if r[1][i] != v { + t.Fatalf("expected page 2 val %v cursor %v, got %v", i, v, r[1][i]) + } + } +} + +//func TestSplitMenuPaginate(t *testing.T) { +// menuCfg := DefaultBrowseConfig() +// menu := NewMenu().WithBrowseConfig(menuCfg) +// menu.Put("0", "foo") +// menu.Put("1", "bar") +// +// vals := []string{"inky", "pinky", "blinky", "clyde", "tinkywinky", "dipsy", "lala", "pu"} +// v := bookmark(vals) +//// vv, err := paginate(v, 15, 0, 0) +//// if err != nil { +//// t.Fatal(err) +//// } +// +// menu = menu.WithPages() +// menuSizes, err := menu.Sizes() +// log.Printf("sizes %v", menuSizes) +// if err != nil { +// t.Fatal(err) +// } +// r, err := paginate(v, 30, menuSizes[1], menuSizes[2]) +// if err != nil { +// t.Fatal(err) +// } +// expect := [][]uint32{ +// []uint32{0, 5, 11}, +// []uint32{18}, +// []uint32{24}, +// []uint32{35, 41, 46}, +// } +// if len(r) != len(expect) { +// t.Fatalf("expected page 1 len %v, got %v", len(expect), len(r)) +// } +// for i, v := range expect { +// for j, vv := range v { +// if r[i][j] != vv { +// t.Fatalf("value mismatch in [%v][%v]", i, j) +// } +// } +// } +// +// s := explode(vals, r) +// expectBytes := append([]byte("inky"), byte(0x00)) +// expectBytes = append(expectBytes, []byte("pinky")...) +// expectBytes = append(expectBytes, byte(0x00)) +// expectBytes = append(expectBytes, []byte("blinky")...) +// expectBytes = append(expectBytes, byte(0x0a)) +// expectBytes = append(expectBytes, []byte("clyde")...) +// expectBytes = append(expectBytes, byte(0x0a)) +// expectBytes = append(expectBytes, []byte("tinkywinky")...) +// expectBytes = append(expectBytes, byte(0x0a)) +// expectBytes = append(expectBytes, []byte("dipsy")...) +// expectBytes = append(expectBytes, byte(0x00)) +// expectBytes = append(expectBytes, []byte("lala")...) +// expectBytes = append(expectBytes, byte(0x00)) +// expectBytes = append(expectBytes, []byte("pu")...) +// +// if !bytes.Equal([]byte(s), expectBytes) { +// t.Fatalf("expected:\n\t%s\ngot:\n\t%x\n", expectBytes, s) +// } +//}