Skip to content

Instantly share code, notes, and snippets.

@atty303
Created April 19, 2022 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atty303/f050746f9c1cedf69a4f70774bd3d509 to your computer and use it in GitHub Desktop.
Save atty303/f050746f9c1cedf69a4f70774bd3d509 to your computer and use it in GitHub Desktop.
mill + JediTerm integration
import $cp.`terminal-2.68.jar`
import $ivy.`com.googlecode.lanterna:lanterna:3.1.1`
import com.jediterm.terminal.TerminalColor
import com.jediterm.terminal.model.TerminalModelListener
import scala.util.Using
import com.jediterm.terminal.model.CharBuffer
import com.jediterm.terminal.util.CharUtils
import com.jediterm.terminal.{StyledTextConsumer, TerminalMode, TextStyle}
import com.googlecode.lanterna
import com.jediterm.terminal.emulator.JediEmulator
import com.jediterm.terminal.emulator.mouse.MouseMode
import com.jediterm.terminal.model.{JediTerminal, StyleState, TerminalSelection, TerminalTextBuffer}
import com.jediterm.terminal.{CursorShape, ProcessTtyConnector, RequestOrigin, TerminalDisplay, TtyBasedArrayDataStream}
import mill._
import java.awt.Dimension
import java.nio.charset.Charset
import scala.jdk.CollectionConverters._
trait Workspace extends Module {
def run() = T.command {
val curses = new lanterna.terminal.DefaultTerminalFactory().createHeadlessTerminal()
curses.enterPrivateMode()
try {
val p1 = os.proc("cat", "README.md").spawn(mergeErrIntoOut = true)
val t1 = SubprocessTerminal(curses, 10, 10, 80, 24, p1)
val p2 = os.proc("cat", "maskfile.md").spawn(mergeErrIntoOut = true)
val t2 = SubprocessTerminal(curses, 0, 40, 80, 24, p2)
Using.resources(t1, t2) { (_, _) =>
Thread.sleep(5000)
}
} finally {
curses.exitPrivateMode()
curses.close()
}
}
}
case class SubprocessTerminal(curses: lanterna.terminal.Terminal, ox: Int, oy: Int, width: Int, height: Int, proc: os.SubProcess) extends AutoCloseable {
private val styleState = new StyleState()
styleState.setDefaultStyle(new TextStyle(TerminalColor.BLACK, TerminalColor.WHITE))
private val terminalTextBuffer = new TerminalTextBuffer(width, height, styleState, 1000, null)
private val terminalDisplay = new LanternaTerminalDisplay(curses, terminalTextBuffer, ox, oy)
private val jediTerminal = new JediTerminal(terminalDisplay, terminalTextBuffer, styleState)
jediTerminal.setModeEnabled(TerminalMode.AutoNewLine, true)
private val ttyConnector = new ProcessTtyConnector(proc.wrapped, Charset.defaultCharset()) with AutoCloseable {
override def getName: String = "Local"
}
private val jediEmulator = new JediEmulator(new TtyBasedArrayDataStream(ttyConnector), jediTerminal)
private val emulatorThread: Thread = {
val t = new Thread(() => {
Using.resource(ttyConnector) { _ =>
try {
while (!Thread.currentThread().isInterrupted && jediEmulator.hasNext) {
jediEmulator.next()
}
} catch {
case e: InterruptedException =>
case e => e.printStackTrace(System.err)
}
}
})
t.start()
t
}
def close(): Unit = emulatorThread.interrupt()
}
class LanternaTerminalDisplay(curses: lanterna.terminal.Terminal, terminalTextBuffer: TerminalTextBuffer, ox: Int, oy: Int) extends TerminalDisplay with AutoCloseable {
override def getRowCount = terminalTextBuffer.getHeight
override def getColumnCount = terminalTextBuffer.getWidth
override def setCursor(x: Int, y: Int) = curses.setCursorPosition(ox + x, oy + y)
override def setCursorShape(cursorShape: CursorShape) = ()
override def beep() = ()
override def requestResize(dimension: Dimension, requestOrigin: RequestOrigin, i: Int, i1: Int, resizeHandler: JediTerminal.ResizeHandler) = ()
override def scrollArea(scrollRegionTop: Int, scrollRegioSize: Int, dy: Int) = ()
override def setCursorVisible(shouldDrawCursor: Boolean) = curses.setCursorVisible(shouldDrawCursor)
override def setScrollingEnabled(enabled: Boolean) = ()
override def setBlinkingCursor(enabled: Boolean) = ()
override def getWindowTitle: String = ""
override def setWindowTitle(s: String) = ()
override def terminalMouseModeSet(mouseMode: MouseMode) = ()
override def getSelection: TerminalSelection = null
override def ambiguousCharsAreDoubleWidth(): Boolean = false
def close(): Unit = terminalTextBuffer.removeModelListener(modelListener)
private val modelListener: TerminalModelListener = () => {
curses.synchronized {
renderTerminalTextBuffer()
}
}
terminalTextBuffer.addModelListener(modelListener)
private def renderTerminalTextBuffer(): Unit = {
clearRegion()
terminalTextBuffer.lock()
terminalTextBuffer.processScreenLines(0, terminalTextBuffer.getHeight, new StyledTextConsumer {
override def consume(x: Int, y: Int, style: TextStyle, characters: CharBuffer, startRow: Int): Unit = {
curses.setCursorPosition(ox + x, oy + y)
// dont' draw second part(fake one) of double width character
characters.chars().iterator().asScala.foreach(n => if (n.toChar != CharUtils.DWC) curses.putCharacter(n.toChar))
curses.flush()
}
override def consumeNul(x: Int, y: Int, nulIndex: Int, style: TextStyle, characters: CharBuffer, startRow: Int): Unit = ()
override def consumeQueue(x: Int, y: Int, nulIndex: Int, startRow: Int): Unit = ()
})
terminalTextBuffer.unlock()
curses.flush()
}
private def clearRegion(): Unit = {
0.until(terminalTextBuffer.getHeight).foreach(y => {
curses.setCursorPosition(ox, oy + y)
curses.putString(" ".repeat(terminalTextBuffer.getWidth))
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment