Move presentation tooling to its own repo
This commit is contained in:
commit
3ea943b1c4
35
3rd-party-licenses/jline3.LICENSE.txt
Normal file
35
3rd-party-licenses/jline3.LICENSE.txt
Normal 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.
|
||||
|
26
build.sc
Normal file
26
build.sc
Normal 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
52
copret/src/Theme.scala
Normal 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
86
copret/src/keys.scala
Normal 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
192
copret/src/slides.scala
Normal 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
38
copret/src/syntax.scala
Normal 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: */
|
25
copret/src/templates.scala
Normal file
25
copret/src/templates.scala
Normal 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
60
copret/src/terminal.scala
Normal 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
48
mill
Executable 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 "$@"
|
Loading…
Reference in a new issue