tconsole: add status bar
This adds a status bar to the bottom of the tconsole. It contains a page
selector and clock.
Change-Id: Ia932fe793ff067f3d096046d8bd93c060bac807a
Reviewed-on: https://review.monogon.dev/c/monogon/+/3381
Tested-by: Jenkins CI
Reviewed-by: Leopold Schabel <leo@monogon.tech>
diff --git a/metropolis/node/core/tconsole/BUILD.bazel b/metropolis/node/core/tconsole/BUILD.bazel
index c8ca99e..fadc7f3 100644
--- a/metropolis/node/core/tconsole/BUILD.bazel
+++ b/metropolis/node/core/tconsole/BUILD.bazel
@@ -6,6 +6,7 @@
"colors.go",
"draw.go",
"page_status.go",
+ "statusbar.go",
"tconsole.go",
],
importpath = "source.monogon.dev/metropolis/node/core/tconsole",
diff --git a/metropolis/node/core/tconsole/draw.go b/metropolis/node/core/tconsole/draw.go
index d88e35b..4fec256 100644
--- a/metropolis/node/core/tconsole/draw.go
+++ b/metropolis/node/core/tconsole/draw.go
@@ -8,13 +8,15 @@
)
// drawText draws a single line of text from left to right, starting at x and y.
-func (c *Console) drawText(x, y int, text string, style tcell.Style) {
+func (c *Console) drawText(x, y int, text string, style tcell.Style) int {
g := uniseg.NewGraphemes(text)
+ xi := 0
for g.Next() {
runes := g.Runes()
- c.screen.SetContent(x, y, runes[0], runes[1:], style)
- x += 1
+ c.screen.SetContent(x+xi, y, runes[0], runes[1:], style)
+ xi += 1
}
+ return xi
}
// drawTextCentered draw a single line of text from left to right, starting at a
diff --git a/metropolis/node/core/tconsole/page_status.go b/metropolis/node/core/tconsole/page_status.go
index 13c4b63..fbf4649 100644
--- a/metropolis/node/core/tconsole/page_status.go
+++ b/metropolis/node/core/tconsole/page_status.go
@@ -73,6 +73,4 @@
if drawLogo {
c.drawLogo(splitH-logoWidth, splitV+center(c.height-splitV, logoHeight), sty2)
}
-
- c.screen.Show()
}
diff --git a/metropolis/node/core/tconsole/statusbar.go b/metropolis/node/core/tconsole/statusbar.go
new file mode 100644
index 0000000..06841c2
--- /dev/null
+++ b/metropolis/node/core/tconsole/statusbar.go
@@ -0,0 +1,44 @@
+package tconsole
+
+import (
+ "time"
+
+ "github.com/gdamore/tcell/v2"
+)
+
+// draw button at coordinates containing text, with the left side of the button
+// at (x, y). The number of columns used up by the button is returned.
+func (c *Console) button(x, y int, caption string, selected bool, sty tcell.Style) int {
+ fg, bg, _ := sty.Decompose()
+ styInv := sty.Background(fg).Foreground(bg)
+
+ xi := 1
+ if selected {
+ c.screen.SetContent(x+xi, y, tcell.RuneBlock, nil, sty)
+ xi += 1
+ xi += c.drawText(x+xi, y, caption, styInv)
+ c.screen.SetContent(x+xi, y, tcell.RuneBlock, nil, sty)
+ xi += 1
+ } else {
+ c.screen.SetContent(x+xi, y, ' ', nil, sty)
+ xi += 1
+ xi += c.drawText(x+xi, y, caption, sty)
+ c.screen.SetContent(x+xi, y, ' ', nil, sty)
+ xi += 1
+ }
+ return xi
+}
+
+// statusBar draw the main status bar at the bottom of the screen, containing
+// page switching buttons and a clock.
+func (c *Console) statusBar(active int, opts ...string) {
+ sty1 := tcell.StyleDefault.Background(c.color(colorBlue)).Foreground(c.color(colorBlack))
+ sty2 := tcell.StyleDefault.Background(c.color(colorPink)).Foreground(c.color(colorBlack))
+ x := 0
+ x += c.drawText(x, c.height-1, " Page (tab to switch): ", sty1)
+ for i, opt := range opts {
+ x += c.button(x, c.height-1, opt, i == active, sty2)
+ }
+
+ c.drawText(c.width-len(time.DateTime)-1, c.height-1, time.Now().Format(time.DateTime), sty1)
+}
diff --git a/metropolis/node/core/tconsole/tconsole.go b/metropolis/node/core/tconsole/tconsole.go
index 52885bd..963be34 100644
--- a/metropolis/node/core/tconsole/tconsole.go
+++ b/metropolis/node/core/tconsole/tconsole.go
@@ -5,6 +5,7 @@
"crypto/sha256"
"encoding/hex"
"strings"
+ "time"
"github.com/gdamore/tcell/v2"
@@ -29,6 +30,9 @@
height int
// palette chosen for the given terminal.
palette palette
+ // activePage expressed within [0...num pages). The number/layout of pages is
+ // constructed dynamically in Run.
+ activePage int
network event.Value[*network.Status]
roles event.Value[*cpb.NodeRoles]
@@ -65,15 +69,17 @@
}
width, height := screen.Size()
+
return &Console{
- ttyPath: ttyPath,
- tty: tty,
- screen: screen,
- width: width,
- height: height,
- network: network,
- palette: pal,
- Quit: make(chan struct{}),
+ ttyPath: ttyPath,
+ tty: tty,
+ screen: screen,
+ width: width,
+ height: height,
+ network: network,
+ palette: pal,
+ Quit: make(chan struct{}),
+ activePage: 0,
roles: roles,
curatorConn: curatorConn,
@@ -92,6 +98,9 @@
if ev.Key() == tcell.KeyCtrlC {
close(c.Quit)
}
+ if ev.Key() == tcell.KeyTab {
+ c.activePage += 1
+ }
case *tcell.EventResize:
c.width, c.height = ev.Size()
}
@@ -120,6 +129,7 @@
}
supervisor.Signal(ctx, supervisor.SignalHealthy)
+ // Per-page data.
pageStatus := pageStatusData{
netAddr: "Waiting...",
roles: "Waiting...",
@@ -127,10 +137,32 @@
fingerprint: "Waiting...",
}
+ // Page references and names.
+ pages := []func(){
+ func() { c.pageStatus(&pageStatus) },
+ }
+ pageNames := []string{
+ "Status",
+ }
+
+ // Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in
+ // the status bar.
+ ticker := time.NewTicker(time.Second / 10)
+ defer ticker.Stop()
+
for {
- c.pageStatus(&pageStatus)
+ // Draw active page.
+ c.activePage %= len(pages)
+ pages[c.activePage]()
+
+ // Draw status bar.
+ c.statusBar(c.activePage, pageNames...)
+
+ // Sync to screen.
+ c.screen.Show()
select {
+ case <-ticker.C:
case <-ctx.Done():
return ctx.Err()
case ev := <-evC: