WIP: Rendering/UI refactor.
This commit is contained in:
		@@ -1,49 +1,42 @@
 | 
			
		||||
import std/[json, options, os, paths, strutils, times, unicode]
 | 
			
		||||
import cliutils, docopt, illwill, mpv
 | 
			
		||||
import std/[json, options, os, paths, sequtils, strutils, terminal, times,
 | 
			
		||||
            unicode]
 | 
			
		||||
import cliutils, docopt, namespaced_logging, mpv
 | 
			
		||||
 | 
			
		||||
import wdiwtlt/[cliconstants, commandbuffer, logging, medialibrary, models,
 | 
			
		||||
                scrolltext]
 | 
			
		||||
import wdiwtlt/[cliconstants, commandbuffer, logging, medialibrary,
 | 
			
		||||
                messagebuffer, models, nonblockingio, scrolltext, usage]
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  CliMode {.pure.} = enum Direct, Command
 | 
			
		||||
 | 
			
		||||
  ScrollingDisplays* = ref object
 | 
			
		||||
    playing: ScrollText
 | 
			
		||||
    status: ScrollText
 | 
			
		||||
    statusDismissal: Option[DateTime]
 | 
			
		||||
  MessageBuffers* = ref object
 | 
			
		||||
    playing: MessageBuffer[ScrollText]
 | 
			
		||||
    status: MessageBuffer[ScrollText]
 | 
			
		||||
    longform: MessageBuffer[string]
 | 
			
		||||
    input: MessageBuffer[string]
 | 
			
		||||
 | 
			
		||||
  History = ref object
 | 
			
		||||
    entries: seq[string]
 | 
			
		||||
    idx: int
 | 
			
		||||
 | 
			
		||||
  RenderDimensions = ref object
 | 
			
		||||
    width, height: int
 | 
			
		||||
    playing: tuple[x, y: int]
 | 
			
		||||
    status: tuple[x, y: int]
 | 
			
		||||
    input: tuple[x, y: int]
 | 
			
		||||
    logs: tuple[x, y: int]
 | 
			
		||||
 | 
			
		||||
  CliContext = ref object
 | 
			
		||||
    cfg: CombinedConfig
 | 
			
		||||
    cmd: CommandBuffer
 | 
			
		||||
    cmdVisibleIdx: int              # first visible character in the cmd buffer
 | 
			
		||||
    curMediaFile: Option[MediaFile]
 | 
			
		||||
    d: ScrollingDisplays
 | 
			
		||||
    dims: RenderDimensions
 | 
			
		||||
    frame: int
 | 
			
		||||
    lib: MediaLibrary
 | 
			
		||||
    log: Option[Logger]
 | 
			
		||||
    logs: seq[LogMessage]
 | 
			
		||||
    logThreshold: Level
 | 
			
		||||
    mode: CliMode
 | 
			
		||||
    msgs: MessageBuffers
 | 
			
		||||
    mpv: ptr handle
 | 
			
		||||
    msgDuration: Duration
 | 
			
		||||
    stop*: bool
 | 
			
		||||
    tb: TerminalBuffer
 | 
			
		||||
 | 
			
		||||
  CursorType {.pure.} = enum
 | 
			
		||||
    Block, BlockBlink, BarBlink, Underline
 | 
			
		||||
    width*: int
 | 
			
		||||
    height*: int
 | 
			
		||||
 | 
			
		||||
var ctx {.threadvar.}: CliContext
 | 
			
		||||
 | 
			
		||||
const FRAME_DUR_MS = 200
 | 
			
		||||
 | 
			
		||||
proc initMpv(): ptr handle =
 | 
			
		||||
  result = mpv.create()
 | 
			
		||||
@@ -66,47 +59,18 @@ proc initMpv(): ptr handle =
 | 
			
		||||
 | 
			
		||||
proc cleanup() =
 | 
			
		||||
  terminate_destroy(ctx.mpv)
 | 
			
		||||
  illWillDeinit()
 | 
			
		||||
  setNonBlockingTty(false)
 | 
			
		||||
  echo ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[
 | 
			
		||||
proc exitHandlerHook() {.noconv.} =
 | 
			
		||||
  # This is called when the user presses Ctrl+C
 | 
			
		||||
  # or when the program exits normally.
 | 
			
		||||
  echo "Ctrl-C"
 | 
			
		||||
  terminate_destroy(ctx.mpv)
 | 
			
		||||
  illWillDeinit()
 | 
			
		||||
  echo ""
 | 
			
		||||
  quit(QuitSuccess)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc clearLine(ctx: var CliContext, y: int) =
 | 
			
		||||
  ctx.tb.write(0, y, ' '.repeat(ctx.tb.width))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc writeAt(ctx: var CliContext, pos: tuple[x, y: int], text: string) =
 | 
			
		||||
  ctx.tb.write(pos.x, pos.y, text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc nextTick(ctx: var CliContext) =
 | 
			
		||||
  if ctx.d.statusDismissal.isSome and ctx.d.statusDismissal.get < now():
 | 
			
		||||
    ctx.d.status.text = ""
 | 
			
		||||
    ctx.d.statusDismissal = none[DateTime]()
 | 
			
		||||
    ctx.clearLine(ctx.dims.status.y)
 | 
			
		||||
 | 
			
		||||
  discard ctx.d.playing.nextTick
 | 
			
		||||
  discard ctx.d.status.nextTick
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc setMode(ctx: var CliContext, mode: CliMode) =
 | 
			
		||||
  ctx.mode = mode
 | 
			
		||||
  case mode
 | 
			
		||||
  of CliMode.Direct:
 | 
			
		||||
    hideCursor()
 | 
			
		||||
    ctx.tb.clear()
 | 
			
		||||
  of CliMode.Command:
 | 
			
		||||
    showCursor()
 | 
			
		||||
    ctx.cmd.mode = EditMode.Insert
 | 
			
		||||
 | 
			
		||||
]#
 | 
			
		||||
 | 
			
		||||
proc initContext(args: Table[string, Value]): CliContext =
 | 
			
		||||
  var wdiwtltCfgFilename: string
 | 
			
		||||
@@ -149,177 +113,201 @@ proc initContext(args: Table[string, Value]): CliContext =
 | 
			
		||||
  let width = min(terminalWidth(), cfg.getVal("maxWidth", "80").parseInt)
 | 
			
		||||
  let height = min(terminalHeight(), cfg.getVal("maxHeight", "60").parseInt)
 | 
			
		||||
 | 
			
		||||
  let msgs = MessageBuffers(
 | 
			
		||||
    playing: initMessageBuffer[ScrollText](
 | 
			
		||||
      dims = BufferDimensions(width: width, height: 1, x: 0, y: 0),
 | 
			
		||||
      initialMsg = "Nothing playing"),
 | 
			
		||||
    status: initMessageBuffer[ScrollText](
 | 
			
		||||
      dims = BufferDimensions(width: width, height: 1, x: 0, y: 1),
 | 
			
		||||
      initialMsg = ""),
 | 
			
		||||
    input: initMessageBuffer[string](
 | 
			
		||||
      dims = BufferDimensions(width: width, height: 1, x: 0, y: 2),
 | 
			
		||||
      initialMsg = ""),
 | 
			
		||||
    longform: initMessageBuffer[string](
 | 
			
		||||
      dims = BufferDimensions(width: width, height: height - 3, x: 0, y: 4),
 | 
			
		||||
      initialMsg = ""))
 | 
			
		||||
 | 
			
		||||
  result = CliContext(
 | 
			
		||||
    cfg: cfg,
 | 
			
		||||
    cmd: initCommandBuffer(),
 | 
			
		||||
    cmdVisibleIdx: 0,
 | 
			
		||||
    cmd: initCommandBuffer(msgs.input),
 | 
			
		||||
    curMediaFile: none(MediaFile),
 | 
			
		||||
    d: ScrollingDisplays(
 | 
			
		||||
      playing: initScrollText("Nothing playing", width),
 | 
			
		||||
      status: initScrollText("Idle", width),
 | 
			
		||||
      statusDismissal: none[DateTime]()),
 | 
			
		||||
    dims: RenderDimensions(
 | 
			
		||||
      width: width,
 | 
			
		||||
      height: height,
 | 
			
		||||
      playing: (0, 0),
 | 
			
		||||
      status: (0, 1),
 | 
			
		||||
      input: (0, 2),
 | 
			
		||||
      logs: (0, 3)),
 | 
			
		||||
    frame: 0,
 | 
			
		||||
    lib: initMediaLibrary(
 | 
			
		||||
      rootDir = getVal(cfg, "libraryRoot").Path,
 | 
			
		||||
      dbPath = getVal(cfg, "dbPath").Path),
 | 
			
		||||
    log: log,
 | 
			
		||||
    mode: CliMode.Direct,
 | 
			
		||||
    msgs: msgs,
 | 
			
		||||
    mpv: initMpv(),
 | 
			
		||||
    msgDuration: initDuration(seconds = 5, milliseconds = 0),
 | 
			
		||||
    stop: false,
 | 
			
		||||
    tb: newTerminalBuffer(width, height))
 | 
			
		||||
    width: width,
 | 
			
		||||
    height: height)
 | 
			
		||||
 | 
			
		||||
  if logService.isSome:
 | 
			
		||||
    let customLogAppender = initCustomLogAppender(doLogMessage =
 | 
			
		||||
      proc (msg: LogMessage): void =
 | 
			
		||||
        ctx.writeAt(ctx.dims.logs, msg.message))
 | 
			
		||||
      proc (msg: LogMessage): void = ctx.logs.add(msg))
 | 
			
		||||
 | 
			
		||||
    #logService.get.clearAppenders()
 | 
			
		||||
    logService.get.clearAppenders()
 | 
			
		||||
    logService.get.addAppender(customLogAppender)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc setCursorType(ct: CursorType) =
 | 
			
		||||
  case ct
 | 
			
		||||
  of CursorType.Block:      stdout.write("\x1b[2 q")
 | 
			
		||||
  of CursorType.BlockBlink: stdout.write("\x1b[1 q")
 | 
			
		||||
  of CursorType.BarBlink:   stdout.write("\x1b[5 q")
 | 
			
		||||
  of CursorType.Underline:  stdout.write("\x1b[4 q")
 | 
			
		||||
proc nextTick(ctx: var CliContext) =
 | 
			
		||||
  ctx.msgs.playing.nextTick
 | 
			
		||||
  ctx.msgs.status.nextTick
 | 
			
		||||
  ctx.msgs.longform.nextTick
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc setCursorPosition(x, y: int) =
 | 
			
		||||
  stdout.write("\x1b[$#;$#H" % [$(y + 1), $(x + 1)])
 | 
			
		||||
proc setMode(ctx: var CliContext, mode: CliMode) =
 | 
			
		||||
  ctx.mode = mode
 | 
			
		||||
  case mode
 | 
			
		||||
 | 
			
		||||
  of CliMode.Direct:
 | 
			
		||||
    hideCursor()
 | 
			
		||||
 | 
			
		||||
  of CliMode.Command:
 | 
			
		||||
    showCursor()
 | 
			
		||||
    ctx.cmd.setMode(EditMode.Insert)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc msg(ctx: var CliContext, msg: string) =
 | 
			
		||||
  ctx.d.status.text = msg
 | 
			
		||||
  if msg.len > ctx.dims.width:
 | 
			
		||||
    ctx.d.statusDismissal = some(now() + ctx.msgDuration +
 | 
			
		||||
    initDuration(milliseconds = (msg.len - ctx.dims.width) * 192))
 | 
			
		||||
  else:
 | 
			
		||||
    ctx.d.statusDismissal = some(now() + ctx.msgDuration)
 | 
			
		||||
proc statusMsg(ctx: var CliContext, msg: string) =
 | 
			
		||||
  ctx.msgs.status.showMsg(msg, some(ctx.msgDuration))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc render(ctx: var CliContext) =
 | 
			
		||||
  # text moves at 5 characters per second (192ms per character)
 | 
			
		||||
  if ctx.frame mod 12 == 0: nextTick(ctx)
 | 
			
		||||
proc longMsg[T: string or seq[string]](ctx: var CliContext, msg: T) =
 | 
			
		||||
  ctx.msgs.longform.showMsg(msg, some(ctx.msgDuration))
 | 
			
		||||
 | 
			
		||||
  ctx.clearLine(ctx.dims.playing.y)
 | 
			
		||||
  ctx.writeAt(ctx.dims.playing, ctx.d.playing)
 | 
			
		||||
 | 
			
		||||
  ctx.clearLine(ctx.dims.status.y)
 | 
			
		||||
  ctx.writeAt(ctx.dims.status, ctx.d.status)
 | 
			
		||||
proc viewLogLoops(ctx: var CliContext) =
 | 
			
		||||
  let logLines = ctx.logs
 | 
			
		||||
    .filterIt(it.level > ctx.logThreshold)
 | 
			
		||||
    .mapIt("$# - [$#]: $#" % [$it.level, $it.scope, it.message])
 | 
			
		||||
    .join("\p")
 | 
			
		||||
 | 
			
		||||
  if ctx.mode == CliMode.Command:
 | 
			
		||||
    case ctx.cmd.mode:
 | 
			
		||||
    of EditMode.Insert:     setCursorType(CursorType.BarBlink)
 | 
			
		||||
    of EditMode.Overwrite:  setCursorType(CursorType.BlockBlink)
 | 
			
		||||
    else:                   setCursorType(CursorType.Block)
 | 
			
		||||
  ctx.msgs.longform.showMsg(logLines, none[Duration]())
 | 
			
		||||
 | 
			
		||||
    ctx.clearLine(ctx.dims.input.y)
 | 
			
		||||
  var viewLogs = true
 | 
			
		||||
  while viewLogs:
 | 
			
		||||
    case getKeyAsync()
 | 
			
		||||
    of Key.Q: viewLogs = false
 | 
			
		||||
    of Key.K, Key.Up: ctx.msgs.longform.scrollLines(-1)
 | 
			
		||||
    of Key.J, Key.Down: ctx.msgs.longform.scrollLines(1)
 | 
			
		||||
    of Key.H, Key.Left: ctx.msgs.longform.scrollColumns(-1)
 | 
			
		||||
    of Key.L, Key.Right: ctx.msgs.longform.scrollColumns(1)
 | 
			
		||||
    of Key.PageUp: ctx.msgs.longform.scrollPages(-1)
 | 
			
		||||
    of Key.PageDown: ctx.msgs.longform.scrollPages(1)
 | 
			
		||||
    else: discard
 | 
			
		||||
 | 
			
		||||
    var inputLine = $ctx.cmd
 | 
			
		||||
    let spaceAvailable = ctx.dims.width - 2
 | 
			
		||||
    # Check to see that the line entered fits entirely within the space
 | 
			
		||||
    # available to display it. We need two characters of space: one for the
 | 
			
		||||
    # colon and one for the cursor. If we don't have enough space, we need to
 | 
			
		||||
    # decide which portion of the line to display.
 | 
			
		||||
    if len(inputLine) >= spaceAvailable:
 | 
			
		||||
      # If the user has moved the cursor left of our visible index, follow them
 | 
			
		||||
      # back
 | 
			
		||||
      if ctx.cmd.idx < ctx.cmdVisibleIdx: ctx.cmdVisibleIdx = ctx.cmd.idx
 | 
			
		||||
 | 
			
		||||
      # If the user has moved right, push our index to the right to follow them
 | 
			
		||||
      elif (ctx.cmd.idx - ctx.cmdVisibleIdx) > spaceAvailable:
 | 
			
		||||
        ctx.cmdVisibleIdx = ctx.cmd.idx - spaceAvailable
 | 
			
		||||
 | 
			
		||||
      # If the user has started backspacing (we are showing less that we
 | 
			
		||||
      # could), follow them back
 | 
			
		||||
      elif (len(inputLine) - ctx.cmdVisibleIdx) < spaceAvailable:
 | 
			
		||||
        ctx.cmdVisibleIdx = len(inputLine) - spaceAvailable
 | 
			
		||||
 | 
			
		||||
      # Show the portion of the line starting at the visible index and ending
 | 
			
		||||
      # either with the end of the line or the end of the visible space
 | 
			
		||||
      # (whichever comes first). We need to check both because the user may
 | 
			
		||||
      # delete characters from the end of the line, which would make the
 | 
			
		||||
      # portion of the line we're currently showing shorter than the visible
 | 
			
		||||
      # space (don't try to access an index past the end of the string).
 | 
			
		||||
      inputLine = inputLine[
 | 
			
		||||
        ctx.cmdVisibleIdx ..<
 | 
			
		||||
        min(ctx.cmdVisibleIdx + spaceAvailable, len(inputLine))]
 | 
			
		||||
 | 
			
		||||
    elif ctx.cmdVisibleIdx > 0:
 | 
			
		||||
      # We know the line fits within the space available, but we're still
 | 
			
		||||
      # showing a subset of the line (probably because it was longer and the
 | 
			
		||||
      # user backspaced). Let's just reset and show the whole line now that we
 | 
			
		||||
      # can.
 | 
			
		||||
      ctx.cmdVisibleIdx = 0
 | 
			
		||||
 | 
			
		||||
    ctx.writeAt(ctx.dims.input, ":" & inputLine)
 | 
			
		||||
    setCursorPosition(
 | 
			
		||||
      ctx.dims.input.x + (ctx.cmd.idx - ctx.cmdVisibleIdx) + 1,
 | 
			
		||||
      ctx.dims.input.y)
 | 
			
		||||
 | 
			
		||||
  else:
 | 
			
		||||
    ctx.clearLine(ctx.dims.input.y)
 | 
			
		||||
    ctx.writeAt(ctx.dims.input, "WDIWTLT v" & VERSION)
 | 
			
		||||
 | 
			
		||||
  ctx.tb.display()
 | 
			
		||||
    sleep(FRAME_DUR_MS)
 | 
			
		||||
 | 
			
		||||
  ctx.msgs.longform.clear()
 | 
			
		||||
 | 
			
		||||
proc scanLibrary(ctx: var CliContext) =
 | 
			
		||||
  ctx.statusMsg("Scanning media library...")
 | 
			
		||||
  let counts = ctx.lib.rescanLibrary()
 | 
			
		||||
 | 
			
		||||
  var scanResults = @[
 | 
			
		||||
    "─".repeat(ctx.width),
 | 
			
		||||
    "Scan complete:",
 | 
			
		||||
    "\t$# files total." % [ $counts.total ]
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  if counts.new > 0:
 | 
			
		||||
    scanResults.add("\t$# new files added." % [ $counts.new ])
 | 
			
		||||
 | 
			
		||||
  if counts.ignored > 0:
 | 
			
		||||
    scanResults.add("\t$# files ignored." % [ $counts.ignored ])
 | 
			
		||||
 | 
			
		||||
  if counts.absent > 0:
 | 
			
		||||
    scanResults.add(
 | 
			
		||||
      "\t$# files in the database but not stored locally." %
 | 
			
		||||
      [ $counts.absent ])
 | 
			
		||||
 | 
			
		||||
  ctx.longMsg(scanResults.join("\p"))
 | 
			
		||||
 | 
			
		||||
proc processLogs(ctx: var CliContext, args: seq[string]) =
 | 
			
		||||
  case args[0]
 | 
			
		||||
  of "view": ctx.viewLogLoops()
 | 
			
		||||
  of "clear": ctx.logs = @[]
 | 
			
		||||
  of "set-threshold":
 | 
			
		||||
    if args.len < 2:
 | 
			
		||||
      ctx.longMsg(("missing $# argument to $#" %
 | 
			
		||||
        [ "log-threshold", "logs set-threshold" ]) &
 | 
			
		||||
        usageOf(@["logs", "set-threshold"]))
 | 
			
		||||
 | 
			
		||||
    # TODO
 | 
			
		||||
  else: ctx.statusMsg("Unrecognized logs command: " & args[0])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc processCommand(ctx: var CliContext, command: string) =
 | 
			
		||||
  let parts = command.strip.split(' ', maxSplit=1)
 | 
			
		||||
  let parts = command.strip.split(' ')
 | 
			
		||||
 | 
			
		||||
  case parts[0]
 | 
			
		||||
  of "scan":                scanLibrary(ctx)
 | 
			
		||||
  of "q", "quit", "exit":   ctx.stop = true
 | 
			
		||||
  else: msg(ctx, "Unrecognized command: " & command)
 | 
			
		||||
  of "logs":                ctx.processLogs(parts[1..^1])
 | 
			
		||||
  else: statusMsg(ctx, "Unrecognized command: " & command)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc handleKey(ctx: var CliContext, key: Key) =
 | 
			
		||||
  if ctx.mode == CliMode.Direct:
 | 
			
		||||
    case key
 | 
			
		||||
    of Key.Q:     ctx.stop = true
 | 
			
		||||
    of Key.Colon, Key.I: ctx.setMode(CliMode.Command)
 | 
			
		||||
    else: discard
 | 
			
		||||
 | 
			
		||||
  elif ctx.mode == CliMode.Command:
 | 
			
		||||
    case key
 | 
			
		||||
    of Key.Enter:
 | 
			
		||||
      let command = $ctx.cmd
 | 
			
		||||
      ctx.cmd.clear
 | 
			
		||||
      ctx.cmd.mode = EditMode.Insert
 | 
			
		||||
      processCommand(ctx, command)
 | 
			
		||||
    of Key.Backspace: ctx.cmd.handleInput(Key.Backspace)
 | 
			
		||||
    of Key.Escape:
 | 
			
		||||
      if ctx.cmd.mode == EditMode.Command:
 | 
			
		||||
        ctx.setMode(CliMode.Direct)
 | 
			
		||||
        ctx.cmd.clear
 | 
			
		||||
      else: ctx.cmd.handleInput(Key.Escape)
 | 
			
		||||
    else: ctx.cmd.handleInput(key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc mainLoop(ctx: var CliContext) =
 | 
			
		||||
 | 
			
		||||
  hideCursor()
 | 
			
		||||
  var frame = 0
 | 
			
		||||
 | 
			
		||||
  while not ctx.stop:
 | 
			
		||||
    let key = getKey()
 | 
			
		||||
 | 
			
		||||
    if ctx.mode == CliMode.Direct:
 | 
			
		||||
      case key
 | 
			
		||||
      of Key.Q:     ctx.stop = true
 | 
			
		||||
      of Key.Colon, Key.I: ctx.setMode(CliMode.Command)
 | 
			
		||||
      else: discard
 | 
			
		||||
 | 
			
		||||
    elif ctx.mode == CliMode.Command:
 | 
			
		||||
      case key
 | 
			
		||||
      of Key.Enter:
 | 
			
		||||
        let command = $ctx.cmd
 | 
			
		||||
        ctx.cmd.clear
 | 
			
		||||
        ctx.cmdVisibleIdx = 0
 | 
			
		||||
        ctx.cmd.mode = EditMode.Insert
 | 
			
		||||
        processCommand(ctx, command)
 | 
			
		||||
      of Key.Backspace: ctx.cmd.handleInput(Key.Backspace)
 | 
			
		||||
      of Key.Escape:
 | 
			
		||||
        if ctx.cmd.mode == EditMode.Command:
 | 
			
		||||
          ctx.setMode(CliMode.Direct)
 | 
			
		||||
          ctx.cmd.clear
 | 
			
		||||
          ctx.cmdVisibleIdx = 0
 | 
			
		||||
        else: ctx.cmd.handleInput(Key.Escape)
 | 
			
		||||
      else: ctx.cmd.handleInput(key)
 | 
			
		||||
 | 
			
		||||
    render(ctx)
 | 
			
		||||
 | 
			
		||||
    # target 60 FPS
 | 
			
		||||
    sleep(16)
 | 
			
		||||
    ctx.frame = (ctx.frame + 1) mod 1920
 | 
			
		||||
    # This sleep below will be the primary driver of the frame rate of the
 | 
			
		||||
    # application (examples below):
 | 
			
		||||
    #
 | 
			
		||||
    # | frames/sec | sec/frame | ms/frame |
 | 
			
		||||
    # |------------|-----------|----------|
 | 
			
		||||
    # |    5.00    |   0.200   |    200   |
 | 
			
		||||
    # |   10.00    |   0.100   |    100   |
 | 
			
		||||
    # |   12.50    |   0.080   |     80   |
 | 
			
		||||
    # |   20.00    |   0.050   |     50   |
 | 
			
		||||
    # |   25.00    |   0.040   |     40   |
 | 
			
		||||
    # |   31.25    |   0.032   |     32   |
 | 
			
		||||
    # |   50.00    |   0.020   |     20   |
 | 
			
		||||
    # |   62.50    |   0.016   |     16   |
 | 
			
		||||
    #
 | 
			
		||||
    # Previously, when we were using illwill and rendering every frame we
 | 
			
		||||
    # targeted a faster FPS to allow for a responsive feeling to the command
 | 
			
		||||
    # input, etc. In this case, we still had a "logic update" period that was
 | 
			
		||||
    # lower than our rendering FPS. Specifically, we were targeting 60fps
 | 
			
		||||
    # rendering speed, but 5fps for logic updates (scrolling text, expiring
 | 
			
		||||
    # messages, etc.).
 | 
			
		||||
    #
 | 
			
		||||
    # Now that we are directly rendering (components of the UI render updates
 | 
			
		||||
    # themselves immediately) we don't need to render at 60fps to have a
 | 
			
		||||
    # responsive UI. Now we really only care about the frequency of logic
 | 
			
		||||
    # updates, so we're targeting 5fps and triggering our logic updates every
 | 
			
		||||
    # frame.
 | 
			
		||||
    handleKey(ctx, getKeyAsync())
 | 
			
		||||
    sleep(FRAME_DUR_MS)
 | 
			
		||||
    nextTick(ctx) # tick on every frame
 | 
			
		||||
    frame = (frame + 1) mod 1000 # use a max frame count to prevent overflow
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
when isMainModule:
 | 
			
		||||
@@ -328,8 +316,8 @@ when isMainModule:
 | 
			
		||||
  try:
 | 
			
		||||
    let args = docopt(USAGE, version=VERSION)
 | 
			
		||||
 | 
			
		||||
    illWillInit(fullscreen=true)
 | 
			
		||||
    ctx = initContext(args)
 | 
			
		||||
    setNonBlockingTty(true)
 | 
			
		||||
 | 
			
		||||
    mainLoop(ctx)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
import std/nre
 | 
			
		||||
 | 
			
		||||
const CSI* = "\x1b["
 | 
			
		||||
const ANSI_REGEX_PATTERN* = "\x1b\\[([0-9;]*)([a-zA-Z])"
 | 
			
		||||
 | 
			
		||||
func stripAnsi*(text: string): string =
 | 
			
		||||
  text.replace(re(ANSI_REGEX_PATTERN), "")
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import std/[options]
 | 
			
		||||
import std/[options, strutils]
 | 
			
		||||
 | 
			
		||||
import illwill
 | 
			
		||||
import ./[messagebuffer, nonblockingio, terminal, util]
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  EditMode* {.pure.} = enum Command, Insert, Overwrite, Visual
 | 
			
		||||
@@ -12,24 +12,19 @@ type
 | 
			
		||||
 | 
			
		||||
  CommandBuffer* = ref object
 | 
			
		||||
    history: seq[Command]
 | 
			
		||||
    mb: MessageBuffer[string]
 | 
			
		||||
    idx: int
 | 
			
		||||
    visibleIdx: int # first visible character in the cmd buffer
 | 
			
		||||
    mode*: EditMode
 | 
			
		||||
 | 
			
		||||
func clamp(value, min, max: int): int =
 | 
			
		||||
  if value < min:
 | 
			
		||||
    return min
 | 
			
		||||
  elif value > max:
 | 
			
		||||
    return max
 | 
			
		||||
  else:
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
func initCommandBuffer*(): CommandBuffer =
 | 
			
		||||
func initCommandBuffer*(mb: MessageBuffer[string]): CommandBuffer =
 | 
			
		||||
  result = CommandBuffer(
 | 
			
		||||
    history: @[Command(
 | 
			
		||||
      buffer: "",
 | 
			
		||||
      idx: 0,
 | 
			
		||||
      selectionStartIdx: none(int))],
 | 
			
		||||
    idx: 0,
 | 
			
		||||
    mb: mb,
 | 
			
		||||
    mode: Insert)
 | 
			
		||||
 | 
			
		||||
proc cur(cb: CommandBuffer): Command = cb.history[cb.idx]
 | 
			
		||||
@@ -72,6 +67,7 @@ proc clear*(cb: var CommandBuffer) =
 | 
			
		||||
    idx: 0,
 | 
			
		||||
    selectionStartIdx: none(int)))
 | 
			
		||||
  cb.idx = cb.history.len - 1
 | 
			
		||||
  cb.visibleIdx = 0
 | 
			
		||||
 | 
			
		||||
proc left*(cb: var CommandBuffer) =
 | 
			
		||||
  cb.cur.idx = clamp(cb.cur.idx - 1, 0, cb.cur.buffer.len)
 | 
			
		||||
@@ -102,11 +98,87 @@ proc endOfWord*(cb: var CommandBuffer) =
 | 
			
		||||
 | 
			
		||||
func toChar(k: Key): char = cast[char](ord(k))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc setMode*(cb: var CommandBuffer, mode: EditMode) =
 | 
			
		||||
  cb.mode = mode
 | 
			
		||||
  case mode
 | 
			
		||||
  of EditMode.Insert:     setCursorType(ctBarBlink)
 | 
			
		||||
  of EditMode.Overwrite:  setCursorType(ctBlock)
 | 
			
		||||
  else:                   setCursorType(ctBlockBlink)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func `$`*(cb: CommandBuffer): string =
 | 
			
		||||
  if cb.cur.selectionStartIdx.isNone:
 | 
			
		||||
    return cb.cur.buffer
 | 
			
		||||
  else:
 | 
			
		||||
    let selIdx = cb.cur.selectionStartIdx.get
 | 
			
		||||
    let curIdx = cb.cur.idx
 | 
			
		||||
 | 
			
		||||
    if selIdx < curIdx:
 | 
			
		||||
      return cb.cur.buffer[0..<selIdx] &
 | 
			
		||||
        invert(cb.cur.buffer[selIdx..<curIdx]) &
 | 
			
		||||
        cb.cur.buffer[curIdx..^1]
 | 
			
		||||
    else:
 | 
			
		||||
      return cb.cur.buffer[0..<curIdx] &
 | 
			
		||||
        invert(cb.cur.buffer[curIdx..<selIdx]) &
 | 
			
		||||
        cb.cur.buffer[selIdx..^1]
 | 
			
		||||
 | 
			
		||||
func idx*(cb: CommandBuffer): int = cb.cur.idx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc render*(cb: var CommandBuffer) =
 | 
			
		||||
  write(cb.mb.dims.x, cb.mb.dims.y, ' '.repeat(cb.mb.dims.width))
 | 
			
		||||
 | 
			
		||||
  let cmd = cb.cur
 | 
			
		||||
  var inputLine = cmd.buffer
 | 
			
		||||
  let spaceAvailable = cb.mb.dims.width - 2
 | 
			
		||||
 | 
			
		||||
  # Check to see that the line entered fits entirely within the space
 | 
			
		||||
  # available to display it. We need two characters of space: one for the
 | 
			
		||||
  # colon and one for the cursor. If we don't have enough space, we need to
 | 
			
		||||
  # decide which portion of the line to display.
 | 
			
		||||
  if len(cmd.buffer) >= spaceAvailable:
 | 
			
		||||
    # If the user has moved the cursor left of our visible index, follow them
 | 
			
		||||
    # back
 | 
			
		||||
    if cmd.idx < cb.visibleIdx: cb.visibleIdx = cmd.idx
 | 
			
		||||
 | 
			
		||||
    # If the user has moved right, push our index to the right to follow them
 | 
			
		||||
    elif (cmd.idx - cb.visibleIdx) > spaceAvailable:
 | 
			
		||||
      cb.visibleIdx = cmd.idx - spaceAvailable
 | 
			
		||||
 | 
			
		||||
    # If the user has started backspacing (we are showing less that we
 | 
			
		||||
    # could), follow them back
 | 
			
		||||
    elif (len(cmd.buffer) - cb.visibleIdx) < spaceAvailable:
 | 
			
		||||
      cb.visibleIdx = len(cmd.buffer) - spaceAvailable
 | 
			
		||||
 | 
			
		||||
    # Show the portion of the line starting at the visible index and ending
 | 
			
		||||
    # either with the end of the line or the end of the visible space
 | 
			
		||||
    # (whichever comes first). We need to check both because the user may
 | 
			
		||||
    # delete characters from the end of the line, which would make the
 | 
			
		||||
    # portion of the line we're currently showing shorter than the visible
 | 
			
		||||
    # space (don't try to access an index past the end of the string).
 | 
			
		||||
    inputLine = cmd.buffer[
 | 
			
		||||
      cb.visibleIdx ..<
 | 
			
		||||
      min(cb.visibleIdx + spaceAvailable, len(cmd.buffer))]
 | 
			
		||||
 | 
			
		||||
  elif cb.visibleIdx > 0:
 | 
			
		||||
    # We know the line fits within the space available, but we're still
 | 
			
		||||
    # showing a subset of the line (probably because it was longer and the
 | 
			
		||||
    # user backspaced). Let's just reset and show the whole line now that we
 | 
			
		||||
    # can.
 | 
			
		||||
    cb.visibleIdx = 0
 | 
			
		||||
 | 
			
		||||
  # TODO: implement VISUAL mode selection highlighting
 | 
			
		||||
  cb.mb.showMsg(":" & inputLine)
 | 
			
		||||
  setCursorPosition(
 | 
			
		||||
    cb.mb.dims.x + (cmd.idx - cb.visibleIdx) + 1,
 | 
			
		||||
    cb.mb.dims.y)
 | 
			
		||||
 | 
			
		||||
proc handleInput*(cb: var CommandBuffer, key: Key) =
 | 
			
		||||
  case cb.mode
 | 
			
		||||
  of EditMode.Insert, EditMode.Overwrite:
 | 
			
		||||
    case key
 | 
			
		||||
    of Key.Escape: cb.mode = EditMode.Command
 | 
			
		||||
    of Key.Escape: cb.setMode(EditMode.Command)
 | 
			
		||||
    of Key.Backspace: cb.delete()
 | 
			
		||||
    of Key.Delete: cb.delete(backspace = false)
 | 
			
		||||
    of Key.Left: cb.left()
 | 
			
		||||
@@ -133,26 +205,27 @@ proc handleInput*(cb: var CommandBuffer, key: Key) =
 | 
			
		||||
    of Key.B: cb.backWord()
 | 
			
		||||
    of Key.W: cb.forwardWord()
 | 
			
		||||
    of Key.E: cb.endOfWord()
 | 
			
		||||
    of Key.I: cb.mode = EditMode.Insert
 | 
			
		||||
    of Key.I: cb.setMode(EditMode.Insert)
 | 
			
		||||
    of Key.A:
 | 
			
		||||
      cb.cur.idx = clamp(cb.cur.idx + 1, 0, cb.cur.buffer.len)
 | 
			
		||||
      cb.mode = EditMode.Insert
 | 
			
		||||
      cb.setMode(EditMode.Insert)
 | 
			
		||||
    of Key.ShiftA:
 | 
			
		||||
      cb.cur.idx = cb.cur.buffer.len
 | 
			
		||||
      cb.mode = EditMode.Insert
 | 
			
		||||
      cb.setMode(EditMode.Insert)
 | 
			
		||||
    of Key.ShiftI:
 | 
			
		||||
      cb.cur.idx = 0
 | 
			
		||||
      cb.mode = EditMode.Insert
 | 
			
		||||
    of Key.ShiftR: cb.mode = EditMode.Overwrite
 | 
			
		||||
      cb.setMode(EditMode.Insert)
 | 
			
		||||
    of Key.ShiftR:
 | 
			
		||||
      cb.setMode(EditMode.Overwrite)
 | 
			
		||||
    of Key.V:
 | 
			
		||||
      cb.mode = EditMode.Visual
 | 
			
		||||
      cb.setMode(EditMode.Visual)
 | 
			
		||||
      cb.cur.selectionStartIdx = some(cb.cur.idx)
 | 
			
		||||
    else: discard
 | 
			
		||||
 | 
			
		||||
  of EditMode.Visual:
 | 
			
		||||
    case key
 | 
			
		||||
    of Key.Escape:
 | 
			
		||||
      cb.mode = EditMode.Command
 | 
			
		||||
      cb.setMode(EditMode.Command)
 | 
			
		||||
      cb.cur.selectionStartIdx = none(int)
 | 
			
		||||
    of Key.Backspace: cb.left()
 | 
			
		||||
    of Key.Left,   Key.H: cb.left()
 | 
			
		||||
@@ -162,17 +235,13 @@ proc handleInput*(cb: var CommandBuffer, key: Key) =
 | 
			
		||||
      cb.prev()
 | 
			
		||||
    of Key.Down,   Key.K:
 | 
			
		||||
      cb.cur.selectionStartIdx = none[int]()
 | 
			
		||||
      cb.next()
 | 
			
		||||
    of Key.Home,   Key.Zero: cb.toHome()
 | 
			
		||||
    of Key.End,    Key.Dollar: cb.toEnd()
 | 
			
		||||
    of Key.B: cb.backWord()
 | 
			
		||||
    of Key.W: cb.forwardWord()
 | 
			
		||||
    of Key.V:
 | 
			
		||||
      cb.mode = EditMode.Command
 | 
			
		||||
      cb.setMode(EditMode.Command)
 | 
			
		||||
      cb.cur.selectionStartIdx = none(int)
 | 
			
		||||
    else: discard
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func `$`*(cb: CommandBuffer): string = cb.cur.buffer
 | 
			
		||||
 | 
			
		||||
func idx*(cb: CommandBuffer): int = cb.cur.idx
 | 
			
		||||
  render(cb)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										167
									
								
								src/main/nim/wdiwtlt/messagebuffer.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/main/nim/wdiwtlt/messagebuffer.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
import std/[options, sequtils, strutils, times, wordwrap]
 | 
			
		||||
 | 
			
		||||
import ./[scrolltext, terminal, util]
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  BufferDimensions* = object
 | 
			
		||||
    width*, height*, x*, y*: int
 | 
			
		||||
 | 
			
		||||
  MessageBuffer*[T: ScrollText or string] = ref object
 | 
			
		||||
    content: T
 | 
			
		||||
    dims*: BufferDimensions
 | 
			
		||||
    dismissAfter: Option[DateTime]
 | 
			
		||||
    firstColumnIdx: int
 | 
			
		||||
    firstLineIdx: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc getLinePortion(line: string, start: int, width: int): string =
 | 
			
		||||
  let cleanLine = stripFormatting(line)
 | 
			
		||||
 | 
			
		||||
  if start == 0 and len(cleanLine) <= width:
 | 
			
		||||
      result = line & ' '.repeat(width - len(cleanLine))
 | 
			
		||||
  else:
 | 
			
		||||
    result = ansiAwareSubstring(line, start, width)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc render[T: ScrollText or string](mb: var MessageBuffer[T]) =
 | 
			
		||||
  hideCursor()
 | 
			
		||||
  when T is ScrollText: write(mb.dims.x, mb.dims.y, $mb.content)
 | 
			
		||||
  when T is string:
 | 
			
		||||
    let renderedContent = splitLines(mb.content)
 | 
			
		||||
      .mapIt(wrapWords(
 | 
			
		||||
        s = it,
 | 
			
		||||
        maxLineWidth = mb.dims.width,
 | 
			
		||||
        newLine = "\p").alignLeft(mb.dims.width))
 | 
			
		||||
      .mapIt(getLinePortion(it, mb.firstColumnIdx, mb.dims.width))
 | 
			
		||||
      .join("\p")
 | 
			
		||||
    let lastLineIdx = min(mb.firstLineIdx + mb.dims.height, len(renderedContent))
 | 
			
		||||
    write(mb.dims.x, mb.dims.y, renderedContent[mb.firstLineIdx..<lastLineIdx])
 | 
			
		||||
  showCursor()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func variableMsgDuration[T: ScrollText or string](
 | 
			
		||||
    mb: MessageBuffer[T],
 | 
			
		||||
    initDur: Duration): Duration =
 | 
			
		||||
  when T is ScrollText:
 | 
			
		||||
    # text moves at 5 characters per second (192ms per character)
 | 
			
		||||
    result = initDur + initDuration(milliseconds = len($mb.content) * 192)
 | 
			
		||||
  when T is string:
 | 
			
		||||
    result = initDur + initDuration(milliseconds = len(splitLines(mb.content)) * 250)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc initMessageBuffer*[T: ScrollText or string](
 | 
			
		||||
    dims: BufferDimensions,
 | 
			
		||||
    initialMsg = ""): MessageBuffer[T] =
 | 
			
		||||
 | 
			
		||||
  when T is ScrollText:
 | 
			
		||||
    result = MessageBuffer[T](
 | 
			
		||||
      content: initScrollText(initialMsg, max(dims.width, 1)),
 | 
			
		||||
      dims: BufferDimensions(
 | 
			
		||||
        width: max(dims.width, 1),
 | 
			
		||||
        height: 1,
 | 
			
		||||
        x: dims.x,
 | 
			
		||||
        y: dims.y),
 | 
			
		||||
      dismissAfter: none[DateTime](),
 | 
			
		||||
      firstColumnIdx: 0,
 | 
			
		||||
      firstLineIdx: 0)
 | 
			
		||||
 | 
			
		||||
  when T is string:
 | 
			
		||||
    result = MessageBuffer[T](
 | 
			
		||||
      content: initialMsg,
 | 
			
		||||
      dims: BufferDimensions(
 | 
			
		||||
        width: max(dims.width, 1),
 | 
			
		||||
        height: dims.height,
 | 
			
		||||
        x: dims.x,
 | 
			
		||||
        y: dims.y),
 | 
			
		||||
      dismissAfter: none[DateTime](),
 | 
			
		||||
      firstColumnIdx: 0,
 | 
			
		||||
      firstLineIdx: 0)
 | 
			
		||||
 | 
			
		||||
  render(result)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc showMsg*[T: ScrollText or string](
 | 
			
		||||
    mb: var MessageBuffer[T],
 | 
			
		||||
    msg: string,
 | 
			
		||||
    dur: Option[Duration] = none[Duration]()) =
 | 
			
		||||
 | 
			
		||||
  when T is ScrollText: mb.content.text = msg
 | 
			
		||||
  when T is string: mb.content = msg
 | 
			
		||||
 | 
			
		||||
  if dur.isSome:
 | 
			
		||||
    mb.dismissAfter = some(now() + mb.variableMsgDuration(dur.get))
 | 
			
		||||
  else:
 | 
			
		||||
    mb.dismissAfter = none[DateTime]()
 | 
			
		||||
 | 
			
		||||
  render(mb)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc clear*[T: ScrollText or string](mb: var MessageBuffer[T]) =
 | 
			
		||||
  when T is ScrollText:
 | 
			
		||||
    let clearText = ' '.repeat(mb.dims.width)
 | 
			
		||||
    mb.content.text = ""
 | 
			
		||||
 | 
			
		||||
  when T is string:
 | 
			
		||||
    let clearText = splitLines(mb.content)
 | 
			
		||||
      .mapIt(' '.repeat(mb.dims.width))
 | 
			
		||||
      .join("\p")
 | 
			
		||||
 | 
			
		||||
    mb.content = ""
 | 
			
		||||
 | 
			
		||||
  hideCursor()
 | 
			
		||||
  write(mb.dims.x, mb.dims.y, clearText)
 | 
			
		||||
  showCursor()
 | 
			
		||||
  mb.dismissAfter = none[DateTime]()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[ TODO: re-implement taking BufferDimensions if needed
 | 
			
		||||
proc `maxWidth=`*[T: ScrollText or string](
 | 
			
		||||
    mb: var MessageBuffer[T],
 | 
			
		||||
    maxWidth: int) =
 | 
			
		||||
  mb.maxWidth = max(maxWidth, 1)
 | 
			
		||||
 | 
			
		||||
  when T is ScrollText: mb.content.maxWidth = mb.maxWidth
 | 
			
		||||
  when T is string: mb.renderStringContent()
 | 
			
		||||
]#
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc nextTick*[T: ScrollText or string](mb: var MessageBuffer[T]) =
 | 
			
		||||
 | 
			
		||||
  if mb.dismissAfter.isSome and now() > mb.dismissAfter.get:
 | 
			
		||||
    mb.clear()
 | 
			
		||||
 | 
			
		||||
  when T is ScrollText:
 | 
			
		||||
    if mb.content.isScrolling:
 | 
			
		||||
      discard mb.content.nextTick()
 | 
			
		||||
      render(mb)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func hasContent*[T: ScrollText or string](
 | 
			
		||||
    mb: MessageBuffer[T]): bool =
 | 
			
		||||
  when T is ScrollText: return len($mb.content) > 0
 | 
			
		||||
  when T is string: return len(mb.content) > 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc scrollLines*[T](mb: var MessageBuffer[T], lines: int) =
 | 
			
		||||
  mb.firstLineIdx = clamp(
 | 
			
		||||
    mb.firstLineIdx + lines,
 | 
			
		||||
    0,
 | 
			
		||||
    max(0, len(mb.content) - mb.dims.height))
 | 
			
		||||
  render(mb)
 | 
			
		||||
 | 
			
		||||
proc scrollPages*[T](mb: var MessageBuffer[T], pages: int) =
 | 
			
		||||
  scrollLines(mb, pages * mb.dims.height)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc scrollColumns*(mb: var MessageBuffer[string], chars: int) =
 | 
			
		||||
  mb.firstColumnIdx = clamp(
 | 
			
		||||
    mb.firstColumnIdx + chars,
 | 
			
		||||
    0,
 | 
			
		||||
    max(0, len(mb.content) - mb.dims.width))
 | 
			
		||||
  render(mb)
 | 
			
		||||
 | 
			
		||||
proc resetScroll*(
 | 
			
		||||
    mb: var MessageBuffer[ScrollText]) =
 | 
			
		||||
  mb.firstColumnIdx = 0
 | 
			
		||||
  mb.firstLineIdx = 0
 | 
			
		||||
  render(mb)
 | 
			
		||||
							
								
								
									
										263
									
								
								src/main/nim/wdiwtlt/nonblockingio.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								src/main/nim/wdiwtlt/nonblockingio.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,263 @@
 | 
			
		||||
# Adapted from [illwill](https://github.com/johnnovak/illwill) by John Novak
 | 
			
		||||
import std/[posix, termios]
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  Key* {.pure.} = enum      ## Supported single key presses and key combinations
 | 
			
		||||
    None = (-1, "None"),
 | 
			
		||||
 | 
			
		||||
    # Special ASCII characters
 | 
			
		||||
    CtrlA  = (1, "CtrlA"),
 | 
			
		||||
    CtrlB  = (2, "CtrlB"),
 | 
			
		||||
    CtrlC  = (3, "CtrlC"),
 | 
			
		||||
    CtrlD  = (4, "CtrlD"),
 | 
			
		||||
    CtrlE  = (5, "CtrlE"),
 | 
			
		||||
    CtrlF  = (6, "CtrlF"),
 | 
			
		||||
    CtrlG  = (7, "CtrlG"),
 | 
			
		||||
    CtrlH  = (8, "CtrlH"),
 | 
			
		||||
    Tab    = (9, "Tab"),     # Ctrl-I
 | 
			
		||||
    CtrlJ  = (10, "CtrlJ"),
 | 
			
		||||
    CtrlK  = (11, "CtrlK"),
 | 
			
		||||
    CtrlL  = (12, "CtrlL"),
 | 
			
		||||
    Enter  = (13, "Enter"),  # Ctrl-M
 | 
			
		||||
    CtrlN  = (14, "CtrlN"),
 | 
			
		||||
    CtrlO  = (15, "CtrlO"),
 | 
			
		||||
    CtrlP  = (16, "CtrlP"),
 | 
			
		||||
    CtrlQ  = (17, "CtrlQ"),
 | 
			
		||||
    CtrlR  = (18, "CtrlR"),
 | 
			
		||||
    CtrlS  = (19, "CtrlS"),
 | 
			
		||||
    CtrlT  = (20, "CtrlT"),
 | 
			
		||||
    CtrlU  = (21, "CtrlU"),
 | 
			
		||||
    CtrlV  = (22, "CtrlV"),
 | 
			
		||||
    CtrlW  = (23, "CtrlW"),
 | 
			
		||||
    CtrlX  = (24, "CtrlX"),
 | 
			
		||||
    CtrlY  = (25, "CtrlY"),
 | 
			
		||||
    CtrlZ  = (26, "CtrlZ"),
 | 
			
		||||
    Escape = (27, "Escape"),
 | 
			
		||||
 | 
			
		||||
    CtrlBackslash    = (28, "CtrlBackslash"),
 | 
			
		||||
    CtrlRightBracket = (29, "CtrlRightBracket"),
 | 
			
		||||
 | 
			
		||||
    # Printable ASCII characters
 | 
			
		||||
    Space           = (32, "Space"),
 | 
			
		||||
    ExclamationMark = (33, "ExclamationMark"),
 | 
			
		||||
    DoubleQuote     = (34, "DoubleQuote"),
 | 
			
		||||
    Hash            = (35, "Hash"),
 | 
			
		||||
    Dollar          = (36, "Dollar"),
 | 
			
		||||
    Percent         = (37, "Percent"),
 | 
			
		||||
    Ampersand       = (38, "Ampersand"),
 | 
			
		||||
    SingleQuote     = (39, "SingleQuote"),
 | 
			
		||||
    LeftParen       = (40, "LeftParen"),
 | 
			
		||||
    RightParen      = (41, "RightParen"),
 | 
			
		||||
    Asterisk        = (42, "Asterisk"),
 | 
			
		||||
    Plus            = (43, "Plus"),
 | 
			
		||||
    Comma           = (44, "Comma"),
 | 
			
		||||
    Minus           = (45, "Minus"),
 | 
			
		||||
    Dot             = (46, "Dot"),
 | 
			
		||||
    Slash           = (47, "Slash"),
 | 
			
		||||
 | 
			
		||||
    Zero  = (48, "Zero"),
 | 
			
		||||
    One   = (49, "One"),
 | 
			
		||||
    Two   = (50, "Two"),
 | 
			
		||||
    Three = (51, "Three"),
 | 
			
		||||
    Four  = (52, "Four"),
 | 
			
		||||
    Five  = (53, "Five"),
 | 
			
		||||
    Six   = (54, "Six"),
 | 
			
		||||
    Seven = (55, "Seven"),
 | 
			
		||||
    Eight = (56, "Eight"),
 | 
			
		||||
    Nine  = (57, "Nine"),
 | 
			
		||||
 | 
			
		||||
    Colon        = (58, "Colon"),
 | 
			
		||||
    Semicolon    = (59, "Semicolon"),
 | 
			
		||||
    LessThan     = (60, "LessThan"),
 | 
			
		||||
    Equals       = (61, "Equals"),
 | 
			
		||||
    GreaterThan  = (62, "GreaterThan"),
 | 
			
		||||
    QuestionMark = (63, "QuestionMark"),
 | 
			
		||||
    At           = (64, "At"),
 | 
			
		||||
 | 
			
		||||
    ShiftA  = (65, "ShiftA"),
 | 
			
		||||
    ShiftB  = (66, "ShiftB"),
 | 
			
		||||
    ShiftC  = (67, "ShiftC"),
 | 
			
		||||
    ShiftD  = (68, "ShiftD"),
 | 
			
		||||
    ShiftE  = (69, "ShiftE"),
 | 
			
		||||
    ShiftF  = (70, "ShiftF"),
 | 
			
		||||
    ShiftG  = (71, "ShiftG"),
 | 
			
		||||
    ShiftH  = (72, "ShiftH"),
 | 
			
		||||
    ShiftI  = (73, "ShiftI"),
 | 
			
		||||
    ShiftJ  = (74, "ShiftJ"),
 | 
			
		||||
    ShiftK  = (75, "ShiftK"),
 | 
			
		||||
    ShiftL  = (76, "ShiftL"),
 | 
			
		||||
    ShiftM  = (77, "ShiftM"),
 | 
			
		||||
    ShiftN  = (78, "ShiftN"),
 | 
			
		||||
    ShiftO  = (79, "ShiftO"),
 | 
			
		||||
    ShiftP  = (80, "ShiftP"),
 | 
			
		||||
    ShiftQ  = (81, "ShiftQ"),
 | 
			
		||||
    ShiftR  = (82, "ShiftR"),
 | 
			
		||||
    ShiftS  = (83, "ShiftS"),
 | 
			
		||||
    ShiftT  = (84, "ShiftT"),
 | 
			
		||||
    ShiftU  = (85, "ShiftU"),
 | 
			
		||||
    ShiftV  = (86, "ShiftV"),
 | 
			
		||||
    ShiftW  = (87, "ShiftW"),
 | 
			
		||||
    ShiftX  = (88, "ShiftX"),
 | 
			
		||||
    ShiftY  = (89, "ShiftY"),
 | 
			
		||||
    ShiftZ  = (90, "ShiftZ"),
 | 
			
		||||
 | 
			
		||||
    LeftBracket  = (91, "LeftBracket"),
 | 
			
		||||
    Backslash    = (92, "Backslash"),
 | 
			
		||||
    RightBracket = (93, "RightBracket"),
 | 
			
		||||
    Caret        = (94, "Caret"),
 | 
			
		||||
    Underscore   = (95, "Underscore"),
 | 
			
		||||
    GraveAccent  = (96, "GraveAccent"),
 | 
			
		||||
 | 
			
		||||
    A = (97, "A"),
 | 
			
		||||
    B = (98, "B"),
 | 
			
		||||
    C = (99, "C"),
 | 
			
		||||
    D = (100, "D"),
 | 
			
		||||
    E = (101, "E"),
 | 
			
		||||
    F = (102, "F"),
 | 
			
		||||
    G = (103, "G"),
 | 
			
		||||
    H = (104, "H"),
 | 
			
		||||
    I = (105, "I"),
 | 
			
		||||
    J = (106, "J"),
 | 
			
		||||
    K = (107, "K"),
 | 
			
		||||
    L = (108, "L"),
 | 
			
		||||
    M = (109, "M"),
 | 
			
		||||
    N = (110, "N"),
 | 
			
		||||
    O = (111, "O"),
 | 
			
		||||
    P = (112, "P"),
 | 
			
		||||
    Q = (113, "Q"),
 | 
			
		||||
    R = (114, "R"),
 | 
			
		||||
    S = (115, "S"),
 | 
			
		||||
    T = (116, "T"),
 | 
			
		||||
    U = (117, "U"),
 | 
			
		||||
    V = (118, "V"),
 | 
			
		||||
    W = (119, "W"),
 | 
			
		||||
    X = (120, "X"),
 | 
			
		||||
    Y = (121, "Y"),
 | 
			
		||||
    Z = (122, "Z"),
 | 
			
		||||
 | 
			
		||||
    LeftBrace  = (123, "LeftBrace"),
 | 
			
		||||
    Pipe       = (124, "Pipe"),
 | 
			
		||||
    RightBrace = (125, "RightBrace"),
 | 
			
		||||
    Tilde      = (126, "Tilde"),
 | 
			
		||||
    Backspace  = (127, "Backspace"),
 | 
			
		||||
 | 
			
		||||
    # Special characters with virtual keycodes
 | 
			
		||||
    Up       = (1001, "Up"),
 | 
			
		||||
    Down     = (1002, "Down"),
 | 
			
		||||
    Right    = (1003, "Right"),
 | 
			
		||||
    Left     = (1004, "Left"),
 | 
			
		||||
    Home     = (1005, "Home"),
 | 
			
		||||
    Insert   = (1006, "Insert"),
 | 
			
		||||
    Delete   = (1007, "Delete"),
 | 
			
		||||
    End      = (1008, "End"),
 | 
			
		||||
    PageUp   = (1009, "PageUp"),
 | 
			
		||||
    PageDown = (1010, "PageDown"),
 | 
			
		||||
 | 
			
		||||
    F1  = (1011, "F1"),
 | 
			
		||||
    F2  = (1012, "F2"),
 | 
			
		||||
    F3  = (1013, "F3"),
 | 
			
		||||
    F4  = (1014, "F4"),
 | 
			
		||||
    F5  = (1015, "F5"),
 | 
			
		||||
    F6  = (1016, "F6"),
 | 
			
		||||
    F7  = (1017, "F7"),
 | 
			
		||||
    F8  = (1018, "F8"),
 | 
			
		||||
    F9  = (1019, "F9"),
 | 
			
		||||
    F10 = (1020, "F10"),
 | 
			
		||||
    F11 = (1021, "F11"),
 | 
			
		||||
    F12 = (1022, "F12"),
 | 
			
		||||
 | 
			
		||||
    Mouse = (5000, "Mouse")
 | 
			
		||||
 | 
			
		||||
const
 | 
			
		||||
  KEYS_D = [Key.Up, Key.Down, Key.Right, Key.Left, Key.None, Key.End, Key.None, Key.Home]
 | 
			
		||||
  KEYS_E = [Key.Delete, Key.End, Key.PageUp, Key.PageDown, Key.Home, Key.End]
 | 
			
		||||
  KEYS_F = [Key.F1, Key.F2, Key.F3, Key.F4, Key.F5, Key.None, Key.F6, Key.F7, Key.F8]
 | 
			
		||||
  KEYS_G = [Key.F9, Key.F10, Key.None, Key.F11, Key.F12]
 | 
			
		||||
 | 
			
		||||
{.push warning[HoleEnumConv]:off.}
 | 
			
		||||
 | 
			
		||||
func toKey(c: int): Key =
 | 
			
		||||
  try:
 | 
			
		||||
    result = Key(c)
 | 
			
		||||
  except RangeDefect:  # ignore unknown keycodes
 | 
			
		||||
    result = Key.None
 | 
			
		||||
 | 
			
		||||
{.pop}
 | 
			
		||||
 | 
			
		||||
proc setNonBlockingTty*(enabled: bool) =
 | 
			
		||||
  var ttyState: Termios
 | 
			
		||||
 | 
			
		||||
  # get the terminal state
 | 
			
		||||
  discard tcGetAttr(STDIN_FILENO, ttyState.addr)
 | 
			
		||||
 | 
			
		||||
  if enabled:
 | 
			
		||||
    # turn off canonical mode & echo
 | 
			
		||||
    ttyState.c_lflag = ttyState.c_lflag and not Cflag(ICANON or ECHO)
 | 
			
		||||
 | 
			
		||||
    # minimum of number input read
 | 
			
		||||
    ttyState.c_cc[VMIN] = 0.char
 | 
			
		||||
 | 
			
		||||
  else:
 | 
			
		||||
    # turn on canonical mode & echo
 | 
			
		||||
    ttyState.c_lflag = ttyState.c_lflag or ICANON or ECHO
 | 
			
		||||
 | 
			
		||||
  # set the terminal attributes.
 | 
			
		||||
  discard tcSetAttr(STDIN_FILENO, TCSANOW, ttyState.addr)
 | 
			
		||||
 | 
			
		||||
proc parseStdin[T](input: T): Key =
 | 
			
		||||
  var ch1, ch2, ch3, ch4, ch5: char
 | 
			
		||||
  result = Key.None
 | 
			
		||||
  if read(input, ch1.addr, 1) > 0:
 | 
			
		||||
    case ch1
 | 
			
		||||
    of '\e':
 | 
			
		||||
      if read(input, ch2.addr, 1) > 0:
 | 
			
		||||
        if ch2 == 'O' and read(input, ch3.addr, 1) > 0:
 | 
			
		||||
          if ch3 in "ABCDFH":
 | 
			
		||||
            result = KEYS_D[int(ch3) - int('A')]
 | 
			
		||||
          elif ch3 in "PQRS":
 | 
			
		||||
            result = KEYS_F[int(ch3) - int('P')]
 | 
			
		||||
        elif ch2 == '[' and read(input, ch3.addr, 1) > 0:
 | 
			
		||||
          if ch3 in "ABCDFH":
 | 
			
		||||
            result = KEYS_D[int(ch3) - int('A')]
 | 
			
		||||
          elif ch3 in "PQRS":
 | 
			
		||||
            result = KEYS_F[int(ch3) - int('P')]
 | 
			
		||||
          elif ch3 == '1' and read(input, ch4.addr, 1) > 0:
 | 
			
		||||
            if ch4 == '~':
 | 
			
		||||
              result = Key.Home
 | 
			
		||||
            elif ch4 in "12345789" and read(input, ch5.addr, 1) > 0 and ch5 == '~':
 | 
			
		||||
              result = KEYS_F[int(ch4) - int('1')]
 | 
			
		||||
          elif ch3 == '2' and read(input, ch4.addr, 1) > 0:
 | 
			
		||||
            if ch4 == '~':
 | 
			
		||||
              result = Key.Insert
 | 
			
		||||
            elif ch4 in "0134" and read(input, ch5.addr, 1) > 0 and ch5 == '~':
 | 
			
		||||
              result = KEYS_G[int(ch4) - int('0')]
 | 
			
		||||
          elif ch3 in "345678" and read(input, ch4.addr, 1) > 0 and ch4 == '~':
 | 
			
		||||
            result = KEYS_E[int(ch3) - int('3')]
 | 
			
		||||
          else:
 | 
			
		||||
            discard   # if cannot parse full seq it is discarded
 | 
			
		||||
        else:
 | 
			
		||||
          discard     # if cannot parse full seq it is discarded
 | 
			
		||||
      else:
 | 
			
		||||
        result = Key.Escape
 | 
			
		||||
    of '\n':
 | 
			
		||||
      result = Key.Enter
 | 
			
		||||
    of '\b':
 | 
			
		||||
      result = Key.Backspace
 | 
			
		||||
    else:
 | 
			
		||||
      result = toKey(int(ch1))
 | 
			
		||||
 | 
			
		||||
proc kbhit(ms: int): cint =
 | 
			
		||||
  var tv: Timeval
 | 
			
		||||
  tv.tv_sec = Time(ms div 1000)
 | 
			
		||||
  tv.tv_usec = 1000 * (int32(ms) mod 1000) # int32 because of macos
 | 
			
		||||
 | 
			
		||||
  var fds: TFdSet
 | 
			
		||||
  FD_ZERO(fds)
 | 
			
		||||
  FD_SET(STDIN_FILENO, fds)
 | 
			
		||||
  discard select(STDIN_FILENO+1, fds.addr, nil, nil, tv.addr)
 | 
			
		||||
  return FD_ISSET(STDIN_FILENO, fds)
 | 
			
		||||
 | 
			
		||||
proc getKeyAsync*(ms: int = 0): Key =
 | 
			
		||||
  result = Key.None
 | 
			
		||||
  if kbhit(ms) > 0:
 | 
			
		||||
    result = parseStdin(cint(STDIN_FILENO))
 | 
			
		||||
@@ -1,48 +1,64 @@
 | 
			
		||||
import std/strutils
 | 
			
		||||
 | 
			
		||||
type ScrollText* = ref object
 | 
			
		||||
  text: string
 | 
			
		||||
  maxWidth: int
 | 
			
		||||
  scrollIdx: int        # Current index of the first character to show.
 | 
			
		||||
  lastRender: string
 | 
			
		||||
 | 
			
		||||
const endBufLen = 4
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc render(st: ScrollText): string =
 | 
			
		||||
  if st.text.len <= st.maxWidth:
 | 
			
		||||
    return st.text
 | 
			
		||||
 | 
			
		||||
  if st.scrollIdx == 0: return st.text[0..<st.maxWidth]
 | 
			
		||||
  elif st.scrollIdx == st.text.len:
 | 
			
		||||
    return " " & st.text[0..<st.maxWidth - 1]
 | 
			
		||||
  elif st.scrollIdx >= st.text.len:
 | 
			
		||||
    return ' '.repeat(endBufLen - (st.scrollIdx - st.text.len)) &
 | 
			
		||||
      st.text[0..<st.maxWidth - 1]
 | 
			
		||||
  elif st.scrollIdx + st.maxWidth < st.text.len:
 | 
			
		||||
    return st.text[st.scrollIdx..<(st.scrollIdx + st.maxWidth)]
 | 
			
		||||
  else:
 | 
			
		||||
    return st.text[st.scrollIdx..<st.text.len] & " " &
 | 
			
		||||
    return st.text[st.scrollIdx..<st.text.len] & ' '.repeat(endBufLen) &
 | 
			
		||||
      st.text[0..<max(0, (st.scrollIdx + st.maxWidth - st.text.len - 1))]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc `text=`*(st: var ScrollText, text: string) =
 | 
			
		||||
  st.text = text
 | 
			
		||||
  st.scrollIdx = 0
 | 
			
		||||
  st.scrollIdx = text.len
 | 
			
		||||
  st.lastRender = render(st)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc `maxWidth=`*(st: var ScrollText, maxWidth: int) =
 | 
			
		||||
  st.maxWidth = max(maxWidth, 1)
 | 
			
		||||
  st.lastRender = render(st)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc initScrollText*(text: string, maxWidth: int): ScrollText =
 | 
			
		||||
  result = ScrollText(
 | 
			
		||||
    text: text,
 | 
			
		||||
    maxWidth: maxWidth,
 | 
			
		||||
    maxWidth: max(1, maxWidth),
 | 
			
		||||
    scrollIdx: 0)
 | 
			
		||||
 | 
			
		||||
  result.lastRender = render(result)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc nextTick*(st: var ScrollText): string =
 | 
			
		||||
 | 
			
		||||
  st.lastRender = render(st)
 | 
			
		||||
 | 
			
		||||
  # Advance the scroll index by one.
 | 
			
		||||
  st.scrollIdx = (st.scrollIdx + 1) mod (st.text.len + 1)
 | 
			
		||||
  st.scrollIdx = (st.scrollIdx + 1) mod (st.text.len + endBufLen)
 | 
			
		||||
 | 
			
		||||
  return st.lastRender
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func isScrolling*(st: ScrollText): bool =
 | 
			
		||||
  return st.text.len > st.maxWidth
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func `$`*(st: ScrollText): string = st.lastRender
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
converter toString*(st: ScrollText): string = st.lastRender
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								src/main/nim/wdiwtlt/terminal.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/main/nim/wdiwtlt/terminal.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
import std/[nre, sequtils]
 | 
			
		||||
 | 
			
		||||
const CSI = "\x1b["
 | 
			
		||||
const RESET_FORMATTING* = "\x1b[0m"
 | 
			
		||||
const ANSI_ESCAPE_CODE_ENDINGS*: seq[char] = toSeq('A'..'Z') & toSeq('a'..'z')
 | 
			
		||||
let FORMATTING_REGEX* = re("\x1b\\[([0-9;]*)([a-zA-Z])")
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  CursorType* = enum
 | 
			
		||||
    ctBlockBlink = 1, ctBlock, ctUnderlineBlink, ctUnderline, ctBarBlink, ctBar
 | 
			
		||||
 | 
			
		||||
  EraseMode* = enum
 | 
			
		||||
    emToEnd, emToStart, emAll
 | 
			
		||||
 | 
			
		||||
  TerminalColors* = enum
 | 
			
		||||
    cBlack, cRed, cGreen, cYellow, cBlue, cMagenta, cCyan, cWhite
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc stripFormatting*(text: string): string =
 | 
			
		||||
  text.replace(FORMATTING_REGEX, "")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func ansiAwareSubstring*(s: string, start, length: int): string =
 | 
			
		||||
  result = ""
 | 
			
		||||
  var curAnsiEscCode = ""
 | 
			
		||||
  var i = 0
 | 
			
		||||
  var visibleLen = 0
 | 
			
		||||
 | 
			
		||||
  while i < len(s) and visibleLen < length:
 | 
			
		||||
 | 
			
		||||
    if len(s) > i + len(CSI) and
 | 
			
		||||
      # We need to notice ANSI escape codes...
 | 
			
		||||
       s[i..<i + len(CSI)] == CSI:
 | 
			
		||||
      var j = i + len(CSI)
 | 
			
		||||
      while j < s.len and s[j] notin ANSI_ESCAPE_CODE_ENDINGS: j += 1
 | 
			
		||||
 | 
			
		||||
      # and remember it if we're before the start of the substring
 | 
			
		||||
      if i < start: curAnsiEscCode = s[i..j]
 | 
			
		||||
 | 
			
		||||
      # or add it without increasing our substring length
 | 
			
		||||
      else: result.add(s[i..j])
 | 
			
		||||
 | 
			
		||||
      # either way we want to pick up after it
 | 
			
		||||
      i = j
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
      result.add(s[i])
 | 
			
		||||
      visibleLen += 1
 | 
			
		||||
 | 
			
		||||
    i += 1
 | 
			
		||||
 | 
			
		||||
  result = curAnsiEscCode & result
 | 
			
		||||
 | 
			
		||||
func color*(text: string, fg = cWhite, bg = cBlack): string =
 | 
			
		||||
  return CSI & $int(fg) & ";" & $(int(bg) + 40) & "m" & text & RESET_FORMATTING
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func color*(text: string, fg: TerminalColors): string =
 | 
			
		||||
  return CSI & $int(fg) & "m" & text & RESET_FORMATTING
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func color*(text: string, bg: TerminalColors): string =
 | 
			
		||||
  return CSI & $(int(bg) + 40) & "m" & text & RESET_FORMATTING
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func invert*(text: string): string = return CSI & "7m" & text & RESET_FORMATTING
 | 
			
		||||
func striketrough*(text: string): string = return CSI & "9m" & text & RESET_FORMATTING
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func eraseDisplay*(mode = emToEnd): string = return CSI & $int(mode) & "J"
 | 
			
		||||
func eraseLine*(mode = emToEnd): string = return CSI & $int(mode) & "K"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func blinkSlow*(text: string): string = return CSI & "5m" & text & RESET_FORMATTING
 | 
			
		||||
func blinkFast*(text: string): string = return CSI & "6m" & text & RESET_FORMATTING
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func cursorUp*(n: int = 1): string = return CSI & $int(n) & "A"
 | 
			
		||||
func cursorDown*(n: int = 1): string = return CSI & $int(n) & "B"
 | 
			
		||||
func cursorForward*(n: int = 1): string = return CSI & $int(n) & "C"
 | 
			
		||||
func cursorBackward*(n: int = 1): string = return CSI & $int(n) & "D"
 | 
			
		||||
func cursorNextLine*(n: int = 1): string = return CSI & $int(n) & "E"
 | 
			
		||||
func cursorPrevLine*(n: int = 1): string = return CSI & $int(n) & "F"
 | 
			
		||||
func cursorHorizontalAbsolute*(col: int): string = return CSI & $int(col) & "G"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func cursorPosition*(row, col: int): string =
 | 
			
		||||
  return CSI & $int(row) & ";" & $int(col) & "H"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc cursorType*(ct: CursorType): string = return CSI & $int(ct) & " q"
 | 
			
		||||
func saveCursorPosition*(): string = return CSI & "s"
 | 
			
		||||
func restoreCursorPosition*(): string = return CSI & "u"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func scrollUp*(n: int = 1): string = return CSI & $int(n) & "S"
 | 
			
		||||
func scrollDown*(n: int = 1): string = return CSI & $int(n) & "T"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc write*(f: File, x, y: int, text: string) =
 | 
			
		||||
  f.write(cursorPosition(y, x) & text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc write*(x, y: int, text: string) =
 | 
			
		||||
  stdout.write(cursorPosition(y, x) & text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
proc setCursorType*(ct: CursorType) = stdout.write(cursorType(ct))
 | 
			
		||||
proc setCursorPosition*(x, y: int) = stdout.write(cursorPosition(y, x))
 | 
			
		||||
proc hideCursor*() = stdout.write(CSI & "?25l")
 | 
			
		||||
proc showCursor*() = stdout.write(CSI & "?25l")
 | 
			
		||||
							
								
								
									
										5
									
								
								src/main/nim/wdiwtlt/usage.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/main/nim/wdiwtlt/usage.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
func usageOf*(command: seq[string]): string =
 | 
			
		||||
  case command[0]
 | 
			
		||||
  of "log":
 | 
			
		||||
    # TODO
 | 
			
		||||
    discard
 | 
			
		||||
							
								
								
									
										7
									
								
								src/main/nim/wdiwtlt/util.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/main/nim/wdiwtlt/util.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
func clamp*(value, min, max: int): int =
 | 
			
		||||
  if value < min:
 | 
			
		||||
    return min
 | 
			
		||||
  elif value > max:
 | 
			
		||||
    return max
 | 
			
		||||
  else:
 | 
			
		||||
    return value
 | 
			
		||||
		Reference in New Issue
	
	Block a user