Move presentation tooling to its own repo

This commit is contained in:
Alexander Gehrke 2021-02-19 15:46:41 +01:00
commit 3ea943b1c4
10 changed files with 563 additions and 0 deletions

View file

@ -0,0 +1,35 @@
Copyright (c) 2002-2018, the original author or authors.
All rights reserved.
https://opensource.org/licenses/BSD-3-Clause
Redistribution and use in source and binary forms, with or
without modification, are permitted provided that the following
conditions are met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with
the distribution.
Neither the name of JLine nor the names of its contributors
may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

1
README.md Normal file
View file

@ -0,0 +1 @@
# copret - COnsole PREsentation Tool

26
build.sc Normal file
View file

@ -0,0 +1,26 @@
import mill._, scalalib._, publish._
import $ivy.`com.lihaoyi::mill-contrib-bloop:0.9.5`
object copret extends ScalaModule with PublishModule {
def scalaVersion = "2.13.3"
def publishVersion = "0.0.1"
def pomSettings = PomSettings(
description = "Use ammonite scripts for command line presentations",
organization = "de.qwertyuiop",
versionControl = VersionControl.github("crater2150", "copret"),
url = "https://qwertyuiop.de/copret/",
licenses = Seq(License.MIT),
developers = Seq(
Developer("crater2150", "Alexander Gehrke", "https://github.com/crater2150")
)
)
def ivyDeps = Agg(
ivy"org.jline:jline:3.19.0",
ivy"com.lihaoyi::ammonite-ops:2.3.8",
ivy"com.lihaoyi::fansi:0.2.10",
)
}

52
copret/src/Theme.scala Normal file
View file

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

86
copret/src/keys.scala Normal file
View file

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

192
copret/src/slides.scala Normal file
View file

@ -0,0 +1,192 @@
package de.qwertyuiop.copret
import ammonite.ops._
import Terminal._
import syntax._
case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty) {
def start(keymap: Keymap = Keymap.default) = {
Terminal.enterRawMode()
run(keymap)
}
import Presentation._
def run(implicit k: Keymap) = {
@annotation.tailrec def rec(p: Presentation, pos: Int, action: SlideAction): Unit = {
action match {
case Start =>
executeSlide(p, pos)()
rec(p, 1, waitkey)
case Next | Other(_) =>
if(pos + 1 < p.slides.size) {
executeSlide(p, pos + 1)()
rec(p, pos + 1, waitkey)
} else rec(p, pos, waitkey)
case QuickNext =>
if(pos + 1 < p.slides.size) {
executeQuick(p, pos + 1)()
rec(p, pos + 1, waitkey)
} else rec(p, pos, waitkey)
case Prev =>
if(pos > 0) {
executeQuick(p, pos - 1)()
rec(p, pos - 1, waitkey)
} else rec(p, pos, waitkey)
case Interactive(cmd, path) =>
%(cmd)(path)
rec(p, pos - 1, QuickNext)
case Goto(target) =>
for (i <- 0 until target) executeSilent(p, i)()
rec(p, target - 1, QuickNext)
case GotoSelect =>
val maxSlide = p.slides.size - 1
val target = prompt(s"Go to slide (0 - $maxSlide):", _.toIntOption)(
(res, input) => res.filter((0 to maxSlide).contains).isEmpty && input.nonEmpty,
in => s"No such slide: $in (empty input to abort)"
)
target match {
case Some(i) => rec(p, pos, Goto(i))
case None => rec(p, pos - 1, QuickNext)
}
case Quit => ()
}
}
rec(this, 0, Start)
}
}
object Presentation {
def executeSlide(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match {
case Paragraph(contents) => println(contents)
case Clear => print("\u001b[2J\u001b[;H")
case PauseKey => waitkey(Keymap.empty)
case Pause(msec) => Thread.sleep(msec)
case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock())
case Image(file) =>
try { %("imgcat", file.toString)(pwd) }
catch { case _: InteractiveShelloutException => println(s"Image missing: $file") }
case cmd: TypedCommand[_] => cmd.show()
case Silent(actions) => actions()
case Group(slides) => slides.foreach(executeSlide(p, pos))
case lios @ LazyIOSlide(_, display) => executeSlide(p, pos)(lios.genSlide())
case Meta(genSlide) => executeSlide(p, pos)(genSlide(p, pos))
case other => println("Error: Unknown slide type:"); println(other)
}
def executeQuick(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match {
case Pause(msec) => ()
case PauseKey => ()
case cmd: TypedCommand[_] => cmd.quickShow()
case Group(slides) => slides.foreach(executeQuick(p, pos))
case lios @ LazyIOSlide(_, display) => executeQuick(p, pos)(lios.genSlide())
case _ => executeSlide(p, pos)(slide)
}
def executeSilent(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match {
case cmd: TypedCommand[_] => cmd.force()
case Group(slides) => slides.foreach(executeSilent(p, pos))
case lios @ LazyIOSlide(_, display) => executeSilent(p, pos)(lios.genSlide())
case Paragraph(_) | Image(_) | Clear | IncludeMarkdown(_) | Meta(_) => ()
case _ => executeQuick(p, pos)(slide)
}
}
sealed trait Slide
case class Paragraph(contents: fansi.Str) extends Slide
case class IncludeMarkdown(path: Path) extends Slide {
def markdownBlock() = %%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, path.toString)(pwd).block
}
case class Image(path: Path) extends Slide
case object Clear extends Slide
case class Pause(millisec: Long) extends Slide
case object PauseKey extends Slide
case class Meta(contents: (Presentation, Int) => Slide) extends Slide
case class TypedCommand[T](exec: T => String, display: String, cmd: T) extends Slide {
private lazy val _output = exec(cmd)
def output = _output
def display(s: String): TypedCommand[T] = TypedCommand(exec, s, cmd)
def show() = {
prompt()
typeCmd()
print(output)
}
def quickShow() = {
prompt()
println(display)
print(output)
}
def prompt() = print(fansi.Color.LightGreen("user@host % "))
def force() = _output
private def typeCmd() = {
for (char <- display) {
print(char)
Thread.sleep(50 + scala.util.Random.nextInt(80))
}
println()
}
/* Conditionally disable execution. Useful for e.g. a debug mode, or a non-interactive mode */
def disable(altDisplay: String = display, condition: Boolean = true) =
if(condition) copy(display = altDisplay, exec = (_:T) => "")
else this
/* Conditionally replace the executed command (but still displaying the same). Useful for e.g. a non-interactive mode,
* where a call to an editor is replaced with a file operation */
def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] =
if(condition) tc.display(display)
else this
}
object TypedCommand {
val shell = sys.env.getOrElse("SHELL", "sh")
def run(implicit wd: Path): Vector[String] => String =
c => safe_%%(c)
def runShell(implicit wd: Path): Vector[String] => String =
c => safe_%%(Vector(shell, "-c", c.mkString(" ")))
def runInteractive(implicit wd: Path): Vector[String] => String =
c => { %(c); ""}
def apply(cmd: String*)(implicit wd: Path): TypedCommand[Vector[String]] =
TypedCommand(run, cmd.mkString(" "), cmd.toVector)
def shell(cmd: String*)(implicit wd: Path): TypedCommand[Vector[String]] =
TypedCommand(runShell, cmd.mkString(" "), cmd.toVector)
def fake(cmd: String): TypedCommand[String] =
TypedCommand(_ => "", cmd, cmd)
def interactive(cmd: String*)(implicit wd: Path): TypedCommand[Vector[String]] =
TypedCommand(runInteractive, cmd.mkString(" "), cmd.toVector)
}
sealed abstract case class Silent[T] private (doStuff: () => T) extends Slide
object Silent { def apply[T](doStuff: => T) = new Silent(() => doStuff){} }
case class Group(slides: List[Slide]) extends Slide
object Group { def apply(slides: Slide*): Group = Group(slides.toList) }
case class LazyIOSlide[T](runOnce: () => T, display: T => Slide) extends Slide {
private lazy val data = runOnce()
def genSlide(): Slide = display(data)
}
trait SlideSyntax {
private[copret] class LazyIOSlideBuilder[T](runOnce: => T) {
def useIn(display: T => Slide) = LazyIOSlide(() => runOnce, display)
}
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = new LazyIOSlideBuilder(runOnce)
}
/* vim:set tw=120: */

