WIP components (as built)
This commit is contained in:
96 files changed
+6468
-16
No files matched your search
@@ -0,0 +1,197 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"math"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// ArchiveEntry describes an archive member with a slash-separated Path.
|
||||
type ArchiveEntry struct {
|
||||
Path string
|
||||
Dir bool
|
||||
// Size is the unpacked size; Packed the compressed size.
|
||||
Size, Packed int64
|
||||
}
|
||||
|
||||
// ArchiveViewerState preserves folders, selection, sort and indexed entries.
|
||||
type ArchiveViewerState struct {
|
||||
table TreeTableState[string]
|
||||
index archiveIndex
|
||||
data *ArchiveEntry
|
||||
n int
|
||||
revision uint64
|
||||
}
|
||||
|
||||
// archiveIndex is the tree built from the entries.
|
||||
type archiveIndex struct {
|
||||
byPath map[string]*ArchiveEntry
|
||||
kids map[string][]string
|
||||
roots []string
|
||||
// sums totals a folder's files.
|
||||
sums map[string][2]int64
|
||||
files int
|
||||
size, pack int64
|
||||
}
|
||||
|
||||
// ArchiveViewerOptions configure an ArchiveViewer.
|
||||
type ArchiveViewerOptions struct {
|
||||
// Revision changes when entries are edited in place.
|
||||
Revision uint64
|
||||
// Status shows the caller's listing in progress or failed.
|
||||
Status DataStatus
|
||||
Error string
|
||||
}
|
||||
|
||||
// ArchiveViewerResult is the viewer and the entry opened this frame.
|
||||
type ArchiveViewerResult struct {
|
||||
Element *ui.Element
|
||||
Opened *ArchiveEntry
|
||||
Retried bool
|
||||
}
|
||||
|
||||
// ArchiveViewer virtualizes archive entries with sizes and compression ratios.
|
||||
func ArchiveViewer(c *ui.Context, entries []ArchiveEntry, s *ArchiveViewerState, opts ArchiveViewerOptions) ArchiveViewerResult {
|
||||
if s == nil {
|
||||
panic("mujica: ArchiveViewer needs a state")
|
||||
}
|
||||
if s.revision != opts.Revision {
|
||||
s.data = nil
|
||||
s.index.byPath = nil
|
||||
s.revision = opts.Revision
|
||||
}
|
||||
archiveBuild(entries, s)
|
||||
ix := &s.index
|
||||
u := Density(c).Unit()
|
||||
size := func(p string) (int64, int64) {
|
||||
if e := ix.byPath[p]; e != nil && !e.Dir {
|
||||
return e.Size, e.Packed
|
||||
}
|
||||
t := ix.sums[p]
|
||||
return t[0], t[1]
|
||||
}
|
||||
cell := func(text string) { codeMono(c, text).TextColor(Tokens(c).TextMuted) }
|
||||
var res ArchiveViewerResult
|
||||
res.Element = ui.Column(c).MinHeight(0).Gap(u * 2)
|
||||
if res.Element.IsDisabled() {
|
||||
copy := *s
|
||||
copy.table.Selection = filesSelectionCopy(s.table.Selection)
|
||||
copy.table.tree.outline.Open = filesSelectionCopy(s.table.tree.outline.Open)
|
||||
copy.table.Layout = ui.TableLayout{Order: slices.Clone(s.table.Layout.Order), Widths: maps.Clone(s.table.Layout.Widths)}
|
||||
copy.table.tree.outline.List.Columns = copy.table.Layout
|
||||
s = ©
|
||||
}
|
||||
res.Element.Children(func() {
|
||||
codeCaption(c, fmt.Sprintf(msg(c, "archive_viewer.summary"), ix.files, processListBytes(uint64(ix.size)),
|
||||
processListBytes(uint64(ix.pack)), archiveRatio(ix.size, ix.pack)))
|
||||
v := TreeTable(c, &s.table, TreeTableOptions[string]{
|
||||
Roots: ix.roots, Children: func(p string) []string { return ix.kids[p] },
|
||||
Label: func(p string) string { return path.Base(p) }, Status: opts.Status, Error: opts.Error,
|
||||
Empty: msg(c, "archive_viewer.empty"),
|
||||
Columns: []TreeColumn[string]{
|
||||
{ID: "name", Title: msg(c, "archive_viewer.name"), Compare: strings.Compare, Cell: func(p string) {
|
||||
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() {
|
||||
FileIcon(c, p, FileIconOptions{Folder: ix.kids[p] != nil})
|
||||
ui.Text(c, path.Base(p)).SingleLine()
|
||||
})
|
||||
}},
|
||||
{ID: "size", Title: msg(c, "archive_viewer.size"), Width: 96, Align: ui.End,
|
||||
Compare: func(a, b string) int { x, _ := size(a); y, _ := size(b); return cmp.Compare(x, y) },
|
||||
Cell: func(p string) { n, _ := size(p); cell(processListBytes(uint64(n))) }},
|
||||
{ID: "packed", Title: msg(c, "archive_viewer.packed"), Width: 96, Align: ui.End,
|
||||
Cell: func(p string) { _, n := size(p); cell(processListBytes(uint64(n))) }},
|
||||
{ID: "ratio", Title: msg(c, "archive_viewer.ratio"), Width: 72, Align: ui.End,
|
||||
Cell: func(p string) { a, b := size(p); cell(archiveRatio(a, b)) }},
|
||||
},
|
||||
})
|
||||
v.Element.Grow(1)
|
||||
res.Retried = v.Retried()
|
||||
if p, ok := s.table.Selected(); ok && v.Submitted() && !res.Element.IsDisabled() {
|
||||
if e := ix.byPath[p]; e != nil && !e.Dir {
|
||||
open := *e
|
||||
res.Opened = &open
|
||||
slog.Debug("mujica: archive_viewer open")
|
||||
}
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// archiveRatio formats the saved-size percentage.
|
||||
func archiveRatio(size, packed int64) string {
|
||||
if size <= 0 {
|
||||
return "—"
|
||||
}
|
||||
return fmt.Sprintf("%.0f%%", 100*(1-float64(packed)/float64(size)))
|
||||
}
|
||||
|
||||
// archiveBuild caches entries and implied parent directories.
|
||||
func archiveBuild(entries []ArchiveEntry, s *ArchiveViewerState) {
|
||||
var first *ArchiveEntry
|
||||
if len(entries) > 0 {
|
||||
first = &entries[0]
|
||||
}
|
||||
if s.data == first && s.n == len(entries) && s.index.byPath != nil {
|
||||
return
|
||||
}
|
||||
s.data, s.n = first, len(entries)
|
||||
ix := archiveIndex{byPath: map[string]*ArchiveEntry{}, kids: map[string][]string{}, sums: map[string][2]int64{}}
|
||||
var addDir func(p string)
|
||||
link := func(p string) {
|
||||
parent := path.Dir(p)
|
||||
if parent == "." {
|
||||
ix.roots = append(ix.roots, p)
|
||||
return
|
||||
}
|
||||
addDir(parent)
|
||||
ix.kids[parent] = append(ix.kids[parent], p)
|
||||
}
|
||||
addDir = func(p string) {
|
||||
if e := ix.byPath[p]; e != nil && !e.Dir {
|
||||
panic("mujica: ArchiveEntry file used as a folder: " + p)
|
||||
}
|
||||
if _, ok := ix.kids[p]; ok {
|
||||
return
|
||||
}
|
||||
ix.kids[p] = []string{}
|
||||
link(p)
|
||||
}
|
||||
for i := range entries {
|
||||
e := &entries[i]
|
||||
p := strings.Trim(e.Path, "/")
|
||||
if p == "" || e.Size < 0 || e.Packed < 0 {
|
||||
panic("mujica: ArchiveEntry needs a Path and sizes not negative")
|
||||
}
|
||||
if _, dup := ix.byPath[p]; dup {
|
||||
panic("mujica: duplicate ArchiveEntry " + p)
|
||||
}
|
||||
if !e.Dir && ix.kids[p] != nil {
|
||||
panic("mujica: ArchiveEntry folder used as a file: " + p)
|
||||
}
|
||||
ix.byPath[p] = e
|
||||
if e.Dir {
|
||||
addDir(p)
|
||||
continue
|
||||
}
|
||||
link(p)
|
||||
ix.files++
|
||||
if e.Size > math.MaxInt64-ix.size || e.Packed > math.MaxInt64-ix.pack {
|
||||
panic("mujica: ArchiveEntry totals overflow")
|
||||
}
|
||||
ix.size += e.Size
|
||||
ix.pack += e.Packed
|
||||
for d := path.Dir(p); d != "."; d = path.Dir(d) {
|
||||
t := ix.sums[d]
|
||||
ix.sums[d] = [2]int64{t[0] + e.Size, t[1] + e.Packed}
|
||||
}
|
||||
}
|
||||
s.index = ix
|
||||
slog.Debug("mujica: archive_viewer index", "files", ix.files)
|
||||
}
|
||||
Reference in new issue
Block a user