diff --git a/examples/.gitignore b/examples/git/.gitignore similarity index 100% rename from examples/.gitignore rename to examples/git/.gitignore diff --git a/examples/git-presentation.sc b/examples/git/git-presentation.sc similarity index 88% rename from examples/git-presentation.sc rename to examples/git/git-presentation.sc index 5a06592..0bc5f7d 100755 --- a/examples/git-presentation.sc +++ b/examples/git/git-presentation.sc @@ -142,9 +142,17 @@ val meta = Map( val presentation = Presentation( Vector( titleSlide, - /* for simple slides, the `markdown` template and `IncludeMarkdown` Slide allow including plain markdown files. - * (currently requires mdcat) */ - markdown("Wozu braucht man das?", pwd / "slides" / "01wozu.md"), + slide("Wozu braucht man das?")( + ulist( + "Versionierungssysteme speichern _Änderungsverlauf_ von Dateien", + "Lässt einen Änderungen rückgängig machen", + "Unterstützt einen bei *Zusammenführung* von mehreren Versionen", + "(z.B. stabile und Entwicklungsversion)", + "Synchronisierung zwischen mehreren Rechnern", + "Fehlersuche", + "...", + ) + ), chapter("Basics", "Grundlegende Befehle"), /* `slide` is a template for a `Group` with an optional title, that clears the screen */ slide("Basics")( @@ -331,9 +339,38 @@ val presentation = Presentation( sh("git add hello.txt; git commit -m \"Extended greeting\""), gitStatus, ), - markdown("Was bisher geschah", pwd / "slides" / "02summary.md"), + slide("Was bisher geschah")( + ulist( + s"${"git init [dir]".yellow}: neues Repo anlegen", + s"${"git status".yellow}: Aktuellen Zustand zeigen (geänderte, ungetrackte Dateien, Stagingbereich)", + s"${"git add ".yellow}: Datei zu Repo hinzufügen / Änderungen in Datei für Commit markieren / \"stagen\"", + s"${"git restore --staged ".yellow}: Änderungen aus Staging wieder herausnehmen", + s"${"git restore ".yellow}: Änderungen in Datei verwerfen (zurück zu Dateizustand aus letzten Commit)", + s"${"git commit [-m message]".yellow}: Neuen Commit mit Änderung im Stagingbereich erstellen", + s"${"git log".yellow}: Bisherige Commits anzeigen", + s"(${"git cat-file".yellow}: Git-Objekte im internen Format anzeigen. Braucht man im Alltag nicht)", + ) + ), chapter("Branches", "Grundlegende Befehle"), - slide("\n".par, IncludeMarkdown(pwd / "slides" / "02branches.md")), + slide("Branches")( + "Branches sind mehrere parallele Entwicklungszweige. Wozu Branches?".par, + ulist( + "Hauptbranch (master, main, etc.) für stabile Version nutzen", + "neue Branches pro zusätzlichem Feature erstellen", + "Arbeit an mehreren Branches unabhängig voneinander möglich", + "Anzeigen aller Änderungen zwischen Branches möglich", + "Springen zwischen Zustand verschiedener Branches", + ), + "Verschiedene Workflows möglich, je nach Projekt unterschiedlich.".par, + "Branches sind in Git \"billig\":".par, + ulist( + "Ein Branch ist ein Pointer auf einen Commit", + "Normalerweise ist ein Branch als aktueller Branch ausgewählt", + "Neuer Commit => aktueller Branch zeigt nun auf diesen", + "Neue Branches erstellen = neue Datei mit Hash anlegen", + "Branches können auch komplett lokal bleiben (im Gegensatz zu SVN)", + ), + ), slide("Branches")( sh("git branch"), ---, diff --git a/examples/img/git-logo.png b/examples/git/img/git-logo.png similarity index 100% rename from examples/img/git-logo.png rename to examples/git/img/git-logo.png diff --git a/examples/img/repo-graph-i.png b/examples/git/img/repo-graph-i.png similarity index 100% rename from examples/img/repo-graph-i.png rename to examples/git/img/repo-graph-i.png diff --git a/examples/img/repo-graph.png b/examples/git/img/repo-graph.png similarity index 100% rename from examples/img/repo-graph.png rename to examples/git/img/repo-graph.png diff --git a/examples/slides/01wozu.md b/examples/git/slides/01wozu.md similarity index 100% rename from examples/slides/01wozu.md rename to examples/git/slides/01wozu.md diff --git a/examples/slides/02branches.md b/examples/git/slides/02branches.md similarity index 100% rename from examples/slides/02branches.md rename to examples/git/slides/02branches.md diff --git a/examples/slides/02summary.md b/examples/git/slides/02summary.md similarity index 100% rename from examples/slides/02summary.md rename to examples/git/slides/02summary.md diff --git a/examples/termescape/figlet-pagga-block b/examples/termescape/figlet-pagga-block new file mode 100755 index 0000000..5e8c96c --- /dev/null +++ b/examples/termescape/figlet-pagga-block @@ -0,0 +1,36 @@ +#!/bin/zsh +zparseopts -D -E p:=padding -padding:=padding +lines=("$@") +if [[ -z $lines ]]; then + lines=() + while read line; do + lines+=($line) + done +fi + +width=0 +for line in $lines; do + if [[ ${#line} -gt $width ]]; then + width=${#line} + fi +done + +lpad_width=$((width + ${padding[2]:-0})) +rpad_width=$((lpad_width * 2)) + +pagga=( $( +for i in {1..$#lines}; do + line=$lines[$i] + padded="${(r:$(( rpad_width - ${#line})):: :)${(l:${lpad_width}:: :)line}}" + spaces=$(echo $line | tr -cd ' ' | wc -c) + if [[ $spaces -gt 0 ]]; then + padded="$padded${(l:spaces:: :)}" + fi + echo $padded +done | figlet -fpagga -t +) ) + +left_space=$(( (COLUMNS - ${#pagga[1]}) / 2 )) +for line in ${pagga[*]}; do + echo "${(l:left_space:: :)}$line" +done diff --git a/examples/termescape/img/csi.png b/examples/termescape/img/csi.png new file mode 100644 index 0000000..543c28a Binary files /dev/null and b/examples/termescape/img/csi.png differ diff --git a/examples/termescape/img/csi.svg b/examples/termescape/img/csi.svg new file mode 100644 index 0000000..1a86b5b --- /dev/null +++ b/examples/termescape/img/csi.svg @@ -0,0 +1,105 @@ + + + +$>_ diff --git a/examples/termescape/img/earth.gif b/examples/termescape/img/earth.gif new file mode 100644 index 0000000..9df6477 Binary files /dev/null and b/examples/termescape/img/earth.gif differ diff --git a/examples/termescape/terminal-presentation.sc b/examples/termescape/terminal-presentation.sc new file mode 100755 index 0000000..5e157aa --- /dev/null +++ b/examples/termescape/terminal-presentation.sc @@ -0,0 +1,529 @@ +#!/usr/bin/env -S scala-cli shebang +//>using scala 3.3 +//>using dep de.qwertyuiop::copret:0.0.2 + +import de.qwertyuiop.copret.Terminal.* + +import de.qwertyuiop.copret.* +import de.qwertyuiop.copret.syntax.* +import TypedCommand.{interactive, shell => sh} +import os.Path + +given presentationRoot: Path = os.Path("/home/crater2150/org/opencolloq/terminal/termescape/") +val imgs = presentationRoot / "img" +given theme: Theme = Theme.default + +def figlet(str: String, font: String): String = String( + os.proc("figlet", "-t", "-f", font, str).call().out.bytes, +) + +def lolcat(str: String): String = String( + os.proc("lolcat", "-b", "-f", "-h", "0.7", "-r").call(stdin = str).out.bytes, +) + +def lolcatCustom(str: String, opts: String*): String = String( + os.proc("lolcat", "-f", opts).call(stdin = str).out.bytes, +) + +def lollet(str: String, font: String): String = lolcat(figlet(str, font)) + +// debug mode +val noInteractiveCommands = false + +val titleSlide = + Group( + Clear, + TypedCommand.interactiveUntyped("./title.zsh").disable("", noInteractiveCommands), + Clear, + header, + Paragraph( + lollet("Terminal", "mono12").block ++ lollet("basics", "mono12").block + ++ "\n" ++ "Was tut so eine Terminalanwendung eigentlich?".right(10), + ), + ) + +/* copret templates are simply normal methods that return slides */ +def chapter(title: String, subtitle: String): Group = + chapter(title, "", subtitle) +def chapter(title1: String, title2: String, subtitle: String): Group = { + val font = + if ((title1.length max title2.length) < columns / 10) "mono12" else "mono9" + Group( + Clear, + header, // a built in template + Paragraph( + lollet(title1, font).block ++ lollet(title2, font).block ++ "\n" ++ subtitle.right(10), + ), + ) +} + +val meta = Map( + "author" -> "Alexander Gehrke", + "title" -> "Terminals", +) + +def sgr(code: String): String = s"\u001b[${code}m" + +def sgr(code: Int*): String = sgr(code.mkString(":")) + +val reset = sgr(0) + +extension (s: String) + def style(code: String): String = sgr(code) + s + reset + def style(code: Int*): String = sgr(code*) + s + reset + def italic = s.style(3) + def bold = s.style(1) + def link(id: Int, uri: String) = s"\u001b]8;id=$id;$uri\u001b\\" + s + "\u001b]8;;\u001b\\" + def emph = sgr(3) + lolcat(s) + reset + def strong = sgr(1) + lolcat(s) + reset + + def underline(style: Int = 1, color: Int = 7) = + sgr("4:" + style) + sgr(58, 5, color) + s + reset + +val rainbow = Vector( + "\u001b[38:5:196m", + "\u001b[38:5:202m", + "\u001b[38:5:208m", + "\u001b[38:5:214m", + "\u001b[38:5:220m", + "\u001b[38:5:226m", + "\u001b[38:5:190m", + "\u001b[38:5:154m", + "\u001b[38:5:118m", + "\u001b[38:5:82m", + "\u001b[38:5:46m", + "\u001b[38:5:47m", + "\u001b[38:5:48m", + "\u001b[38:5:49m", + "\u001b[38:5:50m", + "\u001b[38:5:51m", + "\u001b[38:5:45m", + "\u001b[38:5:39m", + "\u001b[38:5:33m", +) + +def list(bullet: (Int, Int) => String, continued: (Int, Int) => String)(items: String*)(using Theme): Paragraph = + val randbow = util.Random.shuffle(rainbow) + items.zipWithIndex + .map((str, index) => { + def formatFirst(first: String) = + s" ${randbow(index % randbow.length)}${bullet(index, items.size)}\u001b[0m $first" + + str.split("\n").toList match + case first :: Nil => formatFirst(first) + case first :: cont => + formatFirst(first) + + cont.map(line => s" ${continued(index, items.size)}${line}").mkString("\n", "\n", "") + case Nil => "" + }) + .mkString("\n") + .par + +def ulist(items: String*)(using Theme) = list(bullet = (_, _) => "✦ ", continued = (_, _) => " ")(items*) + +def digits(i: Int): Int = math.ceil(math.log10(i)).toInt + +def olist(items: String*)(using Theme) = + list( + bullet = (i, max) => (" " * (digits(max + 1) - digits(i + 1) - 1)) + s"${i + 1}.", + continued = (i, max) => " " * digits(max + 1), + )(items*) + +val slides = Vector( + titleSlide, + slide("Terminal IO")( + "Eine Anwendung in einem Terminal kommuniziert (fast) nur via Standard-IO mit diesem:".par, + ulist( + "stdin, für Benutzereingaben und Nachrichten vom Terminal", + "stdout, für Textausgabe und Nachrichten an das Terminal", + "stderr, wird wie stdout behandelt", + ), + s"=> Alle Steuersignale vom Programm ans Terminal oder umgekehrt sind ${"inband".emph}".par, + ), + slide("Steuersignale")( + s"""|Wie unterscheiden Terminal und Programm normalen IO von Steuersignalen? + |Durch ${"Steuerzeichen".emph}, aufgeteilt in zwei Bereiche:""".par, + s"""|${"C0".strong}: alle Zeichen im Bereich von 0x00 bis 0x1F + |Größtenteils veraltet. Noch relevant sind u.a.:""".par, + ulist( + "Whitespaces (\\n, \\r, \\t, ...) und Backspace (0x08)", + "Bell (\\a), führt bei den meisten Desktops zu blinkendem Icon o.ä. (Urgent Flag)", + "XON/XOFF (0x11/0x13), schaltet Ausgabe an/aus (Flow Control)", + "Escape (\\e, 0x1B), das wichtigste für diesen Talk, startet Escapesequenzen", + ), + s"""|${"C1".strong}: Zeichen im Bereich von 0x80 bis 0x9F + |Wegen Kollisionen mit Unicode inkompatibel. + |Heute üblicherweise durch Escapesequenzen ersetzt""".par, + ), + chapter("Escape-", "sequenzen", "die verschiedenen Typen"), + slide("CSI")( + PauseKey, + Image(imgs / "csi.png"), + ), + slide("CSI")( + """|Control Sequence Introducer (CSI) ist der vermutlich häufigste Typ. + |Wird benutzt für:""".par, + ulist( + "Cursor-Steuerung", + "Löschen von Zeichen oder Zeilen", + s"${lolcat("Farben")} ${"und".italic} ${"andere".style(42)} ${"Formatierung".underline(style = 3, color = 1)}", + ), + "|CSI-Sequenzen haben folgendes Format:\n".par, + s"${"ESC [".style(94)} ${"".style(96)} ${"arg1 : arg2 : ... : argn".style(93)} ${"".style(92)}".block.text, + s"""| + |${"✦ ESC [".style(94)} ist der Teil, der CSI genannt wird + |${"✦ ".style(92)} gibt an, was gemacht werden soll. Immer ein Zeichen zwischen 0x40 (@) und 0x7F (~) + |${"✦ ".style(92)} darf aus dem ASCII-Bereich 0x20 - 0x3F außer Ziffern, ; und : sein + | Gibt es, weil man mehr Befehle wollte, als für Zeichen erlaubt sind. + |${"✦ arg1 : arg2 : ... : argn".style(93)} sind numerische Parameter. + | Statt : wird teilweise ; benutzt (ISO-Standard vs. Realität). + |\n""".stripMargin.block.text, + s"""\nBeispiel: ${"[31m".style(31)} schaltet auf rote Schrift um""".par, + ), + slide("OSC")( + """|Operating System Commands (OSC) haben verschiedenste Anwendungszwecke, + |z.B. Setzen des Fenstertitels, Zugriff aufs Clipboard, etc. + |Im Gegensatz zu CSI Escapes können sie die meisten Zeichen enthalten + | => komplexere Befehle möglich.""".par, + s"${"ESC ]".style(94)} ${"".style(92)} ; ${"args...".style(93)} ${"ST".style(96)}".block.text, + s"""| + |${"✦ ESC ]".style(94)} startet ein OSC + |${"✦ ".style(92)} gibt an, was gemacht werden soll, ein- bis vierstellige Zahl + |${"✦ args...".style(93)} sind je nach Befehl die Argumente. Hier ist alles außer ST erlaubt, oft Base64. + |${"✦ ST".style(96)} ist das String Terminator Zeichen. Entweder BEL (\\a) oder ESC \\ + |""".stripMargin.block.text, + s"""| + |Beispiel: ${"]52;c;SGVsbG9Xb3JsZA==\\".style(38, 5, 145)} + | OSC52 = Clipboard, "c" gibt an welches Clipboard + | base64-codierter Text entspricht "HelloWorld" + """.par, + ), + slide("APC")( + "Application Program Commands (APC) sind ähnlich zu OSC, aber größtenteils\nspezifisch für einzelne Terminals".par, + ulist( + s"fangen mit ${"ESC _".style(3, 94)} an statt mit ${"ESC ]".style(3, 94)}", + s"Format bis zum ${"ST".style(3, 96)} ist uneinheitlich", + "Benutzt z.B. bei screen und tmux zum Setzen von Statusleisten", + "Beispiel für mittlerweile verbreitetere Anwendung: Kitty Graphics Protocol", + ), + ), + chapter("Farben und ", "Formatierung", "Escapes für dargestellte Zellen"), + slide("Terminaloutput")( + """|"Bevor wir uns Formatierung ansehen, erst ein kurzer Überblick darüber, + |wie die Ausgabe eines Terminals funktioniert:""".par, + ulist( + "State Machine, die Cursorposition und aktuelle Formatierung hält", + "Ausgabe aufgeteilt in Zellen, normalerweise ein Zeichen pro Zelle", + "Escapes in Ausgabe können Formatierung und Cursorposition ändern", + "druckbare Zeichen wird an Cursorposition geschrieben\nCursor wandert weiter, Formatierung bleibt", + ), + """|=> Formatierung gilt für die Zelle, in der der Cursor steht + | und alle folgenden Zellen, bis sie geändert wird""".par, + ), + slide("SGR Codes")( + """|Die meisten Formatierungen sind SGR-Sequenzen (Select Graphic Rendition). + |Das sind CSI-Sequenzen mit "m" als Endzeichen. + |Die vermutlich wichtigste ist die zum Zurücksetzen auf Default-Formatierung:""".par, + s"${"ESC [".style(94)} ${"0".style(92)} ${"m".style(96)}".block.text, + s"""| + |Alle SGR-Codes beginnen mit dem ${"CSI".style(94)} und enden mit einem "${"m".style(96)}". + |Die ${"0".style(92)} in der Mitte gibt den eigentlichen Befehl. + |Hier können je nach Code weitere Parameter folgen""".par, + ), + slide("Farben")( + """|Farben sind die wohl am häufigsten genutzte Formatierung. + |Es gibt verschiedene Arten von Farbcodes in Terminals:""".par, + ulist( + "Palettenfarben mit 8 oder 16 Farben", + "256-Farbenpalette, inzwischen auch fast überall unterstützt", + "Truecolor, 24-Bit-Farben, die in modernen Terminals unterstützt werden", + ), + s"""|Beispiel: Farbverlauf von #55ff22 ${"██".style("38:2::85:255:34")} nach #0000ff ${"██".style( + "38:2::0:0:255", + )}:""".par, + ("256 Farbpalette: " + lolcatCustom("█".repeat(60), "-h", "0.18", "-g", "55ff22:0000ff")).par, + ("24-Bit-Farben: " + lolcatCustom("█".repeat(60), "-h", "0.40", "-g", "55ff22:0000ff", "-b")).par, + ), + slide("Die Grundpalette")( + """|Wir fangen an mit den 8/16 Grundfarben. Die Codes zum Setzen bestehen aus mehreren Ziffern. + |Der Anfang gibt an, welche Farbe gesetzt werden soll:""".par, + ulist( + "Beginn mit 3: Vordergrundfarbe", + "Beginn mit 4: Hintergrundfarbe", + "Beginn mit 9: Helle Vordergrundfarbe", + "Beginn mit 10: Helle Hintergrundfarbe", + ), + "Anschließend folgt eine Ziffer für die Farbe (tatsächliche Farbe einstellungsabhängig):".par, + s"""|0 Schwarz ${"abc".style("30")} ${"abc".style("40")} hell: ${"abc".style("90")} ${"abc".style("100")} + |1 Rot ${"abc".style("31")} ${"abc".style("41")} hell: ${"abc".style("91")} ${"abc".style("101")} + |2 Grün ${"abc".style("32")} ${"abc".style("42")} hell: ${"abc".style("92")} ${"abc".style("102")} + |3 Gelb ${"abc".style("33")} ${"abc".style("43")} hell: ${"abc".style("93")} ${"abc".style("103")} + |4 Blau ${"abc".style("34")} ${"abc".style("44")} hell: ${"abc".style("94")} ${"abc".style("104")} + |5 Magenta ${"abc".style("35")} ${"abc".style("45")} hell: ${"abc".style("95")} ${"abc".style("105")} + |6 Cyan ${"abc".style("36")} ${"abc".style("46")} hell: ${"abc".style("96")} ${"abc".style("106")} + |7 Weiß ${"abc".style("37")} ${"abc".style("47")} hell: ${"abc".style("97")} ${"abc".style("107")} + """.stripMargin.block.text, + s"""\nBeispiel von vorhin: ${"[31m".style(31)} schaltet auf rote Schrift um""".par, + ), + slide("256 Farben")( + s"""|Die 256-Farbenpalette ist eine Erweiterung der 8/16-Farbenpalette. + |0-7 entsprechen denen der Terminalpalette, 8-15 den hellen Varianten. + |Dann folgen 216 Farben, die in einem 6x6x6-Würfel angeordnet sind: + | + | ${"✦ ".style(31)} Mit jedem Index steigt der Blauwert eine Stufe, nach 6 Farben wiederholt er sich + | ${"✦ ".style(32)} Mit jedem 6. Index steigt der Grünwert eine Stufe, analog. + | ${"✦ ".style(34)} Mit jedem 36. Index steigt der Rotwert eine Stufe + | + |Die restlichen 24 Farben sind Grautöne von schwarz nach weiß. + | + |Die Codes für die 256-Farbenpalette sind folgendermaßen aufgebaut:""".par, + s"Vordergrund: CSI ${"38".style(92)} : ${"5".style(93)} : ${"".style(95)} m".block.text, + s"Hintergrund: CSI ${"48".style(92)} : ${"5".style(93)} : ${"".style(95)} m".block.text, + s"""| + |${"✦ 5".style(93)} gibt an, dass die 256-Farben-Palette benutzt werden soll. + |${"✦ ".style(95)} ist der Index der Farbe in der Palette. + |""".stripMargin.block.text, + PauseKey, + TypedCommand.interactiveUntyped("sh", "-c", "colortest -w | less -R").disable("", noInteractiveCommands), + ), + slide("24 Bit \"Truecolor\"")( + s"""|Mit Truecolor gibt es keine Indices mehr. + |Es werden direkt die Werte für die Kanäle angegeben + |Der Aufbau ist ähnlich wie bei den 256 Farben (Vordergrund 38, Hintergrund 48):""".par, + s"CSI ${"38".style(92)} : ${"".style(93)} : ${"".style(95)} : ${"".style(94)} m".block.text, + s"""| + |${"✦ ".style(93)} ist entweder 2 für RGB, 3 für CMY, 4 für CMYK (RGB am besten supported) + |${"✦ ".style(95)} gibt den Farbraum an. Schwierig, Doku dazu zu finden, meistens leer. + |${"✦ ".style(94)} sind die Werte für die einzelnen Kanäle in dezimal, getrennt durch : + |✦ Nicht-standardisiertes, aber verbreitetes xterm-Encoding: + | Semikolon statt Doppelpunkt, Farbraumfeld fehlt. + |""".stripMargin.block.text, + s"""\nBeispiel: ${"[38:2::255:166:86m".style( + "38:2::255:166:86", + )} ergibt den Orangeton RGB(255, 166, 86) bzw. #FFA656""".par, + ), + slide("weitere Textformatierungen")( + "Weitere SGR-Codes zum Formatieren von Text (in Klammern: Code zum Abschalten):".par, + ulist( + s"1 (22): ${"Fett".style(1)}", + s"2 (22): ${"Faint".style(2)} (meistens nicht unterstützt oder wechselt nur zu dunklerer Farbe)", + s"3 (23): ${"Kursiv".style(3)}", + s"5 und 6 (25): ${"Blinkend".style(5)} und ${"schnell Blinkend".style(6)}, durchwachsener Support", + s"7 (27): ${"Invertiert".style(7)} (Vorder- und Hintergrundfarbe tauschen)", + s"8 (28): Versteckt (Vordergrundfarbe = Hintergrundfarbe)", + s"9 (29): ${"Durchgestrichen".style(9)}", + ), + ), + slide("weitere Textformatierungen")( + "Weitere SGR-Codes zum Formatieren von Text:".par, + ulist( + s"""|4 (24): ${"Unterstrichen".style(4)}. Moderne Terminals können zusätzlich noch: + | 4:1 ${"Normal unterstrichen".style(4, 1)} + | 4:2 ${"Doppelt unterstrichen".style(4, 2)} + | 4:3 ${"Unterringelt".style(4, 3)} + | 4:4 ${"gepunktet".style(4, 4)} + | 4:5 ${"gestrichelt".style(4, 5)} + | + | Außerdem kann in manchen Terminals die ${"Farbe".style(4).style("58:2::255:0:0")} gesetzt werden. + | 256er-Paletten- oder Truecolor-Code, einfach 38/48 durch 58 ersetzen. + |""".stripMargin, + ), + ), + slide("Bereiche formatieren")( + """Mit der folgenden Escapesequenz kann man direkt einen größeren Bereich formatieren:""".par, + s"CSI ${"Pt; Pl; Pb; Pr".style(93)} ; ${"arg1 ; ... ; argn".style(95)} ${"$"}r".block.text, + s"""| + |${"✦ Pt; Pl; Pb; Pr".style(93)} geben den Bereich an, der formatiert werden soll + | Pt,Pl geben die erste Zelle an (top/left), Pb, Pr die letzte (bottom/right). + | Die oberste linke Zelle des Terminals ist hierbei 1,1. + | Weggelassene Werte entsprechen dem jeweiligen Extremwert. + |${"✦ arg1 ; ... ; argn".style(95)} sind ein SGR-Code (ohne das "m" am Ende) + |✦ Formatiert standardmäßig zeilenweise alles von der ersten bis zur letzten Zelle. + | Mit ${"CSI 2 * x".style(3)} wechselt man stattdessen zu blockweiser Formatierung, + | mit ${"CSI * x".style(3)} wieder zu zeilenweise. + |\n""".stripMargin.block.text, + s"""|Standardisiert sind nur fett, unterstrichen, blinken und invertiert. + |Manche Terminals, z.B. kitty unterstützen alle SGRs, andere wiederum keine.""".par, + ), + chapter("Cursor", "Bewegen und verstecken"), + slide("Einfache Cursorbewegung")( + s"""|Wenn wir komplexere Anwendungen bauen wollen, insbesondere interaktive, + |wollen wir nicht immer ein Zeichen nach dem anderen schreiben. + |Daher gibt es Escape-Codes, die den Cursor bewegen. + | + |Die folgenden Befehle sind alles CSI-Sequenzen mit 1-2 numerischen Argumenten. + |Fehlende Argumente entsprechen in den meisten Fällen einer 1.""".par, + ulist( + "A, B, C, D: Cursor um arg1 Zeichen nach oben, unten, rechts, links bewegen", + "E, F: Cursor auf Anfang der Zeile arg1 Zeilen unter/über aktueller Position setzen", + "G, d: Cursor auf Spalte bzw. Zeile arg1 setzen", + "H: Cursor auf Zeile arg1, Spalte arg2 setzen", + "s: Cursorposition speichern, u: zurück zur gespeicherten Position", + ), + s"""| + |Für absolute Positionen ist die oberste linke Zelle (1, 1). + |Beispiel: + |${"[HHello[3;6HWorld".style(3)} gibt "Hello" oben links aus, + |"World" in der dritten Zeile um 6 Zellen nach rechts verschoben.""".par, + PauseKey, + TypedCommand.interactiveUntyped("./cursorpos.zsh").disable("", noInteractiveCommands), + ), + slide("Cursordarstellung")( + """Der Cursor kann auch versteckt werden (z.B. tut das diese Präsentation):""".par, + ulist( + s"${"CSI ? 25 l".style(3)} versteckt den Cursor", + s"${"CSI ? 25 h".style(3)} zeigt den Cursor wieder an", + ), + s"""|Mit ${"CSI ␠ q".style(3)} kann die Form des Cursors angepasst werden. + |Hierbei ist ␠ ein Leerzeichen und eine der folgenden Zahlen:""".par, + ulist( + "0: Default-Cursor", + "1: Blinkender Block", + "2: Block", + "3: Blinkender Unterstrich", + "4: Unterstrich", + "5: Blinkender vertikaler Strich", + "6: vertikaler Strich", + ), + ), + slide("Dinge entfernen")( + """|Oft ist es praktisch, Text auch wieder löschen zu können, z.B. um den Inhalt + |einer Zeile zu aktualisieren. Einfach überschreiben führt zu Artefakten, wenn + |der neue Text kürzer ist als der alte.""".par, + ulist( + s"${"CSI K".style(3)} oder ${"CSI 0 K".style(3)} löscht vom Cursor bis zum Ende der Zeile", + s"${"CSI 1 K".style(3)} löscht vom Anfang der Zeile bis zum Cursor", + s"${"CSI 2 K".style(3)} löscht die ganze Zeile", + s"${"CSI J".style(3)} oder ${"CSI 0 J".style(3)} löscht vom Cursor bis zum Ende des Bildschirms", + s"${"CSI 1 J".style(3)} löscht vom Anfang des Bildschirms bis zum Cursor", + s"${"CSI 2 J".style(3)} löscht den ganzen Bildschirm (was \"clear\" tut)", + s"${"CSI 3 J".style(3)} löscht den Bildschirm und Scrollback", + ), + ), + chapter("Weitere", "Features", "vor allem neuere"), + slide("Hyperlinks")( + """|Hyperlinks sind ein relativ neues Feature in Terminals. Sie funktionieren ähnlich wie + |Links in Browsern. + |""".par, + s"Start: ${"OSC 8".style(92)} ; ${"".style(93)} ; ${"".style(95)} ST".block.text, + s"Ende: ${"OSC 8".style(92)} ; ; ST".block.text, + s"""| + |${"✦ OSC 8".style(92)} gibt an, dass wir einen Hyperlink wollen (zur Erinnerung: ${"OSC = ]".style(3)}) + |${"✦ ".style(93)} sind key=value Paare, getrennt durch Doppelpunkt + | Aktuell ist nur "id" spezifiziert. + |${"✦ ".style(95)} ist das Linkziel, URI-encoded. Erlaubte Schemas terminalabhängig. + | Wegen Remoteverbindungen wie SSH müssen file:// URIs den Hostnamen enthalten. + |✦ ${"ST".style(3)} ist der String Terminator (ESC \\)""".stripMargin.block.text, + ), + slide("Hyperlinks")( + s"""|Wozu die ID? Beispiel von egmontkob [1]: + | + | ╔═ file1 ════╗ + | ║ ╔═ file2 ═══╗ + | ║${"http://exa".link(42, "http://example.com")}║Lorem ipsum║ + | ║${"le.com".link(42, "http://example.com")} ║ dolor sit ║ + | ║ ║amet, conse║ + | ╚══════════║ctetur adip║ + | ╚═══════════╝ + | + |Umgebrochener Link in der linken Box soll ein Link bleiben. Das Terminal interpretiert aber + |Attribute pro Zelle. ID ist nötig, um nicht aufeinanderfolgende Zellen zu gruppieren. + | + | + |[1]: ${"https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda".style(38, 5, 39)}""".par, + ), + slide("Clipboard-Zugriff")( + s"""|Moderne Terminals erlauben es Programmen, auf das Clipboard zuzugreifen. + |Wozu, wenn das auch über "xclip" u.ä. geht? + | => Mit Escapecode funktioniert es auch über SSH!""".par, + s"${"OSC 52".style(92)} ; ${"".style(93)} ; ${"".style(95)} ST".block.text, + s"""| + |${"✦ OSC 52".style(92)} gibt an, dass wir auf das clipboard zugreifen wollen + |${"✦ ".style(93)} wählt das Clipboard, das wir nutzen möchten. + | "c" steht hierbei für das normale Clipboard, "p" für die "Primary selection". + |${"✦ ".style(95)} ist entweder ein "?", um aus dem Clipboard zu lesen, + | oder die base64-kodierten Daten, die reingeschrieben werden sollen. + |""".stripMargin.block.text, + s"""| + |Beispiel (von vorhin): ${"]52;c;SGVsbG9Xb3JsZA==\\".style(38, 5, 145)} + | kopiert "HelloWorld" in die Zwischenablage""".par, + ), + slide("""Moment, da muss das Terminal antworten""")( + s"""|Bis eben hatten wir nur Escape-Codes, bei denen das Terminal nichts zurückgeben muss. + |Wie erhalten wir die Antworten vom Terminal? => via stdin, auch als Escapecodes. + |Allerdings arbeitet stdin standardmäßig im ${"canonical mode".emph} + | => Eingabe kommt erst nach Newline an + | + |Hier müssen wir tatsächlich via Syscalls das ${"Terminal Device".emph} umkonfigurieren. + |Unter unixoiden Betriebssystemen können wir dafür das stty-Programm benutzen:""".par, + s"${"stty".style(92)} ${"-icanon".style(93)} ${"min 1".style(95)}".block.text, + s"""| + |${"✦ stty".style(92)} ist ein Programm, das diverse Terminalsyscalls kapselt + |${"✦ -icanon".style(93)} deaktiviert den canonical mode => ungepuffertes stdin + |${"✦ min 1".style(95)} setzt die minimale Größe für reads auf ein Byte + | => einzelne Zeichen lesbar, z.B. zum lesen von Tastendrücken""".stripMargin.block.text, + s"""| + |Wer in einer kompilierten Sprache arbeitet und die Syscalls direkt benutzen will, + |Die relevanten Suchbegriffe sind tcgetattr, tcsetattr und termios.h + |Bzw. unter Windows: GetConsoleMode, SetConsoleMode und windows.h""".par, + ), + slide("Clipboard auslesen")( + s"""|Zum Auslesen des Clipboards können wir nun die OSC52-Sequenz + |mit einem "?" als Datenparameter senden. + | + |Die Antwort des Terminals hat das selbe Format wie der Escapecode zum kopieren. + |Wenn wir also mit ${"]52;c;SGVsbG9Xb3JsZA==\\".style(38, 5, 145)} "HelloWorld" + |in die Zwischenablage schreiben können, würde auch genau dieser String zurück kommen + |wenn wir das Clipboard auslesen, während "HelloWorld" darin steht.""".par, + ), + slide("Cursorposition auslesen")( + s"""|Nachdem wir jetzt wissen, wie uns das Terminal Dinge antworten kann, können wir + |auch noch einen CSI-Code anschauen, den wir vorhin ausgelassen haben:""".par, + s"CSI ${"6 n".style(92)} ".block.text, + s"""| + |Auf diese Sequenz antwortet das Terminal mit der aktuellen Cursorposition, im Format:""".par, + s"CSI ${"".style(92)} ; ${"".style(92)} ${"".style(95)} ".block.text, + s"""| + |In Kombination mit anderen Escapes, die wir schon gesehen haben, kann man auf etwas + |hackige Art die Größe des Terminals bekommen:""".par, + s"${"CSI s".style(92)} ${"CSI 999;999 H".style(93)} ${"CSI 6 n".style(94)} ${"CSI u".style(96)}".block.text, + ), + slide("Bilder im Terminal")( + s"""|Man kann mittlerweile in vielen Terminals Grafiken ausgeben. Allerdings gibt es dafür + |verschiedene Protokolle und die meisten Emulatoren können nur eines.""".par, + ulist( + s"""|${"sixel".emph}: Ein altes Protokoll, das schon von Hardwareterminals unterstützt wurde + |Eingeschränkt auf 1024 Farbwerte pro Bild""".stripMargin, + s"""|${"iTerm2 Protocol".emph}: proprietäres, relativ einfaches Protokoll von iTerm2. + |Benutzt Escapes mit unbegrenzter Länge + | => Problemen wenn nicht supported""".stripMargin, + s"""|${"Kitty Graphics Protocol".emph}: proprietäres, aber ausführlich spezifiziertes Protokoll + |von kitty. Hat sehr viele Konfigurationsmöglichkeiten, benutzt eine APC-Sequenz. + |Auch von wenigen anderen implementiert (u.a. KDE Konsole). + |Support über Query prüfbar, wird ignoriert wenn nicht implementiert""".stripMargin, + ), + ), + slide("Bilder im Terminal")( + "Mit dem Kitty-Protokoll geht das auch in bewegt:".par, + TypedCommand.interactiveUntyped("kitten", "icat", "img/earth.gif"), + ), + chapter("Ende", "... des vorbereiteten Teils ;)"), + slide("Sonstige Escapes")( + "Ein paar weitere Typen von Escapesequenzen, die wir uns nicht genauer anschauen".par, + ulist( + "DCS (Device Control String): ähnlich wie OSC, kaum noch genutzt.\nU.a. für ältere Grafikprotokolle", + "SCS, DOCS: für Zeichensatzwechsel. Wir haben jetzt Unicode.", + "DEC: Für Dinge wie doppelte Zeilenhöhe. In manchen Terminals noch supported.", + "SM, RM: Setzen von Terminalmodi, war für Hardwareterminals relevant", + "DECSET, DECRST: Noch mehr Terminalmodi, vendorspezifisch.\nWerden noch genutzt, z.B. für Maus-Support", + ), + ), +) + +val presentation = Presentation(slides, meta = meta) + +presentation.start(using + Keymap.default ++ Map( + Key('i') -> SlideAction.runForeground("tmux"), + ), +) diff --git a/examples/termescape/title.zsh b/examples/termescape/title.zsh new file mode 100755 index 0000000..2b74402 --- /dev/null +++ b/examples/termescape/title.zsh @@ -0,0 +1,8 @@ +#!/bin/zsh +printf '\e[?25l\e[2J' +while true; do + printf "\e[$((LINES / 2 - 14));0H" + ${ZSH_SCRIPT:a:h}/figlet-pagga-block -p 8 "" Bunte Terminals "" "und sonstiger" "Spass mit" ESCAPECODES "" | lolcat --24bit -r + read -s -k -t 0.5 && break +done +printf '\e[?25h'