Improve terminal escape handling

This commit is contained in:
Alexander Gehrke 2022-05-23 23:16:35 +02:00
parent 2b7a4e8d26
commit 0acbca2e62
2 changed files with 63 additions and 14 deletions

View file

@ -6,6 +6,7 @@ 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(using keymap: Keymap = Keymap.default) = def start(using keymap: Keymap = Keymap.default) =
Terminal.enterRawMode() Terminal.enterRawMode()
Terminal.hideCursor()
run() run()
import Presentation._ import Presentation._
@ -34,7 +35,12 @@ case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.e
else else
rec(p, pos, waitkey) rec(p, pos, waitkey)
case Interactive(cmd, path) => case Interactive(cmd, path) =>
Terminal.showCursor()
try
%(cmd)(path) %(cmd)(path)
catch case _ => ()
Terminal.hideCursor()
Terminal.clear()
rec(p, pos - 1, QuickNext) rec(p, pos - 1, QuickNext)
case Goto(target) => case Goto(target) =>
for i <- 0 until target for i <- 0 until target
@ -57,11 +63,12 @@ case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.e
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 => 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, 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 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(p, pos))
@ -80,16 +87,32 @@ object Presentation:
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)
case class ImageSize(width: Double, height: Double, keepAspect: Boolean)
sealed trait Slide 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: 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 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 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
@ -103,8 +126,10 @@ case class TypedCommand[T](exec: T => String, display: String, cmd: T) extends S
def show() = def show() =
prompt() prompt()
Terminal.showCursor()
typeCmd() typeCmd()
print(output) print(output)
Terminal.hideCursor()
def quickShow() = def quickShow() =
prompt() prompt()

View file

@ -14,9 +14,15 @@ object Terminal:
%%("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)
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 width = jterm.getSize.getColumns
def waitkey(using 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() do Console.in.read while Console.in.ready() do Console.in.read
@ -42,22 +48,40 @@ object Terminal:
def isTmux = sys.env.contains("TMUX") || term.startsWith("screen") 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 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) = 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 showImage(img: Path): Unit =
print(
if term == "xterm-kitty" then showImageKitty(img) if term == "xterm-kitty" then showImageKitty(img)
else showImageIterm(img, width, height, keepAspect) else showImageIterm(img, "100%", "100%", true)
)
def showImageIterm(img: Path, width: String = "100%", height: String = "100%", keepAspect: Boolean = 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(read.bytes(img))
val aspect = if keepAspect then 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" 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 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: private[copret] trait TerminalSyntax:
import Terminal._ import Terminal._