tconsole: init
This introduces the 'tconsole' (terminal console), the default
interface to show in /dev/tty1 on a Metropolis node.
Currently it just shows some basic status in a single page. Upcoming
changes will reintroduce a simple log dump on a different page, as well
as entirely new features like supervision tree inspection.
To iterate quickly on the console, a 'standalone' target is added which
exercises the console on the user's terminal with fake node data.
However only the actual console in Linux displays colours as intended.
Change-Id: I5cfba2bdb320daa080a073e76bf0494aeab6a4d4
Reviewed-on: https://review.monogon.dev/c/monogon/+/3371
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/node/core/tconsole/draw.go b/metropolis/node/core/tconsole/draw.go
new file mode 100644
index 0000000..d88e35b
--- /dev/null
+++ b/metropolis/node/core/tconsole/draw.go
@@ -0,0 +1,85 @@
+package tconsole
+
+import (
+ "strings"
+
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/uniseg"
+)
+
+// 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) {
+ g := uniseg.NewGraphemes(text)
+ for g.Next() {
+ runes := g.Runes()
+ c.screen.SetContent(x, y, runes[0], runes[1:], style)
+ x += 1
+ }
+}
+
+// drawTextCentered draw a single line of text from left to right, starting at a
+// position so that the center of the line ends up at x and y.
+func (c *Console) drawTextCentered(x, y int, text string, style tcell.Style) {
+ g := uniseg.NewGraphemes(text)
+ var runes [][]rune
+ for g.Next() {
+ runes = append(runes, g.Runes())
+ }
+
+ x -= len(runes) / 2
+
+ for _, r := range runes {
+ c.screen.SetContent(x, y, r[0], r[1:], style)
+ x += 1
+ }
+}
+
+// fillRectangle fills a given rectangle [x0,x1) [y0,y1) with empty space of a
+// given style.
+func (c *Console) fillRectangle(x0, x1, y0, y1 int, style tcell.Style) {
+ for x := x0; x < x1; x++ {
+ for y := y0; y < y1; y++ {
+ c.screen.SetContent(x, y, ' ', nil, style)
+ }
+ }
+}
+
+const logo = `
+ _g@@@@g_ _g@@@@g_
+ _@@@@@@@@@@a g@@@@@@@@@@b
+ @@@@@@@@@@@@@@___@@@@@@@@@@@@@@
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+ g@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@g
+ g@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@g
+ g@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@g
+ ;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@,
+ |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+ %@@@@@@@@@@P<@@@@@@@@@@@@@>%@@@@@@@@@@P
+ "+B@@B>' "@@@@@@@@B" '<B@@BP"
+ '"
+`
+
+// drawLogo draws the Monogon logo so that its top left corner is at x, y.
+func (c *Console) drawLogo(x, y int, style tcell.Style) {
+ for i, line := range strings.Split(logo, "\n") {
+ c.drawText(x, y+i, line, style)
+ }
+}
+
+// split calculates a mid-point in the [0, capacity) domain so that it splits it
+// into two parts fairly with minA and minB used as minimum size hints for each
+// section.
+func split(capacity, minA, minB int) int {
+ slack := capacity - (minA + minB)
+ propA := float64(minA) / float64(minA+minB)
+ slackA := int(propA * float64(slack))
+ return minA + slackA
+}
+
+// center calculates a point at which to start drawing a 'size'-sized element in
+// a 'capacity'-sized container so that it ends in the middle of said container.
+func center(capacity, size int) int {
+ return (capacity - size) / 2
+}