#!/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)} ${"<ext>".style(96)} ${"arg1 : arg2 : ... : argn".style(93)} ${"<cmd>".style(92)}".block.text,
    s"""|
        |${"✦  ESC [".style(94)} ist der Teil, der CSI genannt wird
        |${"✦  <cmd>".style(92)} gibt an, was gemacht werden soll. Immer ein Zeichen zwischen 0x40 (@) und 0x7F (~)
        |${"✦  <ext>".style(92)} darf aus dem ASCII-Bereich 0x20 - 0x3F außer Ziffern, ; und : sein
        |   Gibt es, weil man mehr Befehle wollte, als für <cmd> 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: ${"<ESC>[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)} ${"<Zahl>".style(92)} ; ${"args...".style(93)} ${"ST".style(96)}".block.text,
    s"""|
        |${"✦  ESC ]".style(94)} startet ein OSC
        |${"✦  <Zahl>".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: ${"<ESC>]52;c;SGVsbG9Xb3JsZA==<ESC>\\".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: ${"<ESC>[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)} : ${"<Farbe>".style(95)} m".block.text,
    s"Hintergrund: CSI ${"48".style(92)} : ${"5".style(93)} : ${"<Farbe>".style(95)} m".block.text,
    s"""|
        |${"✦  5".style(93)} gibt an, dass die 256-Farben-Palette benutzt werden soll.
        |${"✦  <Farbe>".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)} : ${"<Modell>".style(93)} : ${"<Farbraum>".style(95)} : ${"<Kanäle>".style(94)} m".block.text,
    s"""|
        |${"✦  <Modell>".style(93)} ist entweder 2 für RGB, 3 für CMY, 4 für CMYK (RGB am besten supported)
        |${"✦  <Farbraum>".style(95)} gibt den Farbraum an. Schwierig, Doku dazu zu finden, meistens leer.
        |${"✦  <Kanäle>".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: ${"<ESC>[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:
       |${"<ESC>[HHello<ESC>[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 <n> ␠ q".style(3)} kann die Form des Cursors angepasst werden.
        |Hierbei ist ␠ ein Leerzeichen und <n> 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)} ; ${"<params>".style(93)} ; ${"<URI>".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 = <ESC> ]".style(3)})
        |${"✦  <params>".style(93)} sind key=value Paare, getrennt durch Doppelpunkt
        |   Aktuell ist nur "id" spezifiziert.
        |${"✦  <URI>".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)} ; ${"<target>".style(93)} ; ${"<data>".style(95)} ST".block.text,
    s"""|
        |${"✦  OSC 52".style(92)} gibt an, dass wir auf das clipboard zugreifen wollen
        |${"✦  <target>".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".
        |${"✦  <data>".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): ${"<ESC>]52;c;SGVsbG9Xb3JsZA==<ESC>\\".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 ${"<ESC>]52;c;SGVsbG9Xb3JsZA==<ESC>\\".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 ${"<ROW>".style(92)} ; ${"<COL>".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"),
  ),
)
