WIP: Rendering/UI refactor.
This commit is contained in:
		@@ -1,49 +1,42 @@
 | 
				
			|||||||
import std/[json, options, os, paths, strutils, times, unicode]
 | 
					import std/[json, options, os, paths, sequtils, strutils, terminal, times,
 | 
				
			||||||
import cliutils, docopt, illwill, mpv
 | 
					            unicode]
 | 
				
			||||||
 | 
					import cliutils, docopt, namespaced_logging, mpv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import wdiwtlt/[cliconstants, commandbuffer, logging, medialibrary, models,
 | 
					import wdiwtlt/[cliconstants, commandbuffer, logging, medialibrary,
 | 
				
			||||||
                scrolltext]
 | 
					                messagebuffer, models, nonblockingio, scrolltext, usage]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type
 | 
					type
 | 
				
			||||||
  CliMode {.pure.} = enum Direct, Command
 | 
					  CliMode {.pure.} = enum Direct, Command
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ScrollingDisplays* = ref object
 | 
					  MessageBuffers* = ref object
 | 
				
			||||||
    playing: ScrollText
 | 
					    playing: MessageBuffer[ScrollText]
 | 
				
			||||||
    status: ScrollText
 | 
					    status: MessageBuffer[ScrollText]
 | 
				
			||||||
    statusDismissal: Option[DateTime]
 | 
					    longform: MessageBuffer[string]
 | 
				
			||||||
 | 
					    input: MessageBuffer[string]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  History = ref object
 | 
					  History = ref object
 | 
				
			||||||
    entries: seq[string]
 | 
					    entries: seq[string]
 | 
				
			||||||
    idx: int
 | 
					    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
 | 
					  CliContext = ref object
 | 
				
			||||||
    cfg: CombinedConfig
 | 
					    cfg: CombinedConfig
 | 
				
			||||||
    cmd: CommandBuffer
 | 
					    cmd: CommandBuffer
 | 
				
			||||||
    cmdVisibleIdx: int              # first visible character in the cmd buffer
 | 
					 | 
				
			||||||
    curMediaFile: Option[MediaFile]
 | 
					    curMediaFile: Option[MediaFile]
 | 
				
			||||||
    d: ScrollingDisplays
 | 
					 | 
				
			||||||
    dims: RenderDimensions
 | 
					 | 
				
			||||||
    frame: int
 | 
					 | 
				
			||||||
    lib: MediaLibrary
 | 
					    lib: MediaLibrary
 | 
				
			||||||
    log: Option[Logger]
 | 
					    log: Option[Logger]
 | 
				
			||||||
 | 
					    logs: seq[LogMessage]
 | 
				
			||||||
 | 
					    logThreshold: Level
 | 
				
			||||||
    mode: CliMode
 | 
					    mode: CliMode
 | 
				
			||||||
 | 
					    msgs: MessageBuffers
 | 
				
			||||||
    mpv: ptr handle
 | 
					    mpv: ptr handle
 | 
				
			||||||
    msgDuration: Duration
 | 
					    msgDuration: Duration
 | 
				
			||||||
    stop*: bool
 | 
					    stop*: bool
 | 
				
			||||||
    tb: TerminalBuffer
 | 
					    width*: int
 | 
				
			||||||
 | 
					    height*: int
 | 
				
			||||||
  CursorType {.pure.} = enum
 | 
					 | 
				
			||||||
    Block, BlockBlink, BarBlink, Underline
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var ctx {.threadvar.}: CliContext
 | 
					var ctx {.threadvar.}: CliContext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FRAME_DUR_MS = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc initMpv(): ptr handle =
 | 
					proc initMpv(): ptr handle =
 | 
				
			||||||
  result = mpv.create()
 | 
					  result = mpv.create()
 | 
				
			||||||
@@ -66,47 +59,18 @@ proc initMpv(): ptr handle =
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
proc cleanup() =
 | 
					proc cleanup() =
 | 
				
			||||||
  terminate_destroy(ctx.mpv)
 | 
					  terminate_destroy(ctx.mpv)
 | 
				
			||||||
  illWillDeinit()
 | 
					  setNonBlockingTty(false)
 | 
				
			||||||
  echo ""
 | 
					  echo ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[
 | 
				
			||||||
proc exitHandlerHook() {.noconv.} =
 | 
					proc exitHandlerHook() {.noconv.} =
 | 
				
			||||||
  # This is called when the user presses Ctrl+C
 | 
					  # This is called when the user presses Ctrl+C
 | 
				
			||||||
  # or when the program exits normally.
 | 
					  # or when the program exits normally.
 | 
				
			||||||
  echo "Ctrl-C"
 | 
					 | 
				
			||||||
  terminate_destroy(ctx.mpv)
 | 
					  terminate_destroy(ctx.mpv)
 | 
				
			||||||
  illWillDeinit()
 | 
					  echo ""
 | 
				
			||||||
  quit(QuitSuccess)
 | 
					  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 =
 | 
					proc initContext(args: Table[string, Value]): CliContext =
 | 
				
			||||||
  var wdiwtltCfgFilename: string
 | 
					  var wdiwtltCfgFilename: string
 | 
				
			||||||
@@ -149,177 +113,201 @@ proc initContext(args: Table[string, Value]): CliContext =
 | 
				
			|||||||
  let width = min(terminalWidth(), cfg.getVal("maxWidth", "80").parseInt)
 | 
					  let width = min(terminalWidth(), cfg.getVal("maxWidth", "80").parseInt)
 | 
				
			||||||
  let height = min(terminalHeight(), cfg.getVal("maxHeight", "60").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(
 | 
					  result = CliContext(
 | 
				
			||||||
    cfg: cfg,
 | 
					    cfg: cfg,
 | 
				
			||||||
    cmd: initCommandBuffer(),
 | 
					    cmd: initCommandBuffer(msgs.input),
 | 
				
			||||||
    cmdVisibleIdx: 0,
 | 
					 | 
				
			||||||
    curMediaFile: none(MediaFile),
 | 
					    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(
 | 
					    lib: initMediaLibrary(
 | 
				
			||||||
      rootDir = getVal(cfg, "libraryRoot").Path,
 | 
					      rootDir = getVal(cfg, "libraryRoot").Path,
 | 
				
			||||||
      dbPath = getVal(cfg, "dbPath").Path),
 | 
					      dbPath = getVal(cfg, "dbPath").Path),
 | 
				
			||||||
    log: log,
 | 
					    log: log,
 | 
				
			||||||
    mode: CliMode.Direct,
 | 
					    mode: CliMode.Direct,
 | 
				
			||||||
 | 
					    msgs: msgs,
 | 
				
			||||||
    mpv: initMpv(),
 | 
					    mpv: initMpv(),
 | 
				
			||||||
    msgDuration: initDuration(seconds = 5, milliseconds = 0),
 | 
					    msgDuration: initDuration(seconds = 5, milliseconds = 0),
 | 
				
			||||||
    stop: false,
 | 
					    stop: false,
 | 
				
			||||||
    tb: newTerminalBuffer(width, height))
 | 
					    width: width,
 | 
				
			||||||
 | 
					    height: height)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if logService.isSome:
 | 
					  if logService.isSome:
 | 
				
			||||||
    let customLogAppender = initCustomLogAppender(doLogMessage =
 | 
					    let customLogAppender = initCustomLogAppender(doLogMessage =
 | 
				
			||||||
      proc (msg: LogMessage): void =
 | 
					      proc (msg: LogMessage): void = ctx.logs.add(msg))
 | 
				
			||||||
        ctx.writeAt(ctx.dims.logs, msg.message))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #logService.get.clearAppenders()
 | 
					    logService.get.clearAppenders()
 | 
				
			||||||
    logService.get.addAppender(customLogAppender)
 | 
					    logService.get.addAppender(customLogAppender)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc setCursorType(ct: CursorType) =
 | 
					proc nextTick(ctx: var CliContext) =
 | 
				
			||||||
  case ct
 | 
					  ctx.msgs.playing.nextTick
 | 
				
			||||||
  of CursorType.Block:      stdout.write("\x1b[2 q")
 | 
					  ctx.msgs.status.nextTick
 | 
				
			||||||
  of CursorType.BlockBlink: stdout.write("\x1b[1 q")
 | 
					  ctx.msgs.longform.nextTick
 | 
				
			||||||
  of CursorType.BarBlink:   stdout.write("\x1b[5 q")
 | 
					 | 
				
			||||||
  of CursorType.Underline:  stdout.write("\x1b[4 q")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc setCursorPosition(x, y: int) =
 | 
					proc setMode(ctx: var CliContext, mode: CliMode) =
 | 
				
			||||||
  stdout.write("\x1b[$#;$#H" % [$(y + 1), $(x + 1)])
 | 
					  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) =
 | 
					proc statusMsg(ctx: var CliContext, msg: string) =
 | 
				
			||||||
  ctx.d.status.text = msg
 | 
					  ctx.msgs.status.showMsg(msg, some(ctx.msgDuration))
 | 
				
			||||||
  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 render(ctx: var CliContext) =
 | 
					proc longMsg[T: string or seq[string]](ctx: var CliContext, msg: T) =
 | 
				
			||||||
  # text moves at 5 characters per second (192ms per character)
 | 
					  ctx.msgs.longform.showMsg(msg, some(ctx.msgDuration))
 | 
				
			||||||
  if ctx.frame mod 12 == 0: nextTick(ctx)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ctx.clearLine(ctx.dims.playing.y)
 | 
					 | 
				
			||||||
  ctx.writeAt(ctx.dims.playing, ctx.d.playing)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ctx.clearLine(ctx.dims.status.y)
 | 
					proc viewLogLoops(ctx: var CliContext) =
 | 
				
			||||||
  ctx.writeAt(ctx.dims.status, ctx.d.status)
 | 
					  let logLines = ctx.logs
 | 
				
			||||||
 | 
					    .filterIt(it.level > ctx.logThreshold)
 | 
				
			||||||
 | 
					    .mapIt("$# - [$#]: $#" % [$it.level, $it.scope, it.message])
 | 
				
			||||||
 | 
					    .join("\p")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if ctx.mode == CliMode.Command:
 | 
					  ctx.msgs.longform.showMsg(logLines, none[Duration]())
 | 
				
			||||||
    case ctx.cmd.mode:
 | 
					 | 
				
			||||||
    of EditMode.Insert:     setCursorType(CursorType.BarBlink)
 | 
					 | 
				
			||||||
    of EditMode.Overwrite:  setCursorType(CursorType.BlockBlink)
 | 
					 | 
				
			||||||
    else:                   setCursorType(CursorType.Block)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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
 | 
					    sleep(FRAME_DUR_MS)
 | 
				
			||||||
    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()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ctx.msgs.longform.clear()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc scanLibrary(ctx: var CliContext) =
 | 
					proc scanLibrary(ctx: var CliContext) =
 | 
				
			||||||
 | 
					  ctx.statusMsg("Scanning media library...")
 | 
				
			||||||
  let counts = ctx.lib.rescanLibrary()
 | 
					  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) =
 | 
					proc processCommand(ctx: var CliContext, command: string) =
 | 
				
			||||||
  let parts = command.strip.split(' ', maxSplit=1)
 | 
					  let parts = command.strip.split(' ')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case parts[0]
 | 
					  case parts[0]
 | 
				
			||||||
  of "scan":                scanLibrary(ctx)
 | 
					  of "scan":                scanLibrary(ctx)
 | 
				
			||||||
  of "q", "quit", "exit":   ctx.stop = true
 | 
					  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) =
 | 
					proc mainLoop(ctx: var CliContext) =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  hideCursor()
 | 
					  hideCursor()
 | 
				
			||||||
 | 
					  var frame = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  while not ctx.stop:
 | 
					  while not ctx.stop:
 | 
				
			||||||
    let key = getKey()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ctx.mode == CliMode.Direct:
 | 
					    # This sleep below will be the primary driver of the frame rate of the
 | 
				
			||||||
      case key
 | 
					    # application (examples below):
 | 
				
			||||||
      of Key.Q:     ctx.stop = true
 | 
					    #
 | 
				
			||||||
      of Key.Colon, Key.I: ctx.setMode(CliMode.Command)
 | 
					    # | frames/sec | sec/frame | ms/frame |
 | 
				
			||||||
      else: discard
 | 
					    # |------------|-----------|----------|
 | 
				
			||||||
 | 
					    # |    5.00    |   0.200   |    200   |
 | 
				
			||||||
    elif ctx.mode == CliMode.Command:
 | 
					    # |   10.00    |   0.100   |    100   |
 | 
				
			||||||
      case key
 | 
					    # |   12.50    |   0.080   |     80   |
 | 
				
			||||||
      of Key.Enter:
 | 
					    # |   20.00    |   0.050   |     50   |
 | 
				
			||||||
        let command = $ctx.cmd
 | 
					    # |   25.00    |   0.040   |     40   |
 | 
				
			||||||
        ctx.cmd.clear
 | 
					    # |   31.25    |   0.032   |     32   |
 | 
				
			||||||
        ctx.cmdVisibleIdx = 0
 | 
					    # |   50.00    |   0.020   |     20   |
 | 
				
			||||||
        ctx.cmd.mode = EditMode.Insert
 | 
					    # |   62.50    |   0.016   |     16   |
 | 
				
			||||||
        processCommand(ctx, command)
 | 
					    #
 | 
				
			||||||
      of Key.Backspace: ctx.cmd.handleInput(Key.Backspace)
 | 
					    # Previously, when we were using illwill and rendering every frame we
 | 
				
			||||||
      of Key.Escape:
 | 
					    # targeted a faster FPS to allow for a responsive feeling to the command
 | 
				
			||||||
        if ctx.cmd.mode == EditMode.Command:
 | 
					    # input, etc. In this case, we still had a "logic update" period that was
 | 
				
			||||||
          ctx.setMode(CliMode.Direct)
 | 
					    # lower than our rendering FPS. Specifically, we were targeting 60fps
 | 
				
			||||||
          ctx.cmd.clear
 | 
					    # rendering speed, but 5fps for logic updates (scrolling text, expiring
 | 
				
			||||||
          ctx.cmdVisibleIdx = 0
 | 
					    # messages, etc.).
 | 
				
			||||||
        else: ctx.cmd.handleInput(Key.Escape)
 | 
					    #
 | 
				
			||||||
      else: ctx.cmd.handleInput(key)
 | 
					    # 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
 | 
				
			||||||
    render(ctx)
 | 
					    # responsive UI. Now we really only care about the frequency of logic
 | 
				
			||||||
 | 
					    # updates, so we're targeting 5fps and triggering our logic updates every
 | 
				
			||||||
    # target 60 FPS
 | 
					    # frame.
 | 
				
			||||||
    sleep(16)
 | 
					    handleKey(ctx, getKeyAsync())
 | 
				
			||||||
    ctx.frame = (ctx.frame + 1) mod 1920
 | 
					    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:
 | 
					when isMainModule:
 | 
				
			||||||
@@ -328,8 +316,8 @@ when isMainModule:
 | 
				
			|||||||
  try:
 | 
					  try:
 | 
				
			||||||
    let args = docopt(USAGE, version=VERSION)
 | 
					    let args = docopt(USAGE, version=VERSION)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    illWillInit(fullscreen=true)
 | 
					 | 
				
			||||||
    ctx = initContext(args)
 | 
					    ctx = initContext(args)
 | 
				
			||||||
 | 
					    setNonBlockingTty(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mainLoop(ctx)
 | 
					    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
 | 
					type
 | 
				
			||||||
  EditMode* {.pure.} = enum Command, Insert, Overwrite, Visual
 | 
					  EditMode* {.pure.} = enum Command, Insert, Overwrite, Visual
 | 
				
			||||||
@@ -12,24 +12,19 @@ type
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  CommandBuffer* = ref object
 | 
					  CommandBuffer* = ref object
 | 
				
			||||||
    history: seq[Command]
 | 
					    history: seq[Command]
 | 
				
			||||||
 | 
					    mb: MessageBuffer[string]
 | 
				
			||||||
    idx: int
 | 
					    idx: int
 | 
				
			||||||
 | 
					    visibleIdx: int # first visible character in the cmd buffer
 | 
				
			||||||
    mode*: EditMode
 | 
					    mode*: EditMode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func clamp(value, min, max: int): int =
 | 
					func initCommandBuffer*(mb: MessageBuffer[string]): CommandBuffer =
 | 
				
			||||||
  if value < min:
 | 
					 | 
				
			||||||
    return min
 | 
					 | 
				
			||||||
  elif value > max:
 | 
					 | 
				
			||||||
    return max
 | 
					 | 
				
			||||||
  else:
 | 
					 | 
				
			||||||
    return value
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func initCommandBuffer*(): CommandBuffer =
 | 
					 | 
				
			||||||
  result = CommandBuffer(
 | 
					  result = CommandBuffer(
 | 
				
			||||||
    history: @[Command(
 | 
					    history: @[Command(
 | 
				
			||||||
      buffer: "",
 | 
					      buffer: "",
 | 
				
			||||||
      idx: 0,
 | 
					      idx: 0,
 | 
				
			||||||
      selectionStartIdx: none(int))],
 | 
					      selectionStartIdx: none(int))],
 | 
				
			||||||
    idx: 0,
 | 
					    idx: 0,
 | 
				
			||||||
 | 
					    mb: mb,
 | 
				
			||||||
    mode: Insert)
 | 
					    mode: Insert)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc cur(cb: CommandBuffer): Command = cb.history[cb.idx]
 | 
					proc cur(cb: CommandBuffer): Command = cb.history[cb.idx]
 | 
				
			||||||
@@ -72,6 +67,7 @@ proc clear*(cb: var CommandBuffer) =
 | 
				
			|||||||
    idx: 0,
 | 
					    idx: 0,
 | 
				
			||||||
    selectionStartIdx: none(int)))
 | 
					    selectionStartIdx: none(int)))
 | 
				
			||||||
  cb.idx = cb.history.len - 1
 | 
					  cb.idx = cb.history.len - 1
 | 
				
			||||||
 | 
					  cb.visibleIdx = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc left*(cb: var CommandBuffer) =
 | 
					proc left*(cb: var CommandBuffer) =
 | 
				
			||||||
  cb.cur.idx = clamp(cb.cur.idx - 1, 0, cb.cur.buffer.len)
 | 
					  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))
 | 
					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) =
 | 
					proc handleInput*(cb: var CommandBuffer, key: Key) =
 | 
				
			||||||
  case cb.mode
 | 
					  case cb.mode
 | 
				
			||||||
  of EditMode.Insert, EditMode.Overwrite:
 | 
					  of EditMode.Insert, EditMode.Overwrite:
 | 
				
			||||||
    case key
 | 
					    case key
 | 
				
			||||||
    of Key.Escape: cb.mode = EditMode.Command
 | 
					    of Key.Escape: cb.setMode(EditMode.Command)
 | 
				
			||||||
    of Key.Backspace: cb.delete()
 | 
					    of Key.Backspace: cb.delete()
 | 
				
			||||||
    of Key.Delete: cb.delete(backspace = false)
 | 
					    of Key.Delete: cb.delete(backspace = false)
 | 
				
			||||||
    of Key.Left: cb.left()
 | 
					    of Key.Left: cb.left()
 | 
				
			||||||
