Large refactor and cleanup, Kitty graphics WIP
This commit is contained in:
parent
3b7afa3d90
commit
3b2d97d45a
20 changed files with 936 additions and 621 deletions
12
copret/.scalafmt.conf
Normal file
12
copret/.scalafmt.conf
Normal file
|
@ -0,0 +1,12 @@
|
|||
version = "3.7.10"
|
||||
|
||||
maxColumn = 120
|
||||
assumeStandardLibraryStripMargin = true
|
||||
align.preset = most
|
||||
align.multiline = true
|
||||
rewrite.rules = [SortModifiers]
|
||||
trailingCommas = always
|
||||
runner.dialect = scala3
|
||||
includeNoParensInSelectChains = false
|
||||
optIn.breakChainOnFirstMethodDot = false
|
||||
rewrite.scala3.insertEndMarkerMinLines = 30
|
10
copret/project.scala
Normal file
10
copret/project.scala
Normal file
|
@ -0,0 +1,10 @@
|
|||
//> using scala 3.3
|
||||
//> using packaging.packageType library
|
||||
|
||||
//> using publish.organization de.qwertyuiop
|
||||
//> using publish.name copret
|
||||
//> using publish.version 0.0.2
|
||||
//> using dep org.typelevel::cats-core:2.10.0
|
||||
//> using dep org.jline:jline:3.26.1
|
||||
//> using dep com.lihaoyi::os-lib:0.10.0
|
||||
//> using dep com.lihaoyi::fansi:0.5.0
|
|
@ -1,6 +1,8 @@
|
|||
package de.qwertyuiop.copret
|
||||
import de.qwertyuiop.copret.syntax._
|
||||
import ammonite.ops.{%%, pwd}
|
||||
|
||||
def %%(command: os.Shellable*)(cwd: os.Path = null): os.CommandResult =
|
||||
os.proc(command).call(cwd = cwd)
|
||||
|
||||
case class Theme(styles: Map[String, fansi.Attrs], figletFonts: Map[String, String]):
|
||||
def style(key: String, default: fansi.Attrs = fansi.Attrs()) =
|
||||
|
@ -28,7 +30,7 @@ object Format:
|
|||
|
||||
def center(str: String) = " " * ((columns - str.length) / 2) + str
|
||||
|
||||
def figlet(str: String, font: String) = %%("figlet", "-t", "-f", font, str)(pwd).out.string
|
||||
def figlet(str: String, font: String):String = new String(%%("figlet", "-t", "-f", font, str)().out.bytes)
|
||||
|
||||
def centerLines(str: String) = str.split("\n").map(center).mkString("\n")
|
||||
def centerBlock(str: String) =
|
||||
|
|
139
copret/src/images.scala
Normal file
139
copret/src/images.scala
Normal file
|
@ -0,0 +1,139 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
import Terminal.*
|
||||
|
||||
object KittyGraphicsProtocol:
|
||||
val MaxID = 4294967295L // max 32-bit unsigned
|
||||
|
||||
def checkSupport =
|
||||
queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
||||
.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}")
|
||||
|
||||
def showImage(img: os.Path) =
|
||||
import java.util.Base64
|
||||
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||
if image.length > 4096 then
|
||||
val chunks = image.grouped(4096).toVector
|
||||
|
||||
s"${apc}Gf=100,t=d,m=1,a=T;${chunks.head}${st}" +
|
||||
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
||||
s"${apc}Gm=0;${chunks.last}${st}"
|
||||
else s"${apc}Gf=100,t=d,a=T;${image}${st}"
|
||||
|
||||
trait Param:
|
||||
def code: String
|
||||
|
||||
enum Action(val encoded: Char) extends Param:
|
||||
case Transmit extends Action('t') // - transmit data
|
||||
case TransmitShow extends Action('T') // - transmit data and display image
|
||||
case Query extends Action('q') // - query terminal
|
||||
case Show extends Action('p') // - put (display) previous transmitted image
|
||||
case Delete(what: DelType, free: Boolean) extends Action('d') // - delete image
|
||||
case Animation extends Action('a') // - control animation
|
||||
case AnimationFrame extends Action('f') // - transmit data for animation frames
|
||||
case AnimationCompose extends Action('c') // - compose animation frames
|
||||
|
||||
def code: String = this match
|
||||
case Delete(action, free) =>
|
||||
"d=" + (if free then action.code.capitalize else action.code)
|
||||
case other => s"a=${other.encoded}"
|
||||
|
||||
enum Responses(val code: String) extends Param:
|
||||
case All extends Responses("q=0")
|
||||
case NoOk extends Responses("q=1")
|
||||
case NoError extends Responses("q=2")
|
||||
|
||||
enum Format(val code: String) extends Param:
|
||||
case RGB(width: Int, height: Int) extends Format(s"f=24,s=${width},v=${height}")
|
||||
case RGBA(width: Int, height: Int) extends Format(s"f=32,s=${width},v=${height}")
|
||||
case PNG extends Format("f=100")
|
||||
|
||||
enum Medium(val code: String) extends Param:
|
||||
case Direct extends Medium("t=d")
|
||||
case File extends Medium("t=f")
|
||||
case TempFile extends Medium("t=t")
|
||||
case SharedMemory extends Medium("t=s")
|
||||
|
||||
enum AnimationState(val code: String) extends Param:
|
||||
case Stop extends AnimationState("s=1")
|
||||
case WaitFrame extends AnimationState("s=2")
|
||||
case Run extends AnimationState("s=3")
|
||||
|
||||
private def delType(what: Char, freeData: Boolean) =
|
||||
s"a=d,d=${if freeData then what.toUpper else what}"
|
||||
enum DelType extends Param:
|
||||
case AllPlacements
|
||||
case ById(id: Int)
|
||||
case ByNumber(num: Int)
|
||||
case AtCursor
|
||||
case Frames
|
||||
case AtPosition(x: Int, y: Int, z: Int = -1)
|
||||
case ByIdRange(from: Int, to: Int)
|
||||
case ByColumn(x: Int)
|
||||
case ByRow(y: Int)
|
||||
case ByZIndex(z: Int)
|
||||
|
||||
def code: String = this match
|
||||
case AllPlacements => "a"
|
||||
case ById(id) => s"i,i=$id"
|
||||
case ByNumber(num) => s"n,I=$num"
|
||||
case AtCursor => "c"
|
||||
case Frames => "f"
|
||||
case AtPosition(x, y, z) =>
|
||||
val (what, zarg) = if z >= 0 then ("q", s",z=$z") else ("p", "")
|
||||
what + s",x=$x,y=$y" + zarg
|
||||
case ByIdRange(x, y) => s"r,x=$x,y=$y"
|
||||
case ByColumn(x) => s"x,x=$x"
|
||||
case ByRow(y) => s"y,y=$y"
|
||||
case ByZIndex(z) => s"z,z=$z"
|
||||
|
||||
def optionInts(values: (Int, Char)*) =
|
||||
values.flatMap((value, key) => Option.when(value >= 0)(s"${key}=${value}")).mkString(",")
|
||||
|
||||
case class SimpleParam(val code: String) extends Param
|
||||
|
||||
def Filesize(bytes: Int) = SimpleParam(s"S=$bytes")
|
||||
def Offset(bytes: Int) = SimpleParam(s"O=$bytes")
|
||||
def ImageID(id: Int) = SimpleParam(s"i=$id")
|
||||
def ImageNumber(num: Int) = SimpleParam(s"I=$num")
|
||||
def PlacementID(id: Int) = SimpleParam(s"p=$id")
|
||||
def ZIndex(zindex: Int) = SimpleParam(s"z=$zindex")
|
||||
def Parent(id: Int) = SimpleParam(s"P=$id")
|
||||
def ParentPlacement(id: Int) = SimpleParam(s"Q=$id")
|
||||
def RelativeOffset(x: Int = -1, y: Int = -1) = SimpleParam(
|
||||
optionInts((x, 'H'), (y, 'V')),
|
||||
)
|
||||
|
||||
val CompressZlib = SimpleParam("o=z")
|
||||
val ContinueChunk = SimpleParam("m=1")
|
||||
val DontMoveCursor = SimpleParam("C=1")
|
||||
val UnicodePlaceholder = SimpleParam("U=1")
|
||||
|
||||
def Crop(
|
||||
originX: Int = -1,
|
||||
originY: Int = -1,
|
||||
width: Int = -1,
|
||||
height: Int = -1,
|
||||
) =
|
||||
SimpleParam(
|
||||
optionInts((originX, 'x'), (originY, 'y'), (width, 'w'), (height, 'h')),
|
||||
)
|
||||
|
||||
def AnimFrameUpdate(
|
||||
originX: Int = -1,
|
||||
originY: Int = -1,
|
||||
baseID: Int = -1,
|
||||
editID: Int = -1,
|
||||
) =
|
||||
SimpleParam:
|
||||
optionInts((originX, 'x'), (originY, 'y'), (baseID, 'c'), (editID, 'r'))
|
||||
|
||||
def AnimGap(millis: Int) = SimpleParam(s"z=$millis")
|
||||
val AnimUseOverwrite = SimpleParam("X=1")
|
||||
|
||||
def CellOffset(x: Int = -1, y: Int = -1) = SimpleParam:
|
||||
optionInts((x, 'X'), (y, 'Y'))
|
||||
|
||||
def DisplayCells(cols: Int = -1, rows: Int = -1) = SimpleParam:
|
||||
optionInts((cols, 'c'), (rows, 'r'))
|
||||
end KittyGraphicsProtocol
|
|
@ -1,6 +1,6 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
import ammonite.ops.Path
|
||||
import os.Path
|
||||
|
||||
enum SlideAction:
|
||||
case Start
|
||||
|
@ -15,50 +15,59 @@ enum SlideAction:
|
|||
case Other(code: List[Int])
|
||||
|
||||
def show: String = this match
|
||||
case Start => "go to first slide"
|
||||
case Start => "go to first slide"
|
||||
case Goto(slideIndex: Int) => s"jump directly to slide $slideIndex"
|
||||
case GotoSelect => "jump to slide"
|
||||
case Prev => "previous slide (skip animations)"
|
||||
case Next => "next slide"
|
||||
case QuickNext => "next slide (skip animations)"
|
||||
case Quit => "quit"
|
||||
case Help => "show help"
|
||||
case Interactive(cmd: Vector[String], wd: Path) => s"execute command \"${cmd.mkString(" ")}\""
|
||||
case GotoSelect => "jump to slide"
|
||||
case Prev => "previous slide (skip animations)"
|
||||
case Next => "next slide"
|
||||
case QuickNext => "next slide (skip animations)"
|
||||
case Quit => "quit"
|
||||
case Help => "show help"
|
||||
case Interactive(cmd: Vector[String], wd: Path) =>
|
||||
s"execute command \"${cmd.mkString(" ")}\""
|
||||
case Other(code: List[Int]) => s"Unknown key sequence: $code"
|
||||
|
||||
|
||||
object SlideAction:
|
||||
def runForeground(cmd: String*)(implicit wd: Path) = Interactive(cmd.toVector, wd)
|
||||
def runForeground(cmd: String*)(implicit wd: Path) =
|
||||
Interactive(cmd.toVector, wd)
|
||||
|
||||
import SlideAction.*
|
||||
|
||||
case class Keymap(bindings: Map[Key, SlideAction]):
|
||||
private val lookup = bindings.map((k,v) => k.codes -> v)
|
||||
def apply(keycode: List[Int]): SlideAction = lookup.getOrElse(keycode, Other(keycode))
|
||||
private val lookup = bindings.map((k, v) => k.codes -> v)
|
||||
def apply(keycode: List[Int]): SlideAction =
|
||||
lookup.getOrElse(keycode, Other(keycode))
|
||||
|
||||
def extend(newBindings: Map[Key, SlideAction]) = Keymap(bindings ++ newBindings)
|
||||
def extend(newBindings: Map[Key, SlideAction]) = Keymap(
|
||||
bindings ++ newBindings
|
||||
)
|
||||
def ++(newBindings: Map[Key, SlideAction]) = extend(newBindings)
|
||||
def help: String =
|
||||
bindings.toSeq.sortBy(_._2.toString).map((k,v) => k.show.padTo(8, ' ') + " " + v.show).mkString("\n")
|
||||
bindings.toSeq
|
||||
.sortBy(_._2.toString)
|
||||
.map((k, v) => k.show.padTo(8, ' ') + " " + v.show)
|
||||
.mkString("\n")
|
||||
|
||||
object Keymap:
|
||||
val empty = Keymap(Map())
|
||||
val default = Keymap(Map(
|
||||
Key.Up -> Prev,
|
||||
Key.Left -> Prev,
|
||||
Key.PageUp -> Prev,
|
||||
Key('k') -> Prev,
|
||||
Key.Space -> Next,
|
||||
Key('j') -> Next,
|
||||
Key.Down -> QuickNext,
|
||||
Key.Right -> QuickNext,
|
||||
Key.PageDown -> QuickNext,
|
||||
Key('q') -> Quit,
|
||||
Key('g') -> Start,
|
||||
Key.Enter -> Start,
|
||||
Key('s') -> GotoSelect,
|
||||
Key('?') -> Help,
|
||||
))
|
||||
val empty = Keymap(Map())
|
||||
// format: off
|
||||
val default = Keymap(Map(
|
||||
Key.Up -> Prev,
|
||||
Key.Left -> Prev,
|
||||
Key.PageUp -> Prev,
|
||||
Key('k') -> Prev,
|
||||
Key.Space -> Next,
|
||||
Key('j') -> Next,
|
||||
Key.Down -> QuickNext,
|
||||
Key.Right -> QuickNext,
|
||||
Key.PageDown -> QuickNext,
|
||||
Key('q') -> Quit,
|
||||
Key('g') -> Start,
|
||||
Key.Enter -> Start,
|
||||
Key('s') -> GotoSelect,
|
||||
Key('?') -> Help,
|
||||
))
|
||||
//format: on
|
||||
|
||||
enum Key:
|
||||
case Code(name: String, codepoints: List[Int])
|
||||
|
@ -66,13 +75,13 @@ enum Key:
|
|||
|
||||
def codes: List[Int] =
|
||||
this match
|
||||
case Code(_, cp) => cp
|
||||
case Code(_, cp) => cp
|
||||
case Printable(char) => List(char.toInt)
|
||||
|
||||
def show: String =
|
||||
this match
|
||||
case Code(name, _) => name
|
||||
case Printable(c) => c.toString
|
||||
case Printable(c) => c.toString
|
||||
|
||||
object Key:
|
||||
def apply(char: Char): Key = Printable(char)
|
||||
|
|
16
copret/src/main.scala
Normal file
16
copret/src/main.scala
Normal file
|
@ -0,0 +1,16 @@
|
|||
package de.qwertyuiop.copret
|
||||
|
||||
import de.qwertyuiop.copret.Terminal.*
|
||||
|
||||
@main def main =
|
||||
enterRawMode()
|
||||
// print(queryTerm(s"${apc}Gi=31,s=10,v=2,t=s;L3NvbWUtc2hhcmVkLW1lbW9yeS1uYW1lCg==\u001b\\"))
|
||||
// print(queryTerm(s"\u001b[6n"))
|
||||
val canHazGraphics = queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
||||
println(canHazGraphics.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}"))
|
||||
// println(
|
||||
// KittyGraphicsProtocol.showImage(
|
||||
// os.Path("/home/crater2150/org/opencolloq/gittalk/img/repo-graph.png"),
|
||||
// ),
|
||||
// )
|
||||
// println(getSize)
|
|
@ -1,103 +1,121 @@
|
|||
package de.qwertyuiop.copret
|
||||
import ammonite.ops._
|
||||
import Terminal._
|
||||
import syntax._
|
||||
import os.Path
|
||||
|
||||
case class Presentation(slides: Vector[Slide], meta: Map[String, String] = Map.empty):
|
||||
import Terminal.*
|
||||
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()
|
||||
Thread.sleep(1000)
|
||||
run()
|
||||
|
||||
import Presentation._
|
||||
def run()(using keymap: Keymap) =
|
||||
import SlideAction.*
|
||||
@annotation.tailrec def rec(p: Presentation, pos: Int, action: SlideAction): Unit =
|
||||
@annotation.tailrec
|
||||
def rec(pos: Int, action: SlideAction): Unit =
|
||||
|
||||
inline def redraw() = rec(pos - 1, QuickNext)
|
||||
|
||||
inline def navigate(pos: Int, condition: Boolean, direction: Int)(
|
||||
executor: Int => Slide => Unit,
|
||||
) =
|
||||
if condition then
|
||||
executor(pos + direction)(slides(pos))
|
||||
rec(pos + direction, waitkey)
|
||||
else rec(pos, waitkey)
|
||||
|
||||
inline def runInteractive(cmd: Vector[String], path: Path) =
|
||||
Terminal.showCursor()
|
||||
try os.proc(cmd).call(cwd = path)
|
||||
catch case _ => ()
|
||||
Terminal.hideCursor()
|
||||
Terminal.clear()
|
||||
redraw()
|
||||
|
||||
action match
|
||||
case Start =>
|
||||
executeSlide(p, 0)()
|
||||
rec(p, 0, waitkey)
|
||||
case Next =>
|
||||
if pos + 1 < p.slides.size then
|
||||
executeSlide(p, pos + 1)()
|
||||
rec(p, pos + 1, waitkey)
|
||||
else
|
||||
rec(p, pos, waitkey)
|
||||
case QuickNext =>
|
||||
if pos + 1 < p.slides.size then
|
||||
executeQuick(p, pos + 1)()
|
||||
rec(p, pos + 1, waitkey)
|
||||
else rec(p, pos, waitkey)
|
||||
case Prev =>
|
||||
if pos > 0 then
|
||||
executeQuick(p, pos - 1)()
|
||||
rec(p, pos - 1, waitkey)
|
||||
else
|
||||
rec(p, pos, waitkey)
|
||||
case Interactive(cmd, path) =>
|
||||
Terminal.showCursor()
|
||||
try
|
||||
%(cmd)(path)
|
||||
catch case _ => ()
|
||||
Terminal.hideCursor()
|
||||
Terminal.clear()
|
||||
rec(p, pos - 1, QuickNext)
|
||||
case Start => navigate(0, true, 0)(executeSlide)
|
||||
case Next => navigate(pos, pos + 1 < slides.size, 1)(executeSlide)
|
||||
case QuickNext => navigate(pos, pos + 1 < slides.size, 1)(executeQuick)
|
||||
case Prev => navigate(pos, pos > 0, -1)(executeQuick)
|
||||
case Interactive(cmd, path) => runInteractive(cmd, path)
|
||||
|
||||
case Goto(target) =>
|
||||
for i <- 0 until target
|
||||
do executeSilent(p, i)()
|
||||
rec(p, target - 1, QuickNext)
|
||||
do executeSilent(i)()
|
||||
rec(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)
|
||||
promptSlide() match
|
||||
case Some(i) => rec(pos, Goto(i))
|
||||
case None => redraw()
|
||||
|
||||
case Help =>
|
||||
Terminal.clear()
|
||||
println(keymap.help)
|
||||
waitkey
|
||||
rec(p, pos - 1, QuickNext)
|
||||
redraw()
|
||||
|
||||
case Other(codes) =>
|
||||
Terminal.printStatus(action.show)
|
||||
rec(p, pos, waitkey)
|
||||
rec(pos, waitkey)
|
||||
|
||||
case Quit =>
|
||||
()
|
||||
rec(this, 0, Start)
|
||||
Terminal.showCursor()
|
||||
end match
|
||||
end rec
|
||||
rec(0, Start)
|
||||
end run
|
||||
|
||||
def promptSlide() =
|
||||
val maxSlide = slides.size - 1
|
||||
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)",
|
||||
)
|
||||
|
||||
object Presentation:
|
||||
def executeSlide(p: Presentation, pos: Int)(slide: Slide = p.slides(pos)): Unit = slide match
|
||||
case Paragraph(contents) => println(contents)
|
||||
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, 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))
|
||||
case lios @ LazyIOSlide(_, display) => executeSlide(p, pos)(lios.genSlide())
|
||||
case Meta(genSlide) => executeSlide(p, pos)(genSlide(p, pos))
|
||||
def executeSlide(pos: Int)(
|
||||
slide: Slide = slides(pos),
|
||||
): Unit = slide match
|
||||
case Paragraph(contents) => println(contents)
|
||||
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, None) => println(KittyGraphicsProtocol.showImage(file))
|
||||
case Image(file, Some(ImageSize(w, h, aspect))) =>
|
||||
println(KittyGraphicsProtocol.showImage(file)) // TODO
|
||||
case cmd: TypedCommand[_] => cmd.show()
|
||||
case Silent(actions) => actions()
|
||||
case Group(slides) => slides.foreach(executeSlide(pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeSlide(pos)(lios.genSlide())
|
||||
case Meta(genSlide) => executeSlide(pos)(genSlide(this, pos))
|
||||
|
||||
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)
|
||||
def executeQuick(pos: Int)(
|
||||
slide: Slide = slides(pos),
|
||||
): Unit = slide match
|
||||
case Pause(msec) => ()
|
||||
case PauseKey => ()
|
||||
case cmd: TypedCommand[_] => cmd.quickShow()
|
||||
case Group(slides) => slides.foreach(executeQuick(pos))
|
||||
case lios @ LazyIOSlide(_, display) => executeQuick(pos)(lios.genSlide())
|
||||
case _ => executeSlide(pos)(slide)
|
||||
|
||||
def executeSilent(pos: Int)(
|
||||
slide: Slide = slides(pos),
|
||||
): Unit = slide match
|
||||
case cmd: TypedCommand[_] => cmd.force()
|
||||
case Group(slides) => slides.foreach(executeSilent(pos))
|
||||
case lios @ LazyIOSlide(_, display) =>
|
||||
executeSilent(pos)(lios.genSlide())
|
||||
case Paragraph(_) | Image(_, _) | Clear | IncludeMarkdown(_) | Meta(_) => ()
|
||||
case _ => executeQuick(pos)(slide)
|
||||
end Presentation
|
||||
|
||||
case class ImageSize(width: Double, height: Double, keepAspect: Boolean)
|
||||
|
||||
|
@ -105,7 +123,7 @@ sealed trait 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)
|
||||
val pad = "\n" * ((height - lines) / 2)
|
||||
Paragraph(pad + contents + pad)
|
||||
|
||||
object Paragraph:
|
||||
|
@ -113,46 +131,56 @@ object Paragraph:
|
|||
|
||||
case class IncludeMarkdown(path: Path) extends Slide:
|
||||
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 os.pwd).block
|
||||
|
||||
case class Image(path: Path, sizing: Option[ImageSize]) extends Slide
|
||||
object Image:
|
||||
def apply(path: Path) = new Image(path, None)
|
||||
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
|
||||
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, cmdIsHidden: Boolean, outputIsHidden: Boolean) extends Slide:
|
||||
case class TypedCommand[T](
|
||||
exec: T => String,
|
||||
display: String,
|
||||
cmd: T,
|
||||
cmdIsHidden: Boolean,
|
||||
outputIsHidden: Boolean,
|
||||
) extends Slide:
|
||||
private lazy val _output = exec(cmd)
|
||||
def output = _output
|
||||
def output = _output
|
||||
|
||||
infix def showing (s: String): TypedCommand[T] = TypedCommand(exec, s, cmd)
|
||||
infix def showing(s: String): TypedCommand[T] = TypedCommand(exec, s, cmd)
|
||||
|
||||
def show() =
|
||||
force()
|
||||
if ! cmdIsHidden then
|
||||
if !cmdIsHidden then
|
||||
prompt()
|
||||
Terminal.showCursor()
|
||||
typeCmd()
|
||||
if ! outputIsHidden then
|
||||
print(output)
|
||||
if ! cmdIsHidden then
|
||||
Terminal.hideCursor()
|
||||
if !outputIsHidden then print(output)
|
||||
if !cmdIsHidden then Terminal.hideCursor()
|
||||
|
||||
def quickShow() =
|
||||
force()
|
||||
if ! cmdIsHidden then
|
||||
if !cmdIsHidden then
|
||||
prompt()
|
||||
println(display)
|
||||
if ! outputIsHidden then
|
||||
print(output)
|
||||
if !outputIsHidden then print(output)
|
||||
|
||||
def prompt() = print(fansi.Color.LightGreen("user@host % "))
|
||||
def force() = _output
|
||||
def force() = _output
|
||||
|
||||
def display(s: String) = copy(display = s)
|
||||
|
||||
private def typeCmd() =
|
||||
for char <- display do
|
||||
|
@ -162,7 +190,7 @@ case class TypedCommand[T](exec: T => String, display: String, cmd: T, cmdIsHidd
|
|||
|
||||
/* 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 then copy(display = altDisplay, exec = (_:T) => "")
|
||||
if condition then 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,
|
||||
|
@ -170,19 +198,19 @@ case class TypedCommand[T](exec: T => String, display: String, cmd: T, cmdIsHidd
|
|||
def replaceIf(condition: Boolean)(tc: TypedCommand[_]): TypedCommand[_] =
|
||||
if condition then tc.showing(display)
|
||||
else this
|
||||
|
||||
end TypedCommand
|
||||
|
||||
object TypedCommand:
|
||||
val shell = sys.env.getOrElse("SHELL", "sh")
|
||||
|
||||
def run(using Path): Vector[String] => String =
|
||||
c => safe_%%(c)
|
||||
c => runProcess(c)
|
||||
|
||||
def runShell(using Path): Vector[String] => String =
|
||||
c => safe_%%(Vector(shell, "-c", c.mkString(" ")))
|
||||
c => runProcess(Vector(shell, "-c", c.mkString(" ")))
|
||||
|
||||
def runInteractive(using Path): Vector[String] => String =
|
||||
c => { %(c); ""}
|
||||
c => { os.proc(c).call(); "" }
|
||||
|
||||
def apply(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(run, cmd.mkString(" "), cmd.toVector)
|
||||
|
@ -190,7 +218,6 @@ object TypedCommand:
|
|||
def apply[T](exec: T => String, display: String, cmd: T) =
|
||||
new TypedCommand(exec, display, cmd, false, false)
|
||||
|
||||
|
||||
def shell(cmd: String*)(using Path): TypedCommand[Vector[String]] =
|
||||
TypedCommand(runShell, cmd.mkString(" "), cmd.toVector)
|
||||
|
||||
|
@ -200,11 +227,9 @@ object TypedCommand:
|
|||
def interactive(cmd: String*)(using 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){}
|
||||
|
||||
def apply[T](doStuff: => T) = new Silent(() => doStuff) {}
|
||||
|
||||
case class Group(slides: List[Slide]) extends Slide
|
||||
object Group:
|
||||
|
@ -218,7 +243,8 @@ trait SlideSyntax:
|
|||
private[copret] class LazyIOSlideBuilder[T](runOnce: => T):
|
||||
def useIn(display: T => Slide) = LazyIOSlide(() => runOnce, display)
|
||||
|
||||
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = LazyIOSlideBuilder(runOnce)
|
||||
|
||||
def prepare[T](runOnce: => T): LazyIOSlideBuilder[T] = LazyIOSlideBuilder(
|
||||
runOnce,
|
||||
)
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package de.qwertyuiop.copret
|
||||
import syntax.*
|
||||
import ammonite.ops.{Path, %, %%, pwd, ImplicitWd}
|
||||
import os.Path
|
||||
|
||||
trait Templates:
|
||||
def titleLine(title: String)(using theme: Theme) = Paragraph(
|
||||
"\n" + Format.figlet(title, theme.font("titleLine", "pagga")).block.blue + "\n"
|
||||
)
|
||||
"\n" + Format
|
||||
.figlet(title, theme.font("titleLine", "pagga"))
|
||||
.block
|
||||
.blue + "\n"
|
||||
)
|
||||
|
||||
def header(using theme: Theme) = Meta((p, pos) => {
|
||||
val left = p.meta.getOrElse("author", "")
|
||||
|
@ -14,12 +17,21 @@ trait Templates:
|
|||
theme.style("titleLine")(Format.distribute(left, center, right)).text
|
||||
})
|
||||
|
||||
def slide(title: String)(slides: Slide*)(using Theme) = Group(Clear :: header :: titleLine(title) :: slides.toList)
|
||||
def slide(slides: Slide*)(using Theme) = Group(Clear :: header :: slides.toList)
|
||||
def slide(title: String)(slides: Slide*)(using Theme) = Group(
|
||||
Clear :: header :: titleLine(title) :: slides.toList
|
||||
)
|
||||
def slide(slides: Slide*)(using Theme) = Group(
|
||||
Clear :: header :: slides.toList
|
||||
)
|
||||
|
||||
def markdown(title: String, content: Path)(using Theme) = slide(title)(
|
||||
Paragraph(
|
||||
%%%("/usr/bin/mdcat", "--columns", (columns * 0.8).toInt.toString, content.toString)(using ImplicitWd.implicitCwd).block
|
||||
%%%(
|
||||
"/usr/bin/mdcat",
|
||||
"--columns",
|
||||
(columns * 0.8).toInt.toString,
|
||||
content.toString
|
||||
)(using os.pwd).block
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -1,98 +1,112 @@
|
|||
package de.qwertyuiop.copret
|
||||
import ammonite.ops.{Path, ShelloutException, pwd, read, %, %%}
|
||||
import os.Path
|
||||
import org.jline.terminal.TerminalBuilder
|
||||
import org.jline.reader.LineReaderBuilder
|
||||
import scala.io.StdIn
|
||||
import cats.*
|
||||
import scala.collection.IndexedSeqView
|
||||
|
||||
object Terminal:
|
||||
def safe_%%(cmd: Vector[String])(using Path): String =
|
||||
try
|
||||
%%(cmd).out.string
|
||||
catch
|
||||
case e: ShelloutException => e.result.err.string
|
||||
def runProcess(cmd: Vector[String])(using cwd: os.Path): String =
|
||||
try os.proc(cmd.map(identity)).call(cwd = cwd).out.text()
|
||||
catch case e: os.SubprocessException => e.result.err.text()
|
||||
|
||||
def tryCmd[T](cmd: => T, default: => T) =
|
||||
try cmd
|
||||
catch case e: os.SubprocessException => default
|
||||
|
||||
def enterRawMode(): Unit =
|
||||
%%("sh", "-c", "stty -icanon min 1 < /dev/tty")(pwd)
|
||||
%%("sh", "-c", "stty -echo < /dev/tty")(pwd)
|
||||
os.proc("sh", "-c", "stty -icanon min 1 < /dev/tty")
|
||||
.call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)
|
||||
os.proc("sh", "-c", "stty -echo < /dev/tty").call(stdin = os.Inherit, stdout = os.Inherit, stderr = os.Inherit)
|
||||
|
||||
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()
|
||||
|
||||
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 =
|
||||
def waitkey(implicit keymap: Keymap): SlideAction =
|
||||
// ignore keypresses done during slide animations
|
||||
while Console.in.ready() do Console.in.read
|
||||
while (System.in.available() > 0) System.in.read
|
||||
|
||||
var key = scala.collection.mutable.ArrayBuffer[Int]()
|
||||
key += Console.in.read
|
||||
while Console.in.ready do
|
||||
key += Console.in.read
|
||||
key += System.in.read
|
||||
while (System.in.available() > 0)
|
||||
key += System.in.read
|
||||
keymap(key.toList)
|
||||
|
||||
def printStatus(msg: String): Unit =
|
||||
cursorTo(height, 1)
|
||||
print(msg)
|
||||
def queryTerm(query: String): String =
|
||||
val ttyIn = new java.io.FileInputStream("/dev/tty")
|
||||
val ttyOut = new java.io.PrintStream(new java.io.FileOutputStream("/dev/tty"))
|
||||
|
||||
ttyOut.println(query)
|
||||
var response = scala.collection.mutable.ArrayBuffer[Int]()
|
||||
response += ttyIn.read()
|
||||
while (ttyIn.available > 0 && response.last != '\u0007')
|
||||
response += ttyIn.read()
|
||||
new String(response.toArray, 0, response.length)
|
||||
|
||||
case class EscapeSequence()
|
||||
def parseEscape(sequence: String): Option[EscapeSequence] = ???
|
||||
|
||||
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) then
|
||||
println(error(input))
|
||||
prompt(prefix, parse)(retry, error)
|
||||
else result
|
||||
error: String => String = in => s"Invalid input: $in",
|
||||
): T =
|
||||
val input = lineReader.readLine(prefix + " ")
|
||||
val result = parse(input)
|
||||
if retry(result, input) then
|
||||
println(error(input))
|
||||
prompt(prefix, parse)(retry, error)
|
||||
else result
|
||||
|
||||
val term = sys.env("TERM")
|
||||
def isTmux = sys.env.contains("TMUX") || sys.env("TERM").startsWith("screen")
|
||||
|
||||
def isTmux = sys.env.contains("TMUX") || term.startsWith("screen")
|
||||
def csi = if (isTmux) "\u001bPtmux\u001b\u001b[" else "\u001b["
|
||||
def osc = if (isTmux) "\u001bPtmux\u001b\u001b]" else "\u001b]"
|
||||
def apc = if (isTmux) "\u001bPtmux;\u001b\u001b_" else "\u001b_"
|
||||
def st = if (isTmux) "\u0007\u001b\\" else "\u0007"
|
||||
|
||||
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 hideCursor() = print(s"${csi}?25l")
|
||||
def showCursor() = print(s"${csi}?25h")
|
||||
def cursorTo(row: Int, col: Int) = print(s"${csi}${row};${col}H")
|
||||
def clear() = print(s"${csi}2J${csi};H")
|
||||
def printStatus(msg: String): Unit =
|
||||
cursorTo(jterm.getSize.getColumns, 1)
|
||||
print(msg)
|
||||
|
||||
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)
|
||||
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 =
|
||||
def showImage(
|
||||
img: os.Path,
|
||||
width: String = "100%",
|
||||
height: String = "100%",
|
||||
keepAspect: Boolean = true,
|
||||
) =
|
||||
import java.util.Base64
|
||||
val image = Base64.getEncoder.encodeToString(read.bytes(img))
|
||||
val aspect = if keepAspect then 1 else 0
|
||||
osc(s"1337;File=inline=1;width=$width;height=$height;preserveAspectRatio=$aspect:$image")
|
||||
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||
val aspect = if (keepAspect) 1 else 0 // TODO
|
||||
if image.length > 4096 then
|
||||
val chunks = image.grouped(4096).toVector
|
||||
|
||||
def showImageKitty(img: Path): String =
|
||||
import java.util.Base64
|
||||
s"\u001b_Gf=100,t=f,a=T,C=1;${Base64.getEncoder.encodeToString(img.toString.toCharArray.map(_.toByte))}\u001b\\"
|
||||
s"${apc}Gf=100,t=d,m=1,a=T;${chunks.head}${st}" +
|
||||
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
||||
s"${apc}Gm=0;${chunks.last}${st}"
|
||||
else s"${apc}Gf=100,t=d,a=T;${image}${st}"
|
||||
|
||||
def getSize = queryTerm(s"${csi}s${csi}999;999H${csi}6n${csi}u")
|
||||
|
||||
private val SizeResponse = """\u001b\[4;(\d+);(\d+)t""".r
|
||||
|
||||
case class PixelSize(width: Int, height: Int)
|
||||
def getSizePixels: Option[PixelSize] = queryTerm(s"${csi}14t") match
|
||||
case SizeResponse(rows, cols) => Some(PixelSize(cols.toInt, rows.toInt))
|
||||
case _ => None
|
||||
|
||||
end Terminal
|
||||
|
||||
private[copret] trait TerminalSyntax:
|
||||
import Terminal._
|
||||
|
||||
def %%%(cmd: String*)(using Path) = safe_%%(cmd.toVector)
|
||||
def columns = jterm.getSize.getColumns
|
||||
def rows = jterm.getSize.getRows
|
||||
|
||||
def %%%(cmd: String*)(using cwd: os.Path): String = runProcess(cmd.toVector)
|
||||
def columns = jterm.getSize.getColumns
|
||||
def rows = jterm.getSize.getRows
|
||||
|
||||
/* vim:set tw=120: */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue