Aus Linux-Magazin 11/2022

Terminal-UI-Tool Wtf individualisieren

© Pichai Pipatkuldilok / 123RF.com

Mit Erweiterungen in Go und Ruby passt Mike Schilli das Terminal-UI-Tool Wtf an seine privaten Bedürfnisse an.

Eigentlich wollte ich für diese Ausgabe ja ein Terminal-UI schreiben, das mir anhand von Widgets wichtige Daten zum Systemstatus und zum Weltgeschehen anzeigt. Doch Schockschwerenot, mit der August-Ausgabe der Tooltipps [1] im Linux-Magazin fand ich heraus, dass es bereits ein – auch noch in Go geschriebenes – Open-Source-Tool namens Wtf [2] gibt, das all das bereits seit Langem beherrscht und sich außerdem noch einfach mit neuen Widgets erweitern lässt. Bitteschön, dann springen wir halt als Trittbrettfahrer auf diesen Zug auf!

Damit Wtf (oder »wtfutil«, wie das Programm ursprünglich hieß) auf dem Heimcomputer seine Kacheln mit den verschiedenen Widgets in ein Terminalfenster malt wie in der Demo in Abbildung 1, muss man erstens das kompilierte Go-Programm »wtfutil« als »wtf« in ein Bin-Verzeichnis verpflanzen und eine YAML-Datei mit den einzelnen Wtf-Modulen in den verschiedenen Kacheln konfigurieren. Anschließend zaubert der Aufruf »wtf« auf der Kommandozeile die frisch mit Inhalt gefüllten Kacheln ins gerade offene Terminalfenster.

Abbildung 1: Eine voll konfigurierte Installation des Terminal-Dashboards Wtf. Quelle: Chris Cummer, https://wtfutil.com

Abbildung 1: Eine voll konfigurierte Installation des Terminal-Dashboards Wtf. Quelle: Chris Cummer, https://wtfutil.com

Eine Installationsbeschreibung für verschiedenste Betriebssysteme findet sich auf Github, aber letztendlich genügen unter Linux ein »git clone« des Repos und anschließendes »make build« im neu entstandenen Unterverzeichnis, damit der Go-Compiler alle abhängigen Libraries von Github einholt und anschließend die ganze Chose in ein Binary unter »bin/wtfutil/« verpackt (Abbildung 2).

Abbildung 2: Ein »make build« zieht halb Github als Quellcode heran.

Abbildung 2: Ein »make build« zieht halb Github als Quellcode heran.

Wer übrigens denkt, »go build« wäre eine gute Idee, wird kurz vor dem Ende des Kompilierens eines Besseren belehrt, denn Go würde das Ergebnis in einer Datei namens »wtf« ablegen – doch unter diesem Namen steht im Repository bereits ein Verzeichnis. Das Makefile stellt hingegen sicher, dass das erzeugte Binary »wtfutil« heißt und ohne Kollisionen im Verzeichnis »bin/« landet.

Mit Werkzeuggürtel

Nun kommt Wtf bereits mit einem prall gefüllten Werkzeuggürtel an vordefinierten Widgets daher, die es bei Bedarf nur noch zu aktivieren gilt. Zum Beispiel gefiel mir das Widget Ipinfo recht gut, denn oft kommt es vor, dass sich auf meinem Rechner durch allerlei VPN-Spielereien die offizielle IP-Adresse ändert. Da ist es hilfreich, zu wissen, in welchem Land mich genutzte Internet-Dienste gerade wähnen.

Die YAML-Konfiguration aus Listing 1 pflanzt das »ipinfo«-Modul aufs Armaturenbrett. Der Code aktiviert das Wtf-intern definierte Modul »ipinfo« in der Modulsektion »mods«, und zwar links oben in der Anzeige, also mit einem Zeilen- und Spaltenindex von jeweils 0 und einer Höhe und Breite von jeweils 1.

Größe und Position der Kacheln in Wtf bestimmen sich aus der globalen Kachelbreite und -höhe in der Sektion »grid«, gemessen in Terminalzeichen, und die Lage einer Kachel mit den von 0 ab laufenden Indexnummern für Zeile und Spalte. Haben Sie das Terminal anfangs in vier Spalten und zwei Zeilen unterteilt, adressiert »top=0 left=0« also die Kachel links oben und »top=1 left=3« die Kachel rechts unten. Kacheln können sich entsprechend ihrer Einstellungen für »width« und »height« nicht nur über eine Spalte oder Zeile erstrecken, sondern auch mehr Platz belegen.

Listing 1

config.yml

