Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9ed4e9efd7 | ||
![]() |
775abb29d1 | ||
![]() |
e19fe276c4 | ||
![]() |
0bcc8bc656 | ||
![]() |
d6e24a2499 | ||
![]() |
dabbd75c48 | ||
![]() |
35a6dfbe53 | ||
![]() |
38632a992d | ||
![]() |
add8e9a714 | ||
![]() |
3ad18addb0 | ||
![]() |
8111309560 | ||
![]() |
2dc7e0a0e4 | ||
![]() |
1ea8c0faec | ||
![]() |
3b2d97d45a | ||
![]() |
3b7afa3d90 | ||
![]() |
517720c720 | ||
![]() |
0d797ea684 | ||
![]() |
48680338bf | ||
![]() |
a6f22e0a08 | ||
![]() |
18c0f6ea7a | ||
![]() |
0acbca2e62 | ||
![]() |
2b7a4e8d26 | ||
![]() |
7ab755c540 | ||
![]() |
afa854c55d | ||
![]() |
f955a7563c |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.bak
|
||||
out/
|
26
build.sc
26
build.sc
|
@ -1,26 +0,0 @@
|
|||
import mill._, scalalib._, publish._
|
||||
import $ivy.`com.lihaoyi::mill-contrib-bloop:0.9.5`
|
||||
|
||||
|
||||
object copret extends ScalaModule with PublishModule {
|
||||
def scalaVersion = "2.13.3"
|
||||
|
||||
def publishVersion = "0.0.1"
|
||||
def pomSettings = PomSettings(
|
||||
description = "Use ammonite scripts for command line presentations",
|
||||
organization = "de.qwertyuiop",
|
||||
versionControl = VersionControl.github("crater2150", "copret"),
|
||||
url = "https://qwertyuiop.de/copret/",
|
||||
licenses = Seq(License.MIT),
|
||||
developers = Seq(
|
||||
Developer("crater2150", "Alexander Gehrke", "https://github.com/crater2150")
|
||||
)
|
||||
)
|
||||
|
||||
def ivyDeps = Agg(
|
||||
ivy"org.jline:jline:3.19.0",
|
||||
ivy"com.lihaoyi::ammonite-ops:2.3.8",
|
||||
ivy"com.lihaoyi::fansi:0.2.10",
|
||||
)
|
||||
}
|
||||
|
12
copret/.scalafmt.conf
Normal file
12
copret/.scalafmt.conf
Normal file
|
@ -0,0 +1,12 @@
|
|||
version = "3.7.10"
|
||||
|
||||
maxColumn = 120
|
||||
assumeStandardLibraryStripMargin = true
|
||||
align.preset = most
|
||||
align.multiline = true
|
||||
rewrite.rules = [SortModifiers]
|
||||
trailingCommas = always
|
||||
runner.dialect = scala3
|
||||
includeNoParensInSelectChains = false
|
||||
optIn.breakChainOnFirstMethodDot = false
|
||||
rewrite.scala3.insertEndMarkerMinLines = 30
|
11
copret/project.scala
Normal file
11
copret/project.scala
Normal file
|
@ -0,0 +1,11 @@
|
|||
//> using scala 3.3
|
||||
//> using packaging.packageType library
|
||||
|
||||
//> using publish.organization de.qwertyuiop
|
||||
//> using publish.name copret
|
||||
//> using publish.version 0.0.2
|
||||
//> using dep org.typelevel::cats-core:2.12.0
|
||||
//> using dep org.jline:jline:3.28.0
|
||||
//> using dep com.lihaoyi::os-lib:0.11.3
|
||||
//> using dep com.lihaoyi::fansi:0.5.0
|
||||
//> using test.dep com.lihaoyi::utest:0.8.4
|
125
copret/src/Presentation.scala
Normal file
125
copret/src/Presentation.scala
Normal file
|
@ -0,0 +1,125 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
import os.Path
|
||||
import Terminal.*
|
||||
import syntax.*
|
||||
|
||||
case class Presentation(
|
||||
slides: Vector[Slide],
|
||||
meta: Map[String, String] = Map.empty,
|
||||
):
|
||||
def start(using keymap: Keymap = Keymap.default) =
|
||||
Terminal.enterRawMode()
|
||||
Terminal.hideCursor()
|
||||
Thread.sleep(1000)
|
||||
run()
|
||||
|
||||
import Presentation._
|
||||
def run()(using keymap: Keymap) =
|
||||
logger.info("Starting presentation")
|
||||
import SlideAction.*
|
||||
@annotation.tailrec
|
||||
def rec(currentPos: Int, action: SlideAction): Unit =
|
||||
logger.info(s"Executing slide $currentPos with action $action")
|
||||
|
||||
inline def redraw() =
|
||||
logger.info(s"redrawing ${currentPos}")
|
||||
// rec(currentPos - 1, QuickNext)
|
||||
navigate(currentPos, true, 0)(executeQuick)
|
||||
|
||||
inline def navigate(pos: Int, condition: Boolean, direction: Int)(
|
||||
executor: Int => Slide => Unit,
|
||||
) =
|
||||
logger.info(s"Navigating from ${pos}, condition is $condition, direction is ${direction}")
|
||||
if condition then
|
||||
executor(pos + direction)(slides(pos + direction))
|
||||
rec(pos + direction, waitkey)
|
||||
else rec(pos, waitkey)
|
||||
|
||||
inline def runInteractive(cmd: Vector[String], path: Path) =
|
||||
logger.info(s"Running interactive command ${cmd} with working directory ${path}")
|
||||
Terminal.showCursor()
|
||||
try os.proc(cmd).call(cwd = path)
|
||||
catch case _ => ()
|
||||
Terminal.hideCursor()
|
||||
Terminal.clear()
|
||||
redraw()
|
||||
|
||||
action match
|
||||
case Start => navigate(0, true, 0)(executeSlide)
|
||||
case Next => navigate(currentPos, currentPos + 1 < slides.size, 1)(executeSlide)
|
||||
case QuickNext => navigate(currentPos, currentPos + 1 < slides.size, 1)(executeQuick)
|
||||
case Prev => navigate(currentPos, currentPos > 0, -1)(executeQuick)
|
||||
case Interactive(cmd, path) => runInteractive(cmd, path)
|
||||
|
||||
case Goto(target) =>
|
||||
for i <- 0 until target
|
||||
do executeSilent(i)()
|
||||
rec(target - 1, QuickNext)
|
||||
|
||||
case GotoSelect =>
|
||||
promptSlide() match
|
||||
case Some(i) => rec(currentPos, Goto(i))
|
||||
case None => redraw()
|
||||
|
||||
case Help =>
|
||||
Terminal.clear()
|
||||
println(keymap.help)
|
||||
waitkey
|
||||
redraw()
|
||||
|
||||
case Other(codes) =>
|
||||
Terminal.printStatus(action.show)
|
||||
rec(currentPos, waitkey)
|
||||
|
||||
case Quit =>
|
||||
Terminal.showCursor()
|
||||
end match
|
||||
end rec
|
||||
rec(0, Start)
|
||||
end run
|
||||
|
||||
def promptSlide() =
|
||||
val maxSlide = slides.size
|
||||
prompt(s"Go to slide (1 - $maxSlide):", _.toIntOption)(
|
||||
(res, input) => res.filter((1 to maxSlide).contains).isEmpty && input.nonEmpty,
|
||||
in => s"No such slide: $in (empty input to abort)",
|
||||
).map(_ - 1)
|
||||
|
||||
def executeSlide(pos: Int)(
|
||||
slide: Slide = slides(pos),
|
||||
): Unit = slide match
|
||||
case Paragraph(contents) => println(contents)
|
||||
case Clear => Terminal.clear()
|
||||
case PauseKey => waitkey(using Keymap.empty)
|
||||
case Pause(msec) => Thread.sleep(msec)
|
||||
case Image(file, None) => println(KittyGraphicsProtocol.showImage(file))
|
||||
case Image(file, Some(ImageSize(w, h, aspect))) =>
|
||||
import KittyGraphicsProtocol.Sizing.*
|
||||
println(KittyGraphicsProtocol.showImage(file, Absolute(w.toInt), Absolute(h.toInt), aspect)) // TODO
|
||||
case cmd: TypedCommand[_] => cmd.show()
|
||||
case Silent(actions) => actions()
|
||||
case Group(slides) => slides.foreach(executeSlide(pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeSlide(pos)(lios.genSlide())
|
||||
case Meta(genSlide) => executeSlide(pos)(genSlide(this, pos))
|
||||
|
||||
def executeQuick(pos: Int)(
|
||||
slide: Slide = slides(pos),
|
||||
): Unit = slide match
|
||||
case Pause(msec) => ()
|
||||
case PauseKey => ()
|
||||
case cmd: TypedCommand[_] => cmd.quickShow()
|
||||
case Group(slides) => slides.foreach(executeQuick(pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeQuick(pos)(lios.genSlide())
|
||||
case _ => executeSlide(pos)(slide)
|
||||
|
||||
def executeSilent(pos: Int)(
|
||||
slide: Slide = slides(pos),
|
||||
): Unit = slide match
|
||||
case cmd: TypedCommand[_] => cmd.force()
|
||||
case Group(slides) => slides.foreach(executeSilent(pos))
|
||||
case lios @ LazyIOSlide(_, display) =>
|
||||
executeSilent(pos)(lios.genSlide())
|
||||
case Paragraph(_) | Image(_, _) | Clear | Meta(_) => ()
|
||||
case _ => executeQuick(pos)(slide)
|
||||
end Presentation
|
|
@ -1,8 +1,10 @@
|
|||
package de.qwertyuiop.copret
|
||||
import de.qwertyuiop.copret.syntax._
|
||||
import ammonite.ops.{%%, pwd}
|
||||
|
||||
case class Theme(styles: Map[String, fansi.Attrs], figletFonts: Map[String, String]) {
|
||||
def %%(command: os.Shellable*)(cwd: os.Path = null): os.CommandResult =
|
||||
os.proc(command).call(cwd = cwd)
|
||||
|
||||
case class Theme(styles: Map[String, fansi.Attrs], figletFonts: Map[String, String]):
|
||||
def style(key: String, default: fansi.Attrs = fansi.Attrs()) =
|
||||
styles.getOrElse(key, default)
|
||||
|
||||
|
@ -14,39 +16,36 @@ case class Theme(styles: Map[String, fansi.Attrs], figletFonts: Map[String, Stri
|
|||
|
||||
def extend(newFonts: Map[String, String])(implicit d: DummyImplicit) = copy(figletFonts = figletFonts ++ newFonts)
|
||||
def ++(newFonts: Map[String, String])(implicit d: DummyImplicit) = copy(figletFonts = figletFonts ++ newFonts)
|
||||
}
|
||||
object Theme {
|
||||
implicit val default = Theme(Map(
|
||||
"titleLine" -> (fansi.Bold.On ++ fansi.Color.DarkGray),
|
||||
"code" -> fansi.Color.Yellow
|
||||
),
|
||||
Map("titleLine" -> "pagga")
|
||||
)
|
||||
}
|
||||
|
||||
object Format {
|
||||
object Theme:
|
||||
given default: Theme = Theme(
|
||||
Map(
|
||||
"titleLine" -> (fansi.Bold.On ++ fansi.Color.DarkGray),
|
||||
"code" -> fansi.Color.Yellow,
|
||||
),
|
||||
Map("titleLine" -> "smbraille"),
|
||||
)
|
||||
|
||||
object Format:
|
||||
def alignRight(str: String, padding: Int = 2) = " " * (columns - str.length - padding) + str + " " * padding
|
||||
|
||||
def center(str: String) = " " * ((columns - str.length) / 2) + str
|
||||
|
||||
def figlet(str: String, font: String) = %%("figlet", "-t", "-f", font, str)(pwd).out.string
|
||||
def figlet(str: String, font: String): String = new String(%%("figlet", "-t", "-f", font, str)().out.bytes)
|
||||
|
||||
def centerLines(str: String) = str.split("\n").map(center).mkString("\n")
|
||||
def centerBlock(str: String) = {
|
||||
def centerBlock(str: String) =
|
||||
val lines = str.split("\n")
|
||||
val maxLen = lines.map(_.length).max
|
||||
val maxLen = lines.map(s => Terminal.stripEscapes(s).length).max
|
||||
val pad = " " * ((columns - maxLen) / 2)
|
||||
lines.map(pad + _).mkString("\n")
|
||||
}
|
||||
|
||||
def distribute(texts: String*) = {
|
||||
val totalPad = columns - texts.map(_.length).sum
|
||||
def distribute(texts: String*) =
|
||||
val totalPad = columns - texts.map(s => Terminal.stripEscapes(s).length).sum
|
||||
val numPads = texts.size - 1
|
||||
val pad = " " * (totalPad / numPads)
|
||||
texts.init.mkString(pad) + pad + " " * (totalPad % numPads) + texts.last
|
||||
}
|
||||
|
||||
private[copret] val ticks = raw"`([^`]*)`".r
|
||||
}
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
229
copret/src/images.scala
Normal file
229
copret/src/images.scala
Normal file
|
@ -0,0 +1,229 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
import Terminal.*
|
||||
import scala.util.Try
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
object KittyGraphicsProtocol:
|
||||
val MaxID = 4294967295L // max 32-bit unsigned
|
||||
|
||||
def checkSupport() =
|
||||
queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
||||
.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}")
|
||||
|
||||
def imageSize(img: os.Path) =
|
||||
Try(ImageIO.read(img.toIO)).toOption.map: image =>
|
||||
SizePx(image.getWidth, image.getHeight)
|
||||
|
||||
enum Sizing:
|
||||
case Absolute(pixels: Int)
|
||||
case Relative(ratio: Double)
|
||||
case Infer
|
||||
|
||||
case class SizePx(width: Int, height: Int)
|
||||
case class SizeCells(width: Int, height: Int)
|
||||
|
||||
case class ImageSize(px: SizePx, cells: SizeCells)
|
||||
|
||||
def fitCellsToScreen(sizeCells: SizeCells, termSize: TermCells): SizeCells =
|
||||
val widthRatio = sizeCells.width.toDouble / termSize.cols
|
||||
val heightRatio = sizeCells.height.toDouble / termSize.rows
|
||||
if widthRatio > 1 || heightRatio > 1 then
|
||||
val ratio = widthRatio max heightRatio
|
||||
SizeCells((sizeCells.width / ratio).toInt, (sizeCells.height / ratio).toInt)
|
||||
else sizeCells
|
||||
|
||||
def calculateSize(
|
||||
size: SizePx,
|
||||
requestedWidth: Sizing,
|
||||
requestedHeight: Sizing,
|
||||
keepAspect: Boolean,
|
||||
termSize: TermSize,
|
||||
): ImageSize =
|
||||
import Sizing.*
|
||||
|
||||
def scale(dimensionPx: Int, requestedPx: Sizing): Int =
|
||||
requestedPx match
|
||||
case Absolute(pixels) => pixels
|
||||
case Relative(ratio) => (ratio * dimensionPx).toInt
|
||||
case Infer => dimensionPx
|
||||
|
||||
val cell = SizePx(termSize.pixels.width / termSize.cells.cols, termSize.pixels.height / termSize.cells.rows)
|
||||
|
||||
val scaledWidth = scale(size.width, requestedWidth)
|
||||
val scaledHeight = scale(size.height, requestedHeight)
|
||||
|
||||
val pixels =
|
||||
if !keepAspect then SizePx(scaledWidth, scaledHeight)
|
||||
else
|
||||
val widthRatio = scaledWidth.toDouble / size.width
|
||||
val heightRatio = scaledHeight.toDouble / size.height
|
||||
if widthRatio < heightRatio then SizePx(scaledWidth, (size.height * widthRatio).toInt)
|
||||
else SizePx((size.width * heightRatio).toInt, scaledHeight)
|
||||
|
||||
val sizeCells = SizeCells(pixels.width / cell.width, pixels.height / cell.height)
|
||||
|
||||
ImageSize(pixels, sizeCells)
|
||||
end calculateSize
|
||||
|
||||
def showImage(img: os.Path): String = showImage(img, Sizing.Infer, Sizing.Infer, true)
|
||||
def showImage(
|
||||
img: os.Path,
|
||||
requestedWidth: Sizing,
|
||||
requestedHeight: Sizing,
|
||||
keepAspect: Boolean,
|
||||
fitToScreen: Boolean = true,
|
||||
): String =
|
||||
import java.util.Base64
|
||||
(
|
||||
for
|
||||
_ <- Option.when(checkSupport())(true)
|
||||
termSize <- Terminal.getSize()
|
||||
cursorPos <- Terminal.getCursorPos()
|
||||
sizeOrig <- imageSize(img)
|
||||
|
||||
ImageSize(pixels, cells) = calculateSize(sizeOrig, requestedWidth, requestedHeight, keepAspect, termSize)
|
||||
yield
|
||||
val size =
|
||||
ImageSize(
|
||||
pixels,
|
||||
if fitToScreen then fitCellsToScreen(cells, termSize.cells)
|
||||
else cells,
|
||||
)
|
||||
|
||||
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||
val cell = SizePx(termSize.pixels.width / termSize.cells.cols, termSize.pixels.height / termSize.cells.rows)
|
||||
|
||||
val commonParams = s"s=${sizeOrig.width},v=${sizeOrig.height},c=${size.cells.width},r=${size.cells.height}"
|
||||
logger.info(s"Image size (px): ${sizeOrig.width} x ${sizeOrig.height}")
|
||||
logger.info(s"Cellsize (px): ${cell.width} x ${cell.height}")
|
||||
logger.info(s"Preferred size (px) ${size.px}")
|
||||
logger.info(s"Preferred size (Cells) ${size.cells}")
|
||||
|
||||
if image.length > 4096 then
|
||||
val chunks = image.grouped(4096).toVector
|
||||
|
||||
s"${apc}Gf=100,t=d,m=1,a=T,${commonParams};${chunks.head}${st}" +
|
||||
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
||||
s"${apc}Gm=0;${chunks.last}${st}"
|
||||
else s"${apc}Gf=100,t=d,a=T,${commonParams};${image}${st}"
|
||||
).getOrElse("Could not show image")
|
||||
end showImage
|
||||
|
||||
trait Param:
|
||||
def code: String
|
||||
|
||||
enum Action(val encoded: Char) extends Param:
|
||||
case Transmit extends Action('t') // - transmit data
|
||||
case TransmitShow extends Action('T') // - transmit data and display image
|
||||
case Query extends Action('q') // - query terminal
|
||||
case Show extends Action('p') // - put (display) previous transmitted image
|
||||
case Delete(what: DelType, free: Boolean) extends Action('d') // - delete image
|
||||
case Animation extends Action('a') // - control animation
|
||||
case AnimationFrame extends Action('f') // - transmit data for animation frames
|
||||
case AnimationCompose extends Action('c') // - compose animation frames
|
||||
|
||||
def code: String = this match
|
||||
case Delete(action, free) =>
|
||||
"d=" + (if free then action.code.capitalize else action.code)
|
||||
case other => s"a=${other.encoded}"
|
||||
|
||||
enum Responses(val code: String) extends Param:
|
||||
case All extends Responses("q=0")
|
||||
case NoOk extends Responses("q=1")
|
||||
case NoError extends Responses("q=2")
|
||||
|
||||
enum Format(val code: String) extends Param:
|
||||
case RGB(width: Int, height: Int) extends Format(s"f=24,s=${width},v=${height}")
|
||||
case RGBA(width: Int, height: Int) extends Format(s"f=32,s=${width},v=${height}")
|
||||
case PNG extends Format("f=100")
|
||||
|
||||
enum Medium(val code: String) extends Param:
|
||||
case Direct extends Medium("t=d")
|
||||
case File extends Medium("t=f")
|
||||
case TempFile extends Medium("t=t")
|
||||
case SharedMemory extends Medium("t=s")
|
||||
|
||||
enum AnimationState(val code: String) extends Param:
|
||||
case Stop extends AnimationState("s=1")
|
||||
case WaitFrame extends AnimationState("s=2")
|
||||
case Run extends AnimationState("s=3")
|
||||
|
||||
private def delType(what: Char, freeData: Boolean) =
|
||||
s"a=d,d=${if freeData then what.toUpper else what}"
|
||||
enum DelType extends Param:
|
||||
case AllPlacements
|
||||
case ById(id: Int)
|
||||
case ByNumber(num: Int)
|
||||
case AtCursor
|
||||
case Frames
|
||||
case AtPosition(x: Int, y: Int, z: Int = -1)
|
||||
case ByIdRange(from: Int, to: Int)
|
||||
case ByColumn(x: Int)
|
||||
case ByRow(y: Int)
|
||||
case ByZIndex(z: Int)
|
||||
|
||||
def code: String = this match
|
||||
case AllPlacements => "a"
|
||||
case ById(id) => s"i,i=$id"
|
||||
case ByNumber(num) => s"n,I=$num"
|
||||
case AtCursor => "c"
|
||||
case Frames => "f"
|
||||
case AtPosition(x, y, z) =>
|
||||
val (what, zarg) = if z >= 0 then ("q", s",z=$z") else ("p", "")
|
||||
what + s",x=$x,y=$y" + zarg
|
||||
case ByIdRange(x, y) => s"r,x=$x,y=$y"
|
||||
case ByColumn(x) => s"x,x=$x"
|
||||
case ByRow(y) => s"y,y=$y"
|
||||
case ByZIndex(z) => s"z,z=$z"
|
||||
|
||||
def optionInts(values: (Int, Char)*) =
|
||||
values.flatMap((value, key) => Option.when(value >= 0)(s"${key}=${value}")).mkString(",")
|
||||
|
||||
case class SimpleParam(val code: String) extends Param
|
||||
|
||||
def Filesize(bytes: Int) = SimpleParam(s"S=$bytes")
|
||||
def Offset(bytes: Int) = SimpleParam(s"O=$bytes")
|
||||
def ImageID(id: Int) = SimpleParam(s"i=$id")
|
||||
def ImageNumber(num: Int) = SimpleParam(s"I=$num")
|
||||
def PlacementID(id: Int) = SimpleParam(s"p=$id")
|
||||
def ZIndex(zindex: Int) = SimpleParam(s"z=$zindex")
|
||||
def Parent(id: Int) = SimpleParam(s"P=$id")
|
||||
def ParentPlacement(id: Int) = SimpleParam(s"Q=$id")
|
||||
def RelativeOffset(x: Int = -1, y: Int = -1) = SimpleParam(
|
||||
optionInts((x, 'H'), (y, 'V')),
|
||||
)
|
||||
|
||||
val CompressZlib = SimpleParam("o=z")
|
||||
val ContinueChunk = SimpleParam("m=1")
|
||||
val DontMoveCursor = SimpleParam("C=1")
|
||||
val UnicodePlaceholder = SimpleParam("U=1")
|
||||
|
||||
def Crop(
|
||||
originX: Int = -1,
|
||||
originY: Int = -1,
|
||||
width: Int = -1,
|
||||
height: Int = -1,
|
||||
) =
|
||||
SimpleParam(
|
||||
optionInts((originX, 'x'), (originY, 'y'), (width, 'w'), (height, 'h')),
|
||||
)
|
||||
|
||||
def AnimFrameUpdate(
|
||||
originX: Int = -1,
|
||||
originY: Int = -1,
|
||||
baseID: Int = -1,
|
||||
editID: Int = -1,
|
||||
) =
|
||||
SimpleParam:
|
||||
optionInts((originX, 'x'), (originY, 'y'), (baseID, 'c'), (editID, 'r'))
|
||||
|
||||
def AnimGap(millis: Int) = SimpleParam(s"z=$millis")
|
||||
val AnimUseOverwrite = SimpleParam("X=1")
|
||||
|
||||
def CellOffset(x: Int = -1, y: Int = -1) = SimpleParam:
|
||||
optionInts((x, 'X'), (y, 'Y'))
|
||||
|
||||
def DisplayCells(cols: Int = -1, rows: Int = -1) = SimpleParam:
|
||||
optionInts((cols, 'c'), (rows, 'r'))
|
||||
end KittyGraphicsProtocol
|
|
@ -1,86 +1,127 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
import ammonite.ops.Path
|
||||
import os.Path
|
||||
|
||||
sealed trait SlideAction
|
||||
case object Start extends SlideAction
|
||||
case class Goto(slideIndex: Int) extends SlideAction
|
||||
case object GotoSelect extends SlideAction
|
||||
case object Prev extends SlideAction
|
||||
case object Next extends SlideAction
|
||||
case object QuickNext extends SlideAction
|
||||
case object Quit extends SlideAction
|
||||
case class Interactive(cmd: Vector[String], wd: Path) extends SlideAction
|
||||
case class Other(code: List[Int]) extends SlideAction
|
||||
enum SlideAction:
|
||||
case Start
|
||||
case Goto(slideIndex: Int)
|
||||
case GotoSelect
|
||||
case Prev
|
||||
case Next
|
||||
case QuickNext
|
||||
case Quit
|
||||
case Help
|
||||
case Interactive(cmd: Vector[String], wd: Path)
|
||||
case Other(code: List[Int])
|
||||
|
||||
object SlideAction {
|
||||
def runForeground(cmd: String*)(implicit wd: Path) = Interactive(cmd.toVector, wd)
|
||||
}
|
||||
def show: String = this match
|
||||
case Start => "go to first slide"
|
||||
case Goto(slideIndex: Int) => s"jump directly to slide $slideIndex"
|
||||
case GotoSelect => "jump to slide"
|
||||
case Prev => "previous slide (skip animations)"
|
||||
case Next => "next slide"
|
||||
case QuickNext => "next slide (skip animations)"
|
||||
case Quit => "quit"
|
||||
case Help => "show help"
|
||||
case Interactive(cmd: Vector[String], wd: Path) =>
|
||||
s"execute command \"${cmd.mkString(" ")}\""
|
||||
case Other(code: List[Int]) => s"Unknown key sequence: $code"
|
||||
|
||||
object SlideAction:
|
||||
def runForeground(cmd: String*)(implicit wd: Path) =
|
||||
Interactive(cmd.toVector, wd)
|
||||
|
||||
case class Keymap(bindings: Map[List[Int], SlideAction]) {
|
||||
def apply(keycode: List[Int]): SlideAction = bindings.getOrElse(keycode, Other(keycode))
|
||||
import SlideAction.*
|
||||
|
||||
def extend(newBindings: Map[List[Int], SlideAction]) = Keymap(bindings ++ newBindings)
|
||||
def ++(newBindings: Map[List[Int], SlideAction]) = Keymap(bindings ++ newBindings)
|
||||
}
|
||||
object Keymap {
|
||||
case class Keymap(bindings: Map[Key, SlideAction]):
|
||||
private val lookup = bindings.map((k, v) => k.codes -> v)
|
||||
def apply(keycode: List[Int]): SlideAction =
|
||||
lookup.getOrElse(keycode, Other(keycode))
|
||||
|
||||
def extend(newBindings: Map[Key, SlideAction]) = Keymap(
|
||||
bindings ++ newBindings
|
||||
)
|
||||
def ++(newBindings: Map[Key, SlideAction]) = extend(newBindings)
|
||||
def help: String =
|
||||
bindings.toSeq
|
||||
.sortBy(_._2.toString)
|
||||
.map((k, v) => k.show.padTo(8, ' ') + " " + v.show)
|
||||
.mkString("\n")
|
||||
|
||||
object Keymap:
|
||||
val empty = Keymap(Map())
|
||||
// format: off
|
||||
val default = Keymap(Map(
|
||||
Key.Up -> Prev,
|
||||
Key.Left -> Prev,
|
||||
Key.PageUp -> Prev,
|
||||
Key('k') -> Prev,
|
||||
Key(' ') -> Next,
|
||||
Key.Space -> Next,
|
||||
Key('j') -> Next,
|
||||
Key.Down -> QuickNext,
|
||||
Key.Right -> QuickNext,
|
||||
Key.PageDown -> QuickNext,
|
||||
Key('q') -> Quit,
|
||||
Key('g') -> Start,
|
||||
Key.Enter -> Start,
|
||||
Key('s') -> GotoSelect,
|
||||
Key('?') -> Help,
|
||||
))
|
||||
//format: on
|
||||
|
||||
}
|
||||
enum Key:
|
||||
case Code(name: String, codepoints: List[Int])
|
||||
case Printable(char: Char)
|
||||
|
||||
object Key {
|
||||
object codes {
|
||||
val Esc = 27
|
||||
val Backspace = 127
|
||||
}
|
||||
val Esc = List(codes.Esc)
|
||||
val Backspace = List(codes.Backspace)
|
||||
val Delete = List(codes.Esc, '[', '3', '~')
|
||||
def codes: List[Int] =
|
||||
this match
|
||||
case Code(_, cp) => cp
|
||||
case Printable(char) => List(char.toInt)
|
||||
|
||||
val PageUp = List(codes.Esc, '[', '5', '~')
|
||||
val PageDown = List(codes.Esc, '[', '6', '~')
|
||||
def show: String =
|
||||
this match
|
||||
case Code(name, _) => name
|
||||
case Printable(c) => c.toString
|
||||
|
||||
val Home = List(codes.Esc, '[', 'H')
|
||||
val End = List(codes.Esc, '[', 'F')
|
||||
object Key:
|
||||
def apply(char: Char): Key = Printable(char)
|
||||
def apply(name: String, codepoints: Int*): Key = Code(name, codepoints.toList)
|
||||
object codes:
|
||||
val esc = 27
|
||||
val backspace = 127
|
||||
|
||||
val F1 = List(codes.Esc, 'P')
|
||||
val F2 = List(codes.Esc, 'Q')
|
||||
val F3 = List(codes.Esc, 'R')
|
||||
val F4 = List(codes.Esc, 'S')
|
||||
val Esc = Key("Escape", codes.esc)
|
||||
val Backspace = Key("Backspace", codes.backspace)
|
||||
val Delete = Key("Delete", codes.esc, '[', '3', '~')
|
||||
|
||||
val F5 = List(codes.Esc, '1', '5', '~')
|
||||
val F6 = List(codes.Esc, '1', '7', '~')
|
||||
val F7 = List(codes.Esc, '1', '8', '~')
|
||||
val F8 = List(codes.Esc, '1', '9', '~')
|
||||
val PageUp = Key("PageUp", codes.esc, '[', '5', '~')
|
||||
val PageDown = Key("PageDown", codes.esc, '[', '6', '~')
|
||||
|
||||
val F9 = List(codes.Esc, '2', '0', '~')
|
||||
val F10 = List(codes.Esc, '2', '1', '~')
|
||||
val F11 = List(codes.Esc, '2', '3', '~')
|
||||
val F12 = List(codes.Esc, '2', '4', '~')
|
||||
val Home = Key("Home", codes.esc, '[', 'H')
|
||||
val End = Key("End", codes.esc, '[', 'F')
|
||||
|
||||
val Tab = List('\t')
|
||||
val F1 = Key("F1", codes.esc, 'P')
|
||||
val F2 = Key("F2", codes.esc, 'Q')
|
||||
val F3 = Key("F3", codes.esc, 'R')
|
||||
val F4 = Key("F4", codes.esc, 'S')
|
||||
|
||||
val Up = List(codes.Esc, '[', 'A')
|
||||
val Down = List(codes.Esc, '[', 'B')
|
||||
val Right = List(codes.Esc, '[', 'C')
|
||||
val Left = List(codes.Esc, '[', 'D')
|
||||
val F5 = Key("F5", codes.esc, '1', '5', '~')
|
||||
val F6 = Key("F6", codes.esc, '1', '7', '~')
|
||||
val F7 = Key("F7", codes.esc, '1', '8', '~')
|
||||
val F8 = Key("F8", codes.esc, '1', '9', '~')
|
||||
|
||||
def apply(char: Char): List[Int] = List(char.toInt)
|
||||
}
|
||||
val F9 = Key("F9", codes.esc, '2', '0', '~')
|
||||
val F10 = Key("F10", codes.esc, '2', '1', '~')
|
||||
val F11 = Key("F11", codes.esc, '2', '3', '~')
|
||||
val F12 = Key("F12", codes.esc, '2', '4', '~')
|
||||
|
||||
val Space = Key("<space>", ' ')
|
||||
val Tab = Key("<tab>", '\t')
|
||||
val Enter = Key("<enter>", '\n')
|
||||
|
||||
val Up = Key("Up", codes.esc, '[', 'A')
|
||||
val Down = Key("Down", codes.esc, '[', 'B')
|
||||
val Right = Key("Right", codes.esc, '[', 'C')
|
||||
val Left = Key("Left", codes.esc, '[', 'D')
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
16
copret/src/logger.scala
Normal file
16
copret/src/logger.scala
Normal file
|
@ -0,0 +1,16 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
object logger:
|
||||
import java.util.logging.*
|
||||
val log = Logger.getLogger("copret")
|
||||
val fh = FileHandler("/tmp/copret.log")
|
||||
log.addHandler(fh)
|
||||
log.setUseParentHandlers(false)
|
||||
System.setProperty(
|
||||
"java.util.logging.SimpleFormatter.format",
|
||||
"%1$tY-%1$tm-%1$tdT%1$tH:%1$tM:%1$tS.%1$tN %1$Tp %2$s: %4$s: %5$s%6$s%n",
|
||||
)
|
||||
fh.setFormatter(new SimpleFormatter())
|
||||
fh.setLevel(Level.ALL)
|
||||
|
||||
export log.*
|
|
@ -1,190 +1,139 @@
|
|||
package de.qwertyuiop.copret
|
||||
import ammonite.ops._
|
||||
import Terminal._
|
||||
import syntax._
|
||||
import os.Path
|
||||
|
||||
case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty) {
|
||||
def start(keymap: Keymap = Keymap.default) = {
|
||||
Terminal.enterRawMode()
|
||||
run(keymap)
|
||||
}
|
||||
|
||||
import Presentation._
|
||||
def run(implicit k: Keymap) = {
|
||||
@annotation.tailrec def rec(p: Presentation, pos: Int, action: SlideAction): Unit = {
|
||||
action match {
|
||||
case Start =>
|
||||
executeSlide(p, pos)()
|
||||
rec(p, 1, waitkey)
|
||||
case Next | Other(_) =>
|
||||
if(pos + 1 < p.slides.size) {
|
||||
executeSlide(p, pos + 1)()
|
||||
rec(p, pos + 1, waitkey)
|
||||
} else rec(p, pos, waitkey)
|
||||
case QuickNext =>
|
||||
if(pos + 1 < p.slides.size) {
|
||||
executeQuick(p, pos + 1)()
|
||||
rec(p, pos + 1, waitkey)
|
||||
} else rec(p, pos, waitkey)
|
||||
case Prev =>
|
||||
if(pos > 0) {
|
||||
executeQuick(p, pos - 1)()
|
||||
rec(p, pos - 1, waitkey)
|
||||
} else rec(p, pos, waitkey)
|
||||
case Interactive(cmd, path) =>
|
||||
%(cmd)(path)
|
||||
rec(p, pos - 1, QuickNext)
|
||||
case Goto(target) =>
|
||||
for (i <- 0 until target) executeSilent(p, i)()
|
||||
rec(p, target - 1, QuickNext)
|
||||
case GotoSelect =>
|
||||
val maxSlide = p.slides.size - 1
|
||||
val target = prompt(s"Go to slide (0 - $maxSlide):", _.toIntOption)(
|
||||
(res, input) => res.filter((0 to maxSlide).contains).isEmpty && input.nonEmpty,
|
||||
in => s"No such slide: $in (empty input to abort)"
|
||||
)
|
||||
target match {
|
||||
case Some(i) => rec(p, pos, Goto(i))
|
||||
case None => rec(p, pos - 1, QuickNext)
|
||||
}
|
||||
case Quit => ()
|
||||
}
|
||||
}
|
||||
rec(this, 0, Start)
|
||||
}
|
||||
}
|
||||
object Presentation {
|
||||
def executeSlide(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match {
|
||||
case Paragraph(contents) => println(contents)
|
||||
case Clear => print("\u001b[2J\u001b[;H")
|
||||
case PauseKey => waitkey(Keymap.empty)
|
||||
case Pause(msec) => Thread.sleep(msec)
|
||||
case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock())
|
||||
case Image(file, width, height, keepAspect) => print(Terminal.showImage(file, width, height, keepAspect))
|
||||
case cmd: TypedCommand[_] => cmd.show()
|
||||
case Silent(actions) => actions()
|
||||
case Group(slides) => slides.foreach(executeSlide(p, pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeSlide(p, pos)(lios.genSlide())
|
||||
case Meta(genSlide) => executeSlide(p, pos)(genSlide(p, pos))
|
||||
case other => println("Error: Unknown slide type:"); println(other)
|
||||
}
|
||||
|
||||
def executeQuick(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match {
|
||||
case Pause(msec) => ()
|
||||
case PauseKey => ()
|
||||
case cmd: TypedCommand[_] => cmd.quickShow()
|
||||
case Group(slides) => slides.foreach(executeQuick(p, pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeQuick(p, pos)(lios.genSlide())
|
||||
case _ => executeSlide(p, pos)(slide)
|
||||
}
|
||||
|
||||
def executeSilent(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match {
|
||||
case cmd: TypedCommand[_] => cmd.force()
|
||||
case Group(slides) => slides.foreach(executeSilent(p, pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeSilent(p, pos)(lios.genSlide())
|
||||
case Paragraph(_) | Image(_,_,_,_) | Clear | IncludeMarkdown(_) | Meta(_) => ()
|
||||
case _ => executeQuick(p, pos)(slide)
|
||||
}
|
||||
}
|
||||
import Terminal.*
|
||||
import syntax.*
|
||||
|
||||
case class ImageSize(width: Double, height: Double, keepAspect: Boolean)
|
||||
|
||||
sealed trait Slide
|
||||
case class Paragraph(contents: fansi.Str) extends Slide
|
||||
case class IncludeMarkdown(path: Path) extends Slide {
|
||||
def markdownBlock() = %%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, path.toString)(pwd).block
|
||||
}
|
||||
case class Image(path: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) extends Slide
|
||||
case class Paragraph(contents: String) extends Slide:
|
||||
def centerVertical(height: Int): Paragraph =
|
||||
val lines = contents.toString.count(_ == '\n') + 1
|
||||
val pad = "\n" * ((height - lines) / 2)
|
||||
Paragraph(pad + contents + pad)
|
||||
|
||||
object Paragraph:
|
||||
def apply(str: fansi.Str): Paragraph = Paragraph(str.toString)
|
||||
|
||||
case class Image(path: Path, sizing: Option[ImageSize]) extends Slide
|
||||
object Image:
|
||||
def apply(path: Path) = new Image(path, None)
|
||||
def scaled(path: Path, width: Double, height: Double, keepAspect: Boolean) =
|
||||
Image(path, Some(ImageSize(width, height, keepAspect)))
|
||||
|
||||
case object Clear extends Slide
|
||||
case class Pause(millisec: Long) extends Slide
|
||||
case object PauseKey extends Slide
|
||||
case class Meta(contents: (Presentation, Int) => Slide) extends Slide
|
||||
|
||||
|
||||
case class TypedCommand[T](exec: T => String, display: String, cmd: T) extends Slide {
|
||||
case class TypedCommand[T](
|
||||
exec: T => String,
|
||||
display: String,
|
||||
cmd: T,
|
||||
cmdIsHidden: Boolean,
|
||||
outputIsHidden: Boolean,
|
||||
) extends Slide:
|
||||
private lazy val _output = exec(cmd)
|
||||
def output = _output
|
||||
def display(s: String): TypedCommand[T] = TypedCommand(exec, s, cmd)
|
||||
|
||||
def show() = {
|
||||
infix def showing(s: String): TypedCommand[T] = TypedCommand(exec, s, cmd)
|
||||
|
||||
def show() =
|
||||
force()
|
||||
if !cmdIsHidden then
|
||||
prompt()
|
||||
Terminal.showCursor()
|
||||
typeCmd()
|
||||
print(output)
|
||||
}
|
||||
if !outputIsHidden then print(output)
|
||||
if !cmdIsHidden then Terminal.hideCursor()
|
||||
|
||||
def quickShow() = {
|
||||
def quickShow() =
|
||||
force()
|
||||
if !cmdIsHidden then
|
||||
prompt()
|
||||
println(display)
|
||||
print(output)
|
||||
}
|
||||
if !outputIsHidden then print(output)
|
||||
|
||||
def prompt() = print(fansi.Color.LightGreen("user@host % "))
|
||||
def force() = _output
|
||||
|
||||
private def typeCmd() = {
|
||||
for (char <- display) {
|
||||
def display(s: String) = copy(display = s)
|
||||
|
||||
private def typeCmd() =
|
||||
for char <- display do
|
||||
print(char)
|
||||
Thread.sleep(50 + scala.util.Random.nextInt(80))
|
||||
}
|
||||
println()
|
||||
}
|
||||
|
||||
/* Conditionally disable execution. Useful for e.g. a debug mode, or a non-interactive mode */
|
||||
def disable(altDisplay: String = display, condition: Boolean = true) =
|
||||
if(condition) copy(display = altDisplay, exec = (_:T) => "")
|
||||
if condition then copy(display = altDisplay, exec = (_: T) => "")
|
||||
else this
|
||||
|
||||
/* Conditionally replace the executed command (but still displaying the same). Useful for e.g. a non-interactive mode,
|
||||
* where a call to an editor is replaced with a file operation */
|
||||
def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] =
|
||||
if(condition) tc.display(display)
|
||||
if condition then tc.showing(display)
|
||||
else this
|
||||
end TypedCommand
|
||||
|
||||
}
|
||||
|
||||
object TypedCommand {
|
||||
object TypedCommand:
|
||||
val shell = sys.env.getOrElse("SHELL", "sh")
|
||||
|
||||
def run(implicit wd: Path): Vector[String] => String =
|
||||
c => safe_%%(c)
|
||||
def run(using Path): Vector[String] => String =
|
||||
c => runProcess(c)
|
||||
|
||||
def runShell(implicit wd: Path): Vector[String] => String =
|
||||
c => safe_%%(Vector(shell, "-c", c.mkString(" ")))
|
||||
def runShell(using Path): Vector[String] => String =
|
||||
c => runProcess(Vector(shell, "-c", c.mkString(" ")))
|
||||
|
||||
def runInteractive(implicit wd: Path): Vector[String] => String =
|
||||
c => { %(c); ""}
|
||||
def runInteractive(using cwd: Path): Vector[String] => String =
|
||||
c =>
|
||||
Terminal.showCursor()
|
||||
os.proc(c).call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit, cwd = cwd)
|
||||
Terminal.hideCursor()
|
||||
""
|
||||
|
||||
def apply(cmd: String*)(implicit wd: Path): TypedCommand[Vector[String]] =
|
||||
def apply(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(run, cmd.mkString(" "), cmd.toVector)
|
||||
|
||||
def shell(cmd: String*)(implicit wd: Path): TypedCommand[Vector[String]] =
|
||||
def apply[T](exec: T => String, display: String, cmd: T): TypedCommand[T] =
|
||||
TypedCommand(exec, display, cmd, false, false)
|
||||
|
||||
def shell(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(runShell, cmd.mkString(" "), cmd.toVector)
|
||||
|
||||
def fake(cmd: String): TypedCommand[String] =
|
||||
TypedCommand(_ => "", cmd, cmd)
|
||||
|
||||
def interactive(cmd: String*)(implicit wd: Path): TypedCommand[Vector[String]] =
|
||||
def interactive(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(runInteractive, cmd.mkString(" "), cmd.toVector)
|
||||
}
|
||||
|
||||
def interactiveUntyped(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(runInteractive, cmd.mkString(" "), cmd.toVector, cmdIsHidden = true, outputIsHidden = false)
|
||||
|
||||
def untyped(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(exec = run, display = "", cmd.toVector, cmdIsHidden = true, outputIsHidden = false)
|
||||
end TypedCommand
|
||||
|
||||
sealed abstract case class Silent[T] private (doStuff: () => T) extends Slide
|
||||
object Silent { def apply[T](doStuff: => T) = new Silent(() => doStuff){} }
|
||||
|
||||
object Silent:
|
||||
def apply[T](doStuff: => T) = new Silent(() => doStuff) {}
|
||||
|
||||
case class Group(slides: List[Slide]) extends Slide
|
||||
object Group { def apply(slides: Slide*): Group = Group(slides.toList) }
|
||||
object Group:
|
||||
def apply(slides: Slide*): Group = Group(slides.toList)
|
||||
|
||||
case class LazyIOSlide[T](runOnce: () => T, display: T => Slide) extends Slide {
|
||||
case class LazyIOSlide[T](runOnce: () => T, display: T => Slide) extends Slide:
|
||||
private lazy val data = runOnce()
|
||||
def genSlide(): Slide = display(data)
|
||||
}
|
||||
|
||||
trait SlideSyntax {
|
||||
private[copret] class LazyIOSlideBuilder[T](runOnce: => T) {
|
||||
trait SlideSyntax:
|
||||
private[copret] class LazyIOSlideBuilder[T](runOnce: => T):
|
||||
def useIn(display: T => Slide) = LazyIOSlide(() => runOnce, display)
|
||||
}
|
||||
|
||||
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = new LazyIOSlideBuilder(runOnce)
|
||||
}
|
||||
|
||||
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = LazyIOSlideBuilder(
|
||||
runOnce,
|
||||
)
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
|
@ -1,38 +1,34 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
object syntax extends Templates with TerminalSyntax with SlideSyntax {
|
||||
implicit class PresenterStringExtensions(val str: String) {
|
||||
import Format._
|
||||
def code(implicit theme: Theme) = Format.ticks.replaceAllIn(str, m => theme.style("code")("$1").render)
|
||||
def text(implicit theme: Theme) = Paragraph(str)
|
||||
def par(implicit theme: Theme) = Paragraph(str.stripMargin.code.padLeft(2))
|
||||
def style(key: String, default: fansi.Attrs = fansi.Attrs())(implicit theme: Theme) = theme.style(key, default)(str)
|
||||
object syntax extends Templates with TerminalSyntax with SlideSyntax:
|
||||
import Format.*
|
||||
extension (str: String)
|
||||
def code(using theme: Theme) = Format.ticks.replaceAllIn(str, m => theme.style("code")("$1").render)
|
||||
def text(using Theme) = Paragraph(str)
|
||||
def par(using Theme) = Paragraph(str.stripMargin.code.indent(2))
|
||||
def style(key: String, default: fansi.Attrs)(using theme: Theme) = theme.style(key, default)(str)
|
||||
|
||||
def centered = center(str)
|
||||
def block = centerBlock(str)
|
||||
def right = alignRight(str)
|
||||
def right(padding: Int) = alignRight(str, padding)
|
||||
def padLeft(padding: Int) = {
|
||||
def indent(padding: Int) =
|
||||
val pad = " " * padding
|
||||
str.linesIterator.map(pad + _).mkString("\n")
|
||||
}
|
||||
|
||||
def blue = fansi.Color.Blue(str)
|
||||
def green = fansi.Color.Green(str)
|
||||
def yellow = fansi.Color.Yellow(str)
|
||||
def red = fansi.Color.Red(str)
|
||||
}
|
||||
|
||||
implicit class PresenterFansiStringExtensions(val str: fansi.Str) {
|
||||
import Format._
|
||||
def text(implicit theme: Theme) = Paragraph(str)
|
||||
def style(key: String, default: fansi.Attrs = fansi.Attrs())(implicit theme: Theme) = theme.style(key, default)(str)
|
||||
extension (str: fansi.Str)
|
||||
def text(using Theme) = Paragraph(str.toString)
|
||||
def style(key: String, default: fansi.Attrs)(using theme: Theme) = theme.style(key, default)(str)
|
||||
|
||||
def blue = fansi.Color.Blue(str)
|
||||
def green = fansi.Color.Green(str)
|
||||
def yellow = fansi.Color.Yellow(str)
|
||||
def red = fansi.Color.Red(str)
|
||||
}
|
||||
end syntax
|
||||
|
||||
}
|
||||
/* vim:set tw=120: */
|
||||
|
|
|
@ -1,25 +1,64 @@
|
|||
package de.qwertyuiop.copret
|
||||
import syntax._
|
||||
import ammonite.ops.{Path, %, %%, pwd}
|
||||
import syntax.*
|
||||
import os.Path
|
||||
|
||||
trait Templates {
|
||||
def titleLine(title: String)(implicit theme: Theme) = Paragraph(
|
||||
"\n" + Format.figlet(title, theme.font("titleLine", "pagga")).block.blue + "\n"
|
||||
trait Templates:
|
||||
def titleLine(title: String)(using theme: Theme) = Paragraph(
|
||||
"\n" + Format.figlet(title, theme.font("titleLine", "pagga")).block.blue + "\n",
|
||||
)
|
||||
|
||||
def header(implicit theme: Theme) = Meta((p, pos) => {
|
||||
def header(using theme: Theme) = Meta((p, pos) => {
|
||||
val left = p.meta.getOrElse("author", "")
|
||||
val center = p.meta.getOrElse("title", "")
|
||||
val right = s"${pos} / ${p.slides.size - 1}"
|
||||
val right = s"${pos + 1} / ${p.slides.size}"
|
||||
theme.style("titleLine")(Format.distribute(left, center, right)).text
|
||||
})
|
||||
|
||||
def slide(title: String)(slides: Slide*) = Group(Clear :: header :: titleLine(title) :: slides.toList)
|
||||
def slide(slides: Slide*) = Group(Clear :: header :: slides.toList)
|
||||
def slide(title: String)(slides: Slide*)(using Theme) = Group(
|
||||
Clear :: header :: titleLine(title) :: slides.toList,
|
||||
)
|
||||
def slide(slides: Slide*)(using Theme) = Group(
|
||||
Clear :: header :: slides.toList,
|
||||
)
|
||||
|
||||
def markdown(title: String, content: Path) = slide(title)(IncludeMarkdown(content))
|
||||
def markdown(title: String, content: Path)(using Theme) = slide(title)(
|
||||
Paragraph(
|
||||
%%%(
|
||||
"/usr/bin/mdcat",
|
||||
"--columns",
|
||||
(columns * 0.8).toInt.toString,
|
||||
content.toString,
|
||||
)(using os.pwd).block,
|
||||
),
|
||||
)
|
||||
|
||||
lazy val --- = Paragraph(("═" * columns).yellow)
|
||||
}
|
||||
def list(bullet: (Int, Int) => String, continued: (Int, Int) => String)(items: String*)(using Theme): Paragraph =
|
||||
items.zipWithIndex
|
||||
.map((str, index) => {
|
||||
def formatFirst(first: String) =
|
||||
s" ${bullet(index, items.size)} $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 olist(items: String*)(using Theme) =
|
||||
def digits(i: Int): Int = math.ceil(math.log10(i)).toInt
|
||||
list(
|
||||
bullet = (i, max) => (" " * (digits(max + 1) - digits(i + 1) - 1)) + s"${i + 1}.",
|
||||
continued = (i, max) => " " * digits(max + 1),
|
||||
)(items*)
|
||||
|
||||
lazy val --- = Paragraph("\n" + ("-" * columns).yellow.toString + "\n")
|
||||
lazy val === = Paragraph("\n" + ("═" * columns).yellow.toString + "\n")
|
||||
end Templates
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
|
@ -1,72 +1,134 @@
|
|||
package de.qwertyuiop.copret
|
||||
import ammonite.ops.{Path, ShelloutException, pwd, read, %, %%}
|
||||
import os.Path
|
||||
import org.jline.terminal.TerminalBuilder
|
||||
import org.jline.reader.LineReaderBuilder
|
||||
import scala.io.StdIn
|
||||
import cats.*
|
||||
import scala.collection.IndexedSeqView
|
||||
|
||||
object Terminal {
|
||||
def safe_%%(cmd: Vector[String])(implicit wd: Path): String =
|
||||
try {
|
||||
%%(cmd).out.string
|
||||
} catch {
|
||||
case e: ShelloutException => e.result.err.string
|
||||
}
|
||||
|
||||
object Terminal:
|
||||
def runProcess(cmd: Vector[String])(using cwd: os.Path): String =
|
||||
try os.proc(cmd.map(identity)).call(cwd = cwd).out.text()
|
||||
catch case e: os.SubprocessException => e.result.err.text()
|
||||
|
||||
def tryCmd[T](cmd: => T, default: => T) =
|
||||
try { cmd } catch { case e: ShelloutException => default }
|
||||
try cmd
|
||||
catch case e: os.SubprocessException => default
|
||||
|
||||
def enterRawMode(): Unit = {
|
||||
%%("sh", "-c", "stty -icanon min 1 < /dev/tty")(pwd)
|
||||
%%("sh", "-c", "stty -echo < /dev/tty")(pwd)
|
||||
}
|
||||
def enterRawMode(): Unit =
|
||||
os.proc("sh", "-c", "stty -icanon min 1 < /dev/tty")
|
||||
.call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)
|
||||
os.proc("sh", "-c", "stty -echo < /dev/tty").call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)
|
||||
|
||||
private[copret] lazy val jterm = org.jline.terminal.TerminalBuilder.terminal()
|
||||
private[copret] lazy val lineReader = LineReaderBuilder.builder().terminal(jterm).build()
|
||||
private[copret] lazy val lineReader =
|
||||
LineReaderBuilder.builder().terminal(jterm).build()
|
||||
|
||||
def waitkey(implicit keymap: Keymap): SlideAction = {
|
||||
def waitkey(implicit keymap: Keymap): SlideAction =
|
||||
// ignore keypresses done during slide animations
|
||||
while(Console.in.ready()) Console.in.read
|
||||
while (System.in.available() > 0) System.in.read
|
||||
|
||||
var key = scala.collection.mutable.ArrayBuffer[Int]()
|
||||
key += Console.in.read
|
||||
while(Console.in.ready)
|
||||
key += Console.in.read
|
||||
key += System.in.read
|
||||
while (System.in.available() > 0)
|
||||
key += System.in.read
|
||||
keymap(key.toList)
|
||||
}
|
||||
|
||||
def queryTerm(query: String): String =
|
||||
val ttyIn = new java.io.FileInputStream("/dev/tty")
|
||||
val ttyOut = new java.io.PrintStream(new java.io.FileOutputStream("/dev/tty"))
|
||||
ttyOut.print(query)
|
||||
var response = scala.collection.mutable.ArrayBuffer[Int]()
|
||||
response += ttyIn.read()
|
||||
while (ttyIn.available > 0 && response.last != '\u0007')
|
||||
response += ttyIn.read()
|
||||
new String(response.toArray, 0, response.length)
|
||||
|
||||
case class EscapeSequence()
|
||||
def parseEscape(sequence: String): Option[EscapeSequence] = ???
|
||||
|
||||
def prompt[T](prefix: String, parse: String => T)(
|
||||
retry: (T, String) => Boolean = (t: T, s: String) => false,
|
||||
error: String => String = in => s"Invalid input: $in"
|
||||
): T = {
|
||||
error: String => String = in => s"Invalid input: $in",
|
||||
): T =
|
||||
val input = lineReader.readLine(prefix + " ")
|
||||
val result = parse(input)
|
||||
if(retry(result, input)) {
|
||||
if retry(result, input) then
|
||||
println(error(input))
|
||||
prompt(prefix, parse)(retry, error)
|
||||
}
|
||||
else result
|
||||
}
|
||||
|
||||
def isTmux = sys.env.contains("TMUX") || sys.env("TERM").startsWith("screen")
|
||||
|
||||
def csi = if (isTmux) "\u001bPtmux\u001b\u001b[" else "\u001b["
|
||||
def osc = if (isTmux) "\u001bPtmux\u001b\u001b]" else "\u001b]"
|
||||
def st = if (isTmux) "\u0007\u001b\\" else "\u0007"
|
||||
def apc = if (isTmux) "\u001bPtmux;\u001b\u001b_" else "\u001b_"
|
||||
def st = if (isTmux) "\u0007\u001b\\" else "\u001b\\"
|
||||
|
||||
def showImage(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) = {
|
||||
def stripEscapes(str: String) =
|
||||
// match CSI until letter or OSC/APC until ST
|
||||
str.replaceAll("\u001b(\\[[^@-~]*[@-~]|(\\]|_).*?(\u0007|\u001b\\\\))", "")
|
||||
|
||||
def hideCursor() = print(s"${csi}?25l")
|
||||
def showCursor() = print(s"${csi}?25h")
|
||||
def cursorTo(row: Int, col: Int) = print(s"${csi}${row};${col}H")
|
||||
def clear() = print(s"${csi}2J${csi};H")
|
||||
def printStatus(msg: String): Unit =
|
||||
cursorTo(jterm.getSize.getColumns, 1)
|
||||
print(msg)
|
||||
|
||||
def showImage(
|
||||
img: os.Path,
|
||||
width: String = "100%",
|
||||
height: String = "100%",
|
||||
keepAspect: Boolean = true,
|
||||
) =
|
||||
import java.util.Base64
|
||||
val image = Base64.getEncoder.encodeToString(read.bytes(img))
|
||||
val aspect = if(keepAspect) 1 else 0
|
||||
s"${osc}1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image$st"
|
||||
}
|
||||
}
|
||||
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||
val aspect = if (keepAspect) 1 else 0 // TODO
|
||||
if image.length > 4096 then
|
||||
val chunks = image.grouped(4096).toVector
|
||||
|
||||
private[copret] trait TerminalSyntax {
|
||||
s"${apc}Gf=100,t=d,m=1,a=T;${chunks.head}${st}" +
|
||||
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
||||
s"${apc}Gm=0;${chunks.last}${st}"
|
||||
else s"${apc}Gf=100,t=d,a=T;${image}${st}"
|
||||
|
||||
private val CursorPosResponse = """\u001b\[(\d+);(\d+)R""".r
|
||||
|
||||
def getCursorPos(): Option[CursorPos] =
|
||||
queryTerm(s"${csi}6n") match
|
||||
case CursorPosResponse(rows, cols) => Some(CursorPos(cols.toInt, rows.toInt))
|
||||
case _ => None
|
||||
|
||||
def getSizeCells(): Option[TermCells] =
|
||||
queryTerm(s"${csi}s${csi}999;999H${csi}6n${csi}u") match
|
||||
case CursorPosResponse(rows, cols) => Some(TermCells(cols.toInt, rows.toInt))
|
||||
case _ => None
|
||||
|
||||
private val SizeResponse = """\u001b\[4;(\d+);(\d+)t""".r
|
||||
|
||||
case class TermPixels(width: Int, height: Int)
|
||||
case class TermCells(cols: Int, rows: Int)
|
||||
case class TermSize(pixels: TermPixels, cells: TermCells)
|
||||
case class CursorPos(cols: Int, rows: Int)
|
||||
def getSizePixels(): Option[TermPixels] = queryTerm(s"${csi}14t") match
|
||||
case SizeResponse(rows, cols) => Some(TermPixels(cols.toInt, rows.toInt))
|
||||
case _ => None
|
||||
|
||||
def getSize(): Option[TermSize] =
|
||||
for
|
||||
pixels <- getSizePixels()
|
||||
cells <- getSizeCells()
|
||||
yield TermSize(pixels, cells)
|
||||
|
||||
end Terminal
|
||||
|
||||
private[copret] trait TerminalSyntax:
|
||||
import Terminal._
|
||||
|
||||
def %%%(cmd: String*)(implicit wd: Path) = safe_%%(cmd.toVector)
|
||||
def %%%(cmd: String*)(using cwd: os.Path): String = runProcess(cmd.toVector)
|
||||
def columns = jterm.getSize.getColumns
|
||||
def rows = jterm.getSize.getRows
|
||||
|
||||
}
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
1
examples/git/.gitignore
vendored
Normal file
1
examples/git/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
demoRepo
|
462
examples/git/git-presentation.sc
Executable file
462
examples/git/git-presentation.sc
Executable file
|
@ -0,0 +1,462 @@
|
|||
#!/usr/bin/env -S scala-cli shebang
|
||||
//>using scala 3.3
|
||||
//>using dep de.qwertyuiop::copret:0.0.2
|
||||
|
||||
import de.qwertyuiop.copret._
|
||||
import de.qwertyuiop.copret.syntax.*
|
||||
import TypedCommand.{interactive, shell => sh}, Format.figlet
|
||||
import os.{pwd, Path}
|
||||
|
||||
/* Configuration */
|
||||
|
||||
/* store paths for reuse.
|
||||
* most of the commands in this presentation will be git commands in a demo repository, so the path to that repo is set
|
||||
* as implicit (will be the working directory of executed commands) */
|
||||
val imgs = pwd / "img"
|
||||
given repoDir: Path = pwd / "demoRepo"
|
||||
given theme: Theme = Theme.default
|
||||
|
||||
/* You can define any variables and use then in your presentation.
|
||||
* The presentation is pure Scala code, you can use anything that Scala offers . */
|
||||
val noInteractiveCommands = true
|
||||
|
||||
/* image height is defined by the iterm2 image protocol as follows:
|
||||
* N: N character cells.
|
||||
* Npx: N pixels.
|
||||
* N%: N percent of the session's width or height.
|
||||
* auto: The image's inherent size will be used to determine an appropriate dimension.
|
||||
*
|
||||
* The `Image` slide element will default to 100% height and width (preserving aspect ratio). Here the maximum height
|
||||
* is calculated for slides with the default header (1 row status, 3 rows title font, 3 rows spacing)
|
||||
*/
|
||||
val imageHeight = (rows - 7).toDouble //TODO
|
||||
|
||||
/* Make sure, the demo repo is not there in the beginning. We'll create it in the presentation */
|
||||
os.remove.all(repoDir)
|
||||
|
||||
/* the basic building block of copret presentations is the various case classes of the `Slide` trait */
|
||||
val titleSlide =
|
||||
Group( // `Group` combines several `Slide` objects into one
|
||||
Clear, // `Clear` clears the screen, so the next slide element starts at the top
|
||||
Image(
|
||||
imgs / "git-logo.png",
|
||||
Some(ImageSize(width = 0.1, height = 1, keepAspect = true)),
|
||||
), // `Image` currently requires a terminal supporting `imgcat`
|
||||
Paragraph( // `Paragraph` simply prints its text contents
|
||||
s"""${figlet("git", "roman").block.blue}
|
||||
|
|
||||
|${"Wie man es benutzt und was in .git eigentlich passiert".centered.blue}
|
||||
|
|
||||
|${"-- Alexander Gehrke".right(10)}
|
||||
|""".stripMargin,
|
||||
),
|
||||
// Above you see some of the extension methods for strings, `block` for centering multiline text, `centered` for
|
||||
// single lines, `right(10)` for placing text 10 spaces away from the right border and `blue` for coloring.
|
||||
// also, `figlet` calls the external figlet command to render ASCII art text with the given font.
|
||||
)
|
||||
|
||||
/* 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(
|
||||
figlet(title1, font).block.green ++ figlet(
|
||||
title2,
|
||||
font,
|
||||
).block.green ++ "\n" ++ subtitle.right(10).green,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val parentSHApattern = raw"parent ([a-f0-9]{40})".r
|
||||
val treeSHApattern = raw"tree ([a-f0-9]{40})".r
|
||||
val blobSHApattern = raw"blob ([a-f0-9]{40})".r
|
||||
|
||||
/* With `prepare` you can define variables for a slide, that are computed when the presentation reaches the
|
||||
* slide using it. This is useful for running external commands when a specific slide is reached and using their
|
||||
* output (if you'd include them in the string interpolation directly, they'd run before starting the presentation).
|
||||
*
|
||||
* The code given to `prepare` is run exactly once *per slide using it*. See the usages of `gitHashes` below for
|
||||
* details. */
|
||||
case class GitHashes(
|
||||
commit: String,
|
||||
tree: String,
|
||||
blob: String,
|
||||
parent: String,
|
||||
subtree: String,
|
||||
)
|
||||
val gitHashes = prepare {
|
||||
val commitSHA = %%%("git", "show-ref").substring(0, 40)
|
||||
val commit = %%%("git", "cat-file", "-p", "HEAD")
|
||||
val treeSHA =
|
||||
treeSHApattern.findFirstMatchIn(commit).map(_.group(1)).getOrElse("")
|
||||
val parentSHA =
|
||||
parentSHApattern.findFirstMatchIn(commit).map(_.group(1)).getOrElse("")
|
||||
val tree = %%%("git", "cat-file", "-p", treeSHA)
|
||||
val blobSHA =
|
||||
blobSHApattern.findFirstMatchIn(tree).map(_.group(1)).getOrElse("")
|
||||
val subtreeSHA =
|
||||
treeSHApattern.findFirstMatchIn(tree).map(_.group(1)).getOrElse("")
|
||||
GitHashes(commitSHA, treeSHA, blobSHA, parentSHA, subtreeSHA)
|
||||
}
|
||||
|
||||
def gitStatus = TypedCommand("git", "status")
|
||||
def gitCatFile(ref: String) = TypedCommand("git", "cat-file", "-p", ref)
|
||||
|
||||
def gitLogGraph(cutOff: Int = 0) = {
|
||||
val shownCmd = "git log --graph --oneline --all"
|
||||
val execCmd =
|
||||
shownCmd + " --decorate=short --color=always " + (if (cutOff > 0)
|
||||
"| head -n -" + cutOff
|
||||
else "")
|
||||
val typer = sh(execCmd) display shownCmd
|
||||
if (cutOff > 0)
|
||||
Group(typer, "...".text)
|
||||
else
|
||||
typer
|
||||
}
|
||||
|
||||
/* A `Presentation` object can include a map of metadata. Which keys are used, depends on your used templates.
|
||||
* To use them in a slide yourself (or for creating own templates), use the Meta slide class, which can access the
|
||||
* presentation object and state to generate a slide.
|
||||
*
|
||||
* The only keys used in the packaged templates are currently "author" and "title" in the `header` template */
|
||||
val meta = Map(
|
||||
"author" -> "Alexander Gehrke",
|
||||
"title" -> "Git",
|
||||
)
|
||||
|
||||
/* A presentation consists of a `Vector[Slide]`. After each slide in this vector, the presentation pauses and waits for
|
||||
* input. `Group` is used, to display several slides at once, and `PauseKey` can be used inside a `Group` to wait for
|
||||
* input (but limited to displaying the rest of the `Group`, normal navigation is only possible between top level
|
||||
* slides).
|
||||
*
|
||||
* Navigation distinguishes between normal and "quick" steps, the latter disables things like pauses or animations. It
|
||||
* is also possible to jump to a specific slide.
|
||||
* */
|
||||
val presentation = Presentation(
|
||||
Vector(
|
||||
titleSlide,
|
||||
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")(
|
||||
"""Git trackt Inhalt von Dateien innerhalb eines Repositories.
|
||||
|Kann in neuem oder bestehenden Ordner angelegt werden
|
||||
|""".par,
|
||||
/* the `---` template displays a yellow line */
|
||||
---,
|
||||
/* `Pause` waits for the given number of milliseconds */
|
||||
Pause(500),
|
||||
/* `TypedCommand.shell` (here aliased to `sh`) displays a typing animation of that command and then executes it,
|
||||
* showing its output in the presentation */
|
||||
sh("git init demoRepo")(using pwd),
|
||||
/* sometimes it's useful to display something else than is actually executed, e.g. to add comments, or to hide
|
||||
* options only required because we don't call from an interactive shell (stuff like --color) */
|
||||
sh("ls -1a --color=always demoRepo")(using pwd) display "ls -1a demoRepo",
|
||||
/* If you need to run commands that should not display anything in the presentation, use `Silent`.
|
||||
* Here I use it to prepare the repository, but it could also be used e.g. for playing a sound or opening a video.*/
|
||||
Silent {
|
||||
%%%("git", "config", "user.email", "crater2150@example.com")
|
||||
%%%("git", "config", "user.name", "crater2150")
|
||||
%%%("git", "config", "color.status", "always")
|
||||
%%%("git", "config", "color.ui", "always")
|
||||
},
|
||||
),
|
||||
slide("Basics")(
|
||||
"Noch ist das Repo leer. Ändern wir das:".par,
|
||||
---,
|
||||
Pause(500),
|
||||
TypedCommand.fake("cd demoRepo"),
|
||||
sh("echo 'Hello, World!' > hello.txt"),
|
||||
Pause(500),
|
||||
sh("ls -1a"),
|
||||
),
|
||||
slide("Basics")(
|
||||
"Den Zustand des Repos sehen wir mit `git status`\n".par,
|
||||
---,
|
||||
Pause(500),
|
||||
gitStatus,
|
||||
),
|
||||
slide("Basics")(
|
||||
"Damit Git Dateien trackt, müssen diese mit `git add` hinzugefügt werden.\n".par,
|
||||
---,
|
||||
sh("git add hello.txt"),
|
||||
gitStatus,
|
||||
),
|
||||
slide("Basics")(
|
||||
"""Die gespeicherten Zustände eines Repos nennt man Commit.
|
||||
|Zu jedem Commit gibt man eine Zusammenfassung der Änderungen an
|
||||
|""".par,
|
||||
---,
|
||||
Pause(500),
|
||||
sh("git commit --message \"Added first file\""),
|
||||
PauseKey,
|
||||
---,
|
||||
"""Commits werden über ihren SHA1-Hash identifiziert. Dieser wird ggf. abgekürzt.
|
||||
|Mit `git log` können wir uns die bisherigen Commits ansehen:
|
||||
|""".par,
|
||||
---,
|
||||
sh("git log"),
|
||||
),
|
||||
/* here we see `gitHashes`, created with `prepare`, in action:
|
||||
* As the code is run exactly once per `useIn` call i.e. per using slide, the hashes of the latest commit, tree and
|
||||
* parent in the demo repo, that `gitHashes` retrieves, are always the ones for the repository state matching the
|
||||
* slide. The prepared code is run and its result cached, when the slide is first displayed (or skipped over).
|
||||
*/
|
||||
gitHashes useIn { sha =>
|
||||
slide("Was passiert bei einem Commit?")(
|
||||
s"""Schauen wir uns an, was Git aus dem Commit gemacht hat.
|
||||
|`git cat-file $$ref` zeigt Inhalt von Git-Objekten. `$$ref` ist Referenz des Objekts, z.B. der Hash.
|
||||
|Unser Commit hatte den Hash `${sha.commit}`.
|
||||
|Statt diesem geht auch `HEAD` = aktueller Commit.
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile("HEAD"),
|
||||
PauseKey,
|
||||
---,
|
||||
s"""Zum Hashen wird vor den Inhalt noch `<Objekttyp> <Länge des Inhalts in bytes\\0` gehängt.
|
||||
|Hier: `Objekttyp == commit`
|
||||
|""".par,
|
||||
)
|
||||
},
|
||||
gitHashes useIn { sha =>
|
||||
slide("Commits, Trees, Blobs")(
|
||||
s"""Neben den Metadaten ist in unserem Commit noch eine Zeile
|
||||
| `tree ${sha.tree}`
|
||||
|
|
||||
|Trees repräsentieren Verzeichnisse, Tree in einem Commit = Wurzelverzeichnis des Repos
|
||||
|`$$ref^{tree}` = Baum mit Referenz `$$ref`, oder wenn `$$ref` ein Commit ist, der Tree aus diesem
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile("HEAD^{tree}"),
|
||||
PauseKey,
|
||||
---,
|
||||
s"""Hier ist noch nicht viel zu sehen, da wir nur eine Datei haben.
|
||||
|Einträge im Tree haben das Format:
|
||||
| `<berechtigungen> <blob | tree> <sha1 hash> <dateiname>`
|
||||
|""".par,
|
||||
)
|
||||
},
|
||||
gitHashes useIn { sha =>
|
||||
slide("Commits, Trees, Blobs")(
|
||||
s"""Blobs sind die eigentlichen Dateiinhalte.
|
||||
|Unsere `hello.txt` hat, wie im Tree zu sehen, den Hash ${sha.blob.substring(0, 8)}…
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile(sha.blob.substring(0, 8)),
|
||||
)
|
||||
},
|
||||
slide("Commits, Trees, Blobs")(
|
||||
s"""Fügen wir ein paar weitere Dateien und einen Unterordner hinzu
|
||||
|""".par,
|
||||
---,
|
||||
sh("mkdir folder"),
|
||||
sh("echo 'My other file' > other.txt"),
|
||||
sh("echo 'File in folder' > folder/file.txt"),
|
||||
sh("git add other.txt folder"),
|
||||
sh("git commit -m 'Added more files'"),
|
||||
),
|
||||
/* when this slide is reached, our commit hashes have changed. All the previous slides will still display the old ones */
|
||||
gitHashes useIn { sha =>
|
||||
slide("Commits, Trees, Blobs")(
|
||||
s"""Wie sieht der neue Commit aus?
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile("HEAD"),
|
||||
---,
|
||||
PauseKey,
|
||||
s"""Wir haben eine neue Art von Eintrag:
|
||||
| `parent ${sha.parent}`
|
||||
|
|
||||
|Dieser verweist auf den vorherigen Commit.
|
||||
|Durch solche Verweise bilden alle Commits einen gerichteten Graphen.
|
||||
|""".par,
|
||||
)
|
||||
},
|
||||
gitHashes useIn { sha =>
|
||||
slide("Commits, Trees, Blobs")(
|
||||
s"""Sehen wir uns den neuen Tree an:
|
||||
|""".par,
|
||||
gitCatFile("HEAD^{tree}"),
|
||||
---,
|
||||
s"""Eine unserer neuen Dateien ist zu sehen.
|
||||
|Auch `hello.txt` ist noch da, mit selbem Hash wie vorher (da nicht geändert)
|
||||
|Die andere neue Datei ist im Verzeichnis `folder`, und daher im `tree` mit dem Hash `${sha.subtree
|
||||
.substring(0, 8)}…` :
|
||||
|""".par,
|
||||
gitCatFile(sha.subtree.substring(0, 8)),
|
||||
)
|
||||
},
|
||||
slide("Git als Graph")(
|
||||
Image(
|
||||
imgs / "repo-graph.png",
|
||||
Some(ImageSize(width = 1, height = imageHeight, keepAspect = true)),
|
||||
),
|
||||
),
|
||||
slide("Dateien editieren")(
|
||||
s"""Auch Änderungen an schon getrackten Dateien müssen mit `git add` zum Repo hinzugefügt werden.
|
||||
|Erlaubt es, nur Teile der Änderungen zu committen und dadurch commits zu unterteilen.
|
||||
|Ändern wir ein paar Dateien:
|
||||
|""".par,
|
||||
PauseKey,
|
||||
Group(
|
||||
interactive("vim", "hello.txt").replaceIf(noInteractiveCommands)(
|
||||
sh("echo 'New line' >> hello.txt"),
|
||||
),
|
||||
interactive("vim", "folder/file.txt").replaceIf(noInteractiveCommands)(
|
||||
sh("echo 'New line' >> folder/file.txt"),
|
||||
),
|
||||
gitStatus,
|
||||
),
|
||||
),
|
||||
slide("Dateien editieren")(
|
||||
sh("git add folder/file.txt; git status"),
|
||||
---,
|
||||
s"""`git status` zeigt, welche Änderungen in den nächsten Commit aufgenommen werden.
|
||||
|Solche bezeichnet man als "staged" oder "im Stagingbereich".
|
||||
|Auch zu sehen: Befehle um diese zu modifizieren (`git add` / `git restore --staged`).
|
||||
|`git restore <file>` (ohne `--staged`) verwirft Änderungen → Nicht wieder herstellbar!
|
||||
|""".par,
|
||||
),
|
||||
slide("Dateien editieren")(
|
||||
sh("git commit -m \"Modified file in folder\""),
|
||||
sh("git add hello.txt; git commit -m \"Extended greeting\""),
|
||||
gitStatus,
|
||||
),
|
||||
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 <path>".yellow}: Datei zu Repo hinzufügen / Änderungen in Datei für Commit markieren / \"stagen\"",
|
||||
s"${"git restore --staged <path>".yellow}: Änderungen aus Staging wieder herausnehmen",
|
||||
s"${"git restore <path>".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("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"),
|
||||
---,
|
||||
prepare { %%%("git", "branch", "--show-current").trim } useIn { branch =>
|
||||
s"Aktuell sind wir auf dem Branch `$branch` und es gibt keine weiteren Branches\n".par
|
||||
},
|
||||
PauseKey,
|
||||
s"""Neuen Branch anlegen mit `git branch <name>`
|
||||
|Alternativ: Branch anlegen und direkt auf diesen wechseln mit `git switch -c <name>`
|
||||
|""".par,
|
||||
---,
|
||||
sh("git switch -c feature/foo"),
|
||||
sh("git branch"),
|
||||
),
|
||||
slide("Branches")(
|
||||
"Machen wir ein paar Änderungen und committen sie:\n".par,
|
||||
---,
|
||||
sh(
|
||||
"echo 'a new line' >> hello.txt",
|
||||
) display "echo 'a new line' >> hello.txt # appends to hello.txt",
|
||||
sh("git add hello.txt"),
|
||||
sh("git commit -m \"Added line to hello\""),
|
||||
---,
|
||||
"Da der Branch `feature/foo` aktiv war, zeigt dieser auf den neuen Commit. `master` wird nicht geändert:".code.text,
|
||||
PauseKey,
|
||||
sh(
|
||||
"git log --graph --oneline --all --decorate=short --color=always",
|
||||
) display "git log --graph --oneline --all",
|
||||
),
|
||||
slide("Branches")(
|
||||
"Jetzt wechseln wir zurück zu `master`:\n".par,
|
||||
---,
|
||||
sh("git switch master"),
|
||||
sh("echo 'Also a new line' >> other.txt"),
|
||||
sh("git add other.txt"),
|
||||
sh("git commit -m \"Added line to other\""),
|
||||
gitLogGraph(),
|
||||
),
|
||||
slide("Branches")(
|
||||
"Im Prinzip haben wir einfach zwei Commits mit dem selben `parent` hinzugefügt:".code.text,
|
||||
---,
|
||||
gitCatFile("master"),
|
||||
gitCatFile("feature/foo"),
|
||||
),
|
||||
slide("Merges")(
|
||||
"""Im Normalfall möchte man mehrere Branches irgendwann wieder zusammenführen.
|
||||
|Dazu gibt es den Befehl `merge`:""".par,
|
||||
---,
|
||||
sh("git merge feature/foo"),
|
||||
gitLogGraph(2),
|
||||
),
|
||||
slide("Merge Commits")(
|
||||
"Um zwei divergierte Branches zusammenzuführen, gibt es Merge Commits mit mehreren `parent` Einträgen:".code.text,
|
||||
---,
|
||||
gitCatFile("master"),
|
||||
),
|
||||
slide("Merge Konflikte")(
|
||||
"Was passiert, wenn wir nicht kompatible Änderungen haben? Noch mal vom Zustand vor dem Merge aus, auf `master`:".code.text,
|
||||
Silent {
|
||||
%%%("git", "update-ref", "refs/heads/master", "feature/foo~")
|
||||
},
|
||||
---,
|
||||
sh("echo 'a different line' >> hello.txt"),
|
||||
sh("git add hello.txt"),
|
||||
sh("git commit -m \"Added different line to hello\""),
|
||||
gitLogGraph(),
|
||||
),
|
||||
slide("Der Object Store")(
|
||||
"""Git speichert Objekte unter `.git/objects/<erste zwei Stellen vom Hash>/<Rest vom Hash>`
|
||||
|Objekte sind komprimiert.
|
||||
|""".par,
|
||||
sh("tree -C .git/objects/ | head -n 11") display "tree .git/objects/",
|
||||
s"...".par,
|
||||
),
|
||||
),
|
||||
meta = meta,
|
||||
)
|
||||
|
||||
/* When starting the presentation, you can pass a custom keymap to use.
|
||||
* The `runForeground` action lets you run any command, interrupting the presentation until it exits.
|
||||
* Here I bind the 'i' key to open tmux in the demo repo, for having interactive access, so I can call additional git
|
||||
* commands for questions */
|
||||
presentation.start(using
|
||||
Keymap.default ++ Map(
|
||||
Key('i') -> SlideAction.runForeground("tmux"),
|
||||
),
|
||||
)
|
||||
|
||||
/* vim:set tw=120: */
|
BIN
examples/git/img/git-logo.png
Normal file
BIN
examples/git/img/git-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
examples/git/img/repo-graph-i.png
Normal file
BIN
examples/git/img/repo-graph-i.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
BIN
examples/git/img/repo-graph.png
Normal file
BIN
examples/git/img/repo-graph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
7
examples/git/slides/01wozu.md
Normal file
7
examples/git/slides/01wozu.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
- 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
|
||||
- ...
|
18
examples/git/slides/02branches.md
Normal file
18
examples/git/slides/02branches.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
Branches sind mehrere parallele Entwicklungszweige. Wozu Branches?
|
||||
|
||||
- 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.
|
||||
|
||||
Branches sind in Git "billig":
|
||||
|
||||
- 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)
|
||||
|
16
examples/git/slides/02summary.md
Normal file
16
examples/git/slides/02summary.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
- `git init [dir]`:
|
||||
neues Repo anlegen
|
||||
- `git status`:
|
||||
Aktuellen Zustand zeigen (geänderte, ungetrackte Dateien, Stagingbereich)
|
||||
- `git add <path>`:
|
||||
Datei zu Repo hinzufügen / Änderungen in Datei für Commit markieren / "stagen"
|
||||
- `git restore --staged <path>`:
|
||||
Änderungen aus Staging wieder herausnehmen
|
||||
- `git restore <path>`:
|
||||
Änderungen in Datei verwerfen (zurück zu Dateizustand aus letzten Commit)
|
||||
- `git commit [-m message]`:
|
||||
Neuen Commit mit Änderung im Stagingbereich erstellen
|
||||
- `git log`:
|
||||
Bisherige Commits anzeigen
|
||||
- (`git cat-file`: Git-Objekte im internen Format anzeigen. Braucht man im Alltag nicht)
|
||||
|
|
@ -1,374 +0,0 @@
|
|||
#!/usr/bin/env amm
|
||||
import $ivy.`de.qwertyuiop::copret:0.0.1`
|
||||
import de.qwertyuiop.copret._
|
||||
import de.qwertyuiop.copret.syntax._
|
||||
import TypedCommand.{interactive, shell => sh}, Format.figlet
|
||||
|
||||
import ammonite.ops._
|
||||
|
||||
/* Configuration */
|
||||
|
||||
/* store paths for reuse.
|
||||
* most of the commands in this presentation will be git commands in a demo repository, so the path to that repo is set
|
||||
* as implicit (will be the working directory of executed commands) */
|
||||
val imgs = pwd/"img"
|
||||
implicit val repoDir = pwd/"demoRepo"
|
||||
implicit val theme = Theme.default
|
||||
|
||||
/* You can define any variables and use then in your presentation.
|
||||
* The presentation is pure Scala code, you can use anything that Scala offers . */
|
||||
val debug = true
|
||||
|
||||
/* image height is defined by the iterm2 image protocol as follows:
|
||||
* N: N character cells.
|
||||
* Npx: N pixels.
|
||||
* N%: N percent of the session's width or height.
|
||||
* auto: The image's inherent size will be used to determine an appropriate dimension.
|
||||
*
|
||||
* The `Image` slide element will default to 100% height and width (preserving aspect ratio). Here the maximum height
|
||||
* is calculated for slides with the default header (1 row status, 3 rows title font, 3 rows spacing)
|
||||
*/
|
||||
val imageHeight = (rows - 7).toString
|
||||
|
||||
/* Make sure, the demo repo is not there in the beginning. We'll create it in the presentation */
|
||||
rm! repoDir
|
||||
|
||||
/* the basic building block of copret presentations is the various case classes of the `Slide` trait */
|
||||
val titleSlide =
|
||||
Group( // `Group` combines several `Slide` objects into one
|
||||
Clear, // `Clear` clears the screen, so the next slide element starts at the top
|
||||
Image(imgs/"git-logo.png", width = "10%"), // `Image` currently requires a terminal supporting `imgcat`
|
||||
Paragraph( // `Paragraph` simply prints its text contents
|
||||
s"""${figlet("git", "roman").block.blue}
|
||||
|
|
||||
|${"Wie man es benutzt und was in .git eigentlich passiert".centered.blue}
|
||||
|
|
||||
|${"-- Alexander Gehrke".right(10)}
|
||||
|""".stripMargin)
|
||||
// Above you see some of the extension methods for strings, `block` for centering multiline text, `centered` for
|
||||
// single lines, `right(10)` for placing text 10 spaces away from the right border and `blue` for coloring.
|
||||
// also, `figlet` calls the external figlet command to render ASCII art text with the given font.
|
||||
)
|
||||
|
||||
/* 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(
|
||||
figlet(title1, font).block.green ++ figlet(title2, font).block.green + "\n" + subtitle.right(10).green
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val parentSHApattern = raw"parent ([a-f0-9]{40})".r
|
||||
val treeSHApattern = raw"tree ([a-f0-9]{40})".r
|
||||
val blobSHApattern = raw"blob ([a-f0-9]{40})".r
|
||||
|
||||
/* With `prepare` you can define variables for a slide, that are computed when the presentation reaches the
|
||||
* slide using it. This is useful for running external commands when a specific slide is reached and using their
|
||||
* output (if you'd include them in the string interpolation directly, they'd run before starting the presentation).
|
||||
*
|
||||
* The code given to `prepare` is run exactly once *per slide using it*. See the usages of `gitHashes` below for
|
||||
* details. */
|
||||
case class GitHashes(commit: String, tree: String, blob: String, parent: String, subtree: String)
|
||||
val gitHashes = prepare {
|
||||
val commitSHA = %%%("git", "show-ref").substring(0,40)
|
||||
val commit = %%%("git", "cat-file", "-p", "HEAD")
|
||||
val treeSHA = treeSHApattern.findFirstMatchIn(commit).map(_.group(1)).getOrElse("")
|
||||
val parentSHA = parentSHApattern.findFirstMatchIn(commit).map(_.group(1)).getOrElse("")
|
||||
val tree = %%%("git", "cat-file", "-p", treeSHA)
|
||||
val blobSHA = blobSHApattern.findFirstMatchIn(tree).map(_.group(1)).getOrElse("")
|
||||
val subtreeSHA = treeSHApattern.findFirstMatchIn(tree).map(_.group(1)).getOrElse("")
|
||||
GitHashes(commitSHA, treeSHA, blobSHA, parentSHA, subtreeSHA)
|
||||
}
|
||||
|
||||
def gitStatus = TypedCommand("git", "status")
|
||||
def gitCatFile(ref: String) = TypedCommand("git", "cat-file", "-p", ref)
|
||||
|
||||
def gitLogGraph(cutOff: Int = 0) = {
|
||||
val shownCmd = "git log --graph --oneline --all"
|
||||
val execCmd = shownCmd + " --decorate=short --color=always " + (if(cutOff > 0) "| head -n -" + cutOff else "")
|
||||
val typer = sh(execCmd) display shownCmd
|
||||
if(cutOff > 0)
|
||||
Group(typer, "...".text)
|
||||
else
|
||||
typer
|
||||
}
|
||||
|
||||
/* A `Presentation` object can include a map of metadata. Which keys are used, depends on your used templates.
|
||||
* To use them in a slide yourself (or for creating own templates), use the Meta slide class, which can access the
|
||||
* presentation object and state to generate a slide.
|
||||
*
|
||||
* The only keys used in the packaged templates are currently "author" and "title" in the `header` template */
|
||||
val meta = Map(
|
||||
"author" -> "Alexander Gehrke",
|
||||
"title" -> "Git"
|
||||
)
|
||||
|
||||
/* A presentation consists of a `Vector[Slide]`. After each slide in this vector, the presentation pauses and waits for
|
||||
* input. `Group` is used, to display several slides at once, and `PauseKey` can be used inside a `Group` to wait for
|
||||
* input (but limited to displaying the rest of the `Group`, normal navigation is only possible between top level
|
||||
* slides).
|
||||
*
|
||||
* Navigation distinguishes between normal and "quick" steps, the latter disables things like pauses or animations. It
|
||||
* is also possible to jump to a specific slide.
|
||||
* */
|
||||
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"),
|
||||
chapter("Basics", "Grundlegende Befehle"),
|
||||
/* `slide` is a template for a `Group` with an optional title, that clears the screen */
|
||||
slide("Basics")(
|
||||
"""Git trackt Inhalt von Dateien innerhalb eines Repositories.
|
||||
|Kann in neuem oder bestehenden Ordner angelegt werden
|
||||
|""".par,
|
||||
/* the `---` template displays a yellow line */
|
||||
---,
|
||||
/* `Pause` waits for the given number of milliseconds */
|
||||
Pause(500),
|
||||
/* `TypedCommand.shell` (here aliased to `sh`) displays a typing animation of that command and then executes it,
|
||||
* showing its output in the presentation */
|
||||
sh("git init demoRepo")(pwd),
|
||||
/* sometimes it's useful to display something else than is actually executed, e.g. to add comments, or to hide
|
||||
* options only required because we don't call from an interactive shell (stuff like --color) */
|
||||
sh("ls -1a --color=always demoRepo")(pwd) display "ls -1a demoRepo",
|
||||
/* If you need to run commands that should not display anything in the presentation, use `Silent`.
|
||||
* Here I use it to prepare the repository, but it could also be used e.g. for playing a sound or opening a video.*/
|
||||
Silent {
|
||||
%%%("git", "config", "user.email", "crater2150@example.com")
|
||||
%%%("git", "config", "user.name", "crater2150")
|
||||
%%%("git", "config", "color.status", "always")
|
||||
%%%("git", "config", "color.ui", "always")
|
||||
}
|
||||
),
|
||||
slide("Basics")(
|
||||
"Noch ist das Repo leer. Ändern wir das:".par,
|
||||
---,
|
||||
Pause(500),
|
||||
TypedCommand.fake("cd demoRepo"),
|
||||
sh("echo 'Hello, World!' > hello.txt"),
|
||||
Pause(500),
|
||||
sh("ls -1a")
|
||||
),
|
||||
slide("Basics")(
|
||||
"Den Zustand des Repos sehen wir mit `git status`\n".par,
|
||||
---,
|
||||
Pause(500),
|
||||
gitStatus,
|
||||
),
|
||||
slide("Basics")(
|
||||
"Damit Git Dateien trackt, müssen diese mit `git add` hinzugefügt werden.\n".par,
|
||||
---,
|
||||
sh("git add hello.txt"),
|
||||
gitStatus,
|
||||
),
|
||||
slide("Basics")(
|
||||
"""Die gespeicherten Zustände eines Repos nennt man Commit.
|
||||
|Zu jedem Commit gibt man eine Zusammenfassung der Änderungen an
|
||||
|""".par,
|
||||
---,
|
||||
Pause(500),
|
||||
sh("git commit --message \"Added first file\""),
|
||||
PauseKey,
|
||||
---,
|
||||
"""Commits werden über ihren SHA1-Hash identifiziert. Dieser wird ggf. abgekürzt.
|
||||
|Mit `git log` können wir uns die bisherigen Commits ansehen:
|
||||
|""".par,
|
||||
---,
|
||||
sh("git log")
|
||||
),
|
||||
/* here we see `gitHashes`, created with `prepare`, in action:
|
||||
* As the code is run exactly once per `useIn` call i.e. per using slide, the hashes of the latest commit, tree and
|
||||
* parent in the demo repo, that `gitHashes` retrieves, are always the ones for the repository state matching the
|
||||
* slide. The prepared code is run and its result cached, when the slide is first displayed (or skipped over).
|
||||
*/
|
||||
gitHashes useIn { sha => slide("Was passiert bei einem Commit?")(
|
||||
s"""Schauen wir uns an, was Git aus dem Commit gemacht hat.
|
||||
|`git cat-file $$ref` zeigt Inhalt von Git-Objekten. `$$ref` ist Referenz des Objekts, z.B. der Hash.
|
||||
|Unser Commit hatte den Hash `${sha.commit}`.
|
||||
|Statt diesem geht auch `HEAD` = aktueller Commit.
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile("HEAD"),
|
||||
PauseKey,
|
||||
---,
|
||||
s"""Zum Hashen wird vor den Inhalt noch `<Objekttyp> <Länge des Inhalts in bytes\\0` gehängt.
|
||||
|Hier: `Objekttyp == commit`
|
||||
|""".par,
|
||||
)},
|
||||
gitHashes useIn { sha => slide("Commits, Trees, Blobs")(
|
||||
s"""Neben den Metadaten ist in unserem Commit noch eine Zeile
|
||||
| `tree ${sha.tree}`
|
||||
|
|
||||
|Trees repräsentieren Verzeichnisse, Tree in einem Commit = Wurzelverzeichnis des Repos
|
||||
|`$$ref^{tree}` = Baum mit Referenz `$$ref`, oder wenn `$$ref` ein Commit ist, der Tree aus diesem
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile("HEAD^{tree}"),
|
||||
PauseKey,
|
||||
---,
|
||||
s"""Hier ist noch nicht viel zu sehen, da wir nur eine Datei haben.
|
||||
|Einträge im Tree haben das Format:
|
||||
| `<berechtigungen> <blob | tree> <sha1 hash> <dateiname>`
|
||||
|""".par
|
||||
)},
|
||||
gitHashes useIn { sha => slide("Commits, Trees, Blobs")(
|
||||
s"""Blobs sind die eigentlichen Dateiinhalte.
|
||||
|Unsere `hello.txt` hat, wie im Tree zu sehen, den Hash ${sha.blob.substring(0,8)}…
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile(sha.blob.substring(0,8)),
|
||||
)},
|
||||
slide("Commits, Trees, Blobs")(
|
||||
s"""Fügen wir ein paar weitere Dateien und einen Unterordner hinzu
|
||||
|""".par,
|
||||
---,
|
||||
sh("mkdir folder"),
|
||||
sh("echo 'My other file' > other.txt"),
|
||||
sh("echo 'File in folder' > folder/file.txt"),
|
||||
sh("git add other.txt folder"),
|
||||
sh("git commit -m 'Added more files'")
|
||||
),
|
||||
/* when this slide is reached, our commit hashes have changed. All the previous slides will still display the old ones */
|
||||
gitHashes useIn { sha => slide("Commits, Trees, Blobs")(
|
||||
s"""Wie sieht der neue Commit aus?
|
||||
|""".par,
|
||||
---,
|
||||
gitCatFile("HEAD"),
|
||||
---,
|
||||
PauseKey,
|
||||
s"""Wir haben eine neue Art von Eintrag:
|
||||
| `parent ${sha.parent}`
|
||||
|
|
||||
|Dieser verweist auf den vorherigen Commit.
|
||||
|Durch solche Verweise bilden alle Commits einen gerichteten Graphen.
|
||||
|""".par
|
||||
)},
|
||||
gitHashes useIn { sha => slide("Commits, Trees, Blobs")(
|
||||
s"""Sehen wir uns den neuen Tree an:
|
||||
|""".par,
|
||||
gitCatFile("HEAD^{tree}"),
|
||||
---,
|
||||
s"""Eine unserer neuen Dateien ist zu sehen.
|
||||
|Auch `hello.txt` ist noch da, mit selbem Hash wie vorher (da nicht geändert)
|
||||
|Die andere neue Datei ist im Verzeichnis `folder`, und daher im `tree` mit dem Hash `${sha.subtree.substring(0,8)}…` :
|
||||
|""".par,
|
||||
gitCatFile(sha.subtree.substring(0,8)),
|
||||
)},
|
||||
slide("Git als Graph")(Image(imgs/"repo-graph.png", height=imageHeight)),
|
||||
slide("Dateien editieren")(
|
||||
s"""Auch Änderungen an schon getrackten Dateien müssen mit `git add` zum Repo hinzugefügt werden.
|
||||
|Erlaubt es, nur Teile der Änderungen zu committen und dadurch commits zu unterteilen.
|
||||
|Ändern wir ein paar Dateien:
|
||||
|""".par,
|
||||
PauseKey,
|
||||
Group(
|
||||
interactive("vim", "hello.txt").replaceIf(debug)(sh("echo 'New line' >> hello.txt")),
|
||||
interactive("vim", "folder/file.txt").replaceIf(debug)(sh("echo 'New line' >> folder/file.txt")),
|
||||
gitStatus,
|
||||
),
|
||||
),
|
||||
slide("Dateien editieren")(
|
||||
sh("git add folder/file.txt; git status"),
|
||||
---,
|
||||
s"""`git status` zeigt, welche Änderungen in den nächsten Commit aufgenommen werden.
|
||||
|Solche bezeichnet man als "staged" oder "im Stagingbereich".
|
||||
|Auch zu sehen: Befehle um diese zu modifizieren (`git add` / `git restore --staged`).
|
||||
|`git restore <file>` (ohne `--staged`) verwirft Änderungen → Nicht wieder herstellbar!
|
||||
|""".par,
|
||||
),
|
||||
slide("Dateien editieren")(
|
||||
sh("git commit -m \"Modified file in folder\""),
|
||||
sh("git add hello.txt; git commit -m \"Extended greeting\""),
|
||||
gitStatus,
|
||||
),
|
||||
markdown("Was bisher geschah", pwd/"slides"/"02summary.md"),
|
||||
chapter("Branches", "Grundlegende Befehle"),
|
||||
slide("\n".par, IncludeMarkdown(pwd/"slides"/"02branches.md")),
|
||||
slide("Branches")(
|
||||
sh("git branch"),
|
||||
---,
|
||||
prepare { %%%("git", "branch", "--show-current").trim } useIn { branch =>
|
||||
s"Aktuell sind wir auf dem Branch `$branch` und es gibt keine weiteren Branches\n".par
|
||||
},
|
||||
PauseKey,
|
||||
s"""Neuen Branch anlegen mit `git branch <name>`
|
||||
|Alternativ: Branch anlegen und direkt auf diesen wechseln mit `git switch -c <name>`
|
||||
|""".par,
|
||||
---,
|
||||
sh("git switch -c feature/foo"),
|
||||
sh("git branch"),
|
||||
),
|
||||
slide("Branches")(
|
||||
"Machen wir ein paar Änderungen und committen sie:\n".par,
|
||||
---,
|
||||
sh("echo 'a new line' >> hello.txt") display "echo 'a new line' >> hello.txt # appends to hello.txt",
|
||||
sh("git add hello.txt"),
|
||||
sh("git commit -m \"Added line to hello\""),
|
||||
---,
|
||||
"Da der Branch `feature/foo` aktiv war, zeigt dieser auf den neuen Commit. `master` wird nicht geändert:".code.text,
|
||||
PauseKey,
|
||||
sh("git log --graph --oneline --all --decorate=short --color=always") display "git log --graph --oneline --all",
|
||||
),
|
||||
slide("Branches")(
|
||||
"Jetzt wechseln wir zurück zu `master`:\n".par,
|
||||
---,
|
||||
sh("git switch master"),
|
||||
sh("echo 'Also a new line' >> other.txt"),
|
||||
sh("git add other.txt"),
|
||||
sh("git commit -m \"Added line to other\""),
|
||||
gitLogGraph(),
|
||||
),
|
||||
slide("Branches")(
|
||||
"Im Prinzip haben wir einfach zwei Commits mit dem selben `parent` hinzugefügt:".code.text,
|
||||
---,
|
||||
gitCatFile("master"),
|
||||
gitCatFile("feature/foo"),
|
||||
),
|
||||
slide("Merges")(
|
||||
"""Im Normalfall möchte man mehrere Branches irgendwann wieder zusammenführen.
|
||||
|Dazu gibt es den Befehl `merge`:""".par,
|
||||
---,
|
||||
sh("git merge feature/foo"),
|
||||
gitLogGraph(2),
|
||||
),
|
||||
slide("Merge Commits")(
|
||||
"Um zwei divergierte Branches zusammenzuführen, gibt es Merge Commits mit mehreren `parent` Einträgen:".code.text,
|
||||
---,
|
||||
gitCatFile("master"),
|
||||
),
|
||||
slide("Merge Konflikte")(
|
||||
"Was passiert, wenn wir nicht kompatible Änderungen haben? Noch mal vom Zustand vor dem Merge aus, auf `master`:".code.text,
|
||||
Silent {
|
||||
%%%("git", "update-ref", "refs/heads/master", "feature/foo~")
|
||||
},
|
||||
---,
|
||||
sh("echo 'a different line' >> hello.txt"),
|
||||
sh("git add hello.txt"),
|
||||
sh("git commit -m \"Added different line to hello\""),
|
||||
gitLogGraph(),
|
||||
),
|
||||
slide("Der Object Store")(
|
||||
"""Git speichert Objekte unter `.git/objects/<erste zwei Stellen vom Hash>/<Rest vom Hash>`
|
||||
|Objekte sind komprimiert.
|
||||
|""".par,
|
||||
sh("tree -C .git/objects/ | head -n 11") display "tree .git/objects/",
|
||||
s"...".par,
|
||||
),
|
||||
), meta=meta)
|
||||
|
||||
/* When starting the presentation, you can pass a custom keymap to use.
|
||||
* The `runForeground` action lets you run any command, interrupting the presentation until it exits.
|
||||
* Here I bind the 'i' key to open tmux in the demo repo, for having interactive access, so I can call additional git
|
||||
* commands for questions */
|
||||
presentation.start(Keymap.default ++ Map(
|
||||
Key('i') -> SlideAction.runForeground("tmux"),
|
||||
))
|
||||
|
||||
|
||||
/* vim:set tw=120: */
|
14
examples/query.sc
Executable file
14
examples/query.sc
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env -S scala-cli shebang
|
||||
//>using dep de.qwertyuiop::copret:0.0.2
|
||||
import de.qwertyuiop.copret.*
|
||||
import Terminal.*
|
||||
|
||||
enterRawMode()
|
||||
|
||||
val hasGraphics =
|
||||
queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
||||
.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}")
|
||||
|
||||
val size = queryTerm(s"${csi}14t")
|
||||
|
||||
println(s"Has graphics: $hasGraphics")
|
36
examples/termescape/figlet-pagga-block
Executable file
36
examples/termescape/figlet-pagga-block
Executable file
|
@ -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
|
BIN
examples/termescape/img/csi.png
Normal file
BIN
examples/termescape/img/csi.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
105
examples/termescape/img/csi.svg
Normal file
105
examples/termescape/img/csi.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 562 KiB |
BIN
examples/termescape/img/earth.gif
Normal file
BIN
examples/termescape/img/earth.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 978 KiB |
529
examples/termescape/terminal-presentation.sc
Executable file
529
examples/termescape/terminal-presentation.sc
Executable file
|
@ -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)} ${"<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"),
|
||||
),
|
||||
)
|
8
examples/termescape/title.zsh
Executable file
8
examples/termescape/title.zsh
Executable file
|
@ -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'
|
48
mill
48
mill
|
@ -1,48 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# This is a wrapper script, that automatically download mill from GitHub release pages
|
||||
# You can give the required mill version with MILL_VERSION env variable
|
||||
# If no version is given, it falls back to the value of DEFAULT_MILL_VERSION
|
||||
DEFAULT_MILL_VERSION=0.9.5
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$MILL_VERSION" ] ; then
|
||||
if [ -f ".mill-version" ] ; then
|
||||
MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)"
|
||||
elif [ -f "mill" ] && [ "$BASH_SOURCE" != "mill" ] ; then
|
||||
MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2)
|
||||
else
|
||||
MILL_VERSION=$DEFAULT_MILL_VERSION
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "x${XDG_CACHE_HOME}" != "x" ] ; then
|
||||
MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download"
|
||||
else
|
||||
MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download"
|
||||
fi
|
||||
MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}"
|
||||
|
||||
version_remainder="$MILL_VERSION"
|
||||
MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}"
|
||||
MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}"
|
||||
|
||||
if [ ! -x "$MILL_EXEC_PATH" ] ; then
|
||||
mkdir -p $MILL_DOWNLOAD_PATH
|
||||
if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then
|
||||
ASSEMBLY="-assembly"
|
||||
fi
|
||||
DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download
|
||||
MILL_DOWNLOAD_URL="https://github.com/lihaoyi/mill/releases/download/${MILL_VERSION%%-*}/$MILL_VERSION${ASSEMBLY}"
|
||||
curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL"
|
||||
chmod +x "$DOWNLOAD_FILE"
|
||||
mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH"
|
||||
unset DOWNLOAD_FILE
|
||||
unset MILL_DOWNLOAD_URL
|
||||
fi
|
||||
|
||||
unset MILL_DOWNLOAD_PATH
|
||||
unset MILL_VERSION
|
||||
|
||||
exec $MILL_EXEC_PATH "$@"
|
Loading…
Reference in a new issue