porphyry/main.go

306 lines
7.1 KiB
Go

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")
}
}