Befehle login, servers, distros, profiles, presets, vm, lxc, template list|build, batch, status, logs --follow, cancel. Renderer im Aufbau der Vorgabe (zeichengenau per Golden-File), 24-bit/256/ohne Farbe, --ascii, --quiet, --json, --dry-run; live im Terminal, zeilenweise ohne. SSE mit Wiederverbinden und Polling als Rückfall. Typen aus openapi.json per oapi-codegen; Release-Builds per cli/bauen.sh. Refs #013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
// Package ausgabe rendert die CLI-Ausgabe im Aufbau der Vorgabe: Kopfzeile, Phasen, Abschlussbox.
|
||
// Alle Texte kommen aus der Engine; hier wird nur gesetzt und gefärbt.
|
||
package ausgabe
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
// Farbmodus: 24-bit, 256 Farben oder keine.
|
||
type Farbmodus int
|
||
|
||
const (
|
||
Aus Farbmodus = iota
|
||
Farben256
|
||
Echtfarben
|
||
)
|
||
|
||
// Die Markenfarben (siehe README, Abschnitt Branding).
|
||
type rgb struct{ r, g, b uint8 }
|
||
|
||
var (
|
||
paper = rgb{0xE6, 0xED, 0xF3}
|
||
muted = rgb{0x8B, 0x98, 0xA5}
|
||
teal = rgb{0x2D, 0xD4, 0xBF}
|
||
amber = rgb{0xF5, 0xB8, 0x41}
|
||
rot = rgb{0xF8, 0x71, 0x71}
|
||
)
|
||
|
||
// Stil legt fest, wie gesetzt wird: Farben und ob nur ASCII-Symbole erlaubt sind.
|
||
type Stil struct {
|
||
Farbe Farbmodus
|
||
ASCII bool
|
||
}
|
||
|
||
// FarbmodusErmitteln: aus bei NO_COLOR, --no-color oder ohne Terminal; sonst 24-bit, wenn das
|
||
// Terminal es meldet (COLORTERM), sonst 256 Farben.
|
||
func FarbmodusErmitteln(keineFarbe, istTerminal bool) Farbmodus {
|
||
if keineFarbe || !istTerminal {
|
||
return Aus
|
||
}
|
||
if _, gesetzt := os.LookupEnv("NO_COLOR"); gesetzt {
|
||
return Aus
|
||
}
|
||
if os.Getenv("TERM") == "dumb" {
|
||
return Aus
|
||
}
|
||
switch strings.ToLower(os.Getenv("COLORTERM")) {
|
||
case "truecolor", "24bit":
|
||
return Echtfarben
|
||
}
|
||
return Farben256
|
||
}
|
||
|
||
func (s Stil) faerben(f rgb, fett bool, text string) string {
|
||
if s.Farbe == Aus || text == "" {
|
||
return text
|
||
}
|
||
var code string
|
||
switch s.Farbe {
|
||
case Echtfarben:
|
||
code = fmt.Sprintf("38;2;%d;%d;%d", f.r, f.g, f.b)
|
||
default:
|
||
code = fmt.Sprintf("38;5;%d", xterm256(f))
|
||
}
|
||
if fett {
|
||
code = "1;" + code
|
||
}
|
||
return "\x1b[" + code + "m" + text + "\x1b[0m"
|
||
}
|
||
|
||
func (s Stil) Paper(t string) string { return s.faerben(paper, false, t) }
|
||
func (s Stil) Fett(t string) string { return s.faerben(paper, true, t) }
|
||
func (s Stil) Muted(t string) string { return s.faerben(muted, false, t) }
|
||
func (s Stil) Teal(t string) string { return s.faerben(teal, false, t) }
|
||
func (s Stil) TealFett(t string) string { return s.faerben(teal, true, t) }
|
||
func (s Stil) Amber(t string) string { return s.faerben(amber, false, t) }
|
||
func (s Stil) AmberFett(t string) string {
|
||
return s.faerben(amber, true, t)
|
||
}
|
||
func (s Stil) Rot(t string) string { return s.faerben(rot, false, t) }
|
||
|
||
// xterm256 wählt die nächste Farbe aus Würfel (16–231) oder Graustufen (232–255).
|
||
func xterm256(f rgb) int {
|
||
stufen := []int{0, 95, 135, 175, 215, 255}
|
||
naechste := func(wert uint8) int {
|
||
beste, abstand := 0, 1<<30
|
||
for i, s := range stufen {
|
||
d := int(wert) - s
|
||
if d*d < abstand {
|
||
beste, abstand = i, d*d
|
||
}
|
||
}
|
||
return beste
|
||
}
|
||
r, g, b := naechste(f.r), naechste(f.g), naechste(f.b)
|
||
wuerfel := 16 + 36*r + 6*g + b
|
||
dw := quadAbstand(f, stufen[r], stufen[g], stufen[b])
|
||
|
||
grau := (int(f.r) + int(f.g) + int(f.b)) / 3
|
||
gi := (grau - 8) / 10
|
||
if gi < 0 {
|
||
gi = 0
|
||
} else if gi > 23 {
|
||
gi = 23
|
||
}
|
||
gw := 8 + gi*10
|
||
if quadAbstand(f, gw, gw, gw) < dw {
|
||
return 232 + gi
|
||
}
|
||
return wuerfel
|
||
}
|
||
|
||
func quadAbstand(f rgb, r, g, b int) int {
|
||
dr, dg, db := int(f.r)-r, int(f.g)-g, int(f.b)-b
|
||
return dr*dr + dg*dg + db*db
|
||
}
|
||
|
||
// Symbole je Status, mit ASCII-Ersatz (--ascii).
|
||
func (s Stil) symbol(status string) string {
|
||
if s.ASCII {
|
||
switch status {
|
||
case "ERLEDIGT":
|
||
return "[OK]"
|
||
case "LAEUFT":
|
||
return "[..]"
|
||
case "FEHLER":
|
||
return "[!!]"
|
||
default:
|
||
return "[--]"
|
||
}
|
||
}
|
||
switch status {
|
||
case "ERLEDIGT":
|
||
return "✓"
|
||
case "LAEUFT":
|
||
return "◐"
|
||
case "FEHLER":
|
||
return "✗"
|
||
default:
|
||
return "○"
|
||
}
|
||
}
|
||
|
||
// Spinner für die laufende Phase: ◐◓◑◒.
|
||
var spinner = []string{"◐", "◓", "◑", "◒"}
|