package main import ( "fmt" "math/rand" "os" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/spf13/cobra" ) type mode string const ( modeNormal mode = "normal" modeFocus mode = "focus" modeSearch mode = "search" modeEx mode = "ex" ) type UI struct { Mode mode App *tview.Application Fields []field Nodes []*node Viewport *viewport // UI things Pages *tview.Pages TopFlex *tview.Flex Field *tview.Pages FieldBar *tview.Flex BottomBar *tview.Pages ExInput *tview.InputField ExOutput *tview.TextView } func (ui *UI) setMode(m mode) { ui.Mode = m switch ui.Mode { case modeNormal: ui.App.SetFocus(ui.TopFlex) ui.ExInput.SetText("") case modeEx: ui.App.SetFocus(ui.ExInput) } } func (ui *UI) handleInput(event *tcell.EventKey) *tcell.EventKey { switch ui.Mode { case modeNormal: switch event.Rune() { case ':': ui.setMode(modeEx) return nil case 'h': ui.Viewport.X-- return nil case 'j': ui.Viewport.Y++ return nil case 'k': ui.Viewport.Y-- return nil case 'l': ui.Viewport.X++ return nil } case modeEx: case modeFocus: case modeSearch: default: panic("mode?") } return event } func (ui *UI) handleExInput(key tcell.Key) { text := ui.ExInput.GetText() ui.setMode(modeNormal) if key != tcell.KeyEnter { return } switch text { case "q", "quit": ui.quit() } ui.ExOutput.SetText("") fmt.Fprintf(ui.ExOutput, "did not understand '%s', sorry", text) } /* three coordinate systems: - the infinite coordinate plane where nodes live. it is centered at 0,0 but nodes can be placed anywhere. - the viewport representing what part of the infinite plane the user is looking at. the viewport has an origin in the infinite plane and a width and height - the screen onto which characters are drawn. always rooted at 0,0; always has a width and a height that matches the width and height of the viewport the viewport is the most pressing question. nodes exist in an XY plane and the field view is a viewport onto that plane. nodes have a "real" X,Y coordinate pair and a width and a height. the viewport has a width, a height, and point A,B from which it originates. Moving the viewport means changing A,B. To see what should be shown on the screen, compare a node's X,Y to A,B. If X+width > A and Y+height > B && X+width < A+width and Y+maxheight < B+maxheight then it should be shown. For each node, it also has coordinates H,J in the screen based on the viewport position. But how to reconcile this with tview? can i add all of the nodes to the scene tree then update their H,J as the viewport changes and hope tview just does the right thing? I think I should start there. */ type viewport struct { X int Y int W int H int } type field struct { Name string Selected bool } type node struct { Text string Edges []*node X int Y int widg *tview.TextView // "screen" x and y are stored on corresponding tview widget as well as width/height } type Edge struct { nodes []*node } func (e Edge) Draw(screen tcell.Screen) { screen.SetContent(0, 0, tview.BoxDrawingsLightDiagonalUpperLeftToLowerRight, []rune{}, tcell.StyleDefault) screen.SetContent(1, 1, tview.BoxDrawingsLightDiagonalUpperLeftToLowerRight, []rune{}, tcell.StyleDefault) screen.SetContent(2, 2, tview.BoxDrawingsLightDiagonalUpperLeftToLowerRight, []rune{}, tcell.StyleDefault) screen.SetContent(3, 3, tview.BoxDrawingsLightDiagonalUpperLeftToLowerRight, []rune{}, tcell.StyleDefault) screen.SetContent(4, 4, tview.BoxDrawingsLightDiagonalUpperLeftToLowerRight, []rune{}, tcell.StyleDefault) screen.SetContent(5, 5, tview.BoxDrawingsLightDiagonalUpperLeftToLowerRight, []rune{}, tcell.StyleDefault) } func (e Edge) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } func (e Edge) SetRect(x, y, width, height int) { } func (e Edge) InputHandler() func(_ *tcell.EventKey, _ func(p tview.Primitive)) { return func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {} } func (e Edge) HasFocus() bool { return false } func (e Edge) Focus(delegate func(p tview.Primitive)) { } func (e Edge) Blur() { } func (e Edge) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { return func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { return false, nil } } func NewUI() *UI { app := tview.NewApplication() ui := UI{ Mode: modeNormal, App: app, Fields: []field{{"scratch", true}, {"test", false}}, Nodes: []*node{}, Viewport: &viewport{}, Pages: tview.NewPages(), TopFlex: tview.NewFlex(), Field: tview.NewPages(), FieldBar: tview.NewFlex(), BottomBar: tview.NewPages(), ExInput: tview.NewInputField(), ExOutput: tview.NewTextView(), } app.SetInputCapture(ui.handleInput) ui.TopFlex.SetDirection(tview.FlexRow) ui.TopFlex.AddItem(ui.Field, 0, 20, true) ui.TopFlex.AddItem(ui.FieldBar, 0, 1, false) ui.TopFlex.AddItem(ui.BottomBar, 0, 1, false) ui.Pages.AddPage("main", ui.TopFlex, true, true) ui.BottomBar.AddPage("output", ui.ExOutput, true, true) ui.BottomBar.AddPage("input", ui.ExInput, true, false) ui.ExInput.SetLabel(":") ui.ExInput.SetDoneFunc(ui.handleExInput) ui.ExOutput.SetMaxLines(1) fmt.Fprintf(ui.ExOutput, "porphyry has started. :q to quit") ui.FieldBar.SetDirection(tview.FlexColumn) ui.Nodes = append(ui.Nodes, &node{ Text: "foobar\nbaz\nquux", }) ui.Nodes = append(ui.Nodes, &node{ Text: "the wild box", }) ui.Nodes = append(ui.Nodes, &node{ Text: "cool stories bros", }) ui.Nodes = append(ui.Nodes, &node{ Text: "why not", }) ui.Nodes = append(ui.Nodes, &node{ Text: "hello\nthere\nhow", }) x := 60 y := 0 rand.Seed(time.Now().Unix()) for _, n := range ui.Nodes { b := tview.NewTextView() b.SetText(n.Text) b.SetBorder(true) b.SetRect(x, y, 10, 5) n.X = x n.Y = y x -= 20 ui.Field.AddPage(fmt.Sprintf("%d", rand.Intn(10000)), b, false, true) n.widg = b } ui.Field.AddPage("edge", Edge{}, false, true) app.SetBeforeDrawFunc(func(_ tcell.Screen) bool { // Viewport _, _, w, h := ui.Field.GetRect() ui.Viewport.W = w ui.Viewport.H = h // Handle nodes for _, n := range ui.Nodes { _, _, w, h := n.widg.GetRect() x := n.X - ui.Viewport.X y := n.Y - ui.Viewport.Y n.widg.SetRect(x, y, w, h) } // Handle field bar ui.FieldBar.Clear() for _, f := range ui.Fields { t := tview.NewTextView().SetTextStyle(tcell.StyleDefault.Bold(f.Selected)) t.SetMaxLines(1) t.SetBorder(true) fmt.Fprintf(t, f.Name) ui.FieldBar.AddItem(t, 0, 1, false) } // Handle ex mode prompt if ui.Mode == modeEx { ui.BottomBar.SwitchToPage("input") } else { ui.BottomBar.SwitchToPage("output") } return false }) app.SetRoot(ui.Pages, true) return &ui } func (ui *UI) quit() { ui.App.Stop() } func _main() error { cmd := &cobra.Command{ Use: "porphyry", RunE: func(cmd *cobra.Command, args []string) error { return NewUI().App.Run() }, } return cmd.Execute() } func main() { if err := _main(); err != nil { fmt.Fprintf(os.Stderr, err.Error()+"\n") } }