Big One
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// Package controllers
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"esdi/t/internal/events"
|
||||
"esdi/t/internal/models"
|
||||
"esdi/t/internal/ui"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type WindowingController struct {
|
||||
Ctx *ui.UIContext
|
||||
Events *events.Bus
|
||||
}
|
||||
|
||||
func (wc *WindowingController) CreateWindow(x, y,
|
||||
width, height, title string) {
|
||||
wc.Ctx.Log("x : %s\n", x)
|
||||
wc.Ctx.Log("y : %s\n", y)
|
||||
wc.Ctx.Log("width : %s\n", width)
|
||||
wc.Ctx.Log("height: %s\n", height)
|
||||
wc.Ctx.Log("title : %s\n", title)
|
||||
// newWindow := tview.NewTreeNode(title)
|
||||
|
||||
xValue, err := strconv.ParseInt(x, 10, 1)
|
||||
if err != nil {
|
||||
// return err
|
||||
}
|
||||
yValue, err := strconv.ParseInt(y, 10, 1)
|
||||
if err != nil {
|
||||
// return err
|
||||
}
|
||||
widthValue, err := strconv.ParseInt(width, 10, 1)
|
||||
if err != nil {
|
||||
// return err
|
||||
}
|
||||
heightValue, err := strconv.ParseInt(height, 10, 1)
|
||||
if err != nil {
|
||||
// return err
|
||||
}
|
||||
|
||||
window := models.Window{
|
||||
X: uint16(xValue),
|
||||
Y: uint16(yValue),
|
||||
Width: uint16(widthValue),
|
||||
Height: uint16(heightValue),
|
||||
Title: title,
|
||||
}
|
||||
|
||||
wc.Events.Emit(events.WindowCreated{Window: window})
|
||||
}
|
||||
+20
-7
@@ -1,7 +1,12 @@
|
||||
// Package dom will be used to handle the multiple elements of an UI
|
||||
package dom
|
||||
|
||||
import "github.com/rivo/tview"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"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
|
||||
@@ -24,8 +29,16 @@ func NewDOM() *DOM {
|
||||
}
|
||||
}
|
||||
|
||||
// Debugging
|
||||
func (d *DOM) ListRegisteredUINodes(io io.Writer) {
|
||||
fmt.Printf("Registered Nodes:\n")
|
||||
for key := range d.elements {
|
||||
fmt.Fprintf(io, " %s\n", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Private
|
||||
func (d *DOM) idIsRegisterd(ID string) bool {
|
||||
func (d *DOM) idIsRegistered(ID string) bool {
|
||||
_, ok := d.elements[ID]
|
||||
return ok
|
||||
}
|
||||
@@ -33,7 +46,7 @@ func (d *DOM) idIsRegisterd(ID string) bool {
|
||||
// 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) {
|
||||
if d.idIsRegistered(elem.ID) {
|
||||
return &ErrElementAlreadyRegistered{}
|
||||
}
|
||||
|
||||
@@ -78,17 +91,17 @@ func (d *DOM) GetRootElem() tview.Primitive {
|
||||
// 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
|
||||
if d.idIsRegistered(ID) {
|
||||
return d.elements[ID].Self
|
||||
}
|
||||
|
||||
return d.elements[ID].Self
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if d.idIsRegistered(ID) {
|
||||
return d.elements[ID]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Package events defines a simple event handler system
|
||||
package events
|
||||
|
||||
import "sync"
|
||||
|
||||
type Handler func(event any)
|
||||
|
||||
type Bus struct {
|
||||
mu sync.RWMutex
|
||||
handlers map[string][]Handler
|
||||
}
|
||||
|
||||
func NewBus() *Bus {
|
||||
return &Bus{
|
||||
handlers: make(map[string][]Handler),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) On(eventType any, h Handler) {
|
||||
key := typeKey(eventType)
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.handlers[key] = append(b.handlers[key], h)
|
||||
}
|
||||
|
||||
func (b *Bus) Emit(event any) {
|
||||
key := typeKey(event)
|
||||
|
||||
b.mu.RLock()
|
||||
handlers := append([]Handler{}, b.handlers[key]...)
|
||||
b.mu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
h(event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package events
|
||||
|
||||
import "esdi/t/internal/models"
|
||||
|
||||
type WindowCreated struct {
|
||||
Window models.Window
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package events
|
||||
|
||||
import "fmt"
|
||||
|
||||
func typeKey(v any) string {
|
||||
return fmt.Sprintf("%T", v)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Package models
|
||||
package models
|
||||
|
||||
type Window struct {
|
||||
X uint16
|
||||
Y uint16
|
||||
Width uint16
|
||||
Height uint16
|
||||
Title string
|
||||
}
|
||||
+36
-15
@@ -2,7 +2,10 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"esdi/t/internal/controllers"
|
||||
dom "esdi/t/internal/dom"
|
||||
"esdi/t/internal/events"
|
||||
"esdi/t/internal/ui"
|
||||
"esdi/t/internal/views"
|
||||
"fmt"
|
||||
|
||||
@@ -11,22 +14,24 @@ import (
|
||||
)
|
||||
|
||||
type TUI struct {
|
||||
App *tview.Application
|
||||
Dom *dom.DOM
|
||||
App *tview.Application
|
||||
Dom *dom.DOM
|
||||
Events *events.Bus
|
||||
}
|
||||
|
||||
var (
|
||||
application *TUI
|
||||
initiated = false
|
||||
)
|
||||
|
||||
func NewTUI() *TUI {
|
||||
return &TUI{
|
||||
App: tview.NewApplication(),
|
||||
Dom: dom.NewDOM(),
|
||||
App: tview.NewApplication(),
|
||||
Dom: dom.NewDOM(),
|
||||
Events: events.NewBus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TUI) log(formatter string, args ...any) {
|
||||
outputWin := t.Dom.GetElemByID("output-window").(*tview.TextView)
|
||||
fmt.Fprintf(outputWin, formatter, args...)
|
||||
}
|
||||
|
||||
func (t *TUI) Start() error {
|
||||
// The app starts running here!
|
||||
//
|
||||
@@ -40,14 +45,34 @@ func (t *TUI) Start() error {
|
||||
return event
|
||||
})
|
||||
|
||||
ctx := &views.UIContext{
|
||||
ctx := &ui.UIContext{
|
||||
Redraw: func() {
|
||||
t.App.QueueUpdateDraw(func() {})
|
||||
},
|
||||
Log: func(formatter string, args ...any) {
|
||||
t.log(formatter, args...)
|
||||
},
|
||||
ChangeFocus: func(p tview.Primitive) {
|
||||
t.App.SetFocus(p)
|
||||
},
|
||||
}
|
||||
|
||||
// Create the controllers
|
||||
windowingController := &controllers.WindowingController{
|
||||
Events: t.Events,
|
||||
Ctx: ctx,
|
||||
}
|
||||
|
||||
views.BindWindowEvents(ctx, t.Events, nil)
|
||||
|
||||
// Make the output window
|
||||
err := views.BuildOutputWindow(t.Dom, ctx)
|
||||
if err != nil {
|
||||
panic("failed to create output window")
|
||||
}
|
||||
|
||||
// Set the main view
|
||||
mainUINode, err := views.BuildMainFlex(t.Dom, ctx)
|
||||
mainUINode, err := views.BuildMainFlex(t.Dom, ctx, windowingController)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to build main flex - %s", err.Error()))
|
||||
}
|
||||
@@ -71,7 +96,3 @@ func (t *TUI) Start() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Init() {
|
||||
initiated = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Package ui
|
||||
package ui
|
||||
|
||||
import "github.com/rivo/tview"
|
||||
|
||||
type UIContext struct {
|
||||
Redraw func()
|
||||
Log func(formatter string, args ...any)
|
||||
ChangeFocus func(p tview.Primitive)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"esdi/t/internal/dom"
|
||||
"esdi/t/internal/ui"
|
||||
"slices"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func AddAndShowPage(root *dom.DOM, ctx *ui.UIContext,
|
||||
pages *tview.Pages, page *dom.UINode) error {
|
||||
if !slices.Contains(pages.GetPageNames(false), page.ID) {
|
||||
pages.AddAndSwitchToPage(page.ID, page.Self, true)
|
||||
} else {
|
||||
// Only change to page
|
||||
pages.SwitchToPage(page.ID)
|
||||
}
|
||||
|
||||
// Set focus to our new tool
|
||||
ctx.ChangeFocus(page.Self)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// func PageExists(pages *tview.Pages, needle string) bool {
|
||||
// return slices.Contains(pages.GetPageNames(false), needle)
|
||||
// }
|
||||
@@ -1,5 +0,0 @@
|
||||
package views
|
||||
|
||||
type UIContext struct {
|
||||
Redraw func()
|
||||
}
|
||||
@@ -1,14 +1,85 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"esdi/t/internal/controllers"
|
||||
"esdi/t/internal/dom"
|
||||
"esdi/t/internal/events"
|
||||
"esdi/t/internal/ui"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func buildLayoutToolUINode(root *dom.DOM) (*dom.UINode, error) {
|
||||
const (
|
||||
layoutToolFlexID = "layout-tool-flex"
|
||||
)
|
||||
|
||||
func BindWindowEvents(
|
||||
ctx *ui.UIContext,
|
||||
bus *events.Bus,
|
||||
tree *tview.TreeView,
|
||||
) {
|
||||
bus.On(events.WindowCreated{}, func(e any) {
|
||||
ctx.Log("Received a window created event")
|
||||
})
|
||||
}
|
||||
|
||||
func layoutToolTreeViewEvents(root *dom.DOM, ctx *ui.UIContext,
|
||||
wc *controllers.WindowingController) func(
|
||||
event *tcell.EventKey) *tcell.EventKey {
|
||||
return func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEsc:
|
||||
ctx.ChangeFocus(root.GetElemByID(deviceAPIListID))
|
||||
}
|
||||
|
||||
switch event.Rune() {
|
||||
case 'N':
|
||||
// Create a new screen
|
||||
case 'n':
|
||||
// Create a new window
|
||||
ctx.Log("calling new window form\n")
|
||||
createNewWindowForm(root, ctx, wc)
|
||||
case 'x':
|
||||
// Delete selected window
|
||||
case 'm':
|
||||
// Go into move mode
|
||||
case 'e':
|
||||
// Go into edit mode
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func layoutToolUIOnSelect(root *dom.DOM, ctx *ui.UIContext, wc *controllers.WindowingController) {
|
||||
ctx.Log("Opening layout tool UI\n")
|
||||
var err error
|
||||
|
||||
// Get the api pages
|
||||
apiToolPages := root.GetElemByID(apiToolPagesID).(*tview.Pages)
|
||||
if apiToolPages == nil {
|
||||
ctx.Log(" Failed to retrive apiToolPages UI\n")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Log(" Checking if `%s` UINode already exists\n", layoutToolFlexID)
|
||||
layoutToolUINode := root.GetNodeByID(layoutToolFlexID)
|
||||
if layoutToolUINode == nil {
|
||||
layoutToolUINode, err = buildLayoutToolUINode(root, ctx, wc)
|
||||
if err != nil {
|
||||
ctx.Log(" Failed to build layout tool UI: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
AddAndShowPage(root, ctx, apiToolPages, layoutToolUINode)
|
||||
}
|
||||
|
||||
func buildLayoutToolUINode(root *dom.DOM, ctx *ui.UIContext,
|
||||
wc *controllers.WindowingController) (*dom.UINode, error) {
|
||||
// Want a TreeView at the top
|
||||
layoutTree := tview.NewTreeView()
|
||||
layoutTree := tview.NewTreeView().
|
||||
SetInputCapture(layoutToolTreeViewEvents(root, ctx, wc))
|
||||
layoutTree.SetBorder(true).SetTitle("Layout Tree")
|
||||
_, err := root.NewUINode("layout-tree",
|
||||
root.GetElemByID("right-flex"), layoutTree)
|
||||
@@ -26,11 +97,68 @@ func buildLayoutToolUINode(root *dom.DOM) (*dom.UINode, error) {
|
||||
}
|
||||
|
||||
layoutToolFlex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
layoutToolFlexNode, err := root.NewUINode("layout-tool-flex", root.GetElemByID("api-tool-pages"),
|
||||
layoutToolFlexNode, err := root.NewUINode(layoutToolFlexID, root.GetElemByID("api-tool-pages"),
|
||||
layoutToolFlex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layoutToolFlex.
|
||||
AddItem(layoutTree, 0, 2, true).
|
||||
AddItem(actionPages, 0, 5, false)
|
||||
|
||||
return layoutToolFlexNode, nil
|
||||
}
|
||||
|
||||
// Layout tool actions
|
||||
func createNewWindowForm(root *dom.DOM, ctx *ui.UIContext,
|
||||
wc *controllers.WindowingController) {
|
||||
var err error
|
||||
|
||||
x0 := tview.NewInputField().SetLabel("x")
|
||||
y0 := tview.NewInputField().SetLabel("y")
|
||||
width := tview.NewInputField().SetLabel("width")
|
||||
height := tview.NewInputField().SetLabel("height")
|
||||
title := tview.NewInputField().SetLabel("title")
|
||||
|
||||
form := tview.NewForm().
|
||||
AddFormItem(x0).
|
||||
AddFormItem(y0).
|
||||
AddFormItem(width).
|
||||
AddFormItem(height).
|
||||
AddFormItem(title).
|
||||
AddButton("Create", func() {
|
||||
wc.CreateWindow(
|
||||
x0.GetText(),
|
||||
y0.GetText(),
|
||||
width.GetText(),
|
||||
height.GetText(),
|
||||
title.GetText(),
|
||||
)
|
||||
})
|
||||
|
||||
form.SetBorder(true).
|
||||
SetTitle("new window form").
|
||||
SetTitleAlign(tview.AlignLeft).
|
||||
SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
switch ev.Key() {
|
||||
case tcell.KeyEscape:
|
||||
ctx.ChangeFocus(root.GetElemByID(layoutToolFlexID))
|
||||
}
|
||||
|
||||
return ev
|
||||
})
|
||||
|
||||
var formNode *dom.UINode
|
||||
formNode = root.GetNodeByID("new-window-form")
|
||||
if formNode == nil {
|
||||
formNode, err = root.NewUINode("new-window-form", root.GetElemByID("layout-action-pages"),
|
||||
form)
|
||||
if err != nil {
|
||||
panic("failed to create UI node for the new window form: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
AddAndShowPage(root, ctx, root.GetElemByID("layout-action-pages").(*tview.Pages),
|
||||
formNode)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"esdi/t/internal/dom"
|
||||
"fmt"
|
||||
|
||||
"esdi/t/internal/controllers"
|
||||
"esdi/t/internal/dom"
|
||||
"esdi/t/internal/ui"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func buildRightSidePages(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) {
|
||||
const (
|
||||
mainFlexID = "main-flex"
|
||||
apiToolPagesID = "api-tool-pages"
|
||||
rightFlexID = "right-flex"
|
||||
outputPaneID = "output-window"
|
||||
deviceAPIListID = "device-api-list"
|
||||
)
|
||||
|
||||
const (
|
||||
pageName = "empty-page"
|
||||
)
|
||||
|
||||
func buildRightSidePages(root *dom.DOM, ctx *ui.UIContext) (*dom.UINode, error) {
|
||||
// We need to build a set of pages with an empty page
|
||||
emptyPage := tview.NewTextView().
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
@@ -20,9 +35,9 @@ func buildRightSidePages(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) {
|
||||
fmt.Fprintf(emptyPage, "No Tool Selected")
|
||||
|
||||
apiToolPages := tview.NewPages().
|
||||
AddPage("emptypage", emptyPage, true, true)
|
||||
AddPage(pageName, emptyPage, true, true)
|
||||
|
||||
apiToolPagesNode, err := root.NewUINode("api-tool-pages", root.GetElemByID("right-flex"),
|
||||
apiToolPagesNode, err := root.NewUINode(apiToolPagesID, root.GetElemByID(rightFlexID),
|
||||
apiToolPages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -31,7 +46,7 @@ func buildRightSidePages(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) {
|
||||
return apiToolPagesNode, nil
|
||||
}
|
||||
|
||||
func buildRightSideFlex(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) {
|
||||
func buildRightSideFlex(root *dom.DOM, ctx *ui.UIContext) (*dom.UINode, error) {
|
||||
apiToolPagesNode, err := buildRightSidePages(root, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -39,30 +54,28 @@ func buildRightSideFlex(root *dom.DOM, ctx *UIContext) (*dom.UINode, error) {
|
||||
|
||||
// This will be the right side flex
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
flexNode, err := root.NewUINode("right-flex", root.GetElemByID("main-flex"), flex)
|
||||
flexNode, err := root.NewUINode(rightFlexID, root.GetElemByID(mainFlexID), 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
|
||||
outputWin := root.GetElemByID(outputPaneID)
|
||||
if outputWin == nil {
|
||||
panic("failed to attach output win to UI")
|
||||
}
|
||||
|
||||
flex.
|
||||
AddItem(apiToolPagesNode.Self, 0, 5, false).
|
||||
AddItem(outputWinNode.Self, 0, 2, false)
|
||||
AddItem(outputWin, 0, 2, false)
|
||||
|
||||
return flexNode, nil
|
||||
}
|
||||
|
||||
func BuildMainFlex(root *dom.DOM, ctx *UIContext) (*tview.Flex, error) {
|
||||
func BuildMainFlex(root *dom.DOM, ctx *ui.UIContext,
|
||||
wc *controllers.WindowingController) (*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)
|
||||
mainFlexUINode, err := root.NewUINode(mainFlexID, nil, mainFlex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -71,11 +84,11 @@ func BuildMainFlex(root *dom.DOM, ctx *UIContext) (*tview.Flex, error) {
|
||||
|
||||
deviceAPIList := tview.NewList().
|
||||
AddItem("layout", "build a layout for CDashDisplay", 0, func() {
|
||||
// When clicked we want to replace the right side pages with this
|
||||
layoutToolUIOnSelect(root, ctx, wc)
|
||||
})
|
||||
deviceAPIList.SetBorder(true).SetTitle("list")
|
||||
|
||||
apiListWindowUINode, err := root.NewUINode("device-api-list", root.GetRootElem(),
|
||||
apiListWindowUINode, err := root.NewUINode(deviceAPIListID, root.GetRootElem(),
|
||||
deviceAPIList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"esdi/t/internal/dom"
|
||||
"esdi/t/internal/ui"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func BuildOutputWindow(root *dom.DOM, ctx *ui.UIContext) error {
|
||||
var outputWin *tview.TextView
|
||||
outputWin = tview.NewTextView().SetChangedFunc(func() {
|
||||
ctx.Redraw()
|
||||
outputWin.ScrollToEnd()
|
||||
})
|
||||
|
||||
outputWin.SetBorder(true).SetTitle("DebugWindow")
|
||||
_, err := root.NewUINode("output-window", nil, outputWin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user