Port to Scala 3

This commit is contained in:
Alexander Gehrke 2022-05-08 20:27:14 +02:00
parent d9a9e6b445
commit f955a7563c
8 changed files with 158 additions and 187 deletions

View file

@ -1,9 +1,8 @@
import mill._, scalalib._, publish._ import mill._, scalalib._, publish._
import $ivy.`com.lihaoyi::mill-contrib-bloop:0.9.5` import $ivy.`com.lihaoyi::mill-contrib-bloop:0.9.5`
object copret extends ScalaModule with PublishModule { object copret extends ScalaModule with PublishModule {
def scalaVersion = "2.13.3" def scalaVersion = "3.1.0"
def publishVersion = "0.0.1" def publishVersion = "0.0.1"
def pomSettings = PomSettings( def pomSettings = PomSettings(
@ -14,13 +13,13 @@ object copret extends ScalaModule with PublishModule {
licenses = Seq(License.MIT), licenses = Seq(License.MIT),
developers = Seq( developers = Seq(
Developer("crater2150", "Alexander Gehrke", "https://github.com/crater2150") Developer("crater2150", "Alexander Gehrke", "https://github.com/crater2150")
) )
) )
def ivyDeps = Agg( def ivyDeps = Agg(
ivy"org.jline:jline:3.19.0", ivy"org.jline:jline:3.19.0",
ivy"com.lihaoyi::ammonite-ops:2.3.8", ivy"com.lihaoyi::ammonite-ops:2.3.8".withDottyCompat(scalaVersion()),
ivy"com.lihaoyi::fansi:0.2.10", ivy"com.lihaoyi::fansi:0.2.14",
) )
} }

View file

@ -2,7 +2,7 @@ package de.qwertyuiop.copret
import de.qwertyuiop.copret.syntax._ import de.qwertyuiop.copret.syntax._
import ammonite.ops.{%%, pwd} import ammonite.ops.{%%, pwd}
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()) =
styles.getOrElse(key, default) styles.getOrElse(key, default)
@ -14,17 +14,15 @@ 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 extend(newFonts: Map[String, String])(implicit d: DummyImplicit) = copy(figletFonts = figletFonts ++ newFonts)
def ++(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 { given default: Theme = Theme(Map(
implicit val default = Theme(Map(
"titleLine" -> (fansi.Bold.On ++ fansi.Color.DarkGray), "titleLine" -> (fansi.Bold.On ++ fansi.Color.DarkGray),
"code" -> fansi.Color.Yellow "code" -> fansi.Color.Yellow
), ),
Map("titleLine" -> "pagga") Map("titleLine" -> "pagga")
) )
}
object Format { object Format:
def alignRight(str: String, padding: Int = 2) =" " * (columns - str.length - padding) + str + " " * padding def alignRight(str: String, padding: Int = 2) =" " * (columns - str.length - padding) + str + " " * padding
def center(str: String) = " " * ((columns - str.length) / 2) + str def center(str: String) = " " * ((columns - str.length) / 2) + str
@ -32,21 +30,18 @@ object Format {
def figlet(str: String, font: String) = %%("figlet", "-t", "-f", font, str)(pwd).out.string def figlet(str: String, font: String) = %%("figlet", "-t", "-f", font, str)(pwd).out.string
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) =
val lines = str.split("\n") val lines = str.split("\n")
val maxLen = lines.map(_.length).max val maxLen = lines.map(_.length).max
val pad = " " * ((columns - maxLen) / 2) val pad = " " * ((columns - maxLen) / 2)
lines.map(pad + _).mkString("\n") lines.map(pad + _).mkString("\n")
}
def distribute(texts: String*) = { def distribute(texts: String*) =
val totalPad = columns - texts.map(_.length).sum val totalPad = columns - texts.map(_.length).sum
val numPads = texts.size - 1 val numPads = texts.size - 1
val pad = " " * (totalPad / numPads) val pad = " " * (totalPad / numPads)
texts.init.mkString(pad) + pad + " " * (totalPad % numPads) + texts.last texts.init.mkString(pad) + pad + " " * (totalPad % numPads) + texts.last
}
private[copret] val ticks = raw"`([^`]*)`".r private[copret] val ticks = raw"`([^`]*)`".r
}
/* vim:set tw=120: */ /* vim:set tw=120: */

