v0.32.0 lwconf engine update: config by path, schema from code, wasm
This commit is contained in:
1 parent
c2e3d669da
commit
a1e9d21d06
33 files changed
+1278
-421
No files matched your search
+8
-35
@@ -2,7 +2,6 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -164,7 +163,7 @@ func join(g ui.Wires, l layout, col int) {
|
||||
|
||||
// drawNode draws a source or sink: a tinted bar, its type and whether it
|
||||
// listens or dials, then a summary of its options. The chosen node's bar is
|
||||
// solid; a node the first problem names has its bar in the error style.
|
||||
// solid; a node a problem names has its bar in the error style.
|
||||
func (a *app) drawNode(r ui.Region, s spot, n compose.Node, tint ui.Style, chosen bool) {
|
||||
th := a.th
|
||||
bg, bar := th.Text, a.font.bar
|
||||
@@ -295,41 +294,15 @@ func show(v any) string {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
// problemPath reads the part a validation error names: its pipeline, node and
|
||||
// key, as config's key paths write them
|
||||
var problemPath = regexp.MustCompile(`^pipelines?\[(\d+)\](?:\.(plugin_sources|plugin_sinks)\[([^\]]*)\](?:\.config\.?([\w.]*))?|\.flow\.(\w+)(?:\[(\d+)\])?\.?([\w.]*))?`)
|
||||
|
||||
// locate reads a problem's part, and its text past the path
|
||||
func locate(err error) (pi int, n compose.Node, key, text string, ok bool) {
|
||||
at := problemPath.FindStringSubmatchIndex(err.Error())
|
||||
if at == nil {
|
||||
return 0, n, "", err.Error(), false
|
||||
}
|
||||
m := problemPath.FindStringSubmatch(err.Error())
|
||||
text = strings.TrimPrefix(err.Error()[at[1]:], ": ")
|
||||
pi, _ = strconv.Atoi(m[1])
|
||||
switch {
|
||||
case m[2] != "":
|
||||
n = compose.Node{Role: map[string]string{"plugin_sources": "source", "plugin_sinks": "sink"}[m[2]], ID: m[3]}
|
||||
key = m[4]
|
||||
case m[5] != "":
|
||||
n = compose.Node{Role: m[5]}
|
||||
if m[6] != "" {
|
||||
n.Index, _ = strconv.Atoi(m[6])
|
||||
}
|
||||
key = m[7]
|
||||
}
|
||||
return pi, n, key, text, true
|
||||
}
|
||||
|
||||
// faulty reports whether the first problem names this node, or a filter
|
||||
// when n is the filters stage
|
||||
// faulty reports whether a problem names this node, or a filter when n is
|
||||
// the filters stage
|
||||
func (a *app) faulty(n compose.Node) bool {
|
||||
if a.problem == nil {
|
||||
return false
|
||||
for _, p := range compose.Problems(a.problem) {
|
||||
if pi, m, _, _, ok := compose.Locate(p); ok && pi == a.pi && m.Role == n.Role && m.ID == n.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
pi, m, _, _, ok := locate(a.problem)
|
||||
return ok && pi == a.pi && m.Role == n.Role && m.ID == n.ID
|
||||
return false
|
||||
}
|
||||
|
||||
// where names a problem's part as the screen shows it: its pipeline when
|
||||
|
||||
@@ -265,7 +265,7 @@ func (f *form) draw(a *app, r ui.Region) {
|
||||
}
|
||||
summary, noteRows := wrap(f.summary, w, 2), len(wrap(f.err, w, 3))
|
||||
for _, p := range f.params {
|
||||
noteRows = max(noteRows, len(wrap(p.Help, w, 3)))
|
||||
noteRows = max(noteRows, len(wrap(p.Usage(), w, 3)))
|
||||
}
|
||||
top, bottom := len(summary)+min(1, len(summary)), noteRows+min(1, noteRows)
|
||||
in := a.frame(r, w, top+len(f.fields)*tall+bottom, f.title)
|
||||
@@ -281,7 +281,7 @@ func (f *form) draw(a *app, r ui.Region) {
|
||||
v.TextInput(f.fields[i], p.Default, i == f.focus.Index, th)
|
||||
}
|
||||
})
|
||||
note, style := f.params[f.focus.Index].Help, th.Muted
|
||||
note, style := f.params[f.focus.Index].Usage(), th.Muted
|
||||
if f.err != "" {
|
||||
note, style = f.err, th.Error
|
||||
}
|
||||
@@ -456,7 +456,7 @@ func help(f font) dialog {
|
||||
{"p", "start over from a preset"},
|
||||
{"r", "run the pipelines"},
|
||||
{"o", "run, or print a command line, environment or file"},
|
||||
{"!", "the first problem"},
|
||||
{"!", "every problem, moving to the first"},
|
||||
{"paste", "an lw command line replaces the pipelines"},
|
||||
{"q", "quit"},
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ func (a *app) keyLines(out []line, n compose.Node, keys []config.Key, prefix str
|
||||
case "table":
|
||||
out = append(out, line{node: n, key: k, depth: depth, group: name, item: -1})
|
||||
if a.insp.open[a.foldKey(n, k.Name)] {
|
||||
p, _ := a.plugin(n)
|
||||
out = a.keyLines(out, n, sided(a.tables[name], p.Side), k.Name+".", depth+1)
|
||||
out = a.keyLines(out, n, k.Keys, k.Name+".", depth+1)
|
||||
}
|
||||
case "list":
|
||||
out = append(out, line{node: n, key: k, depth: depth, group: name, item: -1})
|
||||
@@ -180,10 +179,10 @@ func (a *app) nodeTitle(n compose.Node) string {
|
||||
return n.Role + " · flow"
|
||||
}
|
||||
|
||||
// help is the focused row's problem, or its key's help and default
|
||||
// help is the focused row's first problem, or its key's help and default
|
||||
func (a *app) help(l line) (string, ui.Style) {
|
||||
if a.problem != nil {
|
||||
if pi, n, key, text, ok := locate(a.problem); ok && pi == a.pi && n == l.node && key == l.key.Name {
|
||||
for _, p := range compose.Problems(a.problem) {
|
||||
if pi, n, key, text, ok := compose.Locate(p); ok && pi == a.pi && n == l.node && key == l.key.Name {
|
||||
return text, a.th.Error
|
||||
}
|
||||
}
|
||||
@@ -264,7 +263,7 @@ func (a *app) groupSummary(l line) string {
|
||||
return show(v)
|
||||
case map[string]any:
|
||||
var out []string
|
||||
for _, k := range a.tables[l.group] {
|
||||
for _, k := range l.key.Keys {
|
||||
if s, ok := v[k.Name]; ok {
|
||||
out = append(out, k.Name+" "+show(s))
|
||||
}
|
||||
|
||||
+13
-21
@@ -85,7 +85,6 @@ type app struct {
|
||||
look look
|
||||
th ui.Theme
|
||||
font font
|
||||
tables map[string][]config.Key
|
||||
pi int // the pipeline shown
|
||||
col int // the chosen column
|
||||
row [3]int // the chosen node of each column
|
||||
@@ -96,14 +95,14 @@ type app struct {
|
||||
dialog dialog // takes every key while open
|
||||
status string // the last edit's refusal
|
||||
note string // what the last action did
|
||||
problem error // the composition's first problem
|
||||
problem error // the composition's problems, joined
|
||||
changed bool
|
||||
done bool
|
||||
result Result
|
||||
}
|
||||
|
||||
func newApp(c *compose.Composition, l look, f font) *app {
|
||||
a := &app{comp: c, look: l, th: l.Theme, font: f, tables: compose.NewSchema().Tables}
|
||||
a := &app{comp: c, look: l, th: l.Theme, font: f}
|
||||
a.th.Glyphs = f.Glyphs
|
||||
a.insp.open = map[string]bool{}
|
||||
if len(c.Pipelines) == 0 {
|
||||
@@ -167,10 +166,10 @@ func (a *app) plugin(n compose.Node) (config.Plugin, bool) {
|
||||
return config.LookupPlugin(n.Role, typ)
|
||||
}
|
||||
|
||||
// keys are the options a node takes: a plugin's of its side, a stage's
|
||||
// keys are the options a node takes: a plugin's, a stage's
|
||||
func (a *app) keys(n compose.Node) []config.Key {
|
||||
if p, ok := a.plugin(n); ok {
|
||||
return sided(p.Keys(), p.Side)
|
||||
return p.Keys()
|
||||
}
|
||||
for _, s := range config.FlowStages() {
|
||||
if s.Key == n.Role {
|
||||
@@ -180,18 +179,6 @@ func (a *app) keys(n compose.Node) []config.Key {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sided drops the keys of the other network side
|
||||
func sided(keys []config.Key, side config.Side) []config.Key {
|
||||
name := map[config.Side]string{config.Listener: "listener", config.ChainListener: "listener", config.Dialer: "dialer"}[side]
|
||||
var out []config.Key
|
||||
for _, k := range keys {
|
||||
if k.Side == "" || k.Side == name {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// edit applies one engine edit: a refusal is the status, a change revalidates
|
||||
func (a *app) edit(err error) bool {
|
||||
if err != nil {
|
||||
@@ -418,13 +405,13 @@ func (a *app) quit() {
|
||||
a.dialog = &confirm{question: "Quit without running or printing the pipelines?", yes: func(a *app) { a.done = true }}
|
||||
}
|
||||
|
||||
// showProblem chooses the part the first problem names and shows it whole
|
||||
// showProblem chooses the part the first problem names and shows them all
|
||||
func (a *app) showProblem() {
|
||||
if a.problem == nil {
|
||||
a.note = "the pipelines are valid"
|
||||
return
|
||||
}
|
||||
if pi, n, key, _, ok := locate(a.problem); ok && pi < len(a.comp.Pipelines) {
|
||||
if pi, n, key, _, ok := compose.Locate(compose.Problems(a.problem)[0]); ok && pi < len(a.comp.Pipelines) {
|
||||
a.pi = pi
|
||||
a.choose(n)
|
||||
a.inspectAt(func(l line) bool { return l.node == n && l.key.Name == key })
|
||||
@@ -539,7 +526,8 @@ func (a *app) paint(cells []terminal.Cell, w, h int) {
|
||||
}
|
||||
|
||||
// bottom is the status rows, wrapped to three at most: an edit's refusal, a
|
||||
// note, the first problem by its part, or the keys while all is valid
|
||||
// note, the first problem by its part and how many follow, or the keys while
|
||||
// all is valid
|
||||
func (a *app) bottom(w int) ([]string, ui.Style) {
|
||||
th, f := a.th, a.font
|
||||
text, style := "", th.Error
|
||||
@@ -549,10 +537,14 @@ func (a *app) bottom(w int) ([]string, ui.Style) {
|
||||
case a.note != "":
|
||||
text, style = a.note, th.Muted
|
||||
case a.problem != nil:
|
||||
pi, n, key, msg, _ := locate(a.problem)
|
||||
problems := compose.Problems(a.problem)
|
||||
pi, n, key, msg, _ := compose.Locate(problems[0])
|
||||
if where := a.where(pi, n, key); where != "" {
|
||||
msg = where + ": " + msg
|
||||
}
|
||||
if len(problems) > 1 {
|
||||
msg += fmt.Sprintf(" (%d more: ! shows them)", len(problems)-1)
|
||||
}
|
||||
text = f.invalid + " " + msg
|
||||
default:
|
||||
return []string{f.valid + " valid"}, th.Accent
|
||||
|
||||
@@ -399,18 +399,28 @@ func TestPasteReplacesThePipelines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The first problem marks the part it names, and ! moves there, to the key
|
||||
// Every problem marks the part it names and its key's row, the status counts
|
||||
// those after the first, and ! moves to the first, to the key
|
||||
func TestProblemNamesItsPart(t *testing.T) {
|
||||
a := mono(pipeline(t, []string{"null"}, []string{"null", "tcp"}))
|
||||
a := mono(pipeline(t, []string{"null"}, []string{"null", "tcp", "http"}))
|
||||
sink := compose.Node{Role: "sink", ID: "tcp"}
|
||||
if !a.faulty(sink) || a.faulty(compose.Node{Role: "source", ID: "null"}) || a.faulty(compose.Node{Role: "sink", ID: "null"}) {
|
||||
if !a.faulty(sink) || !a.faulty(compose.Node{Role: "sink", ID: "http"}) ||
|
||||
a.faulty(compose.Node{Role: "source", ID: "null"}) || a.faulty(compose.Node{Role: "sink", ID: "null"}) {
|
||||
t.Fatalf("problem %v", a.problem)
|
||||
}
|
||||
if status, _ := a.bottom(200); !strings.Contains(strings.Join(status, " "), "sink tcp · port: must be set (1 more") {
|
||||
t.Fatalf("status %q", status)
|
||||
}
|
||||
press(a, "!")
|
||||
n, _ := a.node()
|
||||
if l := a.lines()[a.insp.cursor]; n != sink || !a.inspect || l.key.Name != "port" {
|
||||
t.Fatalf("on %+v, row %+v", n, l)
|
||||
}
|
||||
a.choose(compose.Node{Role: "sink", ID: "http"})
|
||||
lines := a.lines()
|
||||
if text, style := a.help(lines[slices.IndexFunc(lines, func(l line) bool { return l.key.Name == "port" })]); text != "must be set" || style != a.th.Error {
|
||||
t.Fatalf("http's port row: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
// ! and c on the pipeline bar open the inspector, which then takes the keys:
|
||||
@@ -515,6 +525,19 @@ func presetForm(a *app, name string) *form {
|
||||
return f
|
||||
}
|
||||
|
||||
// A preset form's note shows the focused key's choices before its help
|
||||
func TestPresetFormShowsTheChoices(t *testing.T) {
|
||||
a := mono(pipeline(t, nil, nil))
|
||||
presetForm(a, "tail")
|
||||
press(a, terminal.KeyBacktab)
|
||||
ps := config.Presets()
|
||||
tail := ps[slices.IndexFunc(ps, func(p config.Preset) bool { return p.Name == "tail" })].Params
|
||||
format := tail[slices.IndexFunc(tail, func(p config.PresetParam) bool { return p.Name == "format" })]
|
||||
if screen := strings.Join(text(render(a, 80, 24), 80), "\n"); !strings.Contains(screen, format.Usage()) || len(format.Enum) == 0 {
|
||||
t.Fatalf("format's choices not shown:\n%s", screen)
|
||||
}
|
||||
}
|
||||
|
||||
// A preset form is usable at 25 columns: labels stack above their values,
|
||||
// which take the row, so typed text shows whole
|
||||
func TestPresetFormFitsANarrowScreen(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user