From 24fea285c365097ac21a99ab6a2830b44978356f Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 9 Jun 2026 16:39:13 +0100 Subject: [PATCH] very bare bones layout selectable menu --- cdashdisplay/display.go | 24 ++- layouts/beamng.yaml | 193 ++++++++++++++++++ tui/internal/controllers/layout.go | 69 ++++++- tui/internal/services/cdashdisplay.go | 11 +- tui/internal/views/layoutTool.go | 16 +- .../views/layoutTool_layoutsListModal.go | 45 ++++ 6 files changed, 351 insertions(+), 7 deletions(-) create mode 100755 layouts/beamng.yaml create mode 100644 tui/internal/views/layoutTool_layoutsListModal.go diff --git a/cdashdisplay/display.go b/cdashdisplay/display.go index 9cce39c..9f33c14 100644 --- a/cdashdisplay/display.go +++ b/cdashdisplay/display.go @@ -8,6 +8,7 @@ import ( "log/slog" "os" "path" + "time" helper "esdi/helpers" "esdi/peripheral/communication" @@ -298,7 +299,7 @@ func (d *CDashDisplay) ResizeWindow(wID int16, delta *helper.Vector) error { func (d *CDashDisplay) SaveLayout(outputPath string) error { file, err := os.OpenFile(path.Join(layoutsDir, outputPath), - os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) if err != nil { return err } @@ -339,6 +340,27 @@ func (d *CDashDisplay) LoadLayout(layoutName string) error { return nil } +func (d *CDashDisplay) UnloadLayout() error { + var err error + for _, w := range d.State.Layout.Windows { + pLogger.Debug(fmt.Sprintf("= Removing %d ==============================================", + w.UIData.IDX)) + + err = d.DestroyWindow(w.UIData.IDX) + time.Sleep(75 * time.Millisecond) + if err != nil { + pLogger.Error(fmt.Sprintf("failed to destroy window: %+v", err)) + // NOTE: Add a way to handle multiple errors ? + return err + } + + pLogger.Debug(fmt.Sprintf("= Removing %d ==============================================", + w.UIData.IDX)) + } + + return nil +} + func (d *CDashDisplay) SendData(data *telemetry.TelemetryData) { packet := data.Pack() diff --git a/layouts/beamng.yaml b/layouts/beamng.yaml new file mode 100755 index 0000000..03f7d05 --- /dev/null +++ b/layouts/beamng.yaml @@ -0,0 +1,193 @@ +Windows: + 2: + uiwindow: + Dims: + X0: 470 + Y0: 71 + Width: 322 + Height: 57 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 4 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: "9999" + Title: Dig Tacho + uidata: + WID: 2 + TelemetryField: RPM + 4: + uiwindow: + Dims: + X0: 321 + Y0: 195 + Width: 134 + Height: 80 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 7 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: "234" + Title: SPEEDO + uidata: + WID: 4 + TelemetryField: Speed + 5: + uiwindow: + Dims: + X0: 590 + Y0: 367 + Width: 100 + Height: 50 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 3 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: "101.5" + Title: Water T + uidata: + WID: 5 + TelemetryField: Water Temperature + 10: + uiwindow: + Dims: + X0: 470 + Y0: 132 + Width: 169 + Height: 50 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 3 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: "123.4" + Title: Fuel Level + uidata: + WID: 10 + TelemetryField: Fuel Level + 11: + uiwindow: + Dims: + X0: 321 + Y0: 73 + Width: 134 + Height: 111 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 12 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: R + Title: Box + uidata: + WID: 11 + TelemetryField: Gear + 16: + uiwindow: + Dims: + X0: 694 + Y0: 423 + Width: 100 + Height: 50 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 3 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: "4.56" + Title: Oil P + uidata: + WID: 16 + TelemetryField: Oil Pressure + 17: + uiwindow: + Dims: + X0: 694 + Y0: 367 + Width: 100 + Height: 50 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 2 + TextSize: 3 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 2 + PreviewValue: "101.4" + Title: Oil T + uidata: + WID: 17 + TelemetryField: Oil Temperature + 20: + uiwindow: + Dims: + X0: 142 + Y0: 19 + Width: 500 + Height: 20 + Decor: + BGColour: 4161 + FGColour: 65535 + TitleColour: 65535 + BorderColour: 63488 + TitleSize: 1 + TextSize: 2 + HasBorder: 1 + Padding: 0 + Opts: + ShowID: 0 + WinType: 1 + PreviewValue: "5678" + Title: TACHO + uidata: + WID: 20 + TelemetryField: RPM diff --git a/tui/internal/controllers/layout.go b/tui/internal/controllers/layout.go index 142282c..430df27 100644 --- a/tui/internal/controllers/layout.go +++ b/tui/internal/controllers/layout.go @@ -2,6 +2,7 @@ package controllers import ( "fmt" + "os" "strconv" "esdi/cdashdisplay" @@ -20,6 +21,7 @@ type LayoutController struct { Messages chan string DevService *services.CDashService MoveToolState *windowManipState + SelectedLayout string // NOTE: This should be a struct to handle its own things } func NewLayoutController(base *Controller, service *services.CDashService) *LayoutController { @@ -29,6 +31,7 @@ func NewLayoutController(base *Controller, service *services.CDashService) *Layo Messages: make(chan string, 10), DevService: service, MoveToolState: &windowManipState{Mode: moveMode}, + SelectedLayout: "layout.yaml", } lc.registerHooks() @@ -56,6 +59,9 @@ func (lc *LayoutController) registerHooks() { case 'm': lc.Messages <- "calling window manipulation tool\n" lc.moveWindow() + case 'c': + lc.Messages <- "calling change selected layout tool\n" + lc.changeSelectedLoadout() case 's': // Save the current layout lc.Messages <- "calling save layout\n" @@ -64,6 +70,10 @@ func (lc *LayoutController) registerHooks() { // Load the layout lc.Messages <- "calling load layout\n" lc.loadLayout() + case 'u': + // Unload the layout + lc.Messages <- "calling unload layout\n" + lc.unloadLayout() case 'g': // Go -> launches the current set up source // streamingWindow(bus, doc) @@ -339,7 +349,7 @@ func (lc *LayoutController) getCurrentTreeNodeModel() (*tview.TreeNode, int16, e func (lc *LayoutController) loadLayout() { // We would get the layout path from somewhere but for nots its layout.yaml - err := lc.DevService.LoadLayout("layout.yaml") + err := lc.DevService.LoadLayout(lc.SelectedLayout) if err != nil { lc.Messages <- "failed to load layout: " + err.Error() return @@ -348,8 +358,16 @@ func (lc *LayoutController) loadLayout() { lc.displayLoadedLayouts() } +func (lc *LayoutController) unloadLayout() { + err := lc.DevService.UnloadLayout() + if err != nil { + lc.Logger.Error(fmt.Sprintf("Failed to unload layout: %+v", err)) + return + } +} + func (lc *LayoutController) saveLayout() { - err := lc.DevService.SaveLayout("layout.yaml") + err := lc.DevService.SaveLayout(lc.SelectedLayout) if err != nil { lc.Messages <- "failed to save layout: " + err.Error() return @@ -423,3 +441,50 @@ func (lc *LayoutController) moveWindow() { lc.App.SetFocus(formView.Form) } + +// changeSelectedLoadout is used to change the currently selected loadout +// its a very bare bones utility to aid development +// NOTE: FOR THE LOVE OF WHATS HOLY ACTUALLY MAKE THIS GOOD BEFORE ADDING MORE FEATURES +func (lc *LayoutController) changeSelectedLoadout() { + // First we get the files in the layouts directory + entries, err := os.ReadDir("./layouts") + if err != nil { + lc.Logger.Error(fmt.Sprintf("Failed to read directory: %v", err)) + return + } + + // Then we open a modal and pass that list there + lc.Logger.Debug(fmt.Sprintf("Found layouts: %+v", entries)) + + layoutSelection := views.NewLayoutToolLayoutsList() + layoutSelection.AddEntries(entries) + layoutSelection.Tree.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { + switch ev.Key() { + case tcell.KeyESC: + // We have to destroy this window, but first we tell the pages to show something else + // NOTE: will this be left hanging and then given to GC? Can we improve that? + lc.LayoutToolView.LayoutActions.LayoutSelection = nil + lc.LayoutToolView.DeleteLayoutList() + lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree) + } + + return ev + }) + + layoutSelection.Tree.SetSelectedFunc(func(node *tview.TreeNode) { + ref := node.GetReference() + if ref == nil { + return + } + + lc.Messages <- "User selected " + fmt.Sprintf("%+v", ref) + "\n" + lc.SelectedLayout = ref.(string) + + lc.LayoutToolView.LayoutTree.Tree.SetTitle(fmt.Sprintf("%s [%s]", + views.LayoutToolTreeViewTitle, ref.(string))) + }) + + lc.LayoutToolView.LayoutActions.LayoutSelection = layoutSelection + lc.LayoutToolView.ShowLayoutList() + lc.App.SetFocus(lc.LayoutToolView.LayoutActions.LayoutSelection.Tree) +} diff --git a/tui/internal/services/cdashdisplay.go b/tui/internal/services/cdashdisplay.go index f889b35..963818a 100644 --- a/tui/internal/services/cdashdisplay.go +++ b/tui/internal/services/cdashdisplay.go @@ -1,13 +1,14 @@ package services import ( + "fmt" + "log/slog" + "sync/atomic" + "esdi/cdashdisplay" helper "esdi/helpers" "esdi/peripheral" "esdi/telemetry" - "fmt" - "log/slog" - "sync/atomic" ) type CDashService struct { @@ -65,6 +66,10 @@ func (cds *CDashService) SaveLayout(layoutPath string) error { return cds.CDash.SaveLayout(layoutPath) } +func (cds *CDashService) UnloadLayout() error { + return cds.CDash.UnloadLayout() +} + func (cds *CDashService) UpdateWindow(win *cdashdisplay.DesktopUIWindow) error { cds.Messages <- fmt.Sprintf("Updating a window:\n%+v\n", win) return cds.CDash.UpdateWindow(win) diff --git a/tui/internal/views/layoutTool.go b/tui/internal/views/layoutTool.go index 483493e..0c6d150 100644 --- a/tui/internal/views/layoutTool.go +++ b/tui/internal/views/layoutTool.go @@ -12,6 +12,7 @@ import ( ) const ( + LayoutToolTreeViewTitle = "Layout Tree" LayoutToolFlexID = "layout-tool-flex" LayoutToolTreeID = "layout-tool-tree" LayoutToolActionPagesID = "layout-tool-action-pages" @@ -30,7 +31,7 @@ func NewLayoutTreeView() *LayoutTreeView { view.Tree = tview.NewTreeView() - view.Tree.SetBorder(true).SetTitle("Layout Tree") + view.Tree.SetBorder(true).SetTitle(LayoutToolTreeViewTitle) view.Tree.SetInputCapture(view.InputCapture) // Inject ChangedFunc // Inject SelectedFunc @@ -61,6 +62,7 @@ func (lt *LayoutTreeView) AddWindow(win *cdashdisplay.DesktopUIWindow) error { type LayoutToolActionView struct { Pages *tview.Pages CreateWindowView *CreateWindowFormView + LayoutSelection *LayoutToolLayoutsList } func NewLayoutToolActionView() *LayoutToolActionView { @@ -154,6 +156,18 @@ func (ltv *LayoutToolView) ShowWindowFormByID(idx int16) { ) } +func (ltv *LayoutToolView) ShowLayoutList() { + AddAndShowPage( + ltv.LayoutActions.Pages, + "available-layouts-list", + ltv.LayoutActions.LayoutSelection.Tree, + ) +} + +func (ltv *LayoutToolView) DeleteLayoutList() { + ltv.LayoutActions.Pages.RemovePage("available-layouts-list") +} + func (ltv *LayoutToolView) DeleteWindowByNode(node *tview.TreeNode) { root := ltv.LayoutTree.Tree.GetRoot() if root == nil { diff --git a/tui/internal/views/layoutTool_layoutsListModal.go b/tui/internal/views/layoutTool_layoutsListModal.go new file mode 100644 index 0000000..2b14830 --- /dev/null +++ b/tui/internal/views/layoutTool_layoutsListModal.go @@ -0,0 +1,45 @@ +package views + +import ( + "os" + + "github.com/rivo/tview" +) + +type LayoutToolLayoutsList struct { + Tree *tview.TreeView +} + +func NewLayoutToolLayoutsList() *LayoutToolLayoutsList { + view := &LayoutToolLayoutsList{} + + view.Tree = tview.NewTreeView() + + view.Tree.SetBorder(true).SetTitle("Layout List") + // Inject InputCapture + // Inject ChangedFunc + // Inject SelectedFunc + + // Create a root element + rootElem := tview.NewTreeNode(".") + view.Tree.SetRoot(rootElem).SetCurrentNode(rootElem) + + return view +} + +// AddEntries adds the list of files we wish the user to select from +// For now we will send just a list of []string but in the future this should +// receive (or have an alternative to receive) a directory structure +func (ltll *LayoutToolLayoutsList) AddEntries(entries []os.DirEntry) { + root := ltll.Tree.GetRoot() + if root == nil { + // NOTE: shouldn't happen at all + return + } + + for _, e := range entries { + node := tview.NewTreeNode(e.Name()). + SetReference(e.Name()) + root.AddChild(node) + } +}