View file

@ -2,29 +2,28 @@ package de.qwertyuiop.copret
import ammonite.ops.Path import ammonite.ops.Path
sealed trait SlideAction enum SlideAction:
case object Start extends SlideAction case Start
case class Goto(slideIndex: Int) extends SlideAction case Goto(slideIndex: Int)
case object GotoSelect extends SlideAction case GotoSelect
case object Prev extends SlideAction case Prev
case object Next extends SlideAction case Next
case object QuickNext extends SlideAction case QuickNext
case object Quit extends SlideAction case Quit
case class Interactive(cmd: Vector[String], wd: Path) extends SlideAction case Interactive(cmd: Vector[String], wd: Path)
case class Other(code: List[Int]) extends SlideAction case Other(code: List[Int])
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.*
case class Keymap(bindings: Map[List[Int], SlideAction]) { case class Keymap(bindings: Map[List[Int], SlideAction]):
def apply(keycode: List[Int]): SlideAction = bindings.getOrElse(keycode, Other(keycode)) def apply(keycode: List[Int]): SlideAction = bindings.getOrElse(keycode, Other(keycode))
def extend(newBindings: Map[List[Int], SlideAction]) = Keymap(bindings ++ newBindings) def extend(newBindings: Map[List[Int], SlideAction]) = Keymap(bindings ++ newBindings)
def ++(newBindings: Map[List[Int], SlideAction]) = Keymap(bindings ++ newBindings) def ++(newBindings: Map[List[Int], SlideAction]) = Keymap(bindings ++ newBindings)
} object Keymap:
object Keymap {
val empty = Keymap(Map()) val empty = Keymap(Map())
val default = Keymap(Map( val default = Keymap(Map(
Key.Up -> Prev, Key.Up -> Prev,
@ -41,46 +40,43 @@ object Keymap {
Key('s') -> GotoSelect, Key('s') -> GotoSelect,
)) ))
}
object Key { object Key:
object codes { object codes:
val Esc = 27 val Esc = 27
val Backspace = 127 val Backspace = 127
} val Esc = List[Int](codes.Esc)
val Esc = List(codes.Esc) val Backspace = List[Int](codes.Backspace)
val Backspace = List(codes.Backspace) val Delete = List[Int](codes.Esc, '[', '3', '~')
val Delete = List(codes.Esc, '[', '3', '~')
val PageUp = List(codes.Esc, '[', '5', '~') val PageUp = List[Int](codes.Esc, '[', '5', '~')
val PageDown = List(codes.Esc, '[', '6', '~') val PageDown = List[Int](codes.Esc, '[', '6', '~')
val Home = List(codes.Esc, '[', 'H') val Home = List[Int](codes.Esc, '[', 'H')
val End = List(codes.Esc, '[', 'F') val End = List[Int](codes.Esc, '[', 'F')
val F1 = List(codes.Esc, 'P') val F1 = List[Int](codes.Esc, 'P')
val F2 = List(codes.Esc, 'Q') val F2 = List[Int](codes.Esc, 'Q')
val F3 = List(codes.Esc, 'R') val F3 = List[Int](codes.Esc, 'R')
val F4 = List(codes.Esc, 'S') val F4 = List[Int](codes.Esc, 'S')
val F5 = List(codes.Esc, '1', '5', '~') val F5 = List[Int](codes.Esc, '1', '5', '~')
val F6 = List(codes.Esc, '1', '7', '~') val F6 = List[Int](codes.Esc, '1', '7', '~')
val F7 = List(codes.Esc, '1', '8', '~') val F7 = List[Int](codes.Esc, '1', '8', '~')
val F8 = List(codes.Esc, '1', '9', '~') val F8 = List[Int](codes.Esc, '1', '9', '~')
val F9 = List(codes.Esc, '2', '0', '~') val F9 = List[Int](codes.Esc, '2', '0', '~')
val F10 = List(codes.Esc, '2', '1', '~') val F10 = List[Int](codes.Esc, '2', '1', '~')
val F11 = List(codes.Esc, '2', '3', '~') val F11 = List[Int](codes.Esc, '2', '3', '~')
val F12 = List(codes.Esc, '2', '4', '~') val F12 = List[Int](codes.Esc, '2', '4', '~')
val Tab = List('\t') val Tab = List[Int]('\t')
val Up = List(codes.Esc, '[', 'A') val Up = List[Int](codes.Esc, '[', 'A')
val Down = List(codes.Esc, '[', 'B') val Down = List[Int](codes.Esc, '[', 'B')
val Right = List(codes.Esc, '[', 'C') val Right = List[Int](codes.Esc, '[', 'C')
val Left = List(codes.Esc, '[', 'D') val Left = List[Int](codes.Esc, '[', 'D')
def apply(char: Char): List[Int] = List(char.toInt) def apply(char: Char): List[Int] = List(char.toInt)
}
/* vim:set tw=120: */ /* vim:set tw=120: */

View file

@ -3,39 +3,40 @@ import ammonite.ops._
import Terminal._ import Terminal._
import syntax._ import syntax._
case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty) { case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty):
def start(keymap: Keymap = Keymap.default) = { def start(using keymap: Keymap = Keymap.default) =
Terminal.enterRawMode() Terminal.enterRawMode()
run(keymap) run()
}
import Presentation._ import Presentation._
def run(implicit k: Keymap) = { def run()(using Keymap) =
@annotation.tailrec def rec(p: Presentation, pos: Int, action: SlideAction): Unit = { import SlideAction.*
action match { @annotation.tailrec def rec(p: Presentation, pos: Int, action: SlideAction): Unit =
action match
case Start => case Start =>
executeSlide(p, pos)() executeSlide(p, pos)()
rec(p, 1, waitkey) rec(p, 1, waitkey)
case Next | Other(_) => case Next | Other(_) =>
if(pos + 1 < p.slides.size) { if pos + 1 < p.slides.size then
executeSlide(p, pos + 1)() executeSlide(p, pos + 1)()
rec(p, pos + 1, waitkey) rec(p, pos + 1, waitkey)
} else rec(p, pos, waitkey) else rec(p, pos, waitkey)
case QuickNext => case QuickNext =>
if(pos + 1 < p.slides.size) { if pos + 1 < p.slides.size then
executeQuick(p, pos + 1)() executeQuick(p, pos + 1)()
rec(p, pos + 1, waitkey) rec(p, pos + 1, waitkey)
} else rec(p, pos, waitkey) else rec(p, pos, waitkey)
case Prev => case Prev =>
if(pos > 0) { if pos > 0 then
executeQuick(p, pos - 1)() executeQuick(p, pos - 1)()
rec(p, pos - 1, waitkey) rec(p, pos - 1, waitkey)
} else rec(p, pos, waitkey) else rec(p, pos, waitkey)
case Interactive(cmd, path) => case Interactive(cmd, path) =>
%(cmd)(path) %(cmd)(path)
rec(p, pos - 1, QuickNext) rec(p, pos - 1, QuickNext)
case Goto(target) => case Goto(target) =>
for (i <- 0 until target) executeSilent(p, i)() for i <- 0 until target
do executeSilent(p, i)()
rec(p, target - 1, QuickNext) rec(p, target - 1, QuickNext)
case GotoSelect => case GotoSelect =>
val maxSlide = p.slides.size - 1 val maxSlide = p.slides.size - 1
@ -43,21 +44,18 @@ case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.e
(res, input) => res.filter((0 to maxSlide).contains).isEmpty && input.nonEmpty, (res, input) => res.filter((0 to maxSlide).contains).isEmpty && input.nonEmpty,
in => s"No such slide: $in (empty input to abort)" in => s"No such slide: $in (empty input to abort)"
) )
target match { target match
case Some(i) => rec(p, pos, Goto(i)) case Some(i) => rec(p, pos, Goto(i))
case None => rec(p, pos - 1, QuickNext) case None => rec(p, pos - 1, QuickNext)
}
case Quit => () case Quit => ()
}
}
rec(this, 0, Start) rec(this, 0, Start)
}
}
object Presentation { object Presentation:
def executeSlide(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match { def executeSlide(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match
case Paragraph(contents) => println(contents) case Paragraph(contents) => println(contents)
case Clear => print("\u001b[2J\u001b[;H") case Clear => print("\u001b[2J\u001b[;H")
case PauseKey => waitkey(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, width, height, keepAspect) => print(Terminal.showImage(file, width, height, keepAspect)) case Image(file, width, height, keepAspect) => print(Terminal.showImage(file, width, height, keepAspect))
@ -66,125 +64,114 @@ object Presentation {
case Group(slides) => slides.foreach(executeSlide(p, pos)) case Group(slides) => slides.foreach(executeSlide(p, pos))
case lios @ LazyIOSlide(_, display) => executeSlide(p, pos)(lios.genSlide()) case lios @ LazyIOSlide(_, display) => executeSlide(p, pos)(lios.genSlide())
case Meta(genSlide) => executeSlide(p, pos)(genSlide(p, pos)) 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 { def executeQuick(p: Presentation, pos: Int)(slide: Slide = p.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(p, pos))
case lios @ LazyIOSlide(_, display) => executeQuick(p, pos)(lios.genSlide()) case lios @ LazyIOSlide(_, display) => executeQuick(p, pos)(lios.genSlide())
case _ => executeSlide(p, pos)(slide) case _ => executeSlide(p, pos)(slide)
}
def executeSilent(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match { def executeSilent(p: Presentation, pos: Int)(slide: Slide = p.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(p, pos))
case lios @ LazyIOSlide(_, display) => executeSilent(p, pos)(lios.genSlide()) case lios @ LazyIOSlide(_, display) => executeSilent(p, pos)(lios.genSlide())
case Paragraph(_) | Image(_,_,_,_) | Clear | IncludeMarkdown(_) | Meta(_) => () case Paragraph(_) | Image(_,_,_,_) | Clear | IncludeMarkdown(_) | Meta(_) => ()
case _ => executeQuick(p, pos)(slide) case _ => executeQuick(p, pos)(slide)
}
}
sealed trait Slide sealed trait Slide
case class Paragraph(contents: fansi.Str) extends Slide case class Paragraph(contents: fansi.Str) extends Slide
case class IncludeMarkdown(path: Path) extends Slide { case class IncludeMarkdown(path: Path) extends Slide:
def markdownBlock() = %%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, path.toString)(pwd).block def markdownBlock() =
} %%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, path.toString)(using ImplicitWd.implicitCwd).block
case class Image(path: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) extends Slide case class Image(path: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) extends Slide
case object Clear extends Slide case object Clear extends Slide
case class Pause(millisec: Long) extends Slide 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) extends Slide:
case class TypedCommand[T](exec: T => String, display: String, cmd: T) extends Slide {
private lazy val _output = exec(cmd) private lazy val _output = exec(cmd)
def output = _output 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() =
prompt() prompt()
typeCmd() typeCmd()
print(output) print(output)
}
def quickShow() = { def quickShow() =
prompt() prompt()
println(display) println(display)
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
private def typeCmd() = { private def typeCmd() =
for (char <- display) { for char <- display
print(char) do print(char)
Thread.sleep(50 + scala.util.Random.nextInt(80)) Thread.sleep(50 + scala.util.Random.nextInt(80))
}
println() println()
}
/* Conditionally disable execution. Useful for e.g. a debug mode, or a non-interactive mode */ /* Conditionally disable execution. Useful for e.g. a debug mode, or a non-interactive mode */
def disable(altDisplay: String = display, condition: Boolean = true) = 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 else this
/* Conditionally replace the executed command (but still displaying the same). Useful for e.g. a non-interactive mode, /* 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 */ * where a call to an editor is replaced with a file operation */
def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] = def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] =
if(condition) tc.display(display) if condition then tc.showing(display)
else this else this
}
object TypedCommand { object TypedCommand:
val shell = sys.env.getOrElse("SHELL", "sh") val shell = sys.env.getOrElse("SHELL", "sh")
def run(implicit wd: Path): Vector[String] => String = def run(using Path): Vector[String] => String =
c => safe_%%(c) c => safe_%%(c)
def runShell(implicit wd: Path): Vector[String] => String = def runShell(using Path): Vector[String] => String =
c => safe_%%(Vector(shell, "-c", c.mkString(" "))) c => safe_%%(Vector(shell, "-c", c.mkString(" ")))
def runInteractive(implicit wd: Path): Vector[String] => String = def runInteractive(using Path): Vector[String] => String =
c => { %(c); ""} c => { %(c); ""}
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) TypedCommand(run, cmd.mkString(" "), cmd.toVector)
def shell(cmd: String*)(implicit wd: 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)
def fake(cmd: String): TypedCommand[String] = def fake(cmd: String): TypedCommand[String] =
TypedCommand(_ => "", cmd, cmd) 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) 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 { 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 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() private lazy val data = runOnce()
def genSlide(): Slide = display(data) def genSlide(): Slide = display(data)
}
trait SlideSyntax { 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] = new LazyIOSlideBuilder(runOnce) def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = LazyIOSlideBuilder(runOnce)
}
/* vim:set tw=120: */ /* vim:set tw=120: */

