diff --git a/lib/tui/cmd/run.go b/lib/tui/cmd/run.go index 63af8e59e..92a9c051c 100644 --- a/lib/tui/cmd/run.go +++ b/lib/tui/cmd/run.go @@ -99,7 +99,7 @@ func Cmd(args []string, rflags flags.RootPflagpole) error { // some latent locksups occur randomly time.Sleep(time.Millisecond * 23) tui.SendCustomEvent("/router/dispatch", context) - tui.SendCustomEvent("/status/message", "Welcome to [gold]_[ivory]Hofstadter[-]!!") + tui.SendCustomEvent("/status/message", "[blue::b]Welcome to [gold::bi]_[ivory]Hofstadter[-::-]") }() // Start the Main (Blocking) Loop diff --git a/lib/tui/components/cue/browser/codec.go b/lib/tui/components/cue/browser/codec.go index fcc764c92..33f18352f 100644 --- a/lib/tui/components/cue/browser/codec.go +++ b/lib/tui/components/cue/browser/codec.go @@ -1,5 +1,13 @@ package browser +import ( + "fmt" + + "cuelang.org/go/cue/format" + + "github.com/hofstadter-io/hof/lib/gen" +) + func (V *Browser) Encode() (map[string]any, error) { m := map[string]any{ "type": V.TypeName(), @@ -24,3 +32,40 @@ func (V *Browser) Encode() (map[string]any, error) { } +func (C *Browser) GetValueText(mode string) (string, error) { + var ( + b []byte + err error + ) + switch mode { + case "cue": + syn := C.value.Syntax(C.Options()...) + + b, err = format.Node(syn) + if !C.ignore { + if err != nil { + return "", err + } + } + + case "json": + f := &gen.File{} + b, err = f.FormatData(C.value, "json") + if err != nil { + return "", err + } + + case "yaml": + f := &gen.File{} + b, err = f.FormatData(C.value, "yaml") + if err != nil { + return "", err + } + + default: + return "", fmt.Errorf("unknown file type %q", mode) + + } + + return string(b), err +} diff --git a/lib/tui/components/cue/playground/events.go b/lib/tui/components/cue/playground/events.go new file mode 100644 index 000000000..87e950e5b --- /dev/null +++ b/lib/tui/components/cue/playground/events.go @@ -0,0 +1,43 @@ +package playground + +import ( + "github.com/gdamore/tcell/v2" + + "github.com/hofstadter-io/hof/lib/tui/tview" +) +func (C *Playground) setupKeybinds() { + // events (hotkeys) + C.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { + switch evt.Key() { + case tcell.KeyRune: + if (evt.Modifiers() & tcell.ModAlt) == tcell.ModAlt { + switch evt.Rune() { + case 'f': + flexDir := C.GetDirection() + if flexDir == tview.FlexRow { + C.SetDirection(tview.FlexColumn) + } else { + C.SetDirection(tview.FlexRow) + } + + case 'S': + C.useScope = !C.useScope + C.Rebuild(false) + + case 'R': + C.Rebuild(true) + + default: + return evt + } + + return nil + } + + return evt + + default: + return evt + } + }) +} diff --git a/lib/tui/components/cue/playground/output.go b/lib/tui/components/cue/playground/output.go new file mode 100644 index 000000000..1afa14482 --- /dev/null +++ b/lib/tui/components/cue/playground/output.go @@ -0,0 +1,56 @@ +package playground + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/parnurzeal/gorequest" +) + +const HTTP2_GOAWAY_CHECK = "http2: server sent GOAWAY and closed the connection" + +func (C *Playground) PushToPlayground() (string, error) { + src := C.edit.GetText() + + url := "https://cuelang.org/.netlify/functions/snippets" + req := gorequest.New().Post(url) + req.Set("Content-Type", "text/plain") + req.Send(src) + + resp, body, errs := req.End() + + if len(errs) != 0 && !strings.Contains(errs[0].Error(), HTTP2_GOAWAY_CHECK) { + fmt.Println("errs:", errs) + fmt.Println("resp:", resp) + fmt.Println("body:", body) + return body, errs[0] + } + + if len(errs) != 0 || resp.StatusCode >= 500 { + return body, fmt.Errorf("Internal Error: " + body) + } + if resp.StatusCode >= 400 { + return body, fmt.Errorf("Bad Request: " + body) + } + + return body, nil +} + +func (C *Playground) WriteEditToFile(filename string) (error) { + src := C.edit.GetText() + + return os.WriteFile(filename, []byte(src), 0644) +} + +func (C *Playground) ExportFinalToFile(filename string) (error) { + ext := filepath.Ext(filename) + ext = strings.TrimPrefix(ext, ".") + src, err := C.final.viewer.GetValueText(ext) + if err != nil { + return err + } + + return os.WriteFile(filename, []byte(src), 0644) +} diff --git a/lib/tui/components/cue/playground/playground.go b/lib/tui/components/cue/playground/playground.go index c2ce6d00e..4e1b65f87 100644 --- a/lib/tui/components/cue/playground/playground.go +++ b/lib/tui/components/cue/playground/playground.go @@ -1,23 +1,16 @@ package playground import ( - "fmt" - "strings" "time" "cuelang.org/go/cue" - "cuelang.org/go/cue/cuecontext" - "github.com/gdamore/tcell/v2" - "github.com/parnurzeal/gorequest" - "github.com/hofstadter-io/hof/lib/tui" "github.com/hofstadter-io/hof/lib/tui/components/cue/browser" "github.com/hofstadter-io/hof/lib/tui/components/cue/helpers" "github.com/hofstadter-io/hof/lib/tui/tview" "github.com/hofstadter-io/hof/lib/watch" ) - type valPack struct { config helpers.SourceConfig value cue.Value @@ -153,102 +146,6 @@ func (C *Playground) SetFlexDirection(dir int) { } -const HTTP2_GOAWAY_CHECK = "http2: server sent GOAWAY and closed the connection" - -func (C *Playground) PushToPlayground() (string, error) { - src := C.edit.GetText() - - url := "https://cuelang.org/.netlify/functions/snippets" - req := gorequest.New().Post(url) - req.Set("Content-Type", "text/plain") - req.Send(src) - - resp, body, errs := req.End() - - if len(errs) != 0 && !strings.Contains(errs[0].Error(), HTTP2_GOAWAY_CHECK) { - fmt.Println("errs:", errs) - fmt.Println("resp:", resp) - fmt.Println("body:", body) - return body, errs[0] - } - - if len(errs) != 0 || resp.StatusCode >= 500 { - return body, fmt.Errorf("Internal Error: " + body) - } - if resp.StatusCode >= 400 { - return body, fmt.Errorf("Bad Request: " + body) - } - - return body, nil -} - -func (C *Playground) Rebuild(rebuildScope bool) error { - // tui.Log("info", fmt.Sprintf("Play.rebuildScope %v %v %v", rebuildScope, C.useScope, C.scope.config)) - var ( - v cue.Value - err error - ) - - ctx := cuecontext.New() - src := C.edit.GetText() - - // compile a value - if !C.useScope { - // just compile the text - v = ctx.CompileString(src, cue.InferBuiltins(true)) - } else { - // compile the text with a scope - - // tui.Log("warn", fmt.Sprintf("%#v", s)) - sv, serr := C.scope.config.GetValue() - err = serr - - if err != nil { - tui.Log("error", err) - } - // we shouldn't have to worry about this, but we aren't catching all the ways - // that we get into this code, in particular, hotkey can set scope to true when none exists - if !sv.Exists() { - tui.Log("error", "scope value does not exist") - err = fmt.Errorf("scope value does not exist") - } - - if err == nil && sv.Exists() { - if rebuildScope { - // C.scope.config.Rebuild() - cfg := helpers.SourceConfig{Value: sv} - C.scope.viewer.SetSourceConfig(cfg) - C.scope.viewer.Rebuild() - } - - // tui.Log("warn", fmt.Sprintf("recompile with scope: %v", rebuildScope)) - ctx := sv.Context() - v = ctx.CompileString(src, cue.InferBuiltins(true), cue.Scope(sv)) - } - } - - cfg := helpers.SourceConfig{Value: v} - if err != nil { - tui.Log("error", err) - cfg = helpers.SourceConfig{Text: err.Error()} - } - // only update view value, that way, if we erase everything, we still see the value - C.final.viewer.SetUsingScope(C.useScope) - C.final.viewer.SetSourceConfig(cfg) - C.final.viewer.Rebuild() - - // show/hide scope as needed - if C.useScope { - C.SetItem(0, C.scope.viewer, 0, 1, true) - } else { - C.SetItem(0, nil, 0, 0, false) - } - - - // tui.Draw() - return nil -} - func (C *Playground) Mount(context map[string]any) error { return nil @@ -265,39 +162,3 @@ func (C *Playground) Focus(delegate func(p tview.Primitive)) { } -func (C *Playground) setupKeybinds() { - // events (hotkeys) - C.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { - switch evt.Key() { - case tcell.KeyRune: - if (evt.Modifiers() & tcell.ModAlt) == tcell.ModAlt { - switch evt.Rune() { - case 'f': - flexDir := C.GetDirection() - if flexDir == tview.FlexRow { - C.SetDirection(tview.FlexColumn) - } else { - C.SetDirection(tview.FlexRow) - } - - case 'S': - C.useScope = !C.useScope - C.Rebuild(false) - - case 'R': - C.Rebuild(true) - - default: - return evt - } - - return nil - } - - return evt - - default: - return evt - } - }) -} diff --git a/lib/tui/components/cue/playground/rebuild.go b/lib/tui/components/cue/playground/rebuild.go new file mode 100644 index 000000000..3a009bc40 --- /dev/null +++ b/lib/tui/components/cue/playground/rebuild.go @@ -0,0 +1,79 @@ +package playground + +import ( + "fmt" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + + "github.com/hofstadter-io/hof/lib/tui" + "github.com/hofstadter-io/hof/lib/tui/components/cue/helpers" +) + +func (C *Playground) Rebuild(rebuildScope bool) error { + // tui.Log("info", fmt.Sprintf("Play.rebuildScope %v %v %v", rebuildScope, C.useScope, C.scope.config)) + var ( + v cue.Value + err error + ) + + ctx := cuecontext.New() + src := C.edit.GetText() + + // compile a value + if !C.useScope { + // just compile the text + v = ctx.CompileString(src, cue.InferBuiltins(true)) + } else { + // compile the text with a scope + + // tui.Log("warn", fmt.Sprintf("%#v", s)) + sv, serr := C.scope.config.GetValue() + err = serr + + if err != nil { + tui.Log("error", err) + } + // we shouldn't have to worry about this, but we aren't catching all the ways + // that we get into this code, in particular, hotkey can set scope to true when none exists + if !sv.Exists() { + tui.Log("error", "scope value does not exist") + err = fmt.Errorf("scope value does not exist") + } + + if err == nil && sv.Exists() { + if rebuildScope { + // C.scope.config.Rebuild() + cfg := helpers.SourceConfig{Value: sv} + C.scope.viewer.SetSourceConfig(cfg) + C.scope.viewer.Rebuild() + } + + // tui.Log("warn", fmt.Sprintf("recompile with scope: %v", rebuildScope)) + ctx := sv.Context() + v = ctx.CompileString(src, cue.InferBuiltins(true), cue.Scope(sv)) + } + } + + cfg := helpers.SourceConfig{Value: v} + if err != nil { + tui.Log("error", err) + cfg = helpers.SourceConfig{Text: err.Error()} + } + // only update view value, that way, if we erase everything, we still see the value + C.final.viewer.SetUsingScope(C.useScope) + C.final.viewer.SetSourceConfig(cfg) + C.final.viewer.Rebuild() + + // show/hide scope as needed + if C.useScope { + C.SetItem(0, C.scope.viewer, 0, 1, true) + } else { + C.SetItem(0, nil, 0, 0, false) + } + + + // tui.Draw() + return nil +} + diff --git a/lib/tui/modules/eval/argparse.go b/lib/tui/modules/eval/argparse.go index b7f01ee18..7ea39adc8 100644 --- a/lib/tui/modules/eval/argparse.go +++ b/lib/tui/modules/eval/argparse.go @@ -77,6 +77,7 @@ func enrichContext(context map[string]any) (map[string]any) { // this should probably be the new default case "push", + "export", "set.runtime", "set.value.runtime", "set.value.value", @@ -118,18 +119,6 @@ func enrichContext(context map[string]any) (map[string]any) { // value viewer case "view": context["item"] = "view" - // text editor - case "edit", "editor": - context["item"] = "editor" - case "text": - context["item"] = "text" - - // flow panel - //case "flow": - // context["item"] = "flow" - - - // should this be handled lower too? // we might want a more general @@ -221,14 +210,12 @@ argsDone: //args = args[1:] } - // I don't think we want defaults here - // update the current focused item by default + // set action to first arg if available if _, ok := context["action"]; !ok { - context["action"] = "update" - } - - if _, ok := context["item"]; !ok { - context["item"] = "play" + if len(args) > 0 { + context["action"] = args[0] + args = args[1:] + } } // make sure we update the context args diff --git a/lib/tui/modules/eval/creator.go b/lib/tui/modules/eval/creator.go index bc3e15ac9..6b3536fc1 100644 --- a/lib/tui/modules/eval/creator.go +++ b/lib/tui/modules/eval/creator.go @@ -69,6 +69,7 @@ func helpItem(context panel.ItemContext, parent *panel.Panel) (panel.PanelItem, I := panel.NewBaseItem(context, parent) txt := widget.NewTextView() + txt.SetBorderPadding(0,0,1,1) fmt.Fprint(txt, EvalHelpText) I.SetWidget(txt) diff --git a/lib/tui/modules/eval/help.go b/lib/tui/modules/eval/help.go index b31e4ec8d..eb6f1d965 100644 --- a/lib/tui/modules/eval/help.go +++ b/lib/tui/modules/eval/help.go @@ -1,23 +1,26 @@ package eval -const EvalHelpText = `[blue::b]Welcome[-] to [gold]_[ivory]Hofstadter[-::-] +const EvalHelpText = ` +[dodgerblue::b]Welcome to[-] [gold::bi]_[ivory]Hofstadter[-::-] - The Hof TUI gives you access to hof's features. - The main area is a dashboard where you can add - items or widgets, organize them as you wish, - and save/load them and their sources. - There are commands and hotkeys for most actions. + The Hof TUI gives you access to hof's features in a + space where you can dynamically explore and use them. + Build dashboards of views, playgrounds, and workflows. + Organize them as you see fit and save|load|share them. + + Right now, we have support for [violet]hof eval[-]. + New releases will expand support for other + features, [violet]hof flow[-] and [violet]hof gen[-] are next. [darkgray](scroll for more details)[-] -[blue::b]Legend:[-::-] +[dodgerblue::bu]Legend:[-::-] [lime]A[-] = ALT [lime]C[-] = CTRL [lime]M[-] = META [lime]S[-] = SHIFT - [lime]click[-] to focus a box -[blue::b]App Controls:[-::-] +[dodgerblue::bu]App Controls:[-::-] [lime]C-[-] focus the command box [lime]A-[-] focus the main content [lime]A-/[-] show / hide the console @@ -25,7 +28,7 @@ const EvalHelpText = `[blue::b]Welcome[-] to [gold]_[ivory]Hofstadter[-::-] [red]Ctrl-Alt-c[-] close [gold]_[ivory]Hofstadter[-] [red]:q[-] (from the command box) -[blue::b]Panel Management:[-::-] +[dodgerblue::bu]Panel Management:[-::-] [lime]A-J[-] new item before [lime]A-K[-] new item after [lime]A-H[-] move item before @@ -36,19 +39,21 @@ const EvalHelpText = `[blue::b]Welcome[-] to [gold]_[ivory]Hofstadter[-::-] [lime]A-P[-] show borders on panels [lime]A-O[-] show borders on items -[blue::b]Navigation:[-::-] - (vim style) - [lime]A-h[-] focus item left - [lime]A-j[-] focus item down - [lime]A-k[-] focus item up - [lime]A-l[-] focus item right +[dodgerblue::bu]Navigation:[-::-] + (with mouse) + [lime]click[-] to focus any box (with arrows) [lime]C-[-] focus item left [lime]C-[-] focus item down [lime]C-[-] focus item up [lime]C-[-] focus item right + (vim style) + [lime]A-h[-] focus item left + [lime]A-j[-] focus item down + [lime]A-k[-] focus item up + [lime]A-l[-] focus item right -[blue::b]Items and Commands:[-::-] +[dodgerblue::bu]Items and Commands:[-::-] Panels contain items or other panels for layout. Items contain widgets and data sources, and are controlled through the command box ([lime]C-[-]). @@ -65,7 +70,7 @@ const EvalHelpText = `[blue::b]Welcome[-] to [gold]_[ivory]Hofstadter[-::-] [violet]view (args)[-] open a [gold]Value View[-] with data [violet]help[-] open these help contents -[blue::b]Items:[-::-] +[dodgerblue::bu]Items:[-::-] [gold]Value View[-] allows you to explore CUE with control of options. You will see many options at the top-middle, green is enabled @@ -82,9 +87,9 @@ const EvalHelpText = `[blue::b]Welcome[-] to [gold]_[ivory]Hofstadter[-::-] [gold]Playground[-] is a multi-widget Item for working with CUE. You can edit CUE and see the results in real-time, with optional scope. The widgets in the playground are: - 1. the scope (if available) - 2. a value editor - 3. a browswer for the result + 1. a browser for the scope (if available) + 2. an editor for the main value + 3. a browser for the final value [lime]A-f[-] Rotate this item (lowercase of Panel hotkey) [lime]A-R[-] Reload data source and refresh @@ -97,49 +102,51 @@ const EvalHelpText = `[blue::b]Welcome[-] to [gold]_[ivory]Hofstadter[-::-] building CUE that will transform or collect larger values into smaller or new values. You have full access to CUE. -[blue::b]Commands:[-::-] +[dodgerblue::bu]Commands:[-::-] Items and widgets are controlled through the command box ([lime]C-[-]). First, make sure the item you want to change is focused (by [lime]clicking[-] on it). The general format for commands is as follows. - [violet] [-] - - [violet][-] same as [violet] value[-] - [violet] value[-] set the main value - [violet] scope[-] set the scope value + [violet] [lightseagreen][-] - [gold][-] + [gold]Item Commands[-] + [violet][-] [lightseagreen][-] same as [violet] value[-] + [violet] value[-] [lightseagreen][-] set the main value + [violet] scope[-] [lightseagreen][-] set the scope value - [aqua]eval args and flags[-] - [aqua]https://... (any http json response)[-] - [aqua]bash (any bash json output)[-] + [gold]Data Sources[-] + [violet] [lightseagreen] (same as cue and hof)[-] + [violet] [lightseagreen]https://... (any http json response)[-] + [violet] [lightseagreen]bash (any bash json output)[-] - [gold][-] - [violet]push[-] playground, push text value to CUE playground - playground links load like any https://... + [gold]Other Commands[-] + [violet]push[-] playground editor text to cuelang.org/play + [violet]write [-] playground editor text to file + [violet]export [-] playground final value to file -[blue::b]Dashboards:[-::-] +[dodgerblue::bu]Dashboards:[-::-] As you layout panels and items, set their data, it is likely you will want to save, load, and share these. [violet]list[-] list available dashboards - [violet]save[-] save the current view - [violet]show[-] show the save file - [violet]load[-] load and replace current + [violet]save [-] save the current view + [violet]show [-] show the save file + [violet]load [-] load and replace current You can also set panel & item names - [violet]set.item.name[-] - [violet]set.panel.name[-] + [violet]set.panel.name [-] + [violet]set.item.name [-] -[blue::b]Getting help:[-::-] +[dodgerblue::bu]Getting Help:[-::-] [violet]help[-] (open this help content in an item) [violet]hof feedback[-] (run this to start a new GitHub issue) - Docs: [dogerblue]https://docs.hofstadter.io[-] - GitHub: [dogerblue]https://github.com/hofstadter-io/hof[-] - Slack: [dogerblue]https://join.slack.com/t/hofstadter-io/shared_invite/zt-e5f90lmq-u695eJur0zE~AG~njNlT1A[-] - Discord: [dogerblue]https://discord.gg/6vgbKvPs[-] + [gold]Docs: [deepskyblue]https://docs.hofstadter.io[-] + [gold]GitHub: [deepskyblue]https://github.com/hofstadter-io/hof[-] + [gold]Slack: [deepskyblue]https://join.slack.com/t/hofstadter-io/shared_invite/zt-e5f90lmq-u695eJur0zE~AG~njNlT1A[-] + [gold]Discord: [deepskyblue]https://discord.gg/6vgbKvPs[-] + ` diff --git a/lib/tui/modules/eval/refresh.go b/lib/tui/modules/eval/refresh.go index f4e2cdace..50b36e01e 100644 --- a/lib/tui/modules/eval/refresh.go +++ b/lib/tui/modules/eval/refresh.go @@ -29,6 +29,13 @@ func (M *Eval) Refresh(context map[string]any) error { if _action, ok := context["action"]; ok { action = _action.(string) } + // default action to update if item is set + if action == "" { + // default action to update if item is set + if _, ok := context["item"]; ok { + context["action"] = "update" + } + } // intercept our top-level commands first switch action { @@ -120,6 +127,80 @@ func (M *Eval) Refresh(context map[string]any) error { return err } + case "write": + tui.Log("debug", fmt.Sprintf("write cmd: %# v", context)) + cfi := p.ChildFocus() + + if len(args) != 1 { + err := fmt.Errorf("write requires a filename") + tui.Tell("error", err) + tui.Log("error", err) + return err + } + + filename := args[0] + + itm := p.GetItem(cfi).(*panel.BaseItem) + w := itm.Widget() + switch play := w.(type) { + case *playground.Playground: + err := play.WriteEditToFile(filename) + if err != nil { + tui.Tell("error", err) + tui.Log("error", err) + return err + } + + msg := fmt.Sprintf("editor text saved to %s", filename) + + tui.Tell("error", msg) + tui.Log("trace", msg) + return nil + + default: + err := fmt.Errorf("unable to write this item %v", reflect.TypeOf(w)) + tui.Tell("error", err) + tui.Log("error", err) + return err + } + + case "export": + tui.Log("debug", fmt.Sprintf("export cmd: %# v", context)) + cfi := p.ChildFocus() + + if len(args) != 1 { + err := fmt.Errorf("export requires a filename") + tui.Tell("error", err) + tui.Log("error", err) + return err + } + + filename := args[0] + + itm := p.GetItem(cfi).(*panel.BaseItem) + w := itm.Widget() + switch play := w.(type) { + case *playground.Playground: + err := play.ExportFinalToFile(filename) + if err != nil { + tui.Tell("error", err) + tui.Log("error", err) + return err + } + + msg := fmt.Sprintf("value exported to %s", filename) + + tui.Tell("error", msg) + tui.Log("trace", msg) + return nil + + default: + err := fmt.Errorf("unable to export this item %v", reflect.TypeOf(w)) + tui.Tell("error", err) + tui.Log("error", err) + return err + } + case "nav.left", "nav.right", @@ -127,6 +208,15 @@ func (M *Eval) Refresh(context map[string]any) error { "nav.down": M.doNav(p, action) + + case "": + // the empty string should only happen on startup + + default: + err := fmt.Errorf("unknown command %q", action) + tui.Tell("error", err) + tui.Log("error", err) + return err } err := p.Refresh(context) diff --git a/lib/tui/modules/root/rootview.go b/lib/tui/modules/root/rootview.go index eb6991fba..f0a7d1dd2 100644 --- a/lib/tui/modules/root/rootview.go +++ b/lib/tui/modules/root/rootview.go @@ -87,7 +87,7 @@ func (V *RootView) setLastCommand(cmd string) { func (V *RootView) buildTopPanel() { V.cbox = cmdbox.New(V.getLastCommand, V.setLastCommand) V.cbox. - SetTitle(" [gold]_[ivory]Hofstadter[-] "). + SetTitle(" [gold::bi]_[ivory]Hofstadter[-::-] "). SetTitleAlign(tview.AlignLeft). SetTitleColor(tcell.ColorIvory). SetBorder(true).