Improve terminal escape handling
This commit is contained in:
parent
2b7a4e8d26
commit
0acbca2e62
|
@ -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) =>
|
||||||
%(cmd)(path)
|
Terminal.showCursor()
|
||||||
|
try
|
||||||
|
%(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()
|
||||||
|
|
|
@ -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"))
|
||||||
if term == "xterm-kitty" then showImageKitty(img)
|
def showCursor() = print(csi("?25h"))
|
||||||
else showImageIterm(img, width, height, keepAspect)
|
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
|
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._
|
||||||
|
|
Loading…
Reference in a new issue