import terminal import unicode import strutils import sequtils import sugar import utils/twtstr import config/config type LineState = object news: seq[Rune] s: string feedNext: bool escNext: bool cursor: int shift: int minlen: int maxlen: int displen: int spaces: seq[string] proc backward(state: LineState, i: int) = if i > 0: if i == 1: print('\b') else: cursorBackward(i) proc forward(state: LineState, i: int) = if i > 0: cursorForward(i) proc begin(state: LineState) = print('\r') state.forward(state.minlen) proc space(state: LineState, i: int) = print(state.spaces[i]) template kill(state: LineState, i: int) = state.space(i) state.backward(i) #print("\e[K") template kill(state: LineState) = let w = min(state.news.width(state.cursor), state.displen) state.kill(w) proc redraw(state: var LineState) = var dispw = state.news.width(state.shift, state.shift + state.displen) if state.shift + state.displen > state.news.len: state.displen = state.news.len - state.shift while dispw > state.maxlen - 1: dispw -= state.news[state.shift + state.displen - 1].width() dec state.displen state.begin() let os = state.news.substr(state.shift, state.shift + state.displen) printesc($os) state.space(max(state.maxlen - state.minlen - os.width(), 0)) state.begin() state.forward(state.news.width(state.shift, state.cursor)) proc zeroShiftRedraw(state: var LineState) = state.shift = 0 state.displen = state.maxlen - 1 state.redraw() proc fullRedraw(state: var LineState) = state.displen = state.maxlen - 1 if state.cursor > state.shift: var shiftw = state.news.width(state.shift, state.cursor) while shiftw > state.maxlen - 1: inc state.shift shiftw -= state.news[state.shift].width() else: state.shift = max(state.cursor - 1, 0) state.redraw() proc insertCharseq(state: var LineState, cs: var seq[Rune]) = let escNext = state.escNext cs.keepIf((r) => escNext or not r.isControlChar()) state.escNext = false if cs.len == 0: return if state.cursor >= state.news.len and state.news.width(state.shift, state.cursor) + cs.width() < state.displen: state.news &= cs state.cursor += cs.len printesc($cs) else: state.news.insert(cs, state.cursor) state.cursor += cs.len state.fullRedraw() proc readLine*(current: var string, minlen: int, maxlen: int): bool = var state: LineState state.news = current.toRunes() state.cursor = state.news.len state.minlen = minlen state.maxlen = maxlen state.displen = state.maxlen - 1 #cache strings for i in 0..(maxlen - minlen): state.spaces.add(' '.repeat(i)) printesc(current) while true: if not state.feedNext: state.s = "" else: state.feedNext = false let c = getch() state.s &= c var action = getLinedAction(state.s) if state.escNext: action = NO_ACTION case action of ACTION_LINED_CANCEL: return false of ACTION_LINED_SUBMIT: current = $state.news return true of ACTION_LINED_BACKSPACE: if state.cursor > 0: state.news.delete(state.cursor - 1, state.cursor - 1) dec state.cursor if state.cursor == state.news.len and state.shift == 0: state.backward(1) state.kill(1) else: state.fullRedraw() of ACTION_LINED_DELETE: if state.cursor > 0 and state.cursor < state.news.len: state.news.delete(state.cursor, state.cursor) if state.cursor == state.news.len and state.shift == 0: state.kill(1) else: state.fullRedraw() of ACTION_LINED_ESC: state.escNext = true of ACTION_LINED_CLEAR: if state.cursor > 0: state.news.delete(0, state.cursor - 1) state.cursor = 0 state.zeroShiftRedraw() of ACTION_LINED_KILL: if state.cursor < state.news.len: state.kill() state.news.setLen(state.cursor) of ACTION_LINED_BACK: if state.cursor > 0: dec state.cursor if state.cursor > state.shift or state.shift == 0: state.backward(state.news[state.cursor].width()) else: state.fullRedraw() of ACTION_LINED_FORWARD: if state.cursor < state.news.len: inc state.cursor if state.news.width(state.shift, state.cursor) < state.displen: var n = 1 if state.news.len > state.cursor: n = state.news[state.cursor].width() state.forward(n) else: state.fullRedraw() of ACTION_LINED_PREV_WORD: let oc = state.cursor while state.cursor > 0: dec state.cursor if state.news[state.cursor].breaksWord(): break if state.cursor != oc: if state.cursor > state.shift or state.shift == 0: state.backward(state.news.width(state.cursor, oc)) else: state.fullRedraw() of ACTION_LINED_NEXT_WORD: let oc = state.cursor while state.cursor < state.news.len: inc state.cursor if state.cursor < state.news.len: if state.news[state.cursor].breaksWord(): break if state.cursor != oc: let dw = state.news.width(oc, state.cursor) if oc + dw - state.shift < state.displen: state.forward(dw) else: state.fullRedraw() of ACTION_LINED_KILL_WORD: var chars = 0 if state.cursor > chars: inc chars while state.cursor > chars: inc chars if state.news[state.cursor - chars].breaksWord(): dec chars break if chars > 0: let w = state.news.width(state.cursor - chars, state.cursor) state.news.delete(state.cursor - chars, state.cursor - 1) state.cursor -= chars if state.cursor == state.news.len and state.shift == 0: state.backward(w) state.kill(w) else: state.fullRedraw() of ACTION_LINED_BEGIN: if state.cursor > 0: if state.shift == 0: state.backward(state.news.width(0, state.cursor)) else: state.fullRedraw() state.cursor = 0 of ACTION_LINED_END: if state.cursor < state.news.len: if state.news.width(state.shift, state.news.len) < maxlen: state.forward(state.news.width(state.cursor, state.news.len)) else: state.fullRedraw() state.cursor = state.news.len of ACTION_FEED_NEXT: state.feedNext = true elif validateUtf8(state.s) == -1: var cs = state.s.toRunes() state.insertCharseq(cs) else: state.feedNext = true proc readLine*(prompt: string, current: var string, termwidth: int): bool = printesc(prompt) readLine(current, prompt.width(), termwidth - prompt.len)