diff --git a/pkg/etk/combobox.go b/pkg/etk/combobox.go deleted file mode 100644 index 398079b60..000000000 --- a/pkg/etk/combobox.go +++ /dev/null @@ -1,34 +0,0 @@ -package etk - -import ( - "src.elv.sh/pkg/cli/term" -) - -func ComboBox(c Context) (View, React) { - filterView, filterReact := c.Subcomp("filter", TextArea) - filterBufferVar := BindState(c, "filter/buffer", TextBuffer{}) - listView, listReact := c.Subcomp("list", ListBox) - listItemsVar := BindState(c, "list/items", ListItems(nil)) - listSelectedVar := BindState(c, "list/selected", 0) - - genListVar := State(c, "gen-list", func(string) (ListItems, int) { - return nil, -1 - }) - lastFilterContentVar := State(c, "-last-filter-content", "") - - return VBoxView(0, filterView, listView), - c.WithBinding(func(ev term.Event) Reaction { - if reaction := filterReact(ev); reaction != Unused { - filterContent := filterBufferVar.Get().Content - if filterContent != lastFilterContentVar.Get() { - lastFilterContentVar.Set(filterContent) - items, selected := genListVar.Get()(filterContent) - listItemsVar.Set(items) - listSelectedVar.Set(selected) - } - return reaction - } else { - return listReact(ev) - } - }) -} diff --git a/pkg/etk/comps/combobox.go b/pkg/etk/comps/combobox.go new file mode 100644 index 000000000..ccb698a0c --- /dev/null +++ b/pkg/etk/comps/combobox.go @@ -0,0 +1,35 @@ +package comps + +import ( + "src.elv.sh/pkg/cli/term" + "src.elv.sh/pkg/etk" +) + +func ComboBox(c etk.Context) (etk.View, etk.React) { + filterView, filterReact := c.Subcomp("filter", TextArea) + filterBufferVar := etk.BindState(c, "filter/buffer", TextBuffer{}) + listView, listReact := c.Subcomp("list", ListBox) + listItemsVar := etk.BindState(c, "list/items", ListItems(nil)) + listSelectedVar := etk.BindState(c, "list/selected", 0) + + genListVar := etk.State(c, "gen-list", func(string) (ListItems, int) { + return nil, -1 + }) + lastFilterContentVar := etk.State(c, "-last-filter-content", "") + + return etk.VBoxView(0, filterView, listView), + c.WithBinding(func(ev term.Event) etk.Reaction { + if reaction := filterReact(ev); reaction != etk.Unused { + filterContent := filterBufferVar.Get().Content + if filterContent != lastFilterContentVar.Get() { + lastFilterContentVar.Set(filterContent) + items, selected := genListVar.Get()(filterContent) + listItemsVar.Set(items) + listSelectedVar.Set(selected) + } + return reaction + } else { + return listReact(ev) + } + }) +} diff --git a/pkg/etk/listbox.go b/pkg/etk/comps/listbox.go similarity index 77% rename from pkg/etk/listbox.go rename to pkg/etk/comps/listbox.go index a5350e84d..f58243a73 100644 --- a/pkg/etk/listbox.go +++ b/pkg/etk/comps/listbox.go @@ -1,7 +1,8 @@ -package etk +package comps import ( "src.elv.sh/pkg/cli/term" + "src.elv.sh/pkg/etk" "src.elv.sh/pkg/ui" ) @@ -20,10 +21,10 @@ func StringItems(items ...string) ListItems { return stringItems(items) } func (si stringItems) Show(i int) ui.Text { return ui.T(si[i]) } func (si stringItems) Len() int { return len(si) } -func ListBox(c Context) (View, React) { - itemsVar := State(c, "items", ListItems(nil)) - selectedVar := State(c, "selected", 0) - horizontalVar := State(c, "horizontal", false) +func ListBox(c etk.Context) (etk.View, etk.React) { + itemsVar := etk.State(c, "items", ListItems(nil)) + selectedVar := etk.State(c, "selected", 0) + horizontalVar := etk.State(c, "horizontal", false) selected := selectedVar.Get() focus := 0 @@ -46,8 +47,8 @@ func ListBox(c Context) (View, React) { } } - return TextView(focus, spans...), - c.WithBinding(func(e term.Event) Reaction { + return etk.TextView(focus, spans...), + c.WithBinding(func(e term.Event) etk.Reaction { selected := selectedVar.Get() items := itemsVar.Get() if horizontalVar.Get() { @@ -55,12 +56,12 @@ func ListBox(c Context) (View, React) { case term.K(ui.Left): if selected > 0 { selectedVar.Set(selected - 1) - return Consumed + return etk.Consumed } case term.K(ui.Right): if selected < items.Len()-1 { selectedVar.Set(selected + 1) - return Consumed + return etk.Consumed } } } else { @@ -68,15 +69,15 @@ func ListBox(c Context) (View, React) { case term.K(ui.Up): if selected > 0 { selectedVar.Set(selected - 1) - return Consumed + return etk.Consumed } case term.K(ui.Down): if selected < items.Len()-1 { selectedVar.Set(selected + 1) - return Consumed + return etk.Consumed } } } - return Unused + return etk.Unused }) } diff --git a/pkg/etk/textarea.go b/pkg/etk/comps/textarea.go similarity index 74% rename from pkg/etk/textarea.go rename to pkg/etk/comps/textarea.go index e3aa6952c..e6249a301 100644 --- a/pkg/etk/textarea.go +++ b/pkg/etk/comps/textarea.go @@ -1,4 +1,4 @@ -package etk +package comps import ( "strings" @@ -6,19 +6,20 @@ import ( "unicode/utf8" "src.elv.sh/pkg/cli/term" + "src.elv.sh/pkg/etk" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) -func TextArea(c Context) (View, React) { - quotePasteVar := State(c, "quote-paste", false) +func TextArea(c etk.Context) (etk.View, etk.React) { + quotePasteVar := etk.State(c, "quote-paste", false) - pastingVar := State(c, "pasting", false) - pasteBufferVar := State(c, "paste-buffer", &strings.Builder{}) + pastingVar := etk.State(c, "pasting", false) + pasteBufferVar := etk.State(c, "paste-buffer", &strings.Builder{}) innerView, innerReact := textAreaWithAbbr(c) - bufferVar := BindState(c, "buffer", TextBuffer{}) + bufferVar := etk.BindState(c, "buffer", TextBuffer{}) - return innerView, c.WithBinding(func(event term.Event) Reaction { + return innerView, c.WithBinding(func(event term.Event) etk.Reaction { switch event := event.(type) { case term.PasteSetting: startPaste := bool(event) @@ -36,7 +37,7 @@ func TextArea(c Context) (View, React) { } bufferVar.Swap(insertAtDot(text)) } - return Consumed + return etk.Consumed case term.KeyEvent: key := ui.Key(event) if pastingVar.Get() { @@ -46,26 +47,26 @@ func TextArea(c Context) (View, React) { } else { pasteBufferVar.Get().WriteRune(key.Rune) } - return Consumed + return etk.Consumed } } return innerReact(event) }) } -func textAreaWithAbbr(c Context) (View, React) { - abbrVar := State(c, "abbr", func(func(a, f string)) {}) - cmdAbbrVar := State(c, "command-abbr", func(func(a, f string)) {}) - smallWordAbbr := State(c, "small-word-abbr", func(func(a, f string)) {}) +func textAreaWithAbbr(c etk.Context) (etk.View, etk.React) { + abbrVar := etk.State(c, "abbr", func(func(a, f string)) {}) + cmdAbbrVar := etk.State(c, "command-abbr", func(func(a, f string)) {}) + smallWordAbbr := etk.State(c, "small-word-abbr", func(func(a, f string)) {}) - streakVar := State(c, "streak", "") + streakVar := etk.State(c, "streak", "") innerView, innerReact := textAreaCore(c) - bufferVar := BindState(c, "buffer", TextBuffer{}) - return innerView, func(event term.Event) Reaction { + bufferVar := etk.BindState(c, "buffer", TextBuffer{}) + return innerView, func(event term.Event) etk.Reaction { if keyEvent, ok := event.(term.KeyEvent); ok { bufferBefore := bufferVar.Get() reaction := innerReact(event) - if reaction != Consumed { + if reaction != etk.Consumed { return reaction } buffer := bufferVar.Get() @@ -74,23 +75,23 @@ func textAreaWithAbbr(c Context) (View, React) { if newBuffer, ok := expandSimpleAbbr(abbrVar.Get(), buffer, streak); ok { bufferVar.Set(newBuffer) streakVar.Set("") - return Consumed + return etk.Consumed } if newBuffer, ok := expandCmdAbbr(cmdAbbrVar.Get(), buffer, streak); ok { bufferVar.Set(newBuffer) streakVar.Set("") - return Consumed + return etk.Consumed } if newBuffer, ok := expandSmallWordAbbr(smallWordAbbr.Get(), buffer, streak, keyEvent.Rune, categorizeSmallWord); ok { bufferVar.Set(newBuffer) streakVar.Set("") - return Consumed + return etk.Consumed } streakVar.Set(streak) } else { streakVar.Set("") } - return Consumed + return etk.Consumed } else { return innerReact(event) } @@ -111,12 +112,12 @@ func isLiteralInsert(event term.KeyEvent, before, after TextBuffer) (string, boo } } -func textAreaCore(c Context) (View, React) { - promptVar := State(c, "prompt", ui.T("")) - rpromptVar := State(c, "rprompt", ui.T("")) - bufferVar := State(c, "buffer", TextBuffer{}) - pendingVar := State(c, "pending", PendingText{}) - highlighterVar := State(c, "highlighter", +func textAreaCore(c etk.Context) (etk.View, etk.React) { + promptVar := etk.State(c, "prompt", ui.T("")) + rpromptVar := etk.State(c, "rprompt", ui.T("")) + bufferVar := etk.State(c, "buffer", TextBuffer{}) + pendingVar := etk.State(c, "pending", PendingText{}) + highlighterVar := etk.State(c, "highlighter", func(code string) (ui.Text, []ui.Text) { return ui.T(code), nil }) buffer := bufferVar.Get() @@ -133,7 +134,7 @@ func textAreaCore(c Context) (View, React) { promptVar.Get(), rpromptVar.Get(), styledCode, bufferVar.Get().Dot, tips, } - return view, func(event term.Event) Reaction { + return view, func(event term.Event) etk.Reaction { if event, ok := event.(term.KeyEvent); ok { key := ui.Key(event) // Implement the absolute essential functionalities here. Others @@ -141,17 +142,17 @@ func textAreaCore(c Context) (View, React) { switch key { case ui.K(ui.Backspace), ui.K('H', ui.Ctrl): bufferVar.Swap(backspace) - return Consumed + return etk.Consumed case ui.K(ui.Enter): - return Finish + return etk.Finish default: if key == ui.K(ui.Enter, ui.Alt) || (!isFuncKey(key) && unicode.IsGraphic(key.Rune)) { bufferVar.Swap(insertAtDot(string(key.Rune))) - return Consumed + return etk.Consumed } } } - return Unused + return etk.Unused } } diff --git a/pkg/etk/textarea_abbr.go b/pkg/etk/comps/textarea_abbr.go similarity index 99% rename from pkg/etk/textarea_abbr.go rename to pkg/etk/comps/textarea_abbr.go index 58eba1ad9..511c92637 100644 --- a/pkg/etk/textarea_abbr.go +++ b/pkg/etk/comps/textarea_abbr.go @@ -1,4 +1,4 @@ -package etk +package comps import ( "regexp" diff --git a/pkg/etk/textarea_test.elvts b/pkg/etk/comps/textarea_test.elvts similarity index 100% rename from pkg/etk/textarea_test.elvts rename to pkg/etk/comps/textarea_test.elvts diff --git a/pkg/etk/textarea_view.go b/pkg/etk/comps/textarea_view.go similarity index 99% rename from pkg/etk/textarea_view.go rename to pkg/etk/comps/textarea_view.go index 1312bc7ad..3071bf54d 100644 --- a/pkg/etk/textarea_view.go +++ b/pkg/etk/comps/textarea_view.go @@ -1,4 +1,4 @@ -package etk +package comps import ( "src.elv.sh/pkg/cli/term" diff --git a/pkg/etk/transcripts_test.go b/pkg/etk/comps/transcripts_test.go similarity index 85% rename from pkg/etk/transcripts_test.go rename to pkg/etk/comps/transcripts_test.go index 94c726b32..737991bf4 100644 --- a/pkg/etk/transcripts_test.go +++ b/pkg/etk/comps/transcripts_test.go @@ -1,4 +1,4 @@ -package etk_test +package comps_test import ( "embed" @@ -8,6 +8,7 @@ import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/edit/highlight" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/etk/etktest" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" @@ -24,15 +25,15 @@ func TestTranscripts(t *testing.T) { }) evaltest.TestTranscriptsInFS(t, transcripts, - "text-area-fixture", etktest.MakeFixture(etk.TextArea), + "text-area-fixture", etktest.MakeFixture(comps.TextArea), "text-area-demo-fixture", etktest.MakeFixture( - etk.WithInit(etk.TextArea, + etk.WithInit(comps.TextArea, "binding", func(ev term.Event, c etk.Context, r etk.React) etk.Reaction { reaction := r(ev) if reaction != etk.Unused { return reaction } - bufferVar := etk.BindState(c, "buffer", etk.TextBuffer{}) + bufferVar := etk.BindState(c, "buffer", comps.TextBuffer{}) switch ev { case term.K(ui.Left): bufferVar.Swap(makeMove(moveDotLeft)) @@ -63,8 +64,8 @@ func TestTranscripts(t *testing.T) { // For demo -func makeMove(m func(string, int) int) func(etk.TextBuffer) etk.TextBuffer { - return func(buf etk.TextBuffer) etk.TextBuffer { +func makeMove(m func(string, int) int) func(comps.TextBuffer) comps.TextBuffer { + return func(buf comps.TextBuffer) comps.TextBuffer { buf.Dot = m(buf.Content, buf.Dot) return buf } diff --git a/pkg/etk/etk.go b/pkg/etk/etk.go index c1d04fb7c..1691d4887 100644 --- a/pkg/etk/etk.go +++ b/pkg/etk/etk.go @@ -54,6 +54,7 @@ package etk import ( + "fmt" "reflect" "slices" "strings" @@ -335,7 +336,24 @@ func (sv StateVar[T]) setAny(v any) { func (sv StateVar[T]) get() any { return getPath(*sv.state, sv.path) } func (sv StateVar[T]) set(v any) { *sv.state = assocPath(*sv.state, sv.path, v) } -func getPath(m vals.Map, path []string) any { +type StateSubTreeVar Context + +func (v StateSubTreeVar) Get() any { + return getPath(v.g.state, v.path) +} + +func (v StateSubTreeVar) Set(val any) error { + valMap, ok := val.(vals.Map) + if !ok { + return fmt.Errorf("must be map") + } + v.g.state = assocPath(v.g.state, v.path, valMap) + return nil +} + +// TODO: Move the following to vals? + +func getPath[T any](m vals.Map, path []T) any { if len(path) == 0 { return m } @@ -351,7 +369,7 @@ func getPath(m vals.Map, path []string) any { return v } -func assocPath(m vals.Map, path []string, newVal any) vals.Map { +func assocPath[T any](m vals.Map, path []T, newVal any) vals.Map { if len(path) == 0 { return newVal.(vals.Map) } diff --git a/pkg/etk/examples/crud.go b/pkg/etk/examples/crud.go index 8d47c2ba5..c5d72337e 100644 --- a/pkg/etk/examples/crud.go +++ b/pkg/etk/examples/crud.go @@ -4,22 +4,23 @@ import ( "slices" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) func CRUD(c etk.Context) (etk.View, etk.React) { - prefixView, prefixReact := c.Subcomp("prefix", etk.TextArea) + prefixView, prefixReact := c.Subcomp("prefix", comps.TextArea) personsVar := etk.State(c, "list/items", persons{ {"Hans", "Emil"}, {"Max", "Mustermann"}, {"Roman", "Tisch"}}) selectedVar := etk.BindState(c, "list/selected", 0) - listView, listReact := c.Subcomp("list", etk.ListBox) + listView, listReact := c.Subcomp("list", comps.ListBox) - nameView, nameReact := c.Subcomp("name", etk.TextArea) - nameContent := etk.BindState(c, "name/buffer", etk.TextBuffer{}).Get().Content + nameView, nameReact := c.Subcomp("name", comps.TextArea) + nameContent := etk.BindState(c, "name/buffer", comps.TextBuffer{}).Get().Content - surnameView, surnameReact := c.Subcomp("surname", etk.TextArea) - surnameContent := etk.BindState(c, "surname/buffer", etk.TextBuffer{}).Get().Content + surnameView, surnameReact := c.Subcomp("surname", comps.TextArea) + surnameContent := etk.BindState(c, "surname/buffer", comps.TextBuffer{}).Get().Content createFn := func() { personsVar.Swap(func(p persons) persons { diff --git a/pkg/etk/examples/flight.go b/pkg/etk/examples/flight.go index 15b253fee..452c92bcb 100644 --- a/pkg/etk/examples/flight.go +++ b/pkg/etk/examples/flight.go @@ -3,17 +3,18 @@ package main import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) func Flight(c etk.Context) (etk.View, etk.React) { typeView, typeReact := c.Subcomp("type", - etk.WithInit(etk.ListBox, - "items", etk.StringItems("one-way", "return"), + etk.WithInit(comps.ListBox, + "items", comps.StringItems("one-way", "return"), "horizontal", true)) - outboundView, outboundReact := c.Subcomp("outbound", etk.TextArea) + outboundView, outboundReact := c.Subcomp("outbound", comps.TextArea) // TODO: Disable inbound for one-way - inboundView, inboundReact := c.Subcomp("inbound", etk.TextArea) + inboundView, inboundReact := c.Subcomp("inbound", comps.TextArea) bookView, bookReact := c.Subcomp("book", etk.WithInit(Button, "label", "Book")) return Form(c, FormComp{"Type: ", typeView, typeReact, false}, diff --git a/pkg/etk/examples/hiernav.go b/pkg/etk/examples/hiernav.go index 7182aa408..42014d448 100644 --- a/pkg/etk/examples/hiernav.go +++ b/pkg/etk/examples/hiernav.go @@ -7,6 +7,7 @@ import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) @@ -34,7 +35,7 @@ func HierNav(c etk.Context) (etk.View, etk.React) { case map[string]any: // TODO: Don't recalculate? items := makeHierItems(value) - currentView, currentReact = c.Subcomp(pathToName(path), etk.WithInit(etk.ListBox, "items", items)) + currentView, currentReact = c.Subcomp(pathToName(path), etk.WithInit(comps.ListBox, "items", items)) selectedVar := etk.BindState(c, pathToName(path)+"/selected", 0) previewPath := slices.Concat(path, []string{items[selectedVar.Get()].key}) preview = hierNavPanel(c, data, previewPath) @@ -71,7 +72,7 @@ func hierNavPanel(b etk.Context, data map[string]any, path []string) etk.View { switch value := access(data, path).(type) { case map[string]any: items := makeHierItems(value) - view, _ := b.Subcomp(pathToName(path), etk.WithInit(etk.ListBox, "items", items)) + view, _ := b.Subcomp(pathToName(path), etk.WithInit(comps.ListBox, "items", items)) return view case string: return etk.TextView(0, ui.T(value)) diff --git a/pkg/etk/examples/styledown.go b/pkg/etk/examples/styledown.go index 7dc606a81..c4d565f2e 100644 --- a/pkg/etk/examples/styledown.go +++ b/pkg/etk/examples/styledown.go @@ -2,14 +2,15 @@ package main import ( "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" "src.elv.sh/pkg/ui/styledown" ) func Styledown(c etk.Context) (etk.View, etk.React) { codeView, codeReact := c.Subcomp("code", - etk.WithInit(etk.TextArea, "prompt", ui.T("Styledown:\n"))) - content := etk.BindState(c, "code/buffer", etk.TextBuffer{}).Get().Content + etk.WithInit(comps.TextArea, "prompt", ui.T("Styledown:\n"))) + content := etk.BindState(c, "code/buffer", comps.TextBuffer{}).Get().Content rendered, err := styledown.Render(content) if err == nil { rendered = ui.Concat(ui.T("Rendered:\n"), rendered) diff --git a/pkg/etk/examples/temperature.go b/pkg/etk/examples/temperature.go index 54512f078..7285fe726 100644 --- a/pkg/etk/examples/temperature.go +++ b/pkg/etk/examples/temperature.go @@ -6,15 +6,16 @@ import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) func Temperature(c etk.Context) (etk.View, etk.React) { - celsiusView, celsiusReact := c.Subcomp("celsius", etk.WithInit(etk.TextArea, "prompt", ui.T("Celsius: "))) - celsiusBufferVar := etk.BindState(c, "celsius/buffer", etk.TextBuffer{}) + celsiusView, celsiusReact := c.Subcomp("celsius", etk.WithInit(comps.TextArea, "prompt", ui.T("Celsius: "))) + celsiusBufferVar := etk.BindState(c, "celsius/buffer", comps.TextBuffer{}) - fahrenheitView, fahrenheitReact := c.Subcomp("fahrenheit", etk.WithInit(etk.TextArea, "prompt", ui.T("Fahrenheit: "))) - fahrenheitBufferVar := etk.BindState(c, "fahrenheit/buffer", etk.TextBuffer{}) + fahrenheitView, fahrenheitReact := c.Subcomp("fahrenheit", etk.WithInit(comps.TextArea, "prompt", ui.T("Fahrenheit: "))) + fahrenheitBufferVar := etk.BindState(c, "fahrenheit/buffer", comps.TextBuffer{}) focusVar := etk.State(c, "focus", 0) @@ -29,7 +30,7 @@ func Temperature(c etk.Context) (etk.View, etk.React) { if celsiusReact(e) == etk.Consumed { if c, err := strconv.ParseFloat(celsiusBufferVar.Get().Content, 64); err == nil { f := fmt.Sprintf("%.2f", c*9/5+32) - fahrenheitBufferVar.Set(etk.TextBuffer{Content: f, Dot: len(f)}) + fahrenheitBufferVar.Set(comps.TextBuffer{Content: f, Dot: len(f)}) } return etk.Consumed } @@ -37,7 +38,7 @@ func Temperature(c etk.Context) (etk.View, etk.React) { if fahrenheitReact(e) == etk.Consumed { if f, err := strconv.ParseFloat(fahrenheitBufferVar.Get().Content, 64); err == nil { c := fmt.Sprintf("%.2f", (f-32)*5/9) - celsiusBufferVar.Set(etk.TextBuffer{Content: c, Dot: len(c)}) + celsiusBufferVar.Set(comps.TextBuffer{Content: c, Dot: len(c)}) } return etk.Consumed } diff --git a/pkg/etk/examples/textarea.go b/pkg/etk/examples/textarea.go index b689c2a57..adae450a7 100644 --- a/pkg/etk/examples/textarea.go +++ b/pkg/etk/examples/textarea.go @@ -5,11 +5,12 @@ import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/ui" ) -var TextArea = etk.WithInit(etk.TextArea, +var TextArea = etk.WithInit(comps.TextArea, "prompt", ui.T("~> "), "abbr", func(y func(a, f string)) { y("foo", "lorem") }, "binding", @@ -18,7 +19,7 @@ var TextArea = etk.WithInit(etk.TextArea, if reaction != etk.Unused { return reaction } - bufferVar := etk.BindState(c, "buffer", etk.TextBuffer{}) + bufferVar := etk.BindState(c, "buffer", comps.TextBuffer{}) switch ev { case term.K(ui.Left): bufferVar.Swap(makeMove(moveDotLeft)) @@ -34,8 +35,8 @@ var TextArea = etk.WithInit(etk.TextArea, return etk.Consumed }) -func makeMove(m func(string, int) int) func(etk.TextBuffer) etk.TextBuffer { - return func(buf etk.TextBuffer) etk.TextBuffer { +func makeMove(m func(string, int) int) func(comps.TextBuffer) comps.TextBuffer { + return func(buf comps.TextBuffer) comps.TextBuffer { buf.Dot = m(buf.Content, buf.Dot) return buf } diff --git a/pkg/etk/examples/timer.go b/pkg/etk/examples/timer.go index e6f828b2a..a5da743f0 100644 --- a/pkg/etk/examples/timer.go +++ b/pkg/etk/examples/timer.go @@ -5,13 +5,14 @@ import ( "time" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) func Timer(c etk.Context) (etk.View, etk.React) { startTimeVar := etk.State(c, "start-time", time.Now()) - durationView, durationReact := c.Subcomp("duration", etk.TextArea) - durationBufferVar := etk.BindState(c, "duration/buffer", etk.TextBuffer{}) + durationView, durationReact := c.Subcomp("duration", comps.TextArea) + durationBufferVar := etk.BindState(c, "duration/buffer", comps.TextBuffer{}) resetView, resetReact := c.Subcomp("reset", etk.WithInit(Button, "label", "Reset", "submit", func() { startTimeVar.Set(time.Now()) diff --git a/pkg/etk/examples/todo.go b/pkg/etk/examples/todo.go index 03fbaff85..409a0439d 100644 --- a/pkg/etk/examples/todo.go +++ b/pkg/etk/examples/todo.go @@ -5,6 +5,7 @@ import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) @@ -26,12 +27,12 @@ func (ti todoItems) Show(i int) ui.Text { func Todo(c etk.Context) (etk.View, etk.React) { // TODO: API to combine init and bind - listView, listReact := c.Subcomp("list", etk.WithInit(etk.ListBox, "items", todoItems{})) + listView, listReact := c.Subcomp("list", etk.WithInit(comps.ListBox, "items", todoItems{})) itemsVar := etk.BindState(c, "list/items", todoItems(nil)) selectedVar := etk.BindState(c, "list/selected", 0) - newItemView, newItemReact := c.Subcomp("new-item", etk.WithInit(etk.TextArea, "prompt", ui.T("new item: "))) - bufferVar := etk.BindState(c, "new-item/buffer", etk.TextBuffer{}) + newItemView, newItemReact := c.Subcomp("new-item", etk.WithInit(comps.TextArea, "prompt", ui.T("new item: "))) + bufferVar := etk.BindState(c, "new-item/buffer", comps.TextBuffer{}) focusVar := etk.State(c, "focus", 1) focus := focusVar.Get() @@ -64,7 +65,7 @@ func Todo(c etk.Context) (etk.View, etk.React) { return etk.Consumed case term.K(ui.Enter): itemsVar.Set(append(itemsVar.Get(), todoItem{text: bufferVar.Get().Content})) - bufferVar.Set(etk.TextBuffer{}) + bufferVar.Set(comps.TextBuffer{}) return etk.Consumed } } diff --git a/pkg/etk/examples/wizard.go b/pkg/etk/examples/wizard.go index ee907354e..8fb9be601 100644 --- a/pkg/etk/examples/wizard.go +++ b/pkg/etk/examples/wizard.go @@ -3,6 +3,7 @@ package main import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/ui" ) @@ -24,7 +25,7 @@ var tasks = Tasks{ } func Wizard(c etk.Context) (etk.View, etk.React) { - listView, listReact := c.Subcomp("list", etk.WithInit(etk.ListBox, "items", tasks)) + listView, listReact := c.Subcomp("list", etk.WithInit(comps.ListBox, "items", tasks)) selectedVar := etk.BindState(c, "list/selected", 0) selected := selectedVar.Get() description := etk.TextView(0, ui.T(tasks[selected].Description)) diff --git a/pkg/etk/modes/location.go b/pkg/etk/modes/location.go index c9f7a149a..86036e4a1 100644 --- a/pkg/etk/modes/location.go +++ b/pkg/etk/modes/location.go @@ -9,6 +9,7 @@ import ( "strings" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" @@ -82,13 +83,13 @@ func NewLocation(cfg LocationCfg) (etk.Comp, error) { l := locationList{dirs} - return etk.WithInit(etk.ComboBox, - "gen-list", func(p string) (etk.ListItems, int) { + return etk.WithInit(comps.ComboBox, + "gen-list", func(p string) (comps.ListItems, int) { return l.filter(cfg.Filter.makePredicate(p)), 0 }, "filter/prompt", modeLine(" LOCATION ", true), "filter/highlight", cfg.Filter.Highlighter, - "list/submit", func(it etk.ListItems, i int) { + "list/submit", func(it comps.ListItems, i int) { path := it.(locationList).dirs[i].Path if strings.HasPrefix(path, wsKind) { path = wsRoot + path[len(wsKind):] diff --git a/pkg/etkedit/app.go b/pkg/etkedit/app.go index f1b18df2e..29dbade16 100644 --- a/pkg/etkedit/app.go +++ b/pkg/etkedit/app.go @@ -6,6 +6,7 @@ import ( "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/edit/highlight" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" ) type Addons struct { @@ -36,7 +37,7 @@ func App(c etk.Context) (etk.View, etk.React) { }() } codeView, codeReact := c.Subcomp("code", - etk.WithInit(etk.TextArea, "highlighter", hlVar.Get().Get)) + etk.WithInit(comps.TextArea, "highlighter", hlVar.Get().Get)) addonsVar := etk.State(c, "addons", Addons{}) addons := addonsVar.Get().Addons diff --git a/pkg/etkedit/edit.go b/pkg/etkedit/edit.go index 39a84d63a..ccbde02fc 100644 --- a/pkg/etkedit/edit.go +++ b/pkg/etkedit/edit.go @@ -7,6 +7,7 @@ import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" @@ -76,7 +77,7 @@ func (ed *Editor) Comp() etk.Comp { if reaction == etk.Unused { switch ev { case term.K('x', ui.Alt): - PushAddon(c, etk.WithInit(etk.TextArea, "prompt", ui.T("minibuf> "))) + PushAddon(c, etk.WithInit(comps.TextArea, "prompt", ui.T("minibuf> "))) default: if k, ok := ev.(term.KeyEvent); ok { c.AddMsg(ui.T(fmt.Sprintf("Unbound: %s", ui.Key(k)))) @@ -100,7 +101,7 @@ func (ed *Editor) Comp() etk.Comp { ed.mutex.RLock() defer ed.mutex.RUnlock() - bufferContent := etk.BindState(c, "code/buffer", etk.TextBuffer{}).Get().Content + bufferContent := etk.BindState(c, "code/buffer", comps.TextBuffer{}).Get().Content callPrompt(c, "code/prompt", ed.prompt, bufferContent) callPrompt(c, "code/rprompt", ed.rprompt, bufferContent) // These live in the WithBefore rather than WithInit, because we @@ -121,7 +122,7 @@ func (ed *Editor) ReadCode(tty cli.TTY) (string, error) { // TODO: Multi-level indexing should be easier codeArea, _ := m.Index("code") buf, _ := codeArea.(vals.Map).Index("buffer") - return buf.(etk.TextBuffer).Content, nil + return buf.(comps.TextBuffer).Content, nil } // Creates an editVar. This has to be a function because methods can't be diff --git a/pkg/etk/elvish_binding.go b/pkg/mods/etk/etk.go similarity index 68% rename from pkg/etk/elvish_binding.go rename to pkg/mods/etk/etk.go index d4f0d4036..b75427c58 100644 --- a/pkg/etk/elvish_binding.go +++ b/pkg/mods/etk/etk.go @@ -5,6 +5,8 @@ import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" + "src.elv.sh/pkg/etk" + "src.elv.sh/pkg/etk/comps" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" @@ -18,17 +20,17 @@ func (*textViewOpts) SetDefaultOptions() {} var Ns = eval.BuildNsNamed("etk"). AddVars(map[string]vars.Var{ - // Reaction values - "unused": vars.NewReadOnly(Unused), - "consumed": vars.NewReadOnly(Consumed), - "finish": vars.NewReadOnly(Finish), - "finish-eof": vars.NewReadOnly(FinishEOF), + // etk.Reaction values + "unused": vars.NewReadOnly(etk.Unused), + "consumed": vars.NewReadOnly(etk.Consumed), + "finish": vars.NewReadOnly(etk.Finish), + "finish-eof": vars.NewReadOnly(etk.FinishEOF), // Builtin widgets - "textarea": vars.NewReadOnly(TextArea), + "textarea": vars.NewReadOnly(comps.TextArea), }). AddGoFns(map[string]any{ - "-text-view": func(opts textViewOpts, v any) View { - return TextView(opts.DotBefore, ui.T(vals.ToString(v))) + "-text-view": func(opts textViewOpts, v any) etk.View { + return etk.TextView(opts.DotBefore, ui.T(vals.ToString(v))) }, "-key-event": func(s string) (term.Event, error) { k, err := ui.ParseKey(s) @@ -37,7 +39,7 @@ var Ns = eval.BuildNsNamed("etk"). } return term.KeyEvent(k), nil }, - "with-init": func(fm *eval.Frame, compAny any, inits vals.Map) (Comp, error) { + "with-init": func(fm *eval.Frame, compAny any, inits vals.Map) (etk.Comp, error) { // TODO: Integrate the parsing into vals.ScanToGo comp, err := scanComp(fm, compAny) if err != nil { @@ -47,7 +49,7 @@ var Ns = eval.BuildNsNamed("etk"). if err != nil { return nil, err } - return WithInit(comp, initArgs...), nil + return etk.WithInit(comp, initArgs...), nil }, "run": func(fm *eval.Frame, compAny any) error { // TODO: Integrate the parsing into vals.ScanToGo @@ -56,15 +58,15 @@ var Ns = eval.BuildNsNamed("etk"). return err } // TODO: Maybe should use subframe of fm? - _, err = Run(cli.NewTTY(fm.InputFile(), fm.Port(1).File), fm, comp) + _, err = etk.Run(cli.NewTTY(fm.InputFile(), fm.Port(1).File), fm, comp) return err }, }). Ns() -func scanComp(fm *eval.Frame, v any) (Comp, error) { +func scanComp(fm *eval.Frame, v any) (etk.Comp, error) { switch v := v.(type) { - case Comp: + case etk.Comp: return v, nil case eval.Callable: return scanCompFromFn(fm, v), nil @@ -76,27 +78,27 @@ func scanComp(fm *eval.Frame, v any) (Comp, error) { } type compOut struct { - View View + View etk.View React eval.Callable } type viewReact struct { - View View - React React + View etk.View + React etk.React } type vboxOpts struct{ Focus int } func (*vboxOpts) SetDefaultOptions() {} -func scanCompFromFn(fm *eval.Frame, fn eval.Callable) Comp { - return func(c Context) (View, React) { +func scanCompFromFn(fm *eval.Frame, fn eval.Callable) etk.Comp { + return func(c etk.Context) (etk.View, etk.React) { subcomps := map[string]viewReact{} var elvishCtx = eval.BuildNs().AddVars(map[string]vars.Var{ - "state": stateSubTreeVar(c), + "state": etk.StateSubTreeVar(c), }).AddGoFns(map[string]any{ "state": func(name string, _eq string, init any) { - State(c, name, init) + etk.State(c, name, init) }, "subcomp": func(name string, _eq string, compAny any) error { // TODO: Is use of fm correct here? @@ -108,14 +110,14 @@ func scanCompFromFn(fm *eval.Frame, fn eval.Callable) Comp { subcomps[name] = viewReact{v, r} return nil }, - "vbox": func(opts vboxOpts, compNames ...string) View { - views := make([]View, len(compNames)) + "vbox": func(opts vboxOpts, compNames ...string) etk.View { + views := make([]etk.View, len(compNames)) for i, compName := range compNames { views[i] = subcomps[compName].View } - return VBoxView(opts.Focus, views...) + return etk.VBoxView(opts.Focus, views...) }, - "pass": func(compName string, ev term.Event) Reaction { + "pass": func(compName string, ev term.Event) etk.Reaction { // TODO: Error if comp doesn't exist return subcomps[compName].React(ev) }, @@ -140,25 +142,10 @@ func scanCompFromFn(fm *eval.Frame, fn eval.Callable) Comp { return errElement(fmt.Errorf("output should be map with view and react")) } // TODO: Handle scan error - return out.View, must.OK1(ScanToGo[React](out.React, fm)) + return out.View, must.OK1(etk.ScanToGo[etk.React](out.React, fm)) } } -type stateSubTreeVar Context - -func (v stateSubTreeVar) Get() any { - return getPath(v.g.state, v.path) -} - -func (v stateSubTreeVar) Set(val any) error { - valMap, ok := val.(vals.Map) - if !ok { - return fmt.Errorf("must be map") - } - v.g.state = assocPath(v.g.state, v.path, valMap) - return nil -} - func convertInits(m vals.Map) ([]any, error) { var args []any for it := m.Iterator(); it.HasElem(); it.Next() { @@ -172,7 +159,7 @@ func convertInits(m vals.Map) ([]any, error) { return args, nil } -func errElement(err error) (View, React) { - return TextView(1, ui.T(err.Error(), ui.FgRed)), - func(term.Event) Reaction { return Unused } +func errElement(err error) (etk.View, etk.React) { + return etk.TextView(1, ui.T(err.Error(), ui.FgRed)), + func(term.Event) etk.Reaction { return etk.Unused } } diff --git a/pkg/mods/mods.go b/pkg/mods/mods.go index 42451db9f..f2efe0ec3 100644 --- a/pkg/mods/mods.go +++ b/pkg/mods/mods.go @@ -2,10 +2,10 @@ package mods import ( - "src.elv.sh/pkg/etk" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/mods/doc" "src.elv.sh/pkg/mods/epm" + "src.elv.sh/pkg/mods/etk" "src.elv.sh/pkg/mods/file" "src.elv.sh/pkg/mods/flag" "src.elv.sh/pkg/mods/math"