diff --git a/t/internal/dom/dom.go b/t/internal/dom/dom.go new file mode 100644 index 0000000..a1000bf --- /dev/null +++ b/t/internal/dom/dom.go @@ -0,0 +1,101 @@ +// Package dom will be used to handle the multiple elements of an UI +package dom + +import "github.com/rivo/tview" + +// ErrElementAlreadyRegistered defines the error scenario of the user trying +// to register an element to the DOM with an already registered ID +type ErrElementAlreadyRegistered struct{} + +func (e *ErrElementAlreadyRegistered) Error() string { + return "an element with that ID has already been registered in the DOM" +} + +type DOM struct { + Root *UINode + elements map[string]*UINode +} + +// NewDOM constructs a new DOM +func NewDOM() *DOM { + return &DOM{ + Root: nil, + elements: make(map[string]*UINode), + } +} + +// Private +func (d *DOM) idIsRegisterd(ID string) bool { + _, ok := d.elements[ID] + return ok +} + +// registerElem register a new element to the DOM +// should verify that the ID doesn't yet exist and thats it so far +func (d *DOM) registerElem(elem *UINode) error { + if d.idIsRegisterd(elem.ID) { + return &ErrElementAlreadyRegistered{} + } + + d.elements[elem.ID] = elem + return nil +} + +// Public + +// SetRoot defines the root element for the DOM +func (d *DOM) SetRoot(r *UINode) *DOM { + d.Root = r + return d +} + +// NewUINode creates and registers a UI Node +func (d *DOM) NewUINode(ID string, parent, root tview.Primitive) (*UINode, error) { + newNode := &UINode{ + ID: ID, + Self: root, + Children: make(map[string]*UINode), + } + + err := d.registerElem(newNode) + if err != nil { + return nil, err + } + + return newNode, nil +} + +// GetRoot fetches the root UINode of the DOM +func (d *DOM) GetRoot(r *UINode) *UINode { + return d.Root +} + +// GetRootElem fetches the root primitive of the dom +func (d *DOM) GetRootElem() tview.Primitive { + return d.Root.Self +} + +// GetElemByID returns the primitive of a UINode given its ID +// returns nil if the Node isn't registered +func (d *DOM) GetElemByID(ID string) tview.Primitive { + if !d.idIsRegisterd(ID) { + return nil + } + + return d.elements[ID].Self +} + +// GetNodeByID returns a UINode given its ID +// returns nil if the Node isn't registered +func (d *DOM) GetNodeByID(ID string) *UINode { + if !d.idIsRegisterd(ID) { + return d.elements[ID] + } + + return nil +} + +// AppendElem adds a new UINode to a given UINode +func (d *DOM) AppendElem(root *UINode, elem *UINode) error { + return root.AppendItem(elem) +} diff --git a/t/internal/dom/uinode.go b/t/internal/dom/uinode.go new file mode 100644 index 0000000..c7bc6f4 --- /dev/null +++ b/t/internal/dom/uinode.go @@ -0,0 +1,28 @@ +// Package dom +package dom + +import ( + "github.com/rivo/tview" +) + +type UINode struct { + ID string + Self tview.Primitive + Parent tview.Primitive + Children map[string]*UINode +} + +func newUINode(ID string, parent, root tview.Primitive) *UINode { + return &UINode{ + ID: ID, + Self: root, + Children: make(map[string]*UINode), + } +} + +func (l *UINode) AppendItem(item *UINode) error { + // Its not necessary to verify if this ID is already a child of this Node + // because we do that at the UI level + l.Children[item.ID] = item + return nil +} diff --git a/t/internal/tui/tui.go b/t/internal/tui/tui.go new file mode 100644 index 0000000..19bc974 --- /dev/null +++ b/t/internal/tui/tui.go @@ -0,0 +1,77 @@ +// Package tui +package tui + +import ( + dom "esdi/t/internal/dom" + "esdi/t/internal/views" + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type TUI struct { + App *tview.Application + Dom *dom.DOM +} + +var ( + application *TUI + initiated = false +) + +func NewTUI() *TUI { + return &TUI{ + App: tview.NewApplication(), + Dom: dom.NewDOM(), + } +} + +func (t *TUI) Start() error { + // The app starts running here! + // + // Set the event capture for the global app itself here + t.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyCtrlC || event.Rune() == 'q' { + t.App.Stop() + return nil + } + + return event + }) + + ctx := &views.UIContext{ + Redraw: func() { + t.App.QueueUpdateDraw(func() {}) + }, + } + + // Set the main view + mainUINode, err := views.BuildMainFlex(t.Dom, ctx) + if err != nil { + panic(fmt.Errorf("failed to build main flex - %s", err.Error())) + } + + rootNode, err := t.Dom.NewUINode("root", nil, mainUINode) + if err != nil { + panic(fmt.Errorf("failed to create root node - %s", err.Error())) + } + + t.Dom.SetRoot(rootNode) + firstFocus := t.Dom.GetElemByID("device-api-list") + if firstFocus == nil { + panic(fmt.Errorf("`list-window` isn't registered")) + } + + // Star the app + err = t.App.SetRoot(t.Dom.GetRootElem(), true).SetFocus(firstFocus).Run() + if err != nil { + return err + } + + return nil +} + +func Init() { + initiated = true +} diff --git a/t/internal/views/ctx.go b/t/internal/views/ctx.go new file mode 100644 index 0000000..611524a --- /dev/null +++ b/t/internal/views/ctx.go @@ -0,0 +1,5 @@ +package views + +type UIContext struct { + Redraw func() +} diff --git a/t/internal/views/layoutTool.go b/t/internal/views/layoutTool.go new file mode 100644 index 0000000..129c9da --- /dev/null +++ b/t/internal/views/layoutTool.go @@ -0,0 +1,36 @@ +package views + +import ( + "esdi/t/internal/dom" + + "github.com/rivo/tview" +) + +func buildLayoutToolUINode(root *dom.DOM) (*dom.UINode, error) { + // Want a TreeView at the top + layoutTree := tview.NewTreeView() + layoutTree.SetBorder(true).SetTitle("Layout Tree") + _, err := root.NewUINode("layout-tree", + root.GetElemByID("right-flex"), layoutTree) + if err != nil { + return nil, err + } + + // Want pages below to run actions + actionPages := tview.NewPages() + actionPages.SetBorder(true).SetTitle("Action Page") + _, err = root.NewUINode("layout-action-pages", + root.GetElemByID("right-flex"), actionPages) + if err != nil { + return nil, err + } + + layoutToolFlex := tview.NewFlex().SetDirection(tview.FlexRow) + layoutToolFlexNode, err := root.NewUINode("layout-tool-flex", root.GetElemByID("api-tool-pages"), + layoutToolFlex) + if err != nil { + return nil, err + } + + return layoutToolFlexNode, nil +} diff --git a/t/internal/views/mainFlex.go b/t/internal/views/mainFlex.go new file mode 100644 index 0000000..1f37f05 --- /dev/null +++ b/t/internal/views/mainFlex.go @@ -0,0 +1,94 @@ +// Package views +package views + +import ( + "esdi/t/internal/dom" + "fmt" + + "github.com/rivo/tview" +) + +func buildRightSidePages(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) { + // We need to build a set of pages with an empty page + emptyPage := tview.NewTextView(). + SetTextAlign(tview.AlignCenter). + SetChangedFunc(func() { + ctx.Redraw() + }) + emptyPage.SetBorder(true).SetTitle("-- Tool Area --") + + fmt.Fprintf(emptyPage, "No Tool Selected") + + apiToolPages := tview.NewPages(). + AddPage("emptypage", emptyPage, true, true) + + apiToolPagesNode, err := root.NewUINode("api-tool-pages", root.GetElemByID("right-flex"), + apiToolPages) + if err != nil { + return nil, err + } + + return apiToolPagesNode, nil +} + +func buildRightSideFlex(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) { + apiToolPagesNode, err := buildRightSidePages(root, ctx) + if err != nil { + return nil, err + } + + // This will be the right side flex + flex := tview.NewFlex().SetDirection(tview.FlexRow) + flexNode, err := root.NewUINode("right-flex", root.GetElemByID("main-flex"), flex) + if err != nil { + return nil, err + } + + // Make the output window + outputWin := tview.NewTextView().SetBorder(true).SetTitle("DebugWindow") + outputWinNode, err := root.NewUINode("debug-window", root.GetRootElem(), + outputWin) + if err != nil { + return nil, err + } + + flex. + AddItem(apiToolPagesNode.Self, 0, 5, false). + AddItem(outputWinNode.Self, 0, 2, false) + + return flexNode, nil +} + +func BuildMainFlex(root *dom.DOM, ctx *UIContext) (*tview.Flex, error) { + // To build the main view we must set the DOM root + mainFlex := tview.NewFlex().SetDirection(tview.FlexColumn) + mainFlexUINode, err := root.NewUINode("main-flex", nil, mainFlex) + if err != nil { + return nil, err + } + + root.SetRoot(mainFlexUINode) + + deviceAPIList := tview.NewList(). + AddItem("layout", "build a layout for CDashDisplay", 0, func() { + // When clicked we want to replace the right side pages with this + }) + deviceAPIList.SetBorder(true).SetTitle("list") + + apiListWindowUINode, err := root.NewUINode("device-api-list", root.GetRootElem(), + deviceAPIList) + if err != nil { + return nil, err + } + + rightSideFlex, err := buildRightSideFlex(root, ctx) + if err != nil { + return nil, err + } + + mainFlex. + AddItem(apiListWindowUINode.Self, 0, 1, false). + AddItem(rightSideFlex.Self, 0, 4, false) + + return mainFlex, nil +} diff --git a/t/tui.go b/t/tui.go new file mode 100644 index 0000000..b894445 --- /dev/null +++ b/t/tui.go @@ -0,0 +1,9 @@ +// Package t +package t + +import "esdi/t/internal/tui" + +func Run() error { + app := tui.NewTUI() + return app.Start() +}