diff --git a/copret/src/slides.scala b/copret/src/slides.scala index 0b45b84..390f117 100644 --- a/copret/src/slides.scala +++ b/copret/src/slides.scala @@ -6,6 +6,7 @@ import syntax._ case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty): def start(using keymap: Keymap = Keymap.default) = Terminal.enterRawMode() + Terminal.hideCursor() run() import Presentation._ @@ -34,7 +35,12 @@ case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.e else rec(p, pos, waitkey) case Interactive(cmd, path) => - %(cmd)(path) + Terminal.showCursor() + try + %(cmd)(path) + catch case _ => () + Terminal.hideCursor() + Terminal.clear() rec(p, pos - 1, QuickNext) case Goto(target) => for i <- 0 until target @@ -57,11 +63,12 @@ case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.e 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 Clear => Terminal.clear() case PauseKey => waitkey(using Keymap.empty) case Pause(msec) => Thread.sleep(msec) case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock()) - case Image(file, width, height, keepAspect) => print(Terminal.showImage(file, width, height, keepAspect)) + case Image(file, None) => Terminal.showImage(file) + case Image(file, Some(ImageSize(w, h, aspect))) => Terminal.showImageScaled(file, w, h, aspect) case cmd: TypedCommand[_] => cmd.show() case Silent(actions) => actions() case Group(slides) => slides.foreach(executeSlide(p, pos)) @@ -80,16 +87,32 @@ object Presentation: 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 Paragraph(_) | Image(_,_) | Clear | IncludeMarkdown(_) | Meta(_) => () case _ => executeQuick(p, pos)(slide) +case class ImageSize(width: Double, height: Double, keepAspect: Boolean) + sealed trait Slide -case class Paragraph(contents: fansi.Str) extends Slide +case class Paragraph(contents: String) extends Slide: + def centerVertical(height: Int): Paragraph = + val lines = contents.toString.count(_ == '\n') + 1 + val pad = "\n" * ((height - lines) / 2) + Paragraph(pad + contents + pad) + +object Paragraph: + def apply(str: fansi.Str): Paragraph = Paragraph(str.toString) + case class IncludeMarkdown(path: Path) extends Slide: 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, sizing: Option[ImageSize]) extends Slide +object Image: + def apply(path: Path) = new Image(path, None) + def scaled(path: Path, width: Double, height: Double, keepAspect: Boolean) = + Image(path, Some(ImageSize(width, height, keepAspect))) + case object Clear extends Slide case class Pause(millisec: Long) extends Slide case object PauseKey extends Slide @@ -103,8 +126,10 @@ case class TypedCommand[T](exec: T => String, display: String, cmd: T) extends S def show() = prompt() + Terminal.showCursor() typeCmd() print(output) + Terminal.hideCursor() def quickShow() = prompt() diff --git a/copret/src/terminal.scala b/copret/src/terminal.scala index 63f6bdf..520ad9d 100644 --- a/copret/src/terminal.scala +++ b/copret/src/terminal.scala @@ -14,9 +14,15 @@ object Terminal: %%("sh", "-c", "stty -icanon min 1 < /dev/tty")(pwd) %%("sh", "-c", "stty -echo < /dev/tty")(pwd) + extension (percent: Double) + def toPercent: String = s"${(percent * 100).toInt}%" + private[copret] lazy val jterm = org.jline.terminal.TerminalBuilder.terminal() private[copret] lazy val lineReader = LineReaderBuilder.builder().terminal(jterm).build() + def height = jterm.getSize.getRows + def width = jterm.getSize.getColumns + def waitkey(using keymap: Keymap): SlideAction = // ignore keypresses done during slide animations while Console.in.ready() do Console.in.read @@ -42,22 +48,40 @@ object Terminal: def isTmux = sys.env.contains("TMUX") || term.startsWith("screen") - def osc = if isTmux then "\u001bPtmux\u001b\u001b]" else "\u001b]" + def osc(code: String) = (if isTmux then "\u001bPtmux\u001b\u001b]" else "\u001b]") + code + st def st = if isTmux then "\u0007\u001b\\" else "\u0007" + def csi(code: String) = "\u001b[" + code - 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 hideCursor() = print(csi("?25l")) + def showCursor() = print(csi("?25h")) + def cursorTo(row: Int, col: Int) = print(csi(s"${row};${col}H")) + def clear() = print(csi("2J") + csi(";H")) - def showImageIterm(img: Path, width: String = "100%", 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 val image = Base64.getEncoder.encodeToString(read.bytes(img)) val aspect = if keepAspect then 1 else 0 - s"${osc}1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image$st" + osc(s"1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image") - def showImageKitty(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = true) = + def showImageKitty(img: Path): String = import java.util.Base64 - s"\u001b_Gf=100,t=f,a=T;${Base64.getEncoder.encodeToString(img.toString.toCharArray.map(_.toByte))}\u001b\\" + s"\u001b_Gf=100,t=f,a=T,C=1;${Base64.getEncoder.encodeToString(img.toString.toCharArray.map(_.toByte))}\u001b\\" private[copret] trait TerminalSyntax: import Terminal._