wtf:
  colors:
    background: black
    border:
      focusable: darkslateblue
      focused: orange
      normal: gray
  grid:
    columns: [32, 32, 32]
    rows: [10, 10, 10]
  refreshInterval: 1
  mods:
    ipinfo:
      colors:
        name: "lightblue"
        value: "white"
      enabled: true
      position:
        top: 0
        left: 0
        height: 1
        width: 1
      refreshInterval: 150

Abbildung 3 zeigt das Terminal nach dem Aufruf von Wtf mit der Konfigurationsdatei »~/.config/wtf/config.yml« aus Listing 1. Die linke obere Kachel zeigt wie bestellt die gegenwärtig allokierte IPv4-Adresse sowie deren Geolokation in meiner Wahlheimat San Francisco. Ein schönes, nützliches Standard-Widget – aber nun wird es Zeit, Wtf mit unseren eigenen Kreationen zu erweitern.

Abbildung 3: Das Standardmodul Ipinfo zeigt die Geolokation der aktuellen WAN-IP an.

Abbildung 3: Das Standardmodul Ipinfo zeigt die Geolokation der aktuellen WAN-IP an.

Ein Skript, zwo, drei

Das nächste Widget misst die Geschwindigkeit, mit der der Internet-Provider die Daten über die Hausleitung herein- und hinausschaufelt. Die verfügbare Bandbreite in beiden Richtungen exakt in Mbit/s zu messen, hat sich das Tool P0d [3] auf die Fahnen geschrieben. Es lässt sich von Github herunterladen, klonen und mit Go kompilieren. Nach dem »go install«-Kommando aus dem Readme liegt das Binary »p0d« im lokalen Go-Pfad unter »~/go/bin/p0d«, den Sie für später in einen ausführbaren Pfad überführen.

Von der Kommandozeile aus aufgerufen, malt das Tool das Terminal mit wild fortschreitenden Zählern voll (Abbildung 4). Das in Ruby geschriebene Wrapper-Skript aus Listing 2 ruft P0d auf, fängt aber die Ausgabe ab und konzentriert sich nur auf die von P0d per Option »-O« angelegte JSON-Datei mit einigen Eckdaten zur Bandbreitenmessung.

Abbildung 4: Von der Kommandozeile aus aufgerufen, pflastert P0d das Terminal mit Ausgaben voll.

Abbildung 4: Von der Kommandozeile aus aufgerufen, pflastert P0d das Terminal mit Ausgaben voll.

Listing 2

p0d-runner

#!/usr/bin/ruby
require 'open3'
require 'tempfile'
require 'json'
out = Tempfile.new('p0d')
stdin, stdout, stderr, wait_thr =
    Open3.popen3("p0d", "-d", "3", "-O", out.path, "https://netflix.com")
stdin.close
if wait_thr.value.exitstatus != 0
    puts stderr.read
    exit
end
out.rewind
data = JSON.parse(out.read)
printf("Internet Speed:\n");
os = data[0]["OS"]
printf("Download: %d mbits/sec\n", os['InetDlSpeedMBits'].to_i);   printf("Upload:   %d mbits/sec\n", os['InetUlSpeedMBits'].to_i);

Die kürzeste erlaubte Laufzeit von P0d scheint bei 3 Sekunden zu liegen, ohne Begrenzung wären es 10 Sekunden. Daher legt Zeile 7 von Listing 2 im dritten Parameter zu Rubys externem Kommandoausführer »popen3()« aus dem Paket Open3 den Wert 3 fest. Die Ausgabe der JSON-Eckdaten landet in der vorher in Zeile 5 angelegten temporären Datei.

Nach einer Fehlerprüfung spult das Ruby-Skript dann in Zeile 13 diese Datei an den Anfang zurück, und der JSON-Parser analysiert in Zeile 14 via »parse()« die Daten. Im ersten Unter-Array (Index 0) unter dem Schlüssel »OS« stehen dort die beiden gesuchten Mbit/s-Werte für Up- und Download-Geschwindigkeit. P0d liefert sie unter den Schlüsseln »InetUlSpeedMBits« und »InetDlSpeedMBits« als Fließkommazahlen mit unsinnig vielen Nachkommastellen. Rubys String-zu-Integer-Konverter »to_i()« rundet sinngebend auf die nächste Ganzzahl (letzte Zeile).

Der Inhalt von Listing 3 fügt das Tool als Widget zur Wtf-Konfiguration in »config.yml« hinzu. Da Wtf von Haus aus P0d nicht kennt, legt die Direktive »type: “cmdrunner”« fest, dass das Widget vom Typ »cmdrunner« ist. Es nimmt daher ein Kommandozeilenargument samt Parametern entgegen, das es ausführt, dessen Standardausgabe einsammelt und selbige als Widget-Inhalt im Dashboard anzeigt. Abbildung 5 zeigt das Widget in Aktion, unterhalb des eingangs beschriebenen IP-Widgets. Nun umfasst das Cockpit schon zwei nützliche Armaturen, und es bleibt Platz für mindestens vier weitere.