38
copret/src/syntax.scala Normal file
View file

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

View file

@ -0,0 +1,25 @@
package de.qwertyuiop.copret
import syntax._
import ammonite.ops.{Path, %, %%, pwd}
trait Templates {
def titleLine(title: String)(implicit theme: Theme) = Paragraph(
"\n" + Format.figlet(title, theme.font("titleLine", "pagga")).block.blue + "\n"
)
def header(implicit theme: Theme) = Meta((p, pos) => {
val left = p.meta.getOrElse("author", "")
val center = p.meta.getOrElse("title", "")
val right = s"${pos} / ${p.slides.size - 1}"
theme.style("titleLine")(Format.distribute(left, center, right)).text
})
def slide(title: String)(slides: Slide*) = Group(Clear :: header :: titleLine(title) :: slides.toList)
def slide(slides: Slide*) = Group(Clear :: header :: slides.toList)
def markdown(title: String, content: Path) = slide(title)(IncludeMarkdown(content))
lazy val --- = Paragraph(("═" * columns).yellow)
}
/* vim:set tw=120: */

60
copret/src/terminal.scala Normal file
View file

@ -0,0 +1,60 @@
package de.qwertyuiop.copret
import ammonite.ops.{Path, ShelloutException, pwd, %, %%}
import org.jline.terminal.TerminalBuilder
import org.jline.reader.LineReaderBuilder
object Terminal {
def safe_%%(cmd: Vector[String])(implicit wd: Path): String =
try {
%%(cmd).out.string
} catch {
case e: ShelloutException => e.result.err.string
}
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 -echo < /dev/tty")(pwd)
}
private[copret] lazy val jterm = org.jline.terminal.TerminalBuilder.terminal()
private[copret] lazy val lineReader = LineReaderBuilder.builder().terminal(jterm).build()
def waitkey(implicit keymap: Keymap): SlideAction = {
// ignore keypresses done during slide animations
while(Console.in.ready()) Console.in.read
var key = scala.collection.mutable.ArrayBuffer[Int]()
key += Console.in.read
while(Console.in.ready)
key += Console.in.read
keymap(key.toList)
}
def prompt[T](prefix: String, parse: String => T)(
retry: (T, String) => Boolean = (t: T, s: String) => false,
error: String => String = in => s"Invalid input: $in"
): T = {
val input = lineReader.readLine(prefix + " ")
val result = parse(input)
if(retry(result, input)) {
println(error(input))
prompt(prefix, parse)(retry, error)
}
else result
}
}
private[copret] trait TerminalSyntax {
import Terminal._
def %%%(cmd: String*)(implicit wd: Path) = safe_%%(cmd.toVector)
def columns = jterm.getSize.getColumns
def rows = jterm.getSize.getRows
}
/* vim:set tw=120: */

48
mill Executable file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env sh
# This is a wrapper script, that automatically download mill from GitHub release pages
# You can give the required mill version with MILL_VERSION env variable
# If no version is given, it falls back to the value of DEFAULT_MILL_VERSION
DEFAULT_MILL_VERSION=0.9.5
set -e
if [ -z "$MILL_VERSION" ] ; then
if [ -f ".mill-version" ] ; then
MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)"
elif [ -f "mill" ] && [ "$BASH_SOURCE" != "mill" ] ; then
MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2)
else
MILL_VERSION=$DEFAULT_MILL_VERSION
fi
fi
if [ "x${XDG_CACHE_HOME}" != "x" ] ; then
MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download"
else
MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download"
fi
MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}"
version_remainder="$MILL_VERSION"
MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}"
MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}"
if [ ! -x "$MILL_EXEC_PATH" ] ; then
mkdir -p $MILL_DOWNLOAD_PATH
if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then
ASSEMBLY="-assembly"
fi
DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download
MILL_DOWNLOAD_URL="https://github.com/lihaoyi/mill/releases/download/${MILL_VERSION%%-*}/$MILL_VERSION${ASSEMBLY}"
curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL"
chmod +x "$DOWNLOAD_FILE"
mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH"
unset DOWNLOAD_FILE
unset MILL_DOWNLOAD_URL
fi
unset MILL_DOWNLOAD_PATH
unset MILL_VERSION
exec $MILL_EXEC_PATH "$@"