@@ -133,26 +205,27 @@ proc handleInput*(cb: var CommandBuffer, key: Key) =
 | 
				
			|||||||
    of Key.B: cb.backWord()
 | 
					    of Key.B: cb.backWord()
 | 
				
			||||||
    of Key.W: cb.forwardWord()
 | 
					    of Key.W: cb.forwardWord()
 | 
				
			||||||
    of Key.E: cb.endOfWord()
 | 
					    of Key.E: cb.endOfWord()
 | 
				
			||||||
    of Key.I: cb.mode = EditMode.Insert
 | 
					    of Key.I: cb.setMode(EditMode.Insert)
 | 
				
			||||||
    of Key.A:
 | 
					    of Key.A:
 | 
				
			||||||
      cb.cur.idx = clamp(cb.cur.idx + 1, 0, cb.cur.buffer.len)
 | 
					      cb.cur.idx = clamp(cb.cur.idx + 1, 0, cb.cur.buffer.len)
 | 
				
			||||||
      cb.mode = EditMode.Insert
 | 
					      cb.setMode(EditMode.Insert)
 | 
				
			||||||
    of Key.ShiftA:
 | 
					    of Key.ShiftA:
 | 
				
			||||||
      cb.cur.idx = cb.cur.buffer.len
 | 
					      cb.cur.idx = cb.cur.buffer.len
 | 
				
			||||||
      cb.mode = EditMode.Insert
 | 
					      cb.setMode(EditMode.Insert)
 | 
				
			||||||
    of Key.ShiftI:
 | 
					    of Key.ShiftI:
 | 
				
			||||||
      cb.cur.idx = 0
 | 
					      cb.cur.idx = 0
 | 
				
			||||||
      cb.mode = EditMode.Insert
 | 
					      cb.setMode(EditMode.Insert)
 | 
				
			||||||
    of Key.ShiftR: cb.mode = EditMode.Overwrite
 | 
					    of Key.ShiftR:
 | 
				
			||||||
 | 
					      cb.setMode(EditMode.Overwrite)
 | 
				
			||||||
    of Key.V:
 | 
					    of Key.V:
 | 
				
			||||||
      cb.mode = EditMode.Visual
 | 
					      cb.setMode(EditMode.Visual)
 | 
				
			||||||
      cb.cur.selectionStartIdx = some(cb.cur.idx)
 | 
					      cb.cur.selectionStartIdx = some(cb.cur.idx)
 | 
				
			||||||
    else: discard
 | 
					    else: discard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  of EditMode.Visual:
 | 
					  of EditMode.Visual:
 | 
				
			||||||
    case key
 | 
					    case key
 | 
				
			||||||
    of Key.Escape:
 | 
					    of Key.Escape:
 | 
				
			||||||
      cb.mode = EditMode.Command
 | 
					      cb.setMode(EditMode.Command)
 | 
				
			||||||
      cb.cur.selectionStartIdx = none(int)
 | 
					      cb.cur.selectionStartIdx = none(int)
 | 
				
			||||||
    of Key.Backspace: cb.left()
 | 
					    of Key.Backspace: cb.left()
 | 
				
			||||||
    of Key.Left,   Key.H: cb.left()
 | 
					    of Key.Left,   Key.H: cb.left()
 | 
				
			||||||
@@ -162,17 +235,13 @@ proc handleInput*(cb: var CommandBuffer, key: Key) =
 | 
				
			|||||||
      cb.prev()
 | 
					      cb.prev()
 | 
				
			||||||
    of Key.Down,   Key.K:
 | 
					    of Key.Down,   Key.K:
 | 
				
			||||||
      cb.cur.selectionStartIdx = none[int]()
 | 
					      cb.cur.selectionStartIdx = none[int]()
 | 
				
			||||||
      cb.next()
 | 
					 | 
				
			||||||
    of Key.Home,   Key.Zero: cb.toHome()
 | 
					    of Key.Home,   Key.Zero: cb.toHome()
 | 
				
			||||||
    of Key.End,    Key.Dollar: cb.toEnd()
 | 
					    of Key.End,    Key.Dollar: cb.toEnd()
 | 
				
			||||||
    of Key.B: cb.backWord()
 | 
					    of Key.B: cb.backWord()
 | 
				
			||||||
    of Key.W: cb.forwardWord()
 | 
					    of Key.W: cb.forwardWord()
 | 
				
			||||||
    of Key.V:
 | 
					    of Key.V:
 | 
				
			||||||
      cb.mode = EditMode.Command
 | 
					      cb.setMode(EditMode.Command)
 | 
				
			||||||
      cb.cur.selectionStartIdx = none(int)
 | 
					      cb.cur.selectionStartIdx = none(int)
 | 
				
			||||||
    else: discard
 | 
					    else: discard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render(cb)
 | 
				
			||||||
func `$`*(cb: CommandBuffer): string = cb.cur.buffer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func idx*(cb: CommandBuffer): int = cb.cur.idx
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
					type ScrollText* = ref object
 | 
				
			||||||
  text: string
 | 
					  text: string
 | 
				
			||||||
  maxWidth: int
 | 
					  maxWidth: int
 | 
				
			||||||
  scrollIdx: int        # Current index of the first character to show.
 | 
					  scrollIdx: int        # Current index of the first character to show.
 | 
				
			||||||
  lastRender: string
 | 
					  lastRender: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const endBufLen = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc render(st: ScrollText): string =
 | 
					proc render(st: ScrollText): string =
 | 
				
			||||||
  if st.text.len <= st.maxWidth:
 | 
					  if st.text.len <= st.maxWidth:
 | 
				
			||||||
    return st.text
 | 
					    return st.text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if st.scrollIdx == 0: return st.text[0..<st.maxWidth]
 | 
					  if st.scrollIdx == 0: return st.text[0..<st.maxWidth]
 | 
				
			||||||
  elif st.scrollIdx == st.text.len:
 | 
					  elif st.scrollIdx >= st.text.len:
 | 
				
			||||||
    return " " & st.text[0..<st.maxWidth - 1]
 | 
					    return ' '.repeat(endBufLen - (st.scrollIdx - st.text.len)) &
 | 
				
			||||||
 | 
					      st.text[0..<st.maxWidth - 1]
 | 
				
			||||||
  elif st.scrollIdx + st.maxWidth < st.text.len:
 | 
					  elif st.scrollIdx + st.maxWidth < st.text.len:
 | 
				
			||||||
    return st.text[st.scrollIdx..<(st.scrollIdx + st.maxWidth)]
 | 
					    return st.text[st.scrollIdx..<(st.scrollIdx + st.maxWidth)]
 | 
				
			||||||
  else:
 | 
					  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))]
 | 
					      st.text[0..<max(0, (st.scrollIdx + st.maxWidth - st.text.len - 1))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc `text=`*(st: var ScrollText, text: string) =
 | 
					proc `text=`*(st: var ScrollText, text: string) =
 | 
				
			||||||
  st.text = text
 | 
					  st.text = text
 | 
				
			||||||
  st.scrollIdx = 0
 | 
					  st.scrollIdx = text.len
 | 
				
			||||||
  st.lastRender = render(st)
 | 
					  st.lastRender = render(st)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc `maxWidth=`*(st: var ScrollText, maxWidth: int) =
 | 
					proc `maxWidth=`*(st: var ScrollText, maxWidth: int) =
 | 
				
			||||||
  st.maxWidth = max(maxWidth, 1)
 | 
					  st.maxWidth = max(maxWidth, 1)
 | 
				
			||||||
  st.lastRender = render(st)
 | 
					  st.lastRender = render(st)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc initScrollText*(text: string, maxWidth: int): ScrollText =
 | 
					proc initScrollText*(text: string, maxWidth: int): ScrollText =
 | 
				
			||||||
  result = ScrollText(
 | 
					  result = ScrollText(
 | 
				
			||||||
    text: text,
 | 
					    text: text,
 | 
				
			||||||
    maxWidth: maxWidth,
 | 
					    maxWidth: max(1, maxWidth),
 | 
				
			||||||
    scrollIdx: 0)
 | 
					    scrollIdx: 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  result.lastRender = render(result)
 | 
					  result.lastRender = render(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc nextTick*(st: var ScrollText): string =
 | 
					proc nextTick*(st: var ScrollText): string =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  st.lastRender = render(st)
 | 
					  st.lastRender = render(st)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Advance the scroll index by one.
 | 
					  # 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
 | 
					  return st.lastRender
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isScrolling*(st: ScrollText): bool =
 | 
				
			||||||
 | 
					  return st.text.len > st.maxWidth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func `$`*(st: ScrollText): string = st.lastRender
 | 
					func `$`*(st: ScrollText): string = st.lastRender
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
converter toString*(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