View file

@ -1,38 +1,33 @@
package de.qwertyuiop.copret package de.qwertyuiop.copret
object syntax extends Templates with TerminalSyntax with SlideSyntax { object syntax extends Templates with TerminalSyntax with SlideSyntax:
implicit class PresenterStringExtensions(val str: String) { import Format._
import Format._ extension (str: String)
def code(implicit theme: Theme) = Format.ticks.replaceAllIn(str, m => theme.style("code")("$1").render) def code(using theme: Theme) = Format.ticks.replaceAllIn(str, m => theme.style("code")("$1").render)
def text(implicit theme: Theme) = Paragraph(str) def text(using Theme) = Paragraph(str)
def par(implicit theme: Theme) = Paragraph(str.stripMargin.code.padLeft(2)) def par(using Theme) = Paragraph(str.stripMargin.code.padLeft(2))
def style(key: String, default: fansi.Attrs = fansi.Attrs())(implicit theme: Theme) = theme.style(key, default)(str) def style(key: String, default: fansi.Attrs)(using theme: Theme) = theme.style(key, default)(str)
def centered = center(str) def centered = center(str)
def block = centerBlock(str) def block = centerBlock(str)
def right = alignRight(str) def right = alignRight(str)
def right(padding: Int) = alignRight(str, padding) def right(padding: Int) = alignRight(str, padding)
def padLeft(padding: Int) = { def padLeft(padding: Int) =
val pad = " " * padding val pad = " " * padding
str.linesIterator.map(pad + _).mkString("\n") str.linesIterator.map(pad + _).mkString("\n")
}
def blue = fansi.Color.Blue(str) def blue = fansi.Color.Blue(str)
def green = fansi.Color.Green(str) def green = fansi.Color.Green(str)
def yellow = fansi.Color.Yellow(str) def yellow = fansi.Color.Yellow(str)
def red = fansi.Color.Red(str) def red = fansi.Color.Red(str)
}
implicit class PresenterFansiStringExtensions(val str: fansi.Str) { extension (str: fansi.Str)
import Format._ def text(using Theme) = Paragraph(str)
def text(implicit theme: Theme) = Paragraph(str) def style(key: String, default: fansi.Attrs)(using theme: Theme) = theme.style(key, default)(str)
def style(key: String, default: fansi.Attrs = fansi.Attrs())(implicit theme: Theme) = theme.style(key, default)(str)
def blue = fansi.Color.Blue(str) def blue = fansi.Color.Blue(str)
def green = fansi.Color.Green(str) def green = fansi.Color.Green(str)
def yellow = fansi.Color.Yellow(str) def yellow = fansi.Color.Yellow(str)
def red = fansi.Color.Red(str) def red = fansi.Color.Red(str)
}
}
/* vim:set tw=120: */ /* vim:set tw=120: */

View file

@ -2,7 +2,7 @@ package de.qwertyuiop.copret
import syntax._ import syntax._
import ammonite.ops.{Path, %, %%, pwd} import ammonite.ops.{Path, %, %%, pwd}
trait Templates { trait Templates:
def titleLine(title: String)(implicit theme: Theme) = Paragraph( def titleLine(title: String)(implicit 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"
) )
@ -20,6 +20,5 @@ trait Templates {
def markdown(title: String, content: Path) = slide(title)(IncludeMarkdown(content)) def markdown(title: String, content: Path) = slide(title)(IncludeMarkdown(content))
lazy val --- = Paragraph(("═" * columns).yellow) lazy val --- = Paragraph(("═" * columns).yellow)
}
/* vim:set tw=120: */ /* vim:set tw=120: */

View file

@ -3,70 +3,69 @@ import ammonite.ops.{Path, ShelloutException, pwd, read, %, %%}
import org.jline.terminal.TerminalBuilder import org.jline.terminal.TerminalBuilder
import org.jline.reader.LineReaderBuilder import org.jline.reader.LineReaderBuilder
object Terminal { object Terminal:
def safe_%%(cmd: Vector[String])(implicit wd: Path): String = def safe_%%(cmd: Vector[String])(using Path): String =
try { try
%%(cmd).out.string %%(cmd).out.string
} catch { catch
case e: ShelloutException => e.result.err.string case e: ShelloutException => e.result.err.string
}
def enterRawMode(): Unit =
def tryCmd[T](cmd: => T, default: => T) =
try { cmd } catch { case e: ShelloutException => default }
def enterRawMode(): Unit = {
%%("sh", "-c", "stty -icanon min 1 < /dev/tty")(pwd) %%("sh", "-c", "stty -icanon min 1 < /dev/tty")(pwd)
%%("sh", "-c", "stty -echo < /dev/tty")(pwd) %%("sh", "-c", "stty -echo < /dev/tty")(pwd)
}
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 waitkey(implicit keymap: Keymap): SlideAction = { def waitkey(using keymap: Keymap): SlideAction =
// ignore keypresses done during slide animations // ignore keypresses done during slide animations
while(Console.in.ready()) Console.in.read while Console.in.ready() do Console.in.read
var key = scala.collection.mutable.ArrayBuffer[Int]() var key = scala.collection.mutable.ArrayBuffer[Int]()
key += Console.in.read key += Console.in.read
while(Console.in.ready) while Console.in.ready do
key += Console.in.read key += Console.in.read
keymap(key.toList) keymap(key.toList)
}
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)
if(retry(result, input)) { if retry(result, input) then
println(error(input)) println(error(input))
prompt(prefix, parse)(retry, error) prompt(prefix, parse)(retry, error)
}
else result else result
}
def isTmux = sys.env.contains("TMUX") || sys.env("TERM").startsWith("screen") val term = sys.env("TERM")
def osc = if (isTmux) "\u001bPtmux\u001b\u001b]" else "\u001b]" def isTmux = sys.env.contains("TMUX") || term.startsWith("screen")
def st = if (isTmux) "\u0007\u001b\\" else "\u0007"
def showImage(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) = { def osc = if isTmux then "\u001bPtmux\u001b\u001b]" else "\u001b]"
def st = if isTmux then "\u0007\u001b\\" else "\u0007"
def showImage(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) =
if term == "xterm-kitty" then showImageKitty(img)
else showImageIterm(img, width, height, keepAspect)
def showImageIterm(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) =
import java.util.Base64 import java.util.Base64
val image = Base64.getEncoder.encodeToString(read.bytes(img)) val image = Base64.getEncoder.encodeToString(read.bytes(img))
val aspect = if(keepAspect) 1 else 0 val aspect = if keepAspect then 1 else 0
s"${osc}1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image$st" s"${osc}1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image$st"
}
}
private[copret] trait TerminalSyntax { def showImageKitty(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) =
import java.util.Base64
s"\u001b_Gf=100,t=f,a=T;${Base64.getEncoder.encodeToString(img.toString.toCharArray.map(_.toByte))}\u001b\\"
private[copret] trait TerminalSyntax:
import Terminal._ import Terminal._
def %%%(cmd: String*)(implicit wd: Path) = safe_%%(cmd.toVector) def %%%(cmd: String*)(using Path) = safe_%%(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: */

View file

@ -4,7 +4,8 @@ import de.qwertyuiop.copret._
import de.qwertyuiop.copret.syntax._ import de.qwertyuiop.copret.syntax._
import TypedCommand.{interactive, shell => sh}, Format.figlet import TypedCommand.{interactive, shell => sh}, Format.figlet
import ammonite.ops._ import ammonite.ops.{given, *}
/* Configuration */ /* Configuration */
@ -12,8 +13,8 @@ import ammonite.ops._
* most of the commands in this presentation will be git commands in a demo repository, so the path to that repo is set * 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) */ * as implicit (will be the working directory of executed commands) */
val imgs = pwd/"img" val imgs = pwd/"img"
implicit val repoDir = pwd/"demoRepo" given repoDir: Path= pwd/"demoRepo"
implicit val theme = Theme.default import de.qwertyuiop.copret.{given Theme}
/* You can define any variables and use then in your presentation. /* You can define any variables and use then in your presentation.
* The presentation is pure Scala code, you can use anything that Scala offers . */ * The presentation is pure Scala code, you can use anything that Scala offers . */
@ -57,7 +58,7 @@ def chapter(title1: String, title2: String, subtitle: String): Group = {
Group(Clear, Group(Clear,
header, // a built in template header, // a built in template
Paragraph( Paragraph(
figlet(title1, font).block.green ++ figlet(title2, font).block.green + "\n" + subtitle.right(10).green figlet(title1, font).block.green ++ figlet(title2, font).block.green ++ "\n" ++ subtitle.right(10).green
) )
) )
} }
@ -91,7 +92,7 @@ def gitCatFile(ref: String) = TypedCommand("git", "cat-file", "-p", ref)
def gitLogGraph(cutOff: Int = 0) = { def gitLogGraph(cutOff: Int = 0) = {
val shownCmd = "git log --graph --oneline --all" val shownCmd = "git log --graph --oneline --all"
val execCmd = shownCmd + " --decorate=short --color=always " + (if(cutOff > 0) "| head -n -" + cutOff else "") val execCmd = shownCmd + " --decorate=short --color=always " + (if(cutOff > 0) "| head -n -" + cutOff else "")
val typer = sh(execCmd) display shownCmd val typer = sh(execCmd) showing shownCmd
if(cutOff > 0) if(cutOff > 0)
Group(typer, "...".text) Group(typer, "...".text)
else else
@ -133,10 +134,10 @@ val presentation = Presentation(Vector(
Pause(500), Pause(500),
/* `TypedCommand.shell` (here aliased to `sh`) displays a typing animation of that command and then executes it, /* `TypedCommand.shell` (here aliased to `sh`) displays a typing animation of that command and then executes it,
* showing its output in the presentation */ * showing its output in the presentation */
sh("git init demoRepo")(pwd), 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 /* 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) */ * 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", 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`. /* 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.*/ * 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 { Silent {
@ -307,13 +308,13 @@ val presentation = Presentation(Vector(
slide("Branches")( slide("Branches")(
"Machen wir ein paar Änderungen und committen sie:\n".par, "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("echo 'a new line' >> hello.txt") showing "echo 'a new line' >> hello.txt # appends to hello.txt",
sh("git add hello.txt"), sh("git add hello.txt"),
sh("git commit -m \"Added line to hello\""), 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, "Da der Branch `feature/foo` aktiv war, zeigt dieser auf den neuen Commit. `master` wird nicht geändert:".code.text,
PauseKey, PauseKey,
sh("git log --graph --oneline --all --decorate=short --color=always") display "git log --graph --oneline --all", sh("git log --graph --oneline --all --decorate=short --color=always") showing "git log --graph --oneline --all",
), ),
slide("Branches")( slide("Branches")(
"Jetzt wechseln wir zurück zu `master`:\n".par, "Jetzt wechseln wir zurück zu `master`:\n".par,
@ -357,7 +358,7 @@ val presentation = Presentation(Vector(
"""Git speichert Objekte unter `.git/objects/<erste zwei Stellen vom Hash>/<Rest vom Hash>` """Git speichert Objekte unter `.git/objects/<erste zwei Stellen vom Hash>/<Rest vom Hash>`
|Objekte sind komprimiert. |Objekte sind komprimiert.
|""".par, |""".par,
sh("tree -C .git/objects/ | head -n 11") display "tree .git/objects/", sh("tree -C .git/objects/ | head -n 11") showing "tree .git/objects/",
s"...".par, s"...".par,
), ),
), meta=meta) ), meta=meta)
@ -366,7 +367,7 @@ val presentation = Presentation(Vector(
* The `runForeground` action lets you run any command, interrupting the presentation until it exits. * 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 * 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 */ * commands for questions */
presentation.start(Keymap.default ++ Map( presentation.start(using Keymap.default ++ Map(
Key('i') -> SlideAction.runForeground("tmux"), Key('i') -> SlideAction.runForeground("tmux"),
)) ))