Listing 3

P0d-Widget-Definition

p0d:
  args: [""]
  cmd: "p0d-runner"
  colors:
    name: "lightblue"
    value: "white"
  enabled: true
  position:
    top: 1
    left: 0
    height: 1
    width: 1
  refreshInterval: 600
  type: "cmdrunner"
Abbildung 5: Mit dem Internet-Geschwindigkeitsmesser umfasst das Cockpit bereits zwei Widgets.

Abbildung 5: Mit dem Internet-Geschwindigkeitsmesser umfasst das Cockpit bereits zwei Widgets.

Handgestrickt

Widgets im Wtf-Armaturenbrett zeigen aber nicht nur zeilenweise dynamisch eingeholte Daten an, sondern bieten Power-Usern auch an, Zeilen aus dem Fensterinhalt auszuwählen und Aktionen auf die jeweils aktive Zeile einzuleiten.

Das handgestrickte Widget rechts in Abbildung 6 liefert ein Beispiel dafür. Es holt beim sagenumwobenen Portal Perlmeister.com eine Liste mit den neuesten Ausgaben des Programmier-Snapshots ein und zeigt deren Titel mit dem Datum der Ausgabe an. Wählen Sie einen davon im Widget an, startet es sogar einen Webbrowser, um den jeweiligen Inhalt auf der Webseite des Linux-Magazins zu präsentieren. Was steckt hinter diesem Hexenwerk?

Abbildung 6: Ein drittes Fenster zeigt die neuesten Programmier-Snapshots an.

Abbildung 6: Ein drittes Fenster zeigt die neuesten Programmier-Snapshots an.

Um mit einem bestimmten Widget zu interagieren, tippen Sie in der Terminal-UI in Abbildung 6 die neben der Überschrift angezeigte Ziffer ein, worauf die UI den Fokus auf das jeweilige Widget legt. Ein Druck auf [K]+ und [J] bewegt im rechten Widget die grün unterlegte Auswahl nach oben und unten, ganz wie im Editor Vi. Unsichtbar in den Tiefen des Go-Codes der Erweiterung ist mit jedem Eintrag eine URL verbunden. Bei einem Druck auf die Eingabetaste fährt das Widget einen Webbrowser hoch und lädt den ausgewählten Artikel aus dem Netz (Abbildung 7).

Abbildung 7: In der Liste ausgewählte Artikel öffnen sich im Webbrowser.

Abbildung 7: In der Liste ausgewählte Artikel öffnen sich im Webbrowser.

Derlei fortgeschrittene Funktionen erlaubt Wtf nicht von Haus aus, aber mit etwas Go-Code lässt sich da nachhelfen. Dazu klonen Sie das Github-Repository und verändern den Code. Nach einem Neukompilieren mit »make build« stehen neue Widgets wie das mit Listing 5 bis Listing 8 neu geschaffene Snapshot-Widget bereit. Das neue Binary versteht anschließend den Widget-Typ »snapshot«, den Sie wie in Listing 4 gezeigt in die YAML-Konfiguration einbinden.

Listing 4

Snapshot-Konfiguration

snapshot:
  enabled: true
  colors:
    rows:
      even: "black"
      odd: "black"
  position:
    top: 0
    left: 1
    height: 2
    width: 2
  refreshInterval: 86400

Aufgemotzt

Dazu muss Listing 5 im Wtf-Quellcode in der Datei »widget_maker.go« erst einmal das neu geschaffene Wtf-Modul »snapshot« einbinden. Das erfolgt dort über eine neue »import«-Anweisung, die den Code aus Listing 6 hereinzieht, sowie eine zusätzliche »case«-Anweisung, die bei der Initialisierung des Programms die Funktionen »NewSettings()« sowie »NewWidget()« aus dem Go-Paket »snapshot« aufruft. Was dabei hinter den Kulissen abläuft, zeigt Listing 6, das Sie ebenso wie seine Kollegen aus Listing 7 und Listing 8 vor der Neukompilation ins Verzeichnis »modules/snapshot/« des Open-Source-Projekts kopieren.

Listing 5

widget_maker.go (Auszug)

package app
import (
  //...
  "github.com/wtfutil/wtf/modules/snapshot"
  // ...
)
// MakeWidget creates and returns instances of widgets
func MakeWidget(
  // ...
  switch moduleConfig.UString("type", moduleName) {
  case "snapshot":
    // ...
    settings := snapshot.NewSettingsFromYAML(moduleName, moduleConfig, config)
    widget = snapshot.NewWidget(tviewApp, redrawChan, pages, settings)
    // ...
  }
  return widget
}

Das neue Widget »snapshot« rechts in Abbildung 6 leitet sich vom Basistyp »view.ScrollableWidget« ab, wie Zeile 10 von Listing 6 festlegt. Das stellt sicher, dass Sie den Inhalt des Widgets anfahren und durchblättern können. Der Code aus Listing 7 initialisiert das neue Widget mit den YAML-Konfigurationsdaten. Dadurch enthält die »snapshot«-spezifische Struktur »Widget« (Listing 6, Zeile 9) danach eventuell zusätzliche YAML-Daten, die in diesem Fall gar nicht vorkommen, da das Widget keine zusätzliche Konfiguration benötigt. Zusätzlich zu den YAML-Daten führt die Struktur »Widget« aber noch interne Daten in Form der aus dem Netz geholten Snapshot-Artikel mit ihren Überschriften und URL-Pfaden auf der Seite des Linux-Magazins. Die entsprechende Struktur »Link« definiert später erst Listing 8 ab Zeile 10.

Listing 6

widget.go

package snapshot
import (
  "fmt"
  "github.com/gdamore/tcell/v2"
  "github.com/rivo/tview"
  "github.com/wtfutil/wtf/utils"
  "github.com/wtfutil/wtf/view"
)
type Widget struct {
  view.ScrollableWidget
  settings *Settings
  err      error
  links    []Link
}
func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {
  widget := &Widget{
    ScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),
    settings: settings,
  }
  widget.SetRenderFunction(widget.Render)
  widget.InitializeRefreshKeyboardControl(widget.Refresh)
  widget.InitializeHelpTextKeyboardControl(widget.ShowHelp)
  widget.SetKeyboardChar("j", widget.Next, "Select next item")
  widget.SetKeyboardChar("k", widget.Prev, "Select previous item")
  widget.SetKeyboardKey(tcell.KeyEnter, widget.openLink, "Open story in browser")
  return widget
}
func (widget *Widget) Refresh() {
  links, err := scrapeLinks()
  widget.err = err
  widget.links = links
  widget.SetItemCount(len(widget.links))
  widget.Render()
}
func (widget *Widget) Render() {
  widget.Redraw(widget.content)
}
func (widget *Widget) content() (string, string, bool) {
  title := "Programmier-Snapshot"
  content := ""
  for idx, link := range widget.links {
    row := fmt.Sprintf(`[%s]%2d. %s`,
      widget.RowColor(idx), idx+1,
      tview.Escape(link.title),
    )
    content += utils.HighlightableHelper(widget.View, row, idx, len(link.title))
  }
  return title, content, false
}
func (widget *Widget) openLink() {
  sel := widget.GetSelected()
  if sel >= 0 && widget.links != nil && sel < len(widget.links) {
    url := widget.links[sel].url
    utils.OpenFile(url)
  }
}

Die Funktion »NewWidget()« in Listing 6 ab Zeile 15 erzeugt das neue »snapshot«-Widget im Wtf-Universum. Sie füllt die Struktur »Widget« mit dem Nötigsten und registriert das Widget mit dem Renderer, der es später in die Terminal-UI malt. Dass der im Widget aktuelle ausgewählte Eintrag mit [K]+ und [J]+ auf und ab wandert und [Eingabe] den markierten Eintrag auswählt (samt der voreingestellten Browser-Aktion auf die hinterlegte URL), liegt an den Zeilen 23 bis 25, die das über Keyboard-Funktionen festlegen.

Die Funktion »Refresh()« ab Zeile 28 läuft immer dann an, wenn die Terminal-UI das Widget neu zeichnet. Mit »scrapeLinks()« in Zeile 29 holt sie, wie weiter unten in Listing 8 ausgeführt, die Links aktueller und vergangener Programmier-Snapshots von der Perlmeister-Webseite und brezelt sie auf, um sie in einem kompakten Format anzuzeigen und zur Auswahl anzubieten.

Auf ein »Render()«-Kommando hin bringt die UI den aktuellen Inhalt des Snapshot-Widgets auf den Schirm. Den klaubt die Funktion »content()« ab Zeile 38 zusammen. Sie windet sich durch die in der Instanzvariablen »links« hinterlegten Snapshot-Artikel und fügt sie der Reihe nach farblich untermalt in die Zeilen des Widgets ein.

Was passiert, wenn man nach der Auswahl eines Snapshot-Artikels die Eingabetaste drückt, legen die Zeile 25 und die Funktion »openLink()« ab Zeile 50 fest. Mit der Indexnummer des fraglichen Eintrags in »sel« holt Zeile 53 die zum Eintrag passende und in der Datenstruktur hinterlegte URL ab und öffnet sie via »utils.OpenFile()«. Das wiederum fährt den Standard-Webbrowser hoch und lässt ihn den Inhalt der Artikelseite auf der Website des Linux-Magazins anzeigen.

In den YAML-Einstellungen der Konfigurationsdatei passiert bei einem Snapshot-Widget nichts Berauschendes, es werden lediglich die Standardfloskeln abgearbeitet. Eigene Parameter hat das Widget nicht, sodass Listing 7 nur Boilerplate-Code enthält.

Listing 7

settings.go

package snapshot
import (
  "github.com/olebedev/config"
  "github.com/wtfutil/wtf/cfg"
)
const (
  defaultFocusable = true
)
// Settings contains the settings for the snapshot view
type Settings struct {
  *cfg.Common
}
// NewSettingsFromYAML creates the settings for this module from a YAML file
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
  snapshot := ymlConfig.UString("snapshot")
  settings := Settings{
    Common: cfg.NewCommonSettingsFromModule(name, snapshot, defaultFocusable, ymlConfig, globalConfig),
  }
  return &settings
}

Datenfieselei

Wie weiß das Widget nun, welche Snapshot-Artikel überhaupt auf der Webseite des Linux-Magazins vorliegen? Der Datengreifer in Listing 8 durchforstet dazu die komplette Liste aller in den letzten 25 Jahren veröffentlichten Programmier-Snapshots auf der Website http://perlmeister.com. Mit dem einfachen HTML der unter der URL in Zeile 16 publizierten Artikel-Links hat der Go-Scraper »goquery« leichtes Spiel. Seine Funktion »Find()« geht ab Zeile 30 durch alle Links im Webdokument »art_ger.html« und behält nur diejenigen im Auge, die den String »ausgabe« im Pfad haben. Das sind typischerweise Links zu Snapshot-Artikeln auf der Seite des Linux-Magazins.

Listing 8

goquery.go

package snapshot
import (
  "errors"
  "fmt"
  "net/http"
  "regexp"
  "strings"
  "github.com/PuerkitoBio/goquery"
)
type Link struct {
  title string
  url   string
}
func scrapeLinks() ([]Link, error) {
  links := []Link{}
  res, err := http.Get("https://perlmeister.com/art_ger.html")
  if err != nil {
    return links, err
  }
  defer res.Body.Close()
  if res.StatusCode != 200 {
    return links, errors.New("Fetch failed")
  }
  doc, err := goquery.NewDocumentFromReader(res.Body)
  if err != nil {
    return links, err
  }
  var maxHits = 5
  daterx := regexp.MustCompile(`\d{4}/\d{2}`)
  doc.Find("a").Each(func(i int, s *goquery.Selection) {
    if maxHits > 0 {
      link, _ := s.Attr("href")
      if strings.Contains(link, "ausgaben") {
        rs := daterx.FindStringSubmatch(link)
        title := fmt.Sprintf("%s (%s)", s.Text(), rs[0])
        links = append(links, Link{title: title, url: link})
        maxHits--
      }
    }
  })
  return links, nil
}

Gemäß dem in der Variablen »maxHits« (Zeile 28) definierten Wert sammelt die Funktion die URLs von maximal fünf Artikeln ein, extrahiert aus deren Pfad Monat und Jahr der Ausgabe und hängt sie an das Array von Link-Strukturen in »links« an. Jeder Eintrag enthält im Feld »title« die in der Auswahl anzuzeigende Überschrift sowie unter »url« den entsprechenden Link zum Artikel auf der LM-Webseite. Aus dieser Liste generiert der Code später mittels »title« die aufgelisteten Artikel. Bei einem Druck auf die Eingabetaste schnappt er sich das Attribut »url« des Eintrags und bringt den Artikel im Webbrowser hoch.

Ausblick

Fertig ist der Lack! Offensichtlich gibt es aber weit mehr Möglichkeiten, dem Tool Wtf neue Tricks beizubringen. Wie immer sind der Fantasie kreativer Programmierer dabei keine Grenzen gesetzt. (uba/jlu)

Infos

  1. Aktuelle Software im Kurztest: Uwe Vollbracht, “Tooltipps”, LM 08/2022, S. 46, https://www.lm-online.de/47561
  2. Wtf: https://github.com/wtfutil/wtf
  3. P0d: https://github.com/simonmittag/p0d
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben