Improve terminal escape handling
This commit is contained in:
		
							parent
							
								
									2b7a4e8d26
								
							
						
					
					
						commit
						0acbca2e62
					
				
					 2 changed files with 63 additions and 14 deletions
				
			
		| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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._
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue