Attraktives GUI mit Spectre.Console
Best of NuGet, Teil 6
Kommandozeilenbasierte Benutzerinterfaces punkten insbesondere in Remote-Situationen – für das Aufrechterhalten einer SSH-Verbindung ist nun mal nur vergleichsweise wenig Bandbreite erforderlich. Mit der Ausgabe von Semigrafik lassen sich dann „attraktiv aussehende“ Interfaces realisieren, die auch dann verwendbar bleiben, wenn die Endanwender technisch nur moderat begabt sind.
Inbetriebnahme und Farbcodierung
Für das Anlegen einer neuen Beispiel-Kommandozeilenapplikation nutzen wir die Vorlage Konsolen-App. In den folgenden Schritten wird der Autor als Name SpectreDemo1 verwenden, als Version des .NET Frameworks soll 8.0 dienen. Im nächsten Schritt folgt der mittlerweile hinreichend bekannte Wechsel in die NuGet-Konsole, wo fürs Erste die Pakete Spectre.Console und Spectre.Console.Cli ins Projekt wandern. Die Verwendung der Preview-Version .0.51.2-preview.0.1 verursacht keine Probleme.
Für einen ersten Versuch wollen wir folgenden Code zur Ausführung bringen, der das in Bild 1 gezeigte Fenster erzeugt:
using Spectre.Console;
Console.WriteLine("Hello, World!");
AnsiConsole.Write(new Markup("[bold yellow]FAUCHEN![/] [red]ist lästig![/]"));
Console.WriteLine("Byebye, World!");
Technisch orientiert sich Spectre.Console am aus diversen Webforen bekannten BBcode-System. Die nach dem Schema [/] aufgebauten Slots terminieren dabei die einzelnen Formatierungsfelder.
Entsprechend bietet Spectre.Console im Bereich der Formatierungsmöglichkeiten weitgehend dieselben Optionen an, die auch das Webforum BBcode unterstützt. Bild 2 gibt einen Überblick über die Möglichkeiten.
Demo im Dienst der Ruhe (Bild 1)
Autor
Bei sorgfältiger Betrachtung der Dokumentation fällt auf, dass Spectre.Console zwei Ausgabe-Methoden anbietet. Die Markup-Familie der Methoden unterscheidet sich dabei insofern, als sie das Interface IRenderable implementiert und somit für fortgeschrittene Aufgaben besser geeignet erscheint. Für einen kleinen Test bietet es sich an, nach folgendem Schema einen Hyperlink in die Konsole auszugeben:
AnsiConsole.WriteLine("[link=http://www.google.com]Google[/]");
AnsiConsole.MarkupLine("[link=http://www.google.com]Google[/]");
Console.WriteLine("http://www.google.com");
Als primäres Hindernis erweist sich hier der geringe Funktionsumfang der in Visual Studio enthaltenen Konsole – Bild 3 zeigt die wenig beeindruckenden optischen Ergebnisse.
Die Visual-Studio-Entwicklerkonsole kann mit Hyperlinks nichts anfangen (Bild 3)
AutorAngemerkt sei außerdem noch, dass Spectre.Console zur grafischen Ausgestaltung von Exceptions befähigt ist. Dies erfolgt nach dem gezeigten Schema – zu beachten ist, dass eine „synthetisch“ geschaffene Exception keinen Callstack aufweist und die Möglichkeiten des Frameworks deshalb nur leidlich demonstriert:
AnsiConsole.WriteException(new InvalidDataException("Heute möchte ich nicht fauchen!"));
Erzeugung von dynamischen Steuerelementen
Im Linux-Bereich gelten Commandline-GUI-Bibliotheken als absoluter Standard. Das hinter Spectre.Console stehende Entwicklerteam versucht, ähnliche Dienste anzubieten.
Als erstes Beispiel wollen wir ein Balkendiagramm erzeugen, wofür folgender Code erforderlich ist:
AnsiConsole.Write(new BarChart()
.Width(80)
.Label("[red bold underline]Lästigkeit[/] \n")
.CenterLabel()
.AddItem("Annette", 77, Color.Yellow)
.AddItem("Beatrice", 54, Color.Green)
.AddItem("Clara", 33, Color.Red));
Die Methode Label übernimmt eines der weiter oben erwähnten Renderable-Objekte. Aus diesem Grund ist es dem Autor erlaubt, den Formatstring mit einzuschreiben – er sorgt dafür, dass das Diagramm wie in Bild 4 gezeigt einen gewissen Respektabstand zum Label einhält.
Auch in Kommandozeilenapplikationen notwendig: Typografie (Bild 4)
AutorIn der Praxis gibt es immer wieder Situationen, in denen Kommandozeilenapplikationen länger ablaufende Prozesse überwachen – ein klassisches Beispiel hierfür wäre das Bring-up eines Clusters oder das Herunterladen von verschiedenen Paketen. Spectre.Console bietet mit dem Live-Widget eine Sondervariante an, die die dynamische Aktualisierung von bereits am Bildschirm befindlichen Elementen ermöglicht.
Hierzu ist eine Modifikation des Programms erforderlich. Anstatt unser BarChart wie bisher direkt in die Konsole zu verfrachten, verpacken wir es im ersten Schritt in eine lokale Variable:
var barChart = new BarChart()
...
.AddItem("Clara", 33, Color.Red);
Die eigentliche Anzeige des Diagramms erfolgt dann über die Methode Live. Über die Start-Methode wird dann ein Handler angeliefert, der sich um das Einschreiben beziehungsweise Durchführen der Aktualisierungen kümmert:
AnsiConsole.Live(barChart)
.Start(ctx =>
{
barChart.AddItem("Faucherin 1", value: 33, Color.Red);
ctx.Refresh();
Thread.Sleep(1000);
barChart.AddItem("Faucherin 2", value: 33, Color.Red);
ctx.Refresh();
Thread.Sleep(1000);
});
Die Ausführung der vorliegenden Version des Programms sorgt dann dafür, dass das Diagramm permanent neue Quellen der Lästigkeit aufnimmt. Zu beachten ist lediglich, dass das Markieren eines einzigen Zeichens im Kommandozeilenfenster ausreicht, um das Fenster als Ganzes einzufrieren.
Erzeugung eines Kalenders
Insbesondere beim Planen von Downtimes kann es vernünftig sein, wenn die Kommandozeilenapplikation die Nutzer:innen auch visuell über die anstehenden Zeiten informiert. Am einfachsten lässt sich dies in Spectre.Console durch Nutzung des Kalender-Widgets erreichen – hierzu reicht folgender Code aus, der das in Bild 5 gezeigte Ergebnis generiert:
var calendar = new Calendar(2020, 10); AnsiConsole.Write(calendar);
Kalender herbei! (Bild 5)
AutorBesonders relevante Ereignisse beziehungsweise Daten kann der Entwickler durch Nutzung der Methode HighlightStyle hervorheben. Auch hierfür sei ein kleines Beispiel realisiert:
var calendar = new Calendar(2025, 9);
calendar.AddCalendarEvent(2025, 9, 10);
calendar.HighlightStyle(Style.Parse("yellow bold"));
AnsiConsole.Write(calendar);
Wer sich hier für den Highlight-Style yellow bold entscheidet, bekommt dann das in Bild 6 gezeigte Ergebnis.
Der Geburtstag des Autors ist farblich hervorgehoben (Bild 6)
AutorRealisierung von komplexeren Designs
In der Bibliothek finden sich auch an Layout-Manager erinnernde Steuerelemente, die die Anzeige von mehreren Widgets oder mehreren Informationen nebeneinander ermöglichen. Zur Vorführung dieser Funktion wollen wir im ersten Schritt ein schlüsselfertiges Fenster rendern, um danach seine Bestandteile anzusehen.
Der soeben erzeugte Kalender dient dabei als eines der Kinder-Widgets. Entfernen Sie im ersten Schritt die Zeile Live, um die Ausgabe der fertig generierten Instanz im Terminal zu beenden. Danach platzieren sie folgenden Code:
var root = new Tree("Root");
var foo = root.AddNode("[yellow]Foo[/]");
var table = foo.AddNode(new Table()
.RoundedBorder()
.AddColumn("First")
.AddColumn("Second")
.AddRow("1", "2")
.AddRow("3", "4")
.AddRow("5", "6"));
table.AddNode("[blue]Blauer Baum[/]");
foo.AddNode("Kind des Baumes");
var bar = root.AddNode("[yellow]Kalender-Ast[/]");
bar.AddNode(calendar);
AnsiConsole.Write(root);
Ergebnis des Renderings ist das in Bild 7 gezeigte Verhalten.
Spectre.Console kann sehr komplexe Screens generieren (Bild 7)
AutorDer erste Block ist insofern interessant, als der String ("[yellow]Foo[/]"); nicht Kind der Haupt-Wurzel, sondern Kind des Tabellen-Objekts ist. Dies ist dadurch erkennbar, dass alle in einer Tabelle hinzugefügten Notes eine AddNode-Methode aufweisen – analog zur klassischen Datenstruktur erlauben auch sie das Einschreiben von Kind-Widgets, die visuell nachrangig behandelt werden.
Das Einpflegen des Kalenders selbst erfolgt dann wieder durch Nutzung der Methode AddNode, die nun aber gegen das Wurzel-Objekt des Baumes zur Anwendung gelangt.
Exkurs: Einsammeln von Benutzerentscheidungen und Ausgabe von Ergebnissen
Schon aus Platzgründen ist es nicht möglich, die Bibliothek in einem einzigen Artikel komplett zu besprechen. Trotzdem möchte der Autor noch zwei interessante Aspekte vorstellen. Erstens ist es möglich, durch nach folgendem Schema aufgebauten Code eine Auswahl-Liste zu präsentieren. Der Benutzer kann in dieser dann analog zum Linux-Bootloader mit den Cursortasten und der Eingabetaste das Objekt auswählen, das er für die weitere Verarbeitung nutzen möchte:
var fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("What's your [green]favorite fruit[/]?")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut",
}));
AnsiConsole.WriteLine($"I agree. {fruit} is tasty!");
Das Hinzufügen des Pakets Spectre.Console.ImageSharp ermöglicht der Konsolen-Applikation dann sogar die Generierung von Pixel Art. Nach folgendem Schema aufgebauter Code lädt und „pixeliert“ das Bild automatisch:
var image = new CanvasImage("cake.png");
image.MaxWidth(16);
AnsiConsole.Write(image);
Fazit
Wer Kommandozeilenapplikationen mit geringem Aufwand zu einem attraktiven Aussehen verhelfen möchte, sollte auf die Dienste des Pakets Spectre.Console zurückgreifen. Das manuelle Nachimplementieren der hier angelegten Funktionen ist mit Sicherheit wesentlich aufwendiger als das Sich-Einlesen in die – nach Ansicht des Autors exzellent gelungene – Dokumentation der Bibliothek.