import options import terminal import uri import tables import strutils import unicode import types/color import types/enums import utils/twtstr import html/dom import css/box import config/config import io/term import io/lineedit type Cell = object of RootObj fgcolor*: CellColor bgcolor*: CellColor italic: bool bold: bool underline: bool BufferCell = object of Cell rune*: Rune BufferRow = seq[BufferCell] DisplayCell = object of Cell runes*: seq[Rune] DisplayRow = seq[DisplayCell] DrawInstruction = object case t: DrawInstructionType of DRAW_TEXT: text: seq[Rune] of DRAW_GOTO: x: int y: int of DRAW_FGCOLOR, DRAW_BGCOLOR: color: CellColor of DRAW_STYLE: bold: bool italic: bool underline: bool of DRAW_RESET: discard Buffer* = ref BufferObj BufferObj = object title*: string lines*: seq[BufferRow] display*: DisplayRow prevdisplay*: DisplayRow statusmsg*: DisplayRow hovertext*: string width*: int height*: int cursorx*: int cursory*: int xend*: int fromx*: int fromy*: int nodes*: seq[Node] links*: seq[Node] elements*: seq[Element] idelements*: Table[string, Element] selectedlink*: Node attrs*: TermAttributes document*: Document displaycontrols*: bool redraw*: bool location*: Uri source*: string #TODO showsource*: bool func newBuffer*(attrs: TermAttributes): Buffer = new(result) result.width = attrs.termWidth result.height = attrs.termHeight - 1 result.attrs = attrs result.display = newSeq[DisplayCell](result.width * result.height) result.prevdisplay = newSeq[DisplayCell](result.width * result.height) result.statusmsg = newSeq[DisplayCell](result.width) func generateFullOutput*(buffer: Buffer): seq[string] = var x = 0 var y = 0 var s = "" for cell in buffer.display: if x >= buffer.width: inc y result.add(s) x = 0 s = "" s &= $cell.runes inc x result.add(s) # generate a sequence of instructions to replace the previous frame with the # current one. ideally we should have some mechanism in place to determine # where we should use this and where we should just rewrite the frame func generateSwapOutput*(buffer: Buffer): seq[DrawInstruction] = var fgcolor: CellColor var bgcolor: CellColor var italic = false var bold = false var underline = false let max = buffer.width * buffer.height let curr = buffer.display let prev = buffer.prevdisplay var x = 0 var y = 0 var cx = 0 var cy = 0 var i = 0 var text: seq[Rune] while i < max: if x >= buffer.width: x = 0 cx = 0 text &= Rune('\n') inc y inc cy if curr[i] != prev[i]: let currwidth = curr[i].runes.width() let prevwidth = prev[i].runes.width() if (curr[i].runes.len > 0 or currwidth < prevwidth) and (x != cx or y != cy): if text.len > 0: result.add(DrawInstruction(t: DRAW_TEXT, text: text)) text.setLen(0) result.add(DrawInstruction(t: DRAW_GOTO, x: x, y: y)) cx = x cy = y let cancont = (curr[i].fgcolor == fgcolor and curr[i].bgcolor == bgcolor and curr[i].italic == italic and curr[i].bold == bold and curr[i].underline == underline) if text.len > 0 and not cancont: result.add(DrawInstruction(t: DRAW_TEXT, text: text)) text.setLen(0) if curr[i].fgcolor != fgcolor: fgcolor = curr[i].fgcolor result.add(DrawInstruction(t: DRAW_FGCOLOR, color: fgcolor)) if curr[i].bgcolor != bgcolor: bgcolor = curr[i].bgcolor result.add(DrawInstruction(t: DRAW_BGCOLOR, color: bgcolor)) if curr[i].italic != italic or curr[i].bold != bold or curr[i].underline != underline: if italic and not curr[i].italic or bold and not curr[i].bold or underline and not curr[i].underline: result.add(DrawInstruction(t: DRAW_RESET)) if fgcolor != defaultColor: result.add(DrawInstruction(t: DRAW_FGCOLOR, color: fgcolor)) if bgcolor != defaultColor: result.add(DrawInstruction(t: DRAW_BGCOLOR, color: bgcolor)) italic = curr[i].italic bold = curr[i].bold underline = curr[i].underline result.add(DrawInstruction(t: DRAW_STYLE, italic: italic, bold: bold, underline: underline)) text &= curr[i].runes if currwidth < prevwidth: var j = 0 while j < prevwidth - currwidth: text &= Rune(' ') inc j if text.len > 0: inc cx inc x inc i if text.len > 0: result.add(DrawInstruction(t: DRAW_TEXT, text: text)) func generateStatusMessage*(buffer: Buffer): string = for cell in buffer.statusmsg: for r in cell.runes: if r != Rune(0): result &= $r func numLines*(buffer: Buffer): int = buffer.lines.len func lastVisibleLine*(buffer: Buffer): int = min(buffer.fromy + buffer.height, buffer.numLines - 1) func width(line: seq[BufferCell]): int = for c in line: result += c.rune.width() func cellWidthOverlap*(buffer: Buffer, x: int, y: int): int = let row = y * buffer.width var ox = x while buffer.display[row + ox].runes.len == 0 and ox > 0: dec ox return buffer.display[row + ox].runes.width() func currentCellWidth*(buffer: Buffer): int = buffer.cellWidthOverlap(buffer.cursorx - buffer.fromx, buffer.cursory - buffer.fromy) func currentLineWidth*(buffer: Buffer): int = return buffer.lines[buffer.cursory].width() func maxScreenWidth*(buffer: Buffer): int = for line in buffer.lines[buffer.fromy..buffer.lastVisibleLine - 1]: result = max(line.width(), result) func atPercentOf*(buffer: Buffer): int = if buffer.lines.len == 0: return 100 return (100 * (buffer.cursory + 1)) div buffer.numLines func lastNode*(buffer: Buffer): Node = return buffer.nodes[^1] func cursorOnNode*(buffer: Buffer, node: Node): bool = if node.y == node.ey and node.y == buffer.cursory: return buffer.cursorx >= node.x and buffer.cursorx < node.ex else: return (buffer.cursory == node.y and buffer.cursorx >= node.x) or (buffer.cursory > node.y and buffer.cursory < node.ey) or (buffer.cursory == node.ey and buffer.cursorx < node.ex) func findSelectedElement*(buffer: Buffer): Option[HtmlElement] = if buffer.selectedlink != nil and buffer.selectedLink.parentNode of HtmlElement: return some(HtmlElement(buffer.selectedlink.parentNode)) for node in buffer.nodes: if node.isElemNode(): if node.getFmtLen() > 0: if buffer.cursorOnNode(node): return some(HtmlElement(node)) return none(HtmlElement) func canScroll*(buffer: Buffer): bool = return buffer.numLines >= buffer.height func getElementById*(buffer: Buffer, id: string): Element = if buffer.idelements.hasKey(id): return buffer.idelements[id] return nil proc findSelectedNode*(buffer: Buffer): Option[Node] = for node in buffer.nodes: if node.getFmtLen() > 0 and node.displayed(): if buffer.cursory >= node.y and buffer.cursory <= node.y + node.height and buffer.cursorx >= node.x and buffer.cursorx <= node.x + node.width: return some(node) return none(Node) proc writefmt*(buffer: Buffer, str: string) = discard proc writefmt*(buffer: Buffer, c: char) = discard proc writeraw*(buffer: Buffer, str: string) = discard proc writeraw*(buffer: Buffer, c: char) = discard proc write*(buffer: Buffer, str: string) = buffer.writefmt(str) buffer.writeraw(str) proc write*(buffer: Buffer, c: char) = buffer.writefmt(c) buffer.writeraw(c) proc clearText*(buffer: Buffer) = buffer.lines.setLen(0) proc clearNodes*(buffer: Buffer) = buffer.nodes.setLen(0) buffer.links.setLen(0) buffer.elements.setLen(0) buffer.idelements.clear() proc clearBuffer*(buffer: Buffer) = buffer.clearText() buffer.clearNodes() buffer.cursorx = 0 buffer.cursory = 0 buffer.fromx = 0 buffer.fromy = 0 buffer.hovertext = "" buffer.selectedlink = nil proc restoreCursorX(buffer: Buffer) = buffer.cursorx = max(min(buffer.currentLineWidth() - 1, buffer.xend), 0) proc scrollTo*(buffer: Buffer, y: int) = if y == buffer.fromy: return buffer.fromy = min(max(buffer.numLines - buffer.height, 0), y) buffer.cursory = min(max(buffer.fromy, buffer.cursory), buffer.fromy + buffer.height) buffer.redraw = true buffer.restoreCursorX() proc cursorTo*(buffer: Buffer, x: int, y: int) = buffer.redraw = false buffer.cursory = min(max(y, 0), buffer.numLines - 1) if buffer.fromy > buffer.cursory: buffer.fromy = max(buffer.cursory, 0) buffer.redraw = true elif buffer.fromy + buffer.height - 1 <= buffer.cursory: buffer.fromy = max(buffer.cursory - buffer.height + 1, 0) buffer.redraw = true buffer.cursorx = min(max(x, 0), buffer.currentLineWidth()) if buffer.fromx < buffer.cursorx - buffer.width: buffer.fromx = max(0, buffer.cursorx - buffer.width) buffer.redraw = true elif buffer.fromx > buffer.cursorx: buffer.fromx = buffer.cursorx buffer.redraw = true proc cursorDown*(buffer: Buffer) = if buffer.cursory < buffer.numLines - 1: inc buffer.cursory buffer.restoreCursorX() if buffer.cursory - buffer.height >= buffer.fromy: inc buffer.fromy buffer.redraw = true proc cursorUp*(buffer: Buffer) = if buffer.cursory > 0: dec buffer.cursory buffer.restoreCursorX() if buffer.cursory < buffer.fromy: dec buffer.fromy buffer.redraw = true proc cursorRight*(buffer: Buffer) = let cellwidth = buffer.currentCellWidth() let lw = buffer.currentLineWidth() if buffer.cursorx < lw - 1: buffer.cursorx = min(lw - 1, buffer.cursorx + cellwidth) buffer.xend = buffer.cursorx if buffer.cursorx - buffer.width >= buffer.fromx: inc buffer.fromx buffer.redraw = true proc cursorLeft*(buffer: Buffer) = let cellwidth = buffer.currentCellWidth() if buffer.fromx > buffer.cursorx: buffer.fromx = buffer.cursorx buffer.redraw = true elif buffer.cursorx > 0: buffer.cursorx = max(0, buffer.cursorx - cellwidth) if buffer.fromx > buffer.cursorx: buffer.fromx = buffer.cursorx buffer.redraw = true buffer.xend = buffer.cursorx proc cursorLineBegin*(buffer: Buffer) = buffer.cursorx = 0 buffer.xend = 0 if buffer.fromx > 0: buffer.fromx = 0 buffer.redraw = true proc cursorLineEnd*(buffer: Buffer) = buffer.cursorx = buffer.currentLineWidth() - 1 buffer.xend = buffer.cursorx buffer.fromx = max(buffer.cursorx - buffer.width + 1, 0) buffer.redraw = buffer.fromx > 0 iterator revnodes*(buffer: Buffer): Node {.inline.} = var i = buffer.nodes.len - 1 while i >= 0: yield buffer.nodes[i] dec i proc cursorNextWord*(buffer: Buffer) = let llen = buffer.currentLineWidth() - 1 var x = buffer.cursorx var y = buffer.cursory if llen >= 0: while not buffer.lines[y][x].rune.breaksWord(): if x >= llen: break inc x while buffer.lines[y][x].rune.breaksWord(): if x >= llen: break inc x if x >= llen: if y < buffer.numLines: inc y x = 0 buffer.cursorTo(x, y) proc cursorPrevWord*(buffer: Buffer) = var x = buffer.cursorx var y = buffer.cursory if buffer.currentLineWidth() > 0: while not buffer.lines[y][x].rune.breaksWord(): if x == 0: break dec x while buffer.lines[y][x].rune.breaksWord(): if x == 0: break dec x if x == 0: if y > 0: dec y x = buffer.lines[y].len - 1 buffer.cursorTo(x, y) proc cursorNextLink*(buffer: Buffer) = #TODO return proc cursorPrevLink*(buffer: Buffer) = #TODO return proc cursorFirstLine*(buffer: Buffer) = if buffer.fromy > 0: buffer.fromy = 0 buffer.redraw = true else: buffer.redraw = false buffer.cursory = 0 buffer.restoreCursorX() proc cursorLastLine*(buffer: Buffer) = if buffer.fromy < buffer.numLines - buffer.height: buffer.fromy = buffer.numLines - buffer.height buffer.redraw = true buffer.cursory = buffer.numLines - 1 buffer.restoreCursorX() proc cursorTop*(buffer: Buffer) = buffer.cursory = buffer.fromy buffer.restoreCursorX() proc cursorMiddle*(buffer: Buffer) = buffer.cursory = min(buffer.fromy + (buffer.height - 2) div 2, buffer.numLines - 1) buffer.restoreCursorX() proc cursorBottom*(buffer: Buffer) = buffer.cursory = min(buffer.fromy + buffer.height - 1, buffer.numLines) buffer.restoreCursorX() proc centerLine*(buffer: Buffer) = let ny = max(min(buffer.cursory - buffer.height div 2, buffer.numLines - buffer.height), 0) if ny != buffer.fromy: buffer.fromy = ny buffer.redraw = true proc halfPageUp*(buffer: Buffer) = buffer.cursory = max(buffer.cursory - buffer.height div 2 + 1, 0) let nfy = max(0, buffer.fromy - buffer.height div 2 + 1) if nfy != buffer.fromy: buffer.fromy = nfy buffer.redraw = true buffer.restoreCursorX() proc halfPageDown*(buffer: Buffer) = buffer.cursory = min(buffer.cursory + buffer.height div 2 - 1, buffer.numLines - 1) let nfy = min(max(buffer.numLines - buffer.height, 0), buffer.fromy + buffer.height div 2 - 1) if nfy != buffer.fromy: buffer.fromy = nfy buffer.redraw = true buffer.restoreCursorX() proc pageUp*(buffer: Buffer) = buffer.cursory = max(buffer.cursory - buffer.height + 1, 1) buffer.fromy = max(0, buffer.fromy - buffer.height) buffer.redraw = true buffer.restoreCursorX() proc pageDown*(buffer: Buffer) = buffer.cursory = min(buffer.cursory + buffer.height div 2 - 1, buffer.numLines - 1) buffer.fromy = min(max(buffer.numLines - buffer.height, 0), buffer.fromy + buffer.height div 2) buffer.redraw = true buffer.restoreCursorX() proc pageLeft*(buffer: Buffer) = buffer.cursorx = max(buffer.cursorx - buffer.width, 0) buffer.fromx = max(0, buffer.fromx - buffer.width) buffer.redraw = true proc pageRight*(buffer: Buffer) = buffer.cursorx = min(buffer.fromx, buffer.currentLineWidth()) buffer.fromx = min(max(buffer.maxScreenWidth() - buffer.width, 0), buffer.fromx + buffer.width) buffer.redraw = true proc scrollDown*(buffer: Buffer) = if buffer.fromy + buffer.height < buffer.numLines: inc buffer.fromy if buffer.fromy > buffer.cursory: buffer.cursorDown() buffer.redraw = true else: buffer.cursorDown() proc scrollUp*(buffer: Buffer) = if buffer.fromy > 0: dec buffer.fromy if buffer.fromy + buffer.height <= buffer.cursory: buffer.cursorUp() buffer.redraw = true else: buffer.cursorUp() proc scrollRight*(buffer: Buffer) = if buffer.fromx + buffer.width < buffer.maxScreenWidth(): inc buffer.fromx if buffer.fromx >= buffer.cursorx: buffer.cursorRight() buffer.redraw = true proc scrollLeft*(buffer: Buffer) = if buffer.fromx > 0: dec buffer.fromx if buffer.fromx + buffer.height <= buffer.cursorx: buffer.cursorLeft() buffer.redraw = true proc gotoAnchor*(buffer: Buffer): bool = if buffer.location.anchor != "": let node = buffer.getElementById(buffer.location.anchor) if node != nil: buffer.scrollTo(max(node.y - buffer.height div 2, 0)) proc setLocation*(buffer: Buffer, uri: Uri) = buffer.location = uri proc gotoLocation*(buffer: Buffer, uri: Uri) = buffer.location = buffer.location.combine(uri) proc refreshTermAttrs*(buffer: Buffer): bool = let newAttrs = getTermAttributes() if newAttrs != buffer.attrs: buffer.attrs = newAttrs buffer.width = newAttrs.termWidth buffer.height = newAttrs.termHeight return true return false proc setText*(buffer: Buffer, x: int, y: int, text: seq[Rune]) = while buffer.lines.len <= y: buffer.lines.add(newSeq[BufferCell]()) while buffer.lines[y].len < x + text.len: buffer.lines[y].add(BufferCell()) var i = 0 while i < text.len: buffer.lines[y][i].rune = text[i] inc i proc reshape*(buffer: Buffer) = buffer.display = newSeq[DisplayCell](buffer.width * buffer.height) buffer.statusmsg = newSeq[DisplayCell](buffer.width) proc clearDisplay*(buffer: Buffer) = var i = 0 while i < buffer.display.len: buffer.display[i].runes.setLen(0) inc i proc refreshDisplay*(buffer: Buffer) = var y = 0 buffer.prevdisplay = buffer.display buffer.clearDisplay() for line in buffer.lines[buffer.fromy..buffer.lastVisibleLine - 1]: var w = 0 var i = 0 while w < buffer.fromx and i < line.len: w += line[i].rune.width() inc i let dls = y * buffer.width var j = 0 var n = 0 while w < buffer.fromx + buffer.width and i < line.len: w += line[i].rune.width() if line[i].rune.width() == 0 and j != 0: inc n buffer.display[dls + j - n].runes.add(line[i].rune) j += line[i].rune.width() inc i inc y proc renderPlainText*(buffer: Buffer, text: string) = var i = 0 var y = 0 var line = "" while i < text.len: if text[i] == '\n': buffer.setText(0, y, line.toRunes()) inc y line = "" elif text[i] == '\r': discard elif text[i] == '\t': line &= ' '.repeat(8) else: line &= text[i] inc i if line.len > 0: buffer.setText(0, y, line.toRunes()) proc renderDocument*(buffer: Buffer) = var stack: seq[CSSBox] stack.add(buffer.document.root.box) while stack.len > 0: let box = stack.pop() buffer.setText(box.innerEdge.x1, box.innerEdge.y1, box.content) for child in box.children: stack.add(child) proc cursorBufferPos(buffer: Buffer) = let x = max(buffer.cursorx - buffer.fromx, 0) let y = buffer.cursory - buffer.fromy termGoto(x, y) proc clearStatusMessage(buffer: Buffer) = var i = 0 while i < buffer.statusmsg.len: buffer.statusmsg[i].runes.setLen(0) inc i proc setStatusMessage*(buffer: Buffer, str: string) = buffer.clearStatusMessage() let text = str.toRunes() var i = 0 var n = 0 while i < text.len: if text[i].width() == 0: inc n buffer.statusmsg[i - n].runes.add(text[i]) inc i proc statusMsgForBuffer(buffer: Buffer) = var msg = ($(buffer.cursory + 1) & "/" & $buffer.numLines & " (" & $buffer.atPercentOf() & "%) " & "<" & buffer.title & ">").ansiStyle(styleReverse).ansiReset().join() if buffer.hovertext.len > 0: msg &= " " & buffer.hovertext buffer.setStatusMessage(msg) proc displayBufferSwapOutput(buffer: Buffer) = termGoto(0, 0) let instructions = buffer.generateSwapOutput() for inst in instructions: case inst.t of DRAW_TEXT: print(inst.text) of DRAW_GOTO: termGoto(inst.x, inst.y) of DRAW_FGCOLOR: let color = inst.color if inst.color.rgb: let rgb = color.rgbcolor print("\e38;2" & $rgb.r & ";" & $rgb.b & ";" & $rgb.g) else: print("\e[" & $color.color & "m") of DRAW_BGCOLOR: let color = inst.color if inst.color.rgb: let rgb = color.rgbcolor print("\e48;2" & $rgb.r & ";" & $rgb.b & ";" & $rgb.g) else: print("\e[" & $color.color & "m") of DRAW_STYLE: var os = "\e[" var p = false if inst.italic: os &= "3" p = true if inst.bold: if p: os &= ";" os &= "1" p = true if inst.underline: if p: os &= ";" os &= "4" p = true os &= "m" print(os) of DRAW_RESET: print("\e[0m") proc displayBuffer(buffer: Buffer) = termGoto(0, 0) let full = buffer.generateFullOutput() for line in full: print(line) #TODO print("\e[K") print('\n') proc displayStatusMessage(buffer: Buffer) = termGoto(0, buffer.height) eraseLine() print(buffer.generateStatusMessage()) proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool = var s = "" var feedNext = false while true: buffer.redraw = false buffer.displayStatusMessage() stdout.showCursor() buffer.cursorBufferPos() if not feedNext: s = "" else: feedNext = false let c = getch() s &= c let action = getNormalAction(s) var redraw = false var reshape = false var nostatus = false case action of ACTION_QUIT: eraseScreen() setCursorPos(0, 0) return false of ACTION_CURSOR_LEFT: buffer.cursorLeft() of ACTION_CURSOR_DOWN: buffer.cursorDown() of ACTION_CURSOR_UP: buffer.cursorUp() of ACTION_CURSOR_RIGHT: buffer.cursorRight() of ACTION_CURSOR_LINEBEGIN: buffer.cursorLineBegin() of ACTION_CURSOR_LINEEND: buffer.cursorLineEnd() of ACTION_CURSOR_NEXT_WORD: buffer.cursorNextWord() of ACTION_CURSOR_PREV_WORD: buffer.cursorPrevWord() of ACTION_CURSOR_NEXT_LINK: buffer.cursorNextLink() of ACTION_CURSOR_PREV_LINK: buffer.cursorPrevLink() of ACTION_PAGE_DOWN: buffer.pageDown() of ACTION_PAGE_UP: buffer.pageUp() of ACTION_PAGE_RIGHT: buffer.pageRight() of ACTION_PAGE_LEFT: buffer.pageLeft() of ACTION_HALF_PAGE_DOWN: buffer.halfPageDown() of ACTION_HALF_PAGE_UP: buffer.halfPageUp() of ACTION_CURSOR_FIRST_LINE: buffer.cursorFirstLine() of ACTION_CURSOR_LAST_LINE: buffer.cursorLastLine() of ACTION_CURSOR_TOP: buffer.cursorTop() of ACTION_CURSOR_MIDDLE: buffer.cursorMiddle() of ACTION_CURSOR_BOTTOM: buffer.cursorBottom() of ACTION_CENTER_LINE: buffer.centerLine() of ACTION_SCROLL_DOWN: buffer.scrollDown() of ACTION_SCROLL_UP: buffer.scrollUp() of ACTION_SCROLL_LEFT: buffer.scrollLeft() of ACTION_SCROLL_RIGHT: buffer.scrollRight() of ACTION_CLICK: discard of ACTION_CHANGE_LOCATION: var url = $buffer.location termGoto(0, buffer.height) let status = readLine("URL: ", url, buffer.width) if status: buffer.setLocation(parseUri(url)) return true of ACTION_LINE_INFO: buffer.setStatusMessage("line " & $(buffer.cursory + 1) & "/" & $buffer.numLines & " col " & $(buffer.cursorx + 1) & "/" & $buffer.currentLineWidth() & " cell width: " & $buffer.currentCellWidth()) nostatus = true of ACTION_FEED_NEXT: feedNext = true of ACTION_RELOAD: return true of ACTION_RESHAPE: reshape = true redraw = true of ACTION_REDRAW: redraw = true of ACTION_TOGGLE_SOURCE: buffer.showsource = not buffer.showsource if buffer.showsource: buffer.renderPlainText(buffer.source) else: buffer.renderDocument() redraw = true else: discard stdout.hideCursor() if buffer.refreshTermAttrs(): redraw = true reshape = true if buffer.redraw: redraw = true if reshape: buffer.reshape() buffer.displayBuffer() if redraw: buffer.refreshDisplay() buffer.displayBuffer() if not nostatus: buffer.statusMsgForBuffer() else: nostatus = false proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool = discard buffer.gotoAnchor() buffer.refreshDisplay() buffer.displayBuffer() buffer.statusMsgForBuffer() return inputLoop(attrs, buffer)