Large refactor and cleanup, Kitty graphics WIP
This commit is contained in:
parent
3b7afa3d90
commit
3b2d97d45a
31
build.sc
31
build.sc
|
@ -1,31 +0,0 @@
|
||||||
import mill._, scalalib._, publish._
|
|
||||||
import $ivy.`com.lihaoyi::mill-contrib-bloop:0.9.5`
|
|
||||||
|
|
||||||
object copret extends ScalaModule with PublishModule {
|
|
||||||
def scalaVersion = "3.1.0"
|
|
||||||
override def ammoniteVersion = "2.5.3"
|
|
||||||
|
|
||||||
|
|
||||||
def publishVersion = "0.0.2-SNAPSHOT"
|
|
||||||
def pomSettings = PomSettings(
|
|
||||||
description = "Use ammonite scripts for command line presentations",
|
|
||||||
organization = "de.qwertyuiop",
|
|
||||||
versionControl = VersionControl(
|
|
||||||
browsableRepository = Some("https://git.qwertyuiop.de/crater2150/copret"),
|
|
||||||
connection = Some(VersionControlConnection.gitHttps("git.qwertyuiop.de", "crater2150/copret.git")),
|
|
||||||
developerConnection = Some(VersionControlConnection.gitSsh(
|
|
||||||
"qwertyuiop.de", "crater2150/copret.git", username = Some("git")
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
url = "https://qwertyuiop.de/copret/",
|
|
||||||
licenses = Seq(License.MIT),
|
|
||||||
developers = Seq(Developer("crater2150", "Alexander Gehrke", "https://git.qwertyuiop.de/crater2150"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def ivyDeps = Agg(
|
|
||||||
ivy"org.jline:jline:3.19.0",
|
|
||||||
ivy"com.lihaoyi::ammonite-ops:2.3.8".withDottyCompat(scalaVersion()),
|
|
||||||
ivy"com.lihaoyi::fansi:0.2.14",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
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
|
10
copret/project.scala
Normal file
10
copret/project.scala
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
//> 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.10.0
|
||||||
|
//> using dep org.jline:jline:3.26.1
|
||||||
|
//> using dep com.lihaoyi::os-lib:0.10.0
|
||||||
|
//> using dep com.lihaoyi::fansi:0.5.0
|
|
@ -1,6 +1,8 @@
|
||||||
package de.qwertyuiop.copret
|
package de.qwertyuiop.copret
|
||||||
import de.qwertyuiop.copret.syntax._
|
import de.qwertyuiop.copret.syntax._
|
||||||
import ammonite.ops.{%%, pwd}
|
|
||||||
|
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]):
|
case class Theme(styles: Map[String, fansi.Attrs], figletFonts: Map[String, String]):
|
||||||
def style(key: String, default: fansi.Attrs = fansi.Attrs()) =
|
def style(key: String, default: fansi.Attrs = fansi.Attrs()) =
|
||||||
|
@ -28,7 +30,7 @@ object Format:
|
||||||
|
|
||||||
def center(str: String) = " " * ((columns - str.length) / 2) + str
|
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 centerLines(str: String) = str.split("\n").map(center).mkString("\n")
|
||||||
def centerBlock(str: String) =
|
def centerBlock(str: String) =
|
||||||
|
|
139
copret/src/images.scala
Normal file
139
copret/src/images.scala
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package de.qwertyuiop.copret
|
||||||
|
|
||||||
|
import Terminal.*
|
||||||
|
|
||||||
|
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 showImage(img: os.Path) =
|
||||||
|
import java.util.Base64
|
||||||
|
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||||
|
if image.length > 4096 then
|
||||||
|
val chunks = image.grouped(4096).toVector
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
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,6 +1,6 @@
|
||||||
package de.qwertyuiop.copret
|
package de.qwertyuiop.copret
|
||||||
|
|
||||||
import ammonite.ops.Path
|
import os.Path
|
||||||
|
|
||||||
enum SlideAction:
|
enum SlideAction:
|
||||||
case Start
|
case Start
|
||||||
|
@ -23,26 +23,34 @@ enum SlideAction:
|
||||||
case QuickNext => "next slide (skip animations)"
|
case QuickNext => "next slide (skip animations)"
|
||||||
case Quit => "quit"
|
case Quit => "quit"
|
||||||
case Help => "show help"
|
case Help => "show help"
|
||||||
case Interactive(cmd: Vector[String], wd: Path) => s"execute command \"${cmd.mkString(" ")}\""
|
case Interactive(cmd: Vector[String], wd: Path) =>
|
||||||
|
s"execute command \"${cmd.mkString(" ")}\""
|
||||||
case Other(code: List[Int]) => s"Unknown key sequence: $code"
|
case Other(code: List[Int]) => s"Unknown key sequence: $code"
|
||||||
|
|
||||||
|
|
||||||
object SlideAction:
|
object SlideAction:
|
||||||
def runForeground(cmd: String*)(implicit wd: Path) = Interactive(cmd.toVector, wd)
|
def runForeground(cmd: String*)(implicit wd: Path) =
|
||||||
|
Interactive(cmd.toVector, wd)
|
||||||
|
|
||||||
import SlideAction.*
|
import SlideAction.*
|
||||||
|
|
||||||
case class Keymap(bindings: Map[Key, SlideAction]):
|
case class Keymap(bindings: Map[Key, SlideAction]):
|
||||||
private val lookup = bindings.map((k, v) => k.codes -> v)
|
private val lookup = bindings.map((k, v) => k.codes -> v)
|
||||||
def apply(keycode: List[Int]): SlideAction = lookup.getOrElse(keycode, Other(keycode))
|
def apply(keycode: List[Int]): SlideAction =
|
||||||
|
lookup.getOrElse(keycode, Other(keycode))
|
||||||
|
|
||||||
def extend(newBindings: Map[Key, SlideAction]) = Keymap(bindings ++ newBindings)
|
def extend(newBindings: Map[Key, SlideAction]) = Keymap(
|
||||||
|
bindings ++ newBindings
|
||||||
|
)
|
||||||
def ++(newBindings: Map[Key, SlideAction]) = extend(newBindings)
|
def ++(newBindings: Map[Key, SlideAction]) = extend(newBindings)
|
||||||
def help: String =
|
def help: String =
|
||||||
bindings.toSeq.sortBy(_._2.toString).map((k,v) => k.show.padTo(8, ' ') + " " + v.show).mkString("\n")
|
bindings.toSeq
|
||||||
|
.sortBy(_._2.toString)
|
||||||
|
.map((k, v) => k.show.padTo(8, ' ') + " " + v.show)
|
||||||
|
.mkString("\n")
|
||||||
|
|
||||||
object Keymap:
|
object Keymap:
|
||||||
val empty = Keymap(Map())
|
val empty = Keymap(Map())
|
||||||
|
// format: off
|
||||||
val default = Keymap(Map(
|
val default = Keymap(Map(
|
||||||
Key.Up -> Prev,
|
Key.Up -> Prev,
|
||||||
Key.Left -> Prev,
|
Key.Left -> Prev,
|
||||||
|
@ -59,6 +67,7 @@ object Keymap:
|
||||||
Key('s') -> GotoSelect,
|
Key('s') -> GotoSelect,
|
||||||
Key('?') -> Help,
|
Key('?') -> Help,
|
||||||
))
|
))
|
||||||
|
//format: on
|
||||||
|
|
||||||
enum Key:
|
enum Key:
|
||||||
case Code(name: String, codepoints: List[Int])
|
case Code(name: String, codepoints: List[Int])
|
||||||
|
|
16
copret/src/main.scala
Normal file
16
copret/src/main.scala
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package de.qwertyuiop.copret
|
||||||
|
|
||||||
|
import de.qwertyuiop.copret.Terminal.*
|
||||||
|
|
||||||
|
@main def main =
|
||||||
|
enterRawMode()
|
||||||
|
// print(queryTerm(s"${apc}Gi=31,s=10,v=2,t=s;L3NvbWUtc2hhcmVkLW1lbW9yeS1uYW1lCg==\u001b\\"))
|
||||||
|
// print(queryTerm(s"\u001b[6n"))
|
||||||
|
val canHazGraphics = queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
||||||
|
println(canHazGraphics.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}"))
|
||||||
|
// println(
|
||||||
|
// KittyGraphicsProtocol.showImage(
|
||||||
|
// os.Path("/home/crater2150/org/opencolloq/gittalk/img/repo-graph.png"),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// println(getSize)
|
|
@ -1,103 +1,121 @@
|
||||||
package de.qwertyuiop.copret
|
package de.qwertyuiop.copret
|
||||||
import ammonite.ops._
|
import os.Path
|
||||||
import Terminal._
|
|
||||||
import syntax._
|
|
||||||
|
|
||||||
case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty):
|
import Terminal.*
|
||||||
|
import syntax.*
|
||||||
|
|
||||||
|
case class Presentation(
|
||||||
|
slides: Vector[Slide],
|
||||||
|
meta: Map[String, String] = Map.empty,
|
||||||
|
):
|
||||||
def start(using keymap: Keymap = Keymap.default) =
|
def start(using keymap: Keymap = Keymap.default) =
|
||||||
Terminal.enterRawMode()
|
Terminal.enterRawMode()
|
||||||
Terminal.hideCursor()
|
Terminal.hideCursor()
|
||||||
|
Thread.sleep(1000)
|
||||||
run()
|
run()
|
||||||
|
|
||||||
import Presentation._
|
import Presentation._
|
||||||
def run()(using keymap: Keymap) =
|
def run()(using keymap: Keymap) =
|
||||||
import SlideAction.*
|
import SlideAction.*
|
||||||
@annotation.tailrec def rec(p: Presentation, pos: Int, action: SlideAction): Unit =
|
@annotation.tailrec
|
||||||
action match
|
def rec(pos: Int, action: SlideAction): Unit =
|
||||||
case Start =>
|
|
||||||
executeSlide(p, 0)()
|
inline def redraw() = rec(pos - 1, QuickNext)
|
||||||
rec(p, 0, waitkey)
|
|
||||||
case Next =>
|
inline def navigate(pos: Int, condition: Boolean, direction: Int)(
|
||||||
if pos + 1 < p.slides.size then
|
executor: Int => Slide => Unit,
|
||||||
executeSlide(p, pos + 1)()
|
) =
|
||||||
rec(p, pos + 1, waitkey)
|
if condition then
|
||||||
else
|
executor(pos + direction)(slides(pos))
|
||||||
rec(p, pos, waitkey)
|
rec(pos + direction, waitkey)
|
||||||
case QuickNext =>
|
else rec(pos, waitkey)
|
||||||
if pos + 1 < p.slides.size then
|
|
||||||
executeQuick(p, pos + 1)()
|
inline def runInteractive(cmd: Vector[String], path: Path) =
|
||||||
rec(p, pos + 1, waitkey)
|
|
||||||
else rec(p, pos, waitkey)
|
|
||||||
case Prev =>
|
|
||||||
if pos > 0 then
|
|
||||||
executeQuick(p, pos - 1)()
|
|
||||||
rec(p, pos - 1, waitkey)
|
|
||||||
else
|
|
||||||
rec(p, pos, waitkey)
|
|
||||||
case Interactive(cmd, path) =>
|
|
||||||
Terminal.showCursor()
|
Terminal.showCursor()
|
||||||
try
|
try os.proc(cmd).call(cwd = path)
|
||||||
%(cmd)(path)
|
|
||||||
catch case _ => ()
|
catch case _ => ()
|
||||||
Terminal.hideCursor()
|
Terminal.hideCursor()
|
||||||
Terminal.clear()
|
Terminal.clear()
|
||||||
rec(p, pos - 1, QuickNext)
|
redraw()
|
||||||
|
|
||||||
|
action match
|
||||||
|
case Start => navigate(0, true, 0)(executeSlide)
|
||||||
|
case Next => navigate(pos, pos + 1 < slides.size, 1)(executeSlide)
|
||||||
|
case QuickNext => navigate(pos, pos + 1 < slides.size, 1)(executeQuick)
|
||||||
|
case Prev => navigate(pos, pos > 0, -1)(executeQuick)
|
||||||
|
case Interactive(cmd, path) => runInteractive(cmd, path)
|
||||||
|
|
||||||
case Goto(target) =>
|
case Goto(target) =>
|
||||||
for i <- 0 until target
|
for i <- 0 until target
|
||||||
do executeSilent(p, i)()
|
do executeSilent(i)()
|
||||||
rec(p, target - 1, QuickNext)
|
rec(target - 1, QuickNext)
|
||||||
|
|
||||||
case GotoSelect =>
|
case GotoSelect =>
|
||||||
val maxSlide = p.slides.size - 1
|
promptSlide() match
|
||||||
val target = prompt(s"Go to slide (0 - $maxSlide):", _.toIntOption)(
|
case Some(i) => rec(pos, Goto(i))
|
||||||
(res, input) => res.filter((0 to maxSlide).contains).isEmpty && input.nonEmpty,
|
case None => redraw()
|
||||||
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 Help =>
|
case Help =>
|
||||||
Terminal.clear()
|
Terminal.clear()
|
||||||
println(keymap.help)
|
println(keymap.help)
|
||||||
waitkey
|
waitkey
|
||||||
rec(p, pos - 1, QuickNext)
|
redraw()
|
||||||
|
|
||||||
case Other(codes) =>
|
case Other(codes) =>
|
||||||
Terminal.printStatus(action.show)
|
Terminal.printStatus(action.show)
|
||||||
rec(p, pos, waitkey)
|
rec(pos, waitkey)
|
||||||
|
|
||||||
case Quit =>
|
case Quit =>
|
||||||
()
|
Terminal.showCursor()
|
||||||
rec(this, 0, Start)
|
end match
|
||||||
|
end rec
|
||||||
|
rec(0, Start)
|
||||||
|
end run
|
||||||
|
|
||||||
|
def promptSlide() =
|
||||||
|
val maxSlide = slides.size - 1
|
||||||
|
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)",
|
||||||
|
)
|
||||||
|
|
||||||
object Presentation:
|
def executeSlide(pos: Int)(
|
||||||
def executeSlide(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match
|
slide: Slide = slides(pos),
|
||||||
|
): Unit = slide match
|
||||||
case Paragraph(contents) => println(contents)
|
case Paragraph(contents) => println(contents)
|
||||||
case Clear => Terminal.clear()
|
case Clear => Terminal.clear()
|
||||||
case PauseKey => waitkey(using Keymap.empty)
|
case PauseKey => waitkey(using Keymap.empty)
|
||||||
case Pause(msec) => Thread.sleep(msec)
|
case Pause(msec) => Thread.sleep(msec)
|
||||||
case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock())
|
case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock())
|
||||||
case Image(file, None) => Terminal.showImage(file)
|
case Image(file, None) => println(KittyGraphicsProtocol.showImage(file))
|
||||||
case Image(file, Some(ImageSize(w, h, aspect))) => Terminal.showImageScaled(file, w, h, aspect)
|
case Image(file, Some(ImageSize(w, h, aspect))) =>
|
||||||
|
println(KittyGraphicsProtocol.showImage(file)) // TODO
|
||||||
case cmd: TypedCommand[_] => cmd.show()
|
case cmd: TypedCommand[_] => cmd.show()
|
||||||
case Silent(actions) => actions()
|
case Silent(actions) => actions()
|
||||||
case Group(slides) => slides.foreach(executeSlide(p, pos))
|
case Group(slides) => slides.foreach(executeSlide(pos))
|
||||||
case lios @ LazyIOSlide(_, display) => executeSlide(p, pos)(lios.genSlide())
|
case lios @ LazyIOSlide(_, display) => executeSlide(pos)(lios.genSlide())
|
||||||
case Meta(genSlide) => executeSlide(p, pos)(genSlide(p, pos))
|
case Meta(genSlide) => executeSlide(pos)(genSlide(this, pos))
|
||||||
|
|
||||||
def executeQuick(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match
|
def executeQuick(pos: Int)(
|
||||||
|
slide: Slide = slides(pos),
|
||||||
|
): Unit = slide match
|
||||||
case Pause(msec) => ()
|
case Pause(msec) => ()
|
||||||
case PauseKey => ()
|
case PauseKey => ()
|
||||||
case cmd: TypedCommand[_] => cmd.quickShow()
|
case cmd: TypedCommand[_] => cmd.quickShow()
|
||||||
case Group(slides) => slides.foreach(executeQuick(p, pos))
|
case Group(slides) => slides.foreach(executeQuick(pos))
|
||||||
case lios @ LazyIOSlide(_, display) => executeQuick(p, pos)(lios.genSlide())
|
case lios @ LazyIOSlide(_, display) => executeQuick(pos)(lios.genSlide())
|
||||||
case _ => executeSlide(p, pos)(slide)
|
case _ => executeSlide(pos)(slide)
|
||||||
|
|
||||||
def executeSilent(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match
|
def executeSilent(pos: Int)(
|
||||||
|
slide: Slide = slides(pos),
|
||||||
|
): Unit = slide match
|
||||||
case cmd: TypedCommand[_] => cmd.force()
|
case cmd: TypedCommand[_] => cmd.force()
|
||||||
case Group(slides) => slides.foreach(executeSilent(p, pos))
|
case Group(slides) => slides.foreach(executeSilent(pos))
|
||||||
case lios @ LazyIOSlide(_, display) => executeSilent(p, pos)(lios.genSlide())
|
case lios @ LazyIOSlide(_, display) =>
|
||||||
|
executeSilent(pos)(lios.genSlide())
|
||||||
case Paragraph(_) | Image(_, _) | Clear | IncludeMarkdown(_) | Meta(_) => ()
|
case Paragraph(_) | Image(_, _) | Clear | IncludeMarkdown(_) | Meta(_) => ()
|
||||||
case _ => executeQuick(p, pos)(slide)
|
case _ => executeQuick(pos)(slide)
|
||||||
|
end Presentation
|
||||||
|
|
||||||
case class ImageSize(width: Double, height: Double, keepAspect: Boolean)
|
case class ImageSize(width: Double, height: Double, keepAspect: Boolean)
|
||||||
|
|
||||||
|
@ -113,7 +131,12 @@ object Paragraph:
|
||||||
|
|
||||||
case class IncludeMarkdown(path: Path) extends Slide:
|
case class IncludeMarkdown(path: Path) extends Slide:
|
||||||
def markdownBlock() =
|
def markdownBlock() =
|
||||||
%%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, path.toString)(using ImplicitWd.implicitCwd).block
|
%%%(
|
||||||
|
"/usr/bin/mdcat",
|
||||||
|
"--columns",
|
||||||
|
(columns * 0.8).toInt.toString,
|
||||||
|
path.toString,
|
||||||
|
)(using os.pwd).block
|
||||||
|
|
||||||
case class Image(path: Path, sizing: Option[ImageSize]) extends Slide
|
case class Image(path: Path, sizing: Option[ImageSize]) extends Slide
|
||||||
object Image:
|
object Image:
|
||||||
|
@ -126,7 +149,13 @@ case class Pause(millisec: Long) extends Slide
|
||||||
case object PauseKey extends Slide
|
case object PauseKey extends Slide
|
||||||
case class Meta(contents: (Presentation, Int) => Slide) extends Slide
|
case class Meta(contents: (Presentation, Int) => Slide) extends Slide
|
||||||
|
|
||||||
case class TypedCommand[T](exec: T => String, display: String, cmd: T, cmdIsHidden: Boolean, outputIsHidden: Boolean) 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)
|
private lazy val _output = exec(cmd)
|
||||||
def output = _output
|
def output = _output
|
||||||
|
|
||||||
|
@ -138,22 +167,21 @@ case class TypedCommand[T](exec: T => String, display: String, cmd: T, cmdIsHidd
|
||||||
prompt()
|
prompt()
|
||||||
Terminal.showCursor()
|
Terminal.showCursor()
|
||||||
typeCmd()
|
typeCmd()
|
||||||
if ! outputIsHidden then
|
if !outputIsHidden then print(output)
|
||||||
print(output)
|
if !cmdIsHidden then Terminal.hideCursor()
|
||||||
if ! cmdIsHidden then
|
|
||||||
Terminal.hideCursor()
|
|
||||||
|
|
||||||
def quickShow() =
|
def quickShow() =
|
||||||
force()
|
force()
|
||||||
if !cmdIsHidden then
|
if !cmdIsHidden then
|
||||||
prompt()
|
prompt()
|
||||||
println(display)
|
println(display)
|
||||||
if ! outputIsHidden then
|
if !outputIsHidden then print(output)
|
||||||
print(output)
|
|
||||||
|
|
||||||
def prompt() = print(fansi.Color.LightGreen("user@host % "))
|
def prompt() = print(fansi.Color.LightGreen("user@host % "))
|
||||||
def force() = _output
|
def force() = _output
|
||||||
|
|
||||||
|
def display(s: String) = copy(display = s)
|
||||||
|
|
||||||
private def typeCmd() =
|
private def typeCmd() =
|
||||||
for char <- display do
|
for char <- display do
|
||||||
print(char)
|
print(char)
|
||||||
|
@ -170,19 +198,19 @@ case class TypedCommand[T](exec: T => String, display: String, cmd: T, cmdIsHidd
|
||||||
def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] =
|
def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] =
|
||||||
if condition then tc.showing(display)
|
if condition then tc.showing(display)
|
||||||
else this
|
else this
|
||||||
|
end TypedCommand
|
||||||
|
|
||||||
object TypedCommand:
|
object TypedCommand:
|
||||||
val shell = sys.env.getOrElse("SHELL", "sh")
|
val shell = sys.env.getOrElse("SHELL", "sh")
|
||||||
|
|
||||||
def run(using Path): Vector[String] => String =
|
def run(using Path): Vector[String] => String =
|
||||||
c => safe_%%(c)
|
c => runProcess(c)
|
||||||
|
|
||||||
def runShell(using Path): Vector[String] => String =
|
def runShell(using Path): Vector[String] => String =
|
||||||
c => safe_%%(Vector(shell, "-c", c.mkString(" ")))
|
c => runProcess(Vector(shell, "-c", c.mkString(" ")))
|
||||||
|
|
||||||
def runInteractive(using Path): Vector[String] => String =
|
def runInteractive(using Path): Vector[String] => String =
|
||||||
c => { %(c); ""}
|
c => { os.proc(c).call(); "" }
|
||||||
|
|
||||||
def apply(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
def apply(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||||
TypedCommand(run, cmd.mkString(" "), cmd.toVector)
|
TypedCommand(run, cmd.mkString(" "), cmd.toVector)
|
||||||
|
@ -190,7 +218,6 @@ object TypedCommand:
|
||||||
def apply[T](exec: T => String, display: String, cmd: T) =
|
def apply[T](exec: T => String, display: String, cmd: T) =
|
||||||
new TypedCommand(exec, display, cmd, false, false)
|
new TypedCommand(exec, display, cmd, false, false)
|
||||||
|
|
||||||
|
|
||||||
def shell(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
def shell(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||||
TypedCommand(runShell, cmd.mkString(" "), cmd.toVector)
|
TypedCommand(runShell, cmd.mkString(" "), cmd.toVector)
|
||||||
|
|
||||||
|
@ -200,12 +227,10 @@ object TypedCommand:
|
||||||
def interactive(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
def interactive(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||||
TypedCommand(runInteractive, cmd.mkString(" "), cmd.toVector)
|
TypedCommand(runInteractive, cmd.mkString(" "), cmd.toVector)
|
||||||
|
|
||||||
|
|
||||||
sealed abstract case class Silent[T] private (doStuff: () => T) extends Slide
|
sealed abstract case class Silent[T] private (doStuff: () => T) extends Slide
|
||||||
object Silent:
|
object Silent:
|
||||||
def apply[T](doStuff: => T) = new Silent(() => doStuff) {}
|
def apply[T](doStuff: => T) = new Silent(() => doStuff) {}
|
||||||
|
|
||||||
|
|
||||||
case class Group(slides: List[Slide]) extends Slide
|
case class Group(slides: List[Slide]) extends Slide
|
||||||
object Group:
|
object Group:
|
||||||
def apply(slides: Slide*): Group = Group(slides.toList)
|
def apply(slides: Slide*): Group = Group(slides.toList)
|
||||||
|
@ -218,7 +243,8 @@ trait SlideSyntax:
|
||||||
private[copret] class LazyIOSlideBuilder[T](runOnce: => T):
|
private[copret] class LazyIOSlideBuilder[T](runOnce: => T):
|
||||||
def useIn(display: T => Slide) = LazyIOSlide(() => runOnce, display)
|
def useIn(display: T => Slide) = LazyIOSlide(() => runOnce, display)
|
||||||
|
|
||||||
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = LazyIOSlideBuilder(runOnce)
|
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = LazyIOSlideBuilder(
|
||||||
|
runOnce,
|
||||||
|
)
|
||||||
|
|
||||||
/* vim:set tw=120: */
|
/* vim:set tw=120: */
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package de.qwertyuiop.copret
|
package de.qwertyuiop.copret
|
||||||
import syntax.*
|
import syntax.*
|
||||||
import ammonite.ops.{Path, %, %%, pwd, ImplicitWd}
|
import os.Path
|
||||||
|
|
||||||
trait Templates:
|
trait Templates:
|
||||||
def titleLine(title: String)(using theme: Theme) = Paragraph(
|
def titleLine(title: String)(using theme: Theme) = Paragraph(
|
||||||
"\n" + Format.figlet(title, theme.font("titleLine", "pagga")).block.blue + "\n"
|
"\n" + Format
|
||||||
|
.figlet(title, theme.font("titleLine", "pagga"))
|
||||||
|
.block
|
||||||
|
.blue + "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
def header(using theme: Theme) = Meta((p, pos) => {
|
def header(using theme: Theme) = Meta((p, pos) => {
|
||||||
|
@ -14,12 +17,21 @@ trait Templates:
|
||||||
theme.style("titleLine")(Format.distribute(left, center, right)).text
|
theme.style("titleLine")(Format.distribute(left, center, right)).text
|
||||||
})
|
})
|
||||||
|
|
||||||
def slide(title: String)(slides: Slide*)(using Theme) = Group(Clear :: header :: titleLine(title) :: slides.toList)
|
def slide(title: String)(slides: Slide*)(using Theme) = Group(
|
||||||
def slide(slides: Slide*)(using Theme) = Group(Clear :: header :: slides.toList)
|
Clear :: header :: titleLine(title) :: slides.toList
|
||||||
|
)
|
||||||
|
def slide(slides: Slide*)(using Theme) = Group(
|
||||||
|
Clear :: header :: slides.toList
|
||||||
|
)
|
||||||
|
|
||||||
def markdown(title: String, content: Path)(using Theme) = slide(title)(
|
def markdown(title: String, content: Path)(using Theme) = slide(title)(
|
||||||
Paragraph(
|
Paragraph(
|
||||||
%%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, content.toString)(using ImplicitWd.implicitCwd).block
|
%%%(
|
||||||
|
"/usr/bin/mdcat",
|
||||||
|
"--columns",
|
||||||
|
(columns * 0.8).toInt.toString,
|
||||||
|
content.toString
|
||||||
|
)(using os.pwd).block
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,56 @@
|
||||||
package de.qwertyuiop.copret
|
package de.qwertyuiop.copret
|
||||||
import ammonite.ops.{Path, ShelloutException, pwd, read, %, %%}
|
import os.Path
|
||||||
import org.jline.terminal.TerminalBuilder
|
import org.jline.terminal.TerminalBuilder
|
||||||
import org.jline.reader.LineReaderBuilder
|
import org.jline.reader.LineReaderBuilder
|
||||||
|
import scala.io.StdIn
|
||||||
|
import cats.*
|
||||||
|
import scala.collection.IndexedSeqView
|
||||||
|
|
||||||
object Terminal:
|
object Terminal:
|
||||||
def safe_%%(cmd: Vector[String])(using Path): String =
|
def runProcess(cmd: Vector[String])(using cwd: os.Path): String =
|
||||||
try
|
try os.proc(cmd.map(identity)).call(cwd = cwd).out.text()
|
||||||
%%(cmd).out.string
|
catch case e: os.SubprocessException => e.result.err.text()
|
||||||
catch
|
|
||||||
case e: ShelloutException => e.result.err.string
|
def tryCmd[T](cmd: => T, default: => T) =
|
||||||
|
try cmd
|
||||||
|
catch case e: os.SubprocessException => default
|
||||||
|
|
||||||
def enterRawMode(): Unit =
|
def enterRawMode(): Unit =
|
||||||
%%("sh", "-c", "stty -icanon min 1 < /dev/tty")(pwd)
|
os.proc("sh", "-c", "stty -icanon min 1 < /dev/tty")
|
||||||
%%("sh", "-c", "stty -echo < /dev/tty")(pwd)
|
.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)
|
||||||
extension (percent: Double)
|
|
||||||
def toPercent: String = s"${(percent * 100).toInt}%"
|
|
||||||
|
|
||||||
private[copret] lazy val jterm = org.jline.terminal.TerminalBuilder.terminal()
|
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 height = jterm.getSize.getRows
|
def waitkey(implicit keymap: Keymap): SlideAction =
|
||||||
def width = jterm.getSize.getColumns
|
|
||||||
|
|
||||||
def waitkey(using keymap: Keymap): SlideAction =
|
|
||||||
// ignore keypresses done during slide animations
|
// ignore keypresses done during slide animations
|
||||||
while Console.in.ready() do Console.in.read
|
while (System.in.available() > 0) System.in.read
|
||||||
|
|
||||||
var key = scala.collection.mutable.ArrayBuffer[Int]()
|
var key = scala.collection.mutable.ArrayBuffer[Int]()
|
||||||
key += Console.in.read
|
key += System.in.read
|
||||||
while Console.in.ready do
|
while (System.in.available() > 0)
|
||||||
key += Console.in.read
|
key += System.in.read
|
||||||
keymap(key.toList)
|
keymap(key.toList)
|
||||||
|
|
||||||
def printStatus(msg: String): Unit =
|
def queryTerm(query: String): String =
|
||||||
cursorTo(height, 1)
|
val ttyIn = new java.io.FileInputStream("/dev/tty")
|
||||||
print(msg)
|
val ttyOut = new java.io.PrintStream(new java.io.FileOutputStream("/dev/tty"))
|
||||||
|
|
||||||
|
ttyOut.println(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)(
|
def prompt[T](prefix: String, parse: String => T)(
|
||||||
retry: (T, String) => Boolean = (t: T, s: String) => false,
|
retry: (T, String) => Boolean = (t: T, s: String) => false,
|
||||||
error: String => String = in => s"Invalid input: $in"
|
error: String => String = in => s"Invalid input: $in",
|
||||||
): T =
|
): T =
|
||||||
val input = lineReader.readLine(prefix + " ")
|
val input = lineReader.readLine(prefix + " ")
|
||||||
val result = parse(input)
|
val result = parse(input)
|
||||||
|
@ -48,51 +59,54 @@ object Terminal:
|
||||||
prompt(prefix, parse)(retry, error)
|
prompt(prefix, parse)(retry, error)
|
||||||
else result
|
else result
|
||||||
|
|
||||||
val term = sys.env("TERM")
|
def isTmux = sys.env.contains("TMUX") || sys.env("TERM").startsWith("screen")
|
||||||
|
|
||||||
def isTmux = sys.env.contains("TMUX") || term.startsWith("screen")
|
def csi = if (isTmux) "\u001bPtmux\u001b\u001b[" else "\u001b["
|
||||||
|
def osc = if (isTmux) "\u001bPtmux\u001b\u001b]" else "\u001b]"
|
||||||
|
def apc = if (isTmux) "\u001bPtmux;\u001b\u001b_" else "\u001b_"
|
||||||
|
def st = if (isTmux) "\u0007\u001b\\" else "\u0007"
|
||||||
|
|
||||||
def osc(code: String) = (if isTmux then "\u001bPtmux\u001b\u001b]" else "\u001b]") + code + st
|
def hideCursor() = print(s"${csi}?25l")
|
||||||
def st = if isTmux then "\u0007\u001b\\" else "\u0007"
|
def showCursor() = print(s"${csi}?25h")
|
||||||
def csi(code: String) = "\u001b[" + code
|
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 hideCursor() = print(csi("?25l"))
|
def showImage(
|
||||||
def showCursor() = print(csi("?25h"))
|
img: os.Path,
|
||||||
def cursorTo(row: Int, col: Int) = print(csi(s"${row};${col}H"))
|
width: String = "100%",
|
||||||
def clear() = print(csi("2J") + csi(";H"))
|
height: String = "100%",
|
||||||
|
keepAspect: Boolean = true,
|
||||||
def showImage(img: Path): Unit =
|
) =
|
||||||
print(
|
|
||||||
if term == "xterm-kitty" then showImageKitty(img)
|
|
||||||
else showImageIterm(img, "100%", "100%", true)
|
|
||||||
)
|
|
||||||
|
|
||||||
def showImageScaled(img: Path, width: Double, height: Double, keepAspect: Boolean): Unit =
|
|
||||||
print(
|
|
||||||
if term == "xterm-kitty" then
|
|
||||||
val cols = (jterm.getSize.getColumns * width).toInt
|
|
||||||
val rows = (jterm.getSize.getRows * height).toInt
|
|
||||||
showImageKitty(img) // TODO
|
|
||||||
else
|
|
||||||
showImageIterm(img, width.toPercent, height.toPercent, keepAspect)
|
|
||||||
)
|
|
||||||
|
|
||||||
def showImageIterm(img: Path, width: String, height: String, keepAspect: Boolean = true): String =
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
val image = Base64.getEncoder.encodeToString(read.bytes(img))
|
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||||
val aspect = if keepAspect then 1 else 0
|
val aspect = if (keepAspect) 1 else 0 // TODO
|
||||||
osc(s"1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image")
|
if image.length > 4096 then
|
||||||
|
val chunks = image.grouped(4096).toVector
|
||||||
|
|
||||||
def showImageKitty(img: Path): String =
|
s"${apc}Gf=100,t=d,m=1,a=T;${chunks.head}${st}" +
|
||||||
import java.util.Base64
|
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
||||||
s"\u001b_Gf=100,t=f,a=T,C=1;${Base64.getEncoder.encodeToString(img.toString.toCharArray.map(_.toByte))}\u001b\\"
|
s"${apc}Gm=0;${chunks.last}${st}"
|
||||||
|
else s"${apc}Gf=100,t=d,a=T;${image}${st}"
|
||||||
|
|
||||||
|
def getSize = queryTerm(s"${csi}s${csi}999;999H${csi}6n${csi}u")
|
||||||
|
|
||||||
|
private val SizeResponse = """\u001b\[4;(\d+);(\d+)t""".r
|
||||||
|
|
||||||
|
case class PixelSize(width: Int, height: Int)
|
||||||
|
def getSizePixels: Option[PixelSize] = queryTerm(s"${csi}14t") match
|
||||||
|
case SizeResponse(rows, cols) => Some(PixelSize(cols.toInt, rows.toInt))
|
||||||
|
case _ => None
|
||||||
|
|
||||||
|
end Terminal
|
||||||
|
|
||||||
private[copret] trait TerminalSyntax:
|
private[copret] trait TerminalSyntax:
|
||||||
import Terminal._
|
import Terminal._
|
||||||
|
|
||||||
def %%%(cmd: String*)(using Path) = safe_%%(cmd.toVector)
|
def %%%(cmd: String*)(using cwd: os.Path): String = runProcess(cmd.toVector)
|
||||||
def columns = jterm.getSize.getColumns
|
def columns = jterm.getSize.getColumns
|
||||||
def rows = jterm.getSize.getRows
|
def rows = jterm.getSize.getRows
|
||||||
|
|
||||||
|
|
||||||
/* vim:set tw=120: */
|
/* vim:set tw=120: */
|
||||||
|
|
1
examples/.gitignore
vendored
Normal file
1
examples/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
demoRepo
|
425
examples/git-presentation.sc
Executable file
425
examples/git-presentation.sc
Executable file
|
@ -0,0 +1,425 @@
|
||||||
|
#!/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,
|
||||||
|
/* 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")(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,
|
||||||
|
),
|
||||||
|
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(using
|
||||||
|
Keymap.default ++ Map(
|
||||||
|
Key('i') -> SlideAction.runForeground("tmux"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
/* vim:set tw=120: */
|
BIN
examples/img/git-logo.png
Normal file
BIN
examples/img/git-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
examples/img/repo-graph-i.png
Normal file
BIN
examples/img/repo-graph-i.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
BIN
examples/img/repo-graph.png
Normal file
BIN
examples/img/repo-graph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
|
@ -1,375 +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.{given, *}
|
|
||||||
|
|
||||||
|
|
||||||
/* 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"
|
|
||||||
import de.qwertyuiop.copret.{given Theme}
|
|
||||||
|
|
||||||
/* 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) showing 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")(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) showing "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") showing "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") showing "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") showing "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: */
|
|
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")
|
7
examples/slides/01wozu.md
Normal file
7
examples/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/slides/02branches.md
Normal file
18
examples/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/slides/02summary.md
Normal file
16
examples/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)
|
||||||
|
|
Loading…
Reference in a new issue