From b1d027fdad38463ca68acc2bef34d6402cd3d985 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Mon, 25 Dec 2023 19:49:06 -0500 Subject: [PATCH] POC displaying a graph --- go.mod | 1 + go.sum | 2 + internal/lib/go.mod | 7 +- internal/lib/go.sum | 3 + internal/lib/results.go | 162 +++++++++++++++++++++++++++++++++++----- main.go | 20 ++++- 6 files changed, 170 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index a7ade5f..2b4284b 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mum4k/termdash v0.18.0 // indirect github.com/rivo/tview v0.0.0-20231206124440-5f078138442e // indirect github.com/rivo/uniseg v0.4.3 // indirect golang.org/x/sys v0.14.0 // indirect diff --git a/go.sum b/go.sum index 192a23f..07b7ddd 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mum4k/termdash v0.18.0 h1:wpy3FKcVV5s2TOoMTKzqQXwL5VClZIlNrRqZDpeIzBA= +github.com/mum4k/termdash v0.18.0/go.mod h1:VWL18wLZDKVKF/f4TkMRiKZb9Eg8Ax99PtNuGuRAguw= github.com/rivo/tview v0.0.0-20231206124440-5f078138442e h1:mPy47VW9tkqImnSPgcjnEHJuG3XHDBtXj2hDb1qBrRs= github.com/rivo/tview v0.0.0-20231206124440-5f078138442e/go.mod h1:c0SPlNPXkM+/Zgjn/0vD3W0Ds1yxstN7lpquqLDpWCg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/internal/lib/go.mod b/internal/lib/go.mod index 2450b73..6375c86 100644 --- a/internal/lib/go.mod +++ b/internal/lib/go.mod @@ -7,11 +7,14 @@ require ( golang.org/x/exp v0.0.0-20231127185646-65229373498e ) -require pkg/storage v0.0.0 +require ( + github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 + github.com/mum4k/termdash v0.18.0 + pkg/storage v0.0.0 +) require ( github.com/gdamore/encoding v1.0.0 // indirect - github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/rivo/uniseg v0.4.3 // indirect diff --git a/internal/lib/go.sum b/internal/lib/go.sum index cca2eff..3984880 100644 --- a/internal/lib/go.sum +++ b/internal/lib/go.sum @@ -2,10 +2,13 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 h1:SeDV6ZUSVlTAUUPdMzPXgMyj96z+whQJRRUff8dIeic= github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73/go.mod h1:pwzJMyH4Hd0AZMJkWQ+/g01dDvYWEvmJuaiRU71Xl8k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mum4k/termdash v0.18.0 h1:wpy3FKcVV5s2TOoMTKzqQXwL5VClZIlNrRqZDpeIzBA= +github.com/mum4k/termdash v0.18.0/go.mod h1:VWL18wLZDKVKF/f4TkMRiKZb9Eg8Ax99PtNuGuRAguw= github.com/rivo/tview v0.0.0-20231206124440-5f078138442e h1:mPy47VW9tkqImnSPgcjnEHJuG3XHDBtXj2hDb1qBrRs= github.com/rivo/tview v0.0.0-20231206124440-5f078138442e/go.mod h1:c0SPlNPXkM+/Zgjn/0vD3W0Ds1yxstN7lpquqLDpWCg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/internal/lib/results.go b/internal/lib/results.go index 86bc80a..a222471 100644 --- a/internal/lib/results.go +++ b/internal/lib/results.go @@ -1,9 +1,16 @@ // // Results management. +// +// Managing results involves: +// +// - Organizing a storage of results. +// - Managing the TUI libraries, rendering, and interaction for results. +// - Finding a place for accessory output, like logs. package lib import ( + "context" "fmt" "os" "strconv" @@ -14,37 +21,103 @@ import ( "pkg/storage" "github.com/gdamore/tcell/v2" + "github.com/mum4k/termdash" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/container" + termdashTcell "github.com/mum4k/termdash/terminal/tcell" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgetapi" + "github.com/mum4k/termdash/widgets/sparkline" "github.com/rivo/tview" ) +// Represents the display mode. +type display_ int + const ( LOGS_SIZE = 1 // Proportional size of the logs widget. RESULTS_SIZE = 3 // Proportional size of the results widget. TABLE_PADDING = 2 // Padding for table cell entries. ) +const ( + DISPLAY_RAW display_ = iota + 1 // Used for direct output. + DISPLAY_TVIEW // Used when tview is the TUI driver. + DISPLAY_TERMDASH // Used when termdash is the TUI driver. + +) + var ( - app *tview.Application // Application for display. - results storage.Results // Stored results. + // Application for display. Only applicable for tview result modes. + app *tview.Application + // Display mode, dictated by the results. + mode display_ + // Stored results. + results storage.Results // Widget for displaying logs. Publicly offered to allow log configuration. + // Only applicable for tview result modes. LogsView *tview.TextView ) +/////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Private +// +/////////////////////////////////////////////////////////////////////////////////////////////////// + // Set-up the sync for logs used by some result modes. func init() { + // Initialized specifically for showing logs in a tview pane. Currently, + // tview is the only supported display backend that supports logging, and + // termdash will not show logs. + // + // Initializing this is harmless, even if tview won't be used. + // + // TODO This should be probably be managed outside of init and should be made + // display mode agnostic, if possible. LogsView = tview.NewTextView().SetChangedFunc(func() { app.Draw() }) LogsView.SetBorder(true).SetTitle("Logs") } -// Sets-up the flex box, which defines the overall layout. -func initDisplay(resultsView tview.Primitive, logsView tview.Primitive) { +// Sets-up the termdash display and renders a widget. +func initDisplayTermdash(resultsWidget widgetapi.Widget) { + // Set-up the context and enable it to close on key-press. + ctx, cancel := context.WithCancel(context.Background()) + + // Set-up the layout. + t, err := termdashTcell.New() + e(err) + + // Render the widget. + c, err := container.New(t, container.PlaceWidget(resultsWidget)) + e(err) + + // Run the display. + termdash.Run(ctx, t, c, termdash.KeyboardSubscriber( + func(k *terminalapi.Keyboard) { + if k.Key == 'q' { + cancel() + t.Close() + os.Exit(0) + } + }, + )) +} + +// Sets-up the tview flex box with results and logs views, which defines the +// overall layout. +// +// Note that the app needs to be run separately from initialization in the +// coroutine display function. +func initDisplayTview(resultsView tview.Primitive, logsView tview.Primitive) { + // Initialize the app. app = tview.NewApplication() + // Set-up the layout and apply views. flexBox := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(resultsView, 0, RESULTS_SIZE, false). AddItem(logsView, 0, LOGS_SIZE, false) - app.SetRoot(flexBox, true).SetFocus(resultsView) } @@ -54,11 +127,23 @@ func display(f func()) { // Execute the update function. go func() { f() }() - // Start the display. - err := app.Run() - e(err) + switch mode { + case DISPLAY_TVIEW: + // Start the tview-specific display. + err := app.Run() + e(err) + case DISPLAY_TERMDASH: + // Start the termdash-specific display. + // Nothing to do, yet. + } } +/////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Public +// +/////////////////////////////////////////////////////////////////////////////////////////////////// + // Adds a result to the result store. // // TODO In the future, multiple result stores could be implemented by making @@ -108,15 +193,34 @@ func TokenizeResult(result string) (parsedResult []interface{}) { return } +/////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Result Modes +// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Presents raw output. +func RawResults() { + mode = DISPLAY_RAW + + go func() { + for { + fmt.Println(<-storage.PutEvents) + } + }() +} + // Update the results pane with new results as they are generated. func StreamResults() { + mode = DISPLAY_TVIEW + resultsView := tview.NewTextView().SetChangedFunc( func() { app.Draw() }) resultsView.SetBorder(true).SetTitle("Results") - initDisplay(resultsView, LogsView) + initDisplayTview(resultsView, LogsView) display( func() { @@ -127,21 +231,14 @@ func StreamResults() { ) } -// Presents raw output. -func RawResults() { - go func() { - for { - fmt.Println(<-storage.PutEvents) - } - }() -} - // Creates a table of results for the results pane. func TableResults() { + mode = DISPLAY_TVIEW + resultsView := tview.NewTable().SetBorders(true) tableCellPadding := strings.Repeat(" ", TABLE_PADDING) - initDisplay(resultsView, LogsView) + initDisplayTview(resultsView, LogsView) resultsView.SetDoneFunc( func(key tcell.Key) { @@ -180,6 +277,8 @@ func TableResults() { row.SetCellSimple(i, j, tableCellPadding+nextCellContent+tableCellPadding) } + // Re-draw the app with the new results row. + app.Draw() i += 1 } }, @@ -189,3 +288,28 @@ func TableResults() { err := app.Run() e(err) } + +// Creates a graph of results for the results pane. +func GraphResults() { + mode = DISPLAY_TERMDASH + + graph, err := sparkline.New( + sparkline.Label("Test Sparkline", cell.FgColor(cell.ColorNumber(33))), + sparkline.Color(cell.ColorGreen), + ) + e(err) + + display( + func() { + for { + next := <-storage.PutEvents + + graph.Add([]int{int(100 * next.Values[9].(float64))}) + } + + graph.Add([]int{1, 2, 3}) + }, + ) + + initDisplayTermdash(graph) +} diff --git a/main.go b/main.go index a09f163..ebb186b 100644 --- a/main.go +++ b/main.go @@ -53,6 +53,7 @@ const ( RESULT_MODE_RAW resultMode_ = iota + 1 // For running in 'raw' result mode. RESULT_MODE_STREAM // For running in 'stream' result mode. RESULT_MODE_TABLE // For running in 'table' result mode. + RESULT_MODE_GRAPH // For running in 'graph' result mode. ) var ( @@ -124,10 +125,10 @@ func main() { // Execute result viewing. if !silent { - switch { - case resultMode == int(RESULT_MODE_RAW): + switch resultMode { + case int(RESULT_MODE_RAW): lib.RawResults() - case resultMode == int(RESULT_MODE_STREAM): + case int(RESULT_MODE_STREAM): // Pass logs into the logs view pane. slog.SetDefault(slog.New(slog.NewTextHandler( lib.LogsView, @@ -135,7 +136,7 @@ func main() { ))) lib.StreamResults() - case resultMode == int(RESULT_MODE_TABLE): + case int(RESULT_MODE_TABLE): // Pass logs into the logs view pane. slog.SetDefault(slog.New(slog.NewTextHandler( lib.LogsView, @@ -143,6 +144,17 @@ func main() { ))) lib.TableResults() + case int(RESULT_MODE_GRAPH): + // Pass logs into the logs view pane. + // + // FIXME Log management for termdash applications doesn't work the same + // way and needs to be managed. + // slog.SetDefault(slog.New(slog.NewTextHandler( + // lib.LogsView, + // &slog.HandlerOptions{Level: logLevelStrToSlogLevel[logLevel]}, + // ))) + + lib.GraphResults() default: slog.Error(fmt.Sprintf("Invalid result mode: %d\n", resultMode)) os.Exit(1)