Input line rendering logic in width-constrained situations.
This commit is contained in:
parent
c75438d409
commit
a7a9cf8620
@ -1,4 +1,4 @@
|
||||
import std/[json, options, os, paths, strutils]
|
||||
import std/[json, options, os, paths, strutils, times, unicode]
|
||||
import cliutils, docopt, illwill, mpv
|
||||
|
||||
import wdiwtlt/[cliconstants, commandbuffer, logging, medialibrary, models,
|
||||
@ -10,26 +10,41 @@ type
|
||||
ScrollingDisplays* = ref object
|
||||
playing: ScrollText
|
||||
status: ScrollText
|
||||
statusDismissal: Option[DateTime]
|
||||
|
||||
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]
|
||||
mode: CliMode
|
||||
mpv: ptr handle
|
||||
msgDuration: Duration
|
||||
stop*: bool
|
||||
tb: TerminalBuffer
|
||||
d: ScrollingDisplays
|
||||
|
||||
CursorType {.pure.} = enum
|
||||
Block, BlockBlink, BarBlink, Underline
|
||||
|
||||
var ctx {.threadvar.}: CliContext
|
||||
|
||||
|
||||
proc initMpv(): ptr handle =
|
||||
result = mpv.create()
|
||||
|
||||
@ -48,12 +63,13 @@ proc initMpv(): ptr handle =
|
||||
# Done setting up options.
|
||||
check_error result.initialize()
|
||||
|
||||
|
||||
proc cleanup() =
|
||||
terminate_destroy(ctx.mpv)
|
||||
illWillDeinit()
|
||||
showCursor()
|
||||
echo ""
|
||||
|
||||
|
||||
proc exitHandlerHook() {.noconv.} =
|
||||
# This is called when the user presses Ctrl+C
|
||||
# or when the program exits normally.
|
||||
@ -62,6 +78,25 @@ proc exitHandlerHook() {.noconv.} =
|
||||
illWillDeinit()
|
||||
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
|
||||
@ -72,6 +107,7 @@ proc setMode(ctx: var CliContext, mode: CliMode) =
|
||||
showCursor()
|
||||
ctx.cmd.mode = EditMode.Insert
|
||||
|
||||
|
||||
proc initContext(args: Table[string, Value]): CliContext =
|
||||
var wdiwtltCfgFilename: string
|
||||
|
||||
@ -110,13 +146,25 @@ proc initContext(args: Table[string, Value]): CliContext =
|
||||
log.debug("loading config from '$#'" % [wdiwtltCfgFilename])
|
||||
let cfg = initCombinedConfig(wdiwtltCfgFilename, args)
|
||||
|
||||
let width = min(terminalWidth(), cfg.getVal("maxWidth", "80").parseInt)
|
||||
let height = min(terminalHeight(), cfg.getVal("maxHeight", "60").parseInt)
|
||||
|
||||
result = CliContext(
|
||||
cfg: cfg,
|
||||
cmd: initCommandBuffer(),
|
||||
cmdVisibleIdx: 0,
|
||||
curMediaFile: none(MediaFile),
|
||||
d: ScrollingDisplays(
|
||||
playing: initScrollText("Nothing playing", terminalWidth() - 1),
|
||||
status: initScrollText("Idle", terminalWidth() - 1)),
|
||||
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,
|
||||
@ -124,31 +172,119 @@ proc initContext(args: Table[string, Value]): CliContext =
|
||||
log: log,
|
||||
mode: CliMode.Direct,
|
||||
mpv: initMpv(),
|
||||
msgDuration: initDuration(seconds = 5, milliseconds = 0),
|
||||
stop: false,
|
||||
tb: newTerminalBuffer(terminalWidth(), terminalHeight()))
|
||||
tb: newTerminalBuffer(width, height))
|
||||
|
||||
if logService.isSome:
|
||||
let customLogAppender = initCustomLogAppender(doLogMessage =
|
||||
proc (msg: LogMessage): void =
|
||||
ctx.tb.write(0, 0, msg.message))
|
||||
ctx.writeAt(ctx.dims.logs, msg.message))
|
||||
|
||||
logService.get.clearAppenders()
|
||||
#logService.get.clearAppenders()
|
||||
logService.get.addAppender(customLogAppender)
|
||||
|
||||
proc render(ctx: CliContext) =
|
||||
if ctx.frame mod 20 == 0: discard ctx.d.playing.nextTick
|
||||
ctx.tb.write(0, 0, ctx.d.playing)
|
||||
|
||||
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 setCursorPosition(x, y: int) =
|
||||
stdout.write("\x1b[$#;$#H" % [$(y + 1), $(x + 1)])
|
||||
|
||||
|
||||
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 render(ctx: var CliContext) =
|
||||
# text moves at 5 characters per second (192ms per character)
|
||||
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)
|
||||
ctx.writeAt(ctx.dims.status, ctx.d.status)
|
||||
|
||||
if ctx.mode == CliMode.Command:
|
||||
case ctx.cmd.mode:
|
||||
of EditMode.Insert: stdout.write("\x1b[5 q")
|
||||
of EditMode.Overwrite: stdout.write("\x1b[0 q")
|
||||
else:
|
||||
stdout.write("\x1b[2 q")
|
||||
ctx.tb.write(0, 1, ":$#" % [$ctx.cmd])
|
||||
of EditMode.Insert: setCursorType(CursorType.BarBlink)
|
||||
of EditMode.Overwrite: setCursorType(CursorType.BlockBlink)
|
||||
else: setCursorType(CursorType.Block)
|
||||
|
||||
ctx.clearLine(ctx.dims.input.y)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
proc scanLibrary(ctx: var CliContext) =
|
||||
let counts = ctx.lib.rescanLibrary()
|
||||
|
||||
|
||||
proc processCommand(ctx: var CliContext, command: string) =
|
||||
let parts = command.strip.split(' ', maxSplit=1)
|
||||
|
||||
case parts[0]
|
||||
of "scan": scanLibrary(ctx)
|
||||
of "q", "quit", "exit": ctx.stop = true
|
||||
else: msg(ctx, "Unrecognized command: " & command)
|
||||
|
||||
proc mainLoop(ctx: var CliContext) =
|
||||
|
||||
hideCursor()
|
||||
@ -163,25 +299,28 @@ proc mainLoop(ctx: var CliContext) =
|
||||
else: discard
|
||||
|
||||
elif ctx.mode == CliMode.Command:
|
||||
ctx.tb.write(0, 1, ":", ' '.repeat(($ctx.cmd).len))
|
||||
case key
|
||||
of Key.Enter:
|
||||
let command = $ctx.cmd
|
||||
ctx.cmd.clear
|
||||
# TODO: process command
|
||||
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 50 FPS
|
||||
sleep(20)
|
||||
ctx.frame = (ctx.frame + 1 mod 50)
|
||||
# target 60 FPS
|
||||
sleep(16)
|
||||
ctx.frame = (ctx.frame + 1) mod 1920
|
||||
|
||||
|
||||
when isMainModule:
|
||||
|
||||
|
@ -60,12 +60,11 @@ proc write*(cb: var CommandBuffer, s: string) =
|
||||
|
||||
proc delete*(cb: var CommandBuffer, backspace = true) =
|
||||
var cmd = cb.history[cb.idx]
|
||||
if cmd.idx > 0:
|
||||
if backspace:
|
||||
cmd.buffer = cmd.buffer[0..<cmd.idx - 1] & cmd.buffer[cmd.idx..^1]
|
||||
else:
|
||||
cmd.buffer = cmd.buffer[0..<cmd.idx] & cmd.buffer[cmd.idx + 1..^1]
|
||||
if backspace and cmd.idx > 0:
|
||||
cmd.buffer = cmd.buffer[0..<cmd.idx - 1] & cmd.buffer[cmd.idx..^1]
|
||||
cmd.idx -= 1
|
||||
elif not backspace and cmd.idx < cmd.buffer.len:
|
||||
cmd.buffer = cmd.buffer[0..<cmd.idx] & cmd.buffer[cmd.idx + 1..^1]
|
||||
|
||||
proc clear*(cb: var CommandBuffer) =
|
||||
cb.history.add(Command(
|
||||
@ -90,9 +89,15 @@ proc backWord*(cb: var CommandBuffer) =
|
||||
|
||||
proc forwardWord*(cb: var CommandBuffer) =
|
||||
var cmd = cb.history[cb.idx]
|
||||
while cmd.idx < cmd.buffer.len and cmd.buffer[cmd.idx] == ' ':
|
||||
cmd.idx += 1
|
||||
while cmd.idx < cmd.buffer.len and cmd.buffer[cmd.idx] != ' ':
|
||||
while cmd.idx < cmd.buffer.len and cmd.buffer[cmd.idx] != ' ': cmd.idx += 1
|
||||
while cmd.idx < cmd.buffer.len and cmd.buffer[cmd.idx] == ' ': cmd.idx += 1
|
||||
|
||||
proc endOfWord*(cb: var CommandBuffer) =
|
||||
var cmd = cb.history[cb.idx]
|
||||
cmd.idx = clamp(cmd.idx + 1, 0, cmd.buffer.len - 1)
|
||||
while cmd.idx < cmd.buffer.len and cmd.buffer[cmd.idx] == ' ': cmd.idx += 1
|
||||
while cmd.idx < cmd.buffer.len - 1 and
|
||||
cmd.buffer[cmd.idx + 1] != ' ':
|
||||
cmd.idx += 1
|
||||
|
||||
func toChar(k: Key): char = cast[char](ord(k))
|
||||
@ -127,7 +132,17 @@ proc handleInput*(cb: var CommandBuffer, key: Key) =
|
||||
of Key.End, Key.Dollar: cb.toEnd()
|
||||
of Key.B: cb.backWord()
|
||||
of Key.W: cb.forwardWord()
|
||||
of Key.E: cb.endOfWord()
|
||||
of Key.I: cb.mode = EditMode.Insert
|
||||
of Key.A:
|
||||
cb.cur.idx = clamp(cb.cur.idx + 1, 0, cb.cur.buffer.len)
|
||||
cb.mode = EditMode.Insert
|
||||
of Key.ShiftA:
|
||||
cb.cur.idx = cb.cur.buffer.len
|
||||
cb.mode = EditMode.Insert
|
||||
of Key.ShiftI:
|
||||
cb.cur.idx = 0
|
||||
cb.mode = EditMode.Insert
|
||||
of Key.ShiftR: cb.mode = EditMode.Overwrite
|
||||
of Key.V:
|
||||
cb.mode = EditMode.Visual
|
||||
@ -159,3 +174,5 @@ proc handleInput*(cb: var CommandBuffer, key: Key) =
|
||||
|
||||
|
||||
func `$`*(cb: CommandBuffer): string = cb.cur.buffer
|
||||
|
||||
func idx*(cb: CommandBuffer): int = cb.cur.idx
|
||||
|
@ -19,7 +19,7 @@ proc render(st: ScrollText): string =
|
||||
|
||||
proc `text=`*(st: var ScrollText, text: string) =
|
||||
st.text = text
|
||||
st.scrollIdx = max(0, text.len - 10)
|
||||
st.scrollIdx = 0
|
||||
st.lastRender = render(st)
|
||||
|
||||
proc `maxWidth=`*(st: var ScrollText, maxWidth: int) =
|
||||
|
Loading…
x
Reference in New Issue
Block a user