From 5268aada71dd554dca29c5bc273702d39d14bb5c Mon Sep 17 00:00:00 2001 From: bptato Date: Wed, 20 Jan 2021 13:26:00 +0100 Subject: Some things work, most things don't --- Makefile | 3 + buffer.nim | 444 ++++++++++++++++ config.nim | 138 +++++ display.nim | 365 +++++++++++++ htmlelement.nim | 340 ++++++++++++ keymap | 53 ++ main.nim | 59 +++ readme.md | 29 + search.html | 1586 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ termattrs.nim | 12 + twtio.nim | 125 +++++ twtstr.nim | 28 + 12 files changed, 3182 insertions(+) create mode 100644 Makefile create mode 100644 buffer.nim create mode 100644 config.nim create mode 100644 display.nim create mode 100644 htmlelement.nim create mode 100644 keymap create mode 100644 main.nim create mode 100644 readme.md create mode 100644 search.html create mode 100644 termattrs.nim create mode 100644 twtio.nim create mode 100644 twtstr.nim diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..68cb1ff5 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +all: build +build: + nim compile -d:ssl -o:twt main.nim diff --git a/buffer.nim b/buffer.nim new file mode 100644 index 00000000..5f4cf227 --- /dev/null +++ b/buffer.nim @@ -0,0 +1,444 @@ +import options +import uri +import tables + +import fusion/htmlparser/xmltree +import fusion/htmlparser + +import termattrs +import htmlelement +import twtio + +type + Buffer* = ref BufferObj + BufferObj = object + text*: string + rawText*: string + lines*: seq[int] + rawlines*: seq[int] + title*: string + hovertext*: string + htmlSource*: XmlNode + width*: int + height*: int + cursorX*: int + cursorY*: int + xend*: int + fromX*: int + fromY*: int + nodes*: seq[HtmlNode] + links*: seq[HtmlNode] + elements*: seq[HtmlNode] + idelements*: Table[string, HtmlNode] + selectedlink*: HtmlNode + location*: Uri + printwrite*: bool + attrs*: TermAttributes + +proc newBuffer*(attrs: TermAttributes): Buffer = + return Buffer(lines: @[0], + rawlines: @[0], + width: attrs.termWidth, + height: attrs.termHeight, + cursorY: 1) + + + +func lastLine*(buffer: Buffer): int = + assert buffer.rawlines.len == buffer.lines.len + return buffer.lines.len - 1 + +func lastVisibleLine*(buffer: Buffer): int = + return min(buffer.fromY + buffer.height, buffer.lastLine() + 1) - 1 + +#doesn't include newline +func lineLength*(buffer: Buffer, line: int): int = + assert buffer.lines.len > line + let len = buffer.lines[line] - buffer.lines[line - 1] - 2 + if len >= 0: + return len + else: + return 0 + +func currentLine*(buffer: Buffer): int = + return buffer.cursorY - 1 + +func rawLineLength*(buffer: Buffer, line: int): int = + assert buffer.rawlines.len > line + let len = buffer.rawlines[line] - buffer.rawlines[line - 1] - 2 + if len >= 0: + return len + else: + return 0 + +func currentLineLength*(buffer: Buffer): int = + return buffer.lineLength(buffer.cursorY) + +func currentRawLineLength*(buffer: Buffer): int = + return buffer.rawLineLength(buffer.cursorY) + +func cursorAtLineEnd*(buffer: Buffer): bool = + return buffer.cursorX == buffer.currentRawLineLength() + +func atPercentOf*(buffer: Buffer): int = + return (100 * buffer.cursorY) div buffer.lastLine() + +func visibleText*(buffer: Buffer): string = + return buffer.text.substr(buffer.lines[buffer.fromY], buffer.lines[buffer.lastVisibleLine() - 1]) + +func lastNode*(buffer: Buffer): HtmlNode = + return buffer.nodes[^1] + +func onNewLine*(buffer: Buffer): bool = + return buffer.text.len == 0 or buffer.text[^1] == '\n' + +func onSpace*(buffer: Buffer): bool = + return buffer.text.len > 0 and buffer.text[^1] == ' ' + +func findSelectedElement*(buffer: Buffer): Option[HtmlElement] = + if buffer.selectedlink != nil: + return some(buffer.selectedlink.text.parent) + for node in buffer.nodes: + if node.nodeType == NODE_ELEMENT: + if node.element.formattedElem.len > 0: + 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.element) + return none(HtmlElement) + +func cursorAt*(buffer: Buffer): int = + return buffer.rawlines[buffer.currentLine()] + buffer.cursorX + +func cursorChar*(buffer: Buffer): char = + return buffer.text[buffer.cursorAt()] + +func canScroll*(buffer: Buffer): bool = + return buffer.lastLine() > buffer.height + +func cursorOnNode*(buffer: Buffer, node: HtmlNode): bool = + return buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width + +func getElementById*(buffer: Buffer, id: string): HtmlNode = + if buffer.idelements.hasKey(id): + return buffer.idelements[id] + return nil + +proc findSelectedNode*(buffer: Buffer): Option[HtmlNode] = + for node in buffer.nodes: + if node.getFormattedLen() > 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(HtmlNode) + +proc addNode*(buffer: Buffer, htmlNode: HtmlNode) = + buffer.nodes.add(htmlNode) + if htmlNode.isTextNode() and htmlNode.text.parent.htmlTag == tagA: + buffer.links.add(htmlNode) + if htmlNode.isElemNode(): + buffer.elements.add(htmlNode) + if htmlNode.element.id != "": + buffer.idelements[htmlNode.element.id] = htmlNode + +proc writefmt*(buffer: Buffer, str: string) = + buffer.text &= str + if buffer.printwrite: + stdout.write(str) + +proc writefmt*(buffer: Buffer, c: char) = + buffer.text &= c + if buffer.printwrite: + stdout.write(c) + +proc writeraw*(buffer: Buffer, str: string) = + buffer.rawtext &= str + +proc writeraw*(buffer: Buffer, c: char) = + buffer.rawtext &= c + +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.text = "" + buffer.rawtext = "" + buffer.lines = @[0] + buffer.rawlines = @[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 = 1 + buffer.fromX = 0 + buffer.fromY = 0 + buffer.hovertext = "" + +proc cursorDown*(buffer: Buffer): bool = + if buffer.cursorY < buffer.lastLine(): + buffer.cursorY += 1 + if buffer.cursorX > buffer.currentRawLineLength(): + if buffer.xend == 0: + buffer.xend = buffer.cursorX + buffer.cursorX = buffer.currentRawLineLength() + elif buffer.xend > 0: + buffer.cursorX = min(buffer.currentRawLineLength(), buffer.xend) + if buffer.cursorY > buffer.lastVisibleLine(): + buffer.fromY += 1 + return true + return false + +proc cursorUp*(buffer: Buffer): bool = + if buffer.cursorY > 1: + buffer.cursorY -= 1 + if buffer.cursorX > buffer.currentRawLineLength(): + if buffer.xend == 0: + buffer.xend = buffer.cursorX + buffer.cursorX = buffer.currentRawLineLength() + elif buffer.xend > 0: + buffer.cursorX = min(buffer.currentRawLineLength(), buffer.xend) + if buffer.cursorY <= buffer.fromY: + buffer.fromY -= 1 + return true + return false + +proc cursorRight*(buffer: Buffer): bool = + if buffer.cursorX < buffer.currentRawLineLength(): + buffer.cursorX += 1 + buffer.xend = 0 + else: + buffer.xend = buffer.cursorX + return false + +proc cursorLeft*(buffer: Buffer): bool = + if buffer.cursorX > 0: + buffer.cursorX -= 1 + buffer.xend = 0 + return false + +proc cursorLineBegin*(buffer: Buffer) = + buffer.cursorX = 0 + buffer.xend = 0 + +proc cursorLineEnd*(buffer: Buffer) = + buffer.cursorX = buffer.currentRawLineLength() + buffer.xend = buffer.cursorX + +proc cursorNextNode*(buffer: Buffer): bool = + if buffer.cursorAtLineEnd(): + if buffer.cursorY < buffer.lastLine(): + let ret = buffer.cursorDown() + buffer.cursorLineEnd() + return ret + else: + buffer.cursorLineBegin() + return false + + let selectedNode = buffer.findSelectedNode() + var res = buffer.cursorRight() + if selectedNode.isNone: + return res + while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get(): + if buffer.cursorAtLineEnd(): + return res + res = buffer.cursorRight() + +proc cursorNextWord*(buffer: Buffer): bool = + if buffer.cursorAtLineEnd(): + if buffer.cursorY < buffer.lastLine(): + let ret = buffer.cursorDown() + buffer.cursorLineBegin() + return ret + else: + buffer.cursorLineEnd() + return false + + var res = buffer.cursorRight() + while buffer.rawtext[buffer.rawlines[buffer.currentLine()] + buffer.cursorX] != ' ': + if buffer.cursorAtLineEnd(): + return res + res = res or buffer.cursorRight() + +proc cursorPrevNode*(buffer: Buffer): bool = + if buffer.cursorX <= 1: + if buffer.cursorY > 1: + let res = buffer.cursorUp() + buffer.cursorLineEnd() + return res + else: + buffer.cursorLineBegin() + return false + + let selectedNode = buffer.findSelectedNode() + var res = buffer.cursorLeft() + if selectedNode.isNone: + return res + while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get(): + if buffer.cursorX == 0: + return res + res = res or buffer.cursorLeft() + +proc cursorPrevWord*(buffer: Buffer): bool = + if buffer.cursorX <= 1: + if buffer.cursorY > 1: + let ret = buffer.cursorUp() + buffer.cursorLineEnd() + return ret + else: + buffer.cursorLineBegin() + return false + + discard buffer.cursorLeft() + while buffer.rawtext[buffer.rawlines[buffer.currentLine()] + buffer.cursorX] != ' ': + if buffer.cursorX == 0: + return false + discard buffer.cursorLeft() + +iterator revNodes*(buffer: Buffer): HtmlNode {.inline.} = + var i = buffer.nodes.len - 1 + while i >= 0: + yield buffer.nodes[i] + i -= 1 + +proc cursorNextLink*(buffer: Buffer): bool = + var next = false + for node in buffer.nodes: + if node.nodeType == NODE_ELEMENT: + case node.element.htmlTag + of tagInput, tagA: + if next: + var res = false + while buffer.cursorY < node.y: + res = res or buffer.cursorDown() + buffer.cursorLineBegin() + while buffer.cursorX < node.x: + res = res or buffer.cursorRight() + return res + if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width: + next = true + else: discard + +proc cursorPrevLink*(buffer: Buffer): bool = + var next = false + for node in buffer.revNodes: + if node.nodeType == NODE_ELEMENT and node.element.htmlTag == tagInput: + if next: + var res = false + while buffer.cursorY < node.y: + res = res or buffer.cursorDown() + buffer.cursorLineBegin() + while buffer.cursorX < node.x: + res = res or buffer.cursorRight() + return true + if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width: + next = true + +proc cursorFirstLine*(buffer: Buffer): bool = + if buffer.fromY > 0: + buffer.fromY = 0 + result = true + else: + result = false + + buffer.cursorY = 1 + buffer.cursorLineBegin() + +proc cursorLastLine*(buffer: Buffer): bool = + if buffer.fromY < buffer.lastLine() - buffer.height: + buffer.fromY = buffer.lastLine() - (buffer.height - 1) + result = true + else: + result = false + buffer.cursorY = buffer.lastLine() + buffer.cursorLineBegin() + +proc halfPageUp*(buffer: Buffer): bool = + buffer.cursorY = max(buffer.cursorY - buffer.height div 2 + 1, 1) + if buffer.fromY - 1 > buffer.cursorY or true: + buffer.fromY = max(0, buffer.fromY - buffer.height div 2 + 1) + return true + return false + +proc halfPageDown*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine()) + buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), buffer.fromY + buffer.height div 2 - 1) + return true + +proc pageUp*(buffer: Buffer): bool = + buffer.cursorY = max(buffer.cursorY - buffer.height + 1, 1) + buffer.fromY = max(0, buffer.fromY - buffer.height) + return true + +proc pageDown*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine()) + buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), buffer.fromY + buffer.height div 2) + return true + +proc cursorTop*(buffer: Buffer): bool = + buffer.cursorY = buffer.fromY + 1 + return false + +proc cursorMiddle*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.fromY + buffer.height div 2, buffer.lastLine()) + return false + +proc cursorBottom*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.fromY + buffer.height - 1, buffer.lastLine()) + return false + +proc scrollTo*(buffer: Buffer, y: int): bool = + if y == buffer.fromY: + return false + buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), y - buffer.height) + return true + +proc scrollDown*(buffer: Buffer): bool = + if buffer.fromY + buffer.height <= buffer.lastLine(): + buffer.fromY += 1 + if buffer.fromY >= buffer.cursorY: + discard buffer.cursorDown() + return true + discard buffer.cursorDown() + return false + +proc scrollUp*(buffer: Buffer): bool = + if buffer.fromY > 0: + buffer.fromY -= 1 + if buffer.fromY + buffer.height <= buffer.cursorY: + discard buffer.cursorUp() + return true + discard buffer.cursorUp() + return false + +proc checkLinkSelection*(buffer: Buffer): bool = + if buffer.selectedlink != nil: + if buffer.cursorOnNode(buffer.selectedlink): + return false + else: + buffer.selectedlink = nil + buffer.hovertext = "" + for node in buffer.links: + if buffer.cursorOnNode(node): + buffer.selectedlink = node + node.text.parent.selected = true + buffer.hovertext = node.text.parent.href + return true + return false + +proc gotoAnchor*(buffer: Buffer): bool = + if buffer.location.anchor != "": + let node = buffer.getElementById(buffer.location.anchor) + if node != nil: + return buffer.scrollTo(node.y) + return false + +proc setLocation*(buffer: Buffer, uri: Uri) = + buffer.location = buffer.location.combine(uri) diff --git a/config.nim b/config.nim new file mode 100644 index 00000000..ca136b9e --- /dev/null +++ b/config.nim @@ -0,0 +1,138 @@ +import tables +import strutils +import macros + +type + TwtAction* = + enum + NO_ACTION, + ACTION_FEED_NEXT, + ACTION_QUIT, + ACTION_CURSOR_UP, ACTION_CURSOR_DOWN, ACTION_CURSOR_LEFT, ACTION_CURSOR_RIGHT, + ACTION_CURSOR_LINEEND, ACTION_CURSOR_LINEBEGIN, + ACTION_CURSOR_NEXT_WORD, ACTION_CURSOR_PREV_WORD, + ACTION_CURSOR_NEXT_NODE, ACTION_CURSOR_PREV_NODE, + ACTION_CURSOR_NEXT_LINK, ACTION_CURSOR_PREV_LINK, + ACTION_PAGE_DOWN, ACTION_PAGE_UP, + ACTION_HALF_PAGE_DOWN, ACTION_HALF_PAGE_UP, + ACTION_SCROLL_DOWN, ACTION_SCROLL_UP, + ACTION_CLICK, + ACTION_CHANGE_LOCATION, + ACTION_RELOAD, ACTION_RESHAPE, ACTION_REDRAW, + ACTION_CURSOR_FIRST_LINE, ACTION_CURSOR_LAST_LINE, + ACTION_CURSOR_TOP, ACTION_CURSOR_MIDDLE, ACTION_CURSOR_BOTTOM, + ACTION_LINED_SUBMIT, ACTION_LINED_CANCEL, + ACTION_LINED_BACKSPACE, ACTION_LINED_CLEAR, ACTION_LINED_KILL, ACTION_LINED_KILL_WORD, + ACTION_LINED_BACK, ACTION_LINED_FORWARD, + ACTION_LINED_PREV_WORD, ACTION_LINED_NEXT_WORD, + ACTION_LINED_ESC + +var normalActionRemap*: Table[string, TwtAction] +var linedActionRemap*: Table[string, TwtAction] + +func getControlChar(c: char): char = + if int(c) >= int('a'): + return char(int(c) - int('a') + 1) + elif c == '?': + return char(127) + assert(false) + +proc getRealKey(key: string): string = + var realk: string + var currchar: char + var control = 0 + var skip = false + for c in key: + if c == '\\': + skip = true + elif skip: + if c == 'e': + realk &= '\e' + else: + realk &= c + skip = false + elif c == 'C': + control += 1 + currchar = c + elif c == '-' and control == 1: + control += 1 + elif control == 1: + realk &= 'C' & c + control = 0 + elif control == 2: + realk &= getControlChar(c) + control = 0 + else: + realk &= c + if control == 1: + realk &= 'C' + return realk + +proc constructActionTable*(origTable: var Table[string, TwtAction]): Table[string, TwtAction] = + var newTable: Table[string, TwtAction] + var strs = newSeq[string](0) + for k in origTable.keys: + let realk = getRealKey(k) + var teststr = "" + for c in realk: + teststr &= c + strs.add(teststr) + + for k, v in origTable.mpairs: + let realk = getRealKey(k) + var teststr = "" + for c in realk: + teststr &= c + if strs.contains(teststr): + newTable[teststr] = ACTION_FEED_NEXT + newTable[realk] = v + return newTable + +var keymapStr*: string +macro staticReadKeymap(): untyped = + var keymap = staticRead"keymap" + + let keymapLit = newLit(keymap) + result = quote do: + keymapStr = `keymapLit` + +staticReadKeymap() + +proc readKeymap*(filename: string): bool = + var f: File + let status = f.open(filename, fmRead) + var normalActionMap: Table[string, TwtAction] + var linedActionMap: Table[string, TwtAction] + if status: + var line: TaintedString + while f.readLine(line): + if line.string.len == 0 or line.string[0] == '#': + continue + let cmd = line.split(' ') + if cmd.len == 3: + if cmd[0] == "nmap": + normalActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2]) + elif cmd[0] == "lemap": + linedActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2]) + + normalActionRemap = constructActionTable(normalActionMap) + linedActionRemap = constructActionTable(linedActionMap) + return true + else: + return false + +proc parseKeymap*(keymap: string) = + var normalActionMap: Table[string, TwtAction] + var linedActionMap: Table[string, TwtAction] + for line in keymap.split('\n'): + if line.len == 0 or line[0] == '#': + continue + let cmd = line.split(' ') + if cmd.len == 3: + if cmd[0] == "nmap": + normalActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2]) + elif cmd[0] == "lemap": + linedActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2]) + + normalActionRemap = constructActionTable(normalActionMap) + linedActionRemap = constructActionTable(linedActionMap) diff --git a/display.nim b/display.nim new file mode 100644 index 00000000..f4eaa806 --- /dev/null +++ b/display.nim @@ -0,0 +1,365 @@ +import terminal +import options +import uri +import strutils + +import fusion/htmlparser/xmltree +import fusion/htmlparser + +import buffer +import termattrs +import htmlelement +import twtstr +import twtio +import config + +proc clearStatusMsg*(at: int) = + setCursorPos(0, at) + eraseLine() + +proc statusMsg*(str: string, at: int) = + clearStatusMsg(at) + print(str.addAnsiStyle(styleReverse)) + +type + RenderState = ref RenderStateObj + RenderStateObj = object + x: int + y: int + atchar: int + atrawchar: int + centerqueue: int + centerlen: int + blanklines: int + blankspaces: int + nextspaces: int + docenter: bool + +func newRenderState(): RenderState = + return RenderState() + +func nodeAttr(node: HtmlNode): HtmlElement = + case node.nodeType + of NODE_TEXT: return node.text.parent + of NODE_ELEMENT: return node.element + else: assert(false) + +proc flushLine(buffer: Buffer, state: RenderState) = + if buffer.onNewLine(): + state.blanklines += 1 + buffer.write('\n') + state.x = 0 + state.y += 1 + state.atchar += 1 + state.atrawchar += 1 + state.nextspaces = 0 + buffer.lines.add(state.atchar) + buffer.rawlines.add(state.atrawchar) + assert(buffer.onNewLine()) + +proc addSpaces(buffer: Buffer, state: RenderState, n: int) = + if state.x + n > buffer.width: + buffer.flushLine(state) + return + state.blankspaces += n + buffer.write(' '.repeat(n)) + state.x += n + state.atchar += n + state.atrawchar += n + +proc addSpace(buffer: Buffer, state: RenderState) = + buffer.addSpaces(state, 1) + +proc assignCoords(node: HtmlNode, state: RenderState) = + node.x = state.x + node.y = state.y + +proc addSpacePadding(buffer: Buffer, state: RenderState) = + if not buffer.onSpace(): + buffer.addSpace(state) + +proc writeWrappedText(buffer: Buffer, state: RenderState, fmttext: string, rawtext: string) = + var n = 0 + var fmtword = "" + var rawword = "" + var prevl = false + for c in fmttext: + fmtword &= c + if n >= rawtext.len or rawtext[n] != c: + continue + + state.x += 1 + rawword &= c + + if state.x > buffer.width: + if buffer.rawtext.len > 0 and buffer.rawtext[^1] == ' ': + buffer.rawtext = buffer.rawtext.substr(0, buffer.rawtext.len - 2) + buffer.text = buffer.text.substr(0, buffer.text.len - 2) + state.atchar -= 1 + state.atrawchar -= 1 + state.x -= 1 + buffer.flushLine(state) + prevl = true + + if c == ' ': + buffer.writefmt(fmtword) + buffer.writeraw(rawword) + state.atchar += fmtword.len + state.atrawchar += rawword.len + if prevl: + state.x += fmtword.len + prevl = false + fmtword = "" + rawword = "" + n += 1 + buffer.writefmt(fmtword) + buffer.writeraw(rawword) + state.atchar += fmtword.len + state.atrawchar += rawword.len + +proc preAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) = + let elem = node.nodeAttr() + if not buffer.onNewLine() and node.openblock and state.blanklines == 0: + buffer.flushLine(state) + + if node.openblock: + while state.blanklines < max(elem.margin, elem.margintop): + buffer.flushLine(state) + + if not buffer.onNewLine() and state.blanklines == 0 and node.displayed(): + buffer.addSpaces(state, state.nextspaces) + state.nextspaces = 0 + if elem.pad: + buffer.addSpacePadding(state) + + if state.blankspaces < max(elem.margin, elem.marginleft): + buffer.addSpaces(state, max(elem.margin, elem.marginleft) - state.blankspaces) + + if elem.centered and buffer.onNewLine() and node.displayed(): + buffer.addSpaces(state, max(buffer.width div 2 - state.centerlen div 2, 0)) + state.centerlen = 0 + +proc postAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) = + let elem = node.nodeAttr() + + if node.getRawLen() > 0: + state.blanklines = 0 + state.blankspaces = 0 + + if not buffer.onNewLine() and state.blanklines == 0: + if elem.pad: + state.nextspaces = 1 + state.nextspaces += max(elem.margin, elem.marginright) + if node.closeblock: + buffer.flushLine(state) + + if node.closeblock: + while state.blanklines < max(elem.margin, elem.marginbottom): + buffer.flushLine(state) + + if elem.htmlTag == tagBr and not node.openblock: + buffer.flushLine(state) + +proc renderNode(buffer: Buffer, node: HtmlNode, state: RenderState) = + if not node.visibleNode(): + return + if node.isElemNode(): + node.element.formattedElem = node.element.getFormattedElem() + let elem = node.nodeAttr() + if elem.htmlTag == tagTitle: + if isTextNode(node): + buffer.title = node.text.text + return + else: discard + if elem.hidden: return + + node.height = 1 + node.width = node.getRawLen() + + if not state.docenter: + if elem.centered: + if not node.closeblock and elem.htmlTag != tagBr: + state.centerqueue += 1 + return + if state.centerqueue > 0: + state.docenter = true + state.centerlen = 0 + var i = state.centerqueue + while i > 0: + state.centerlen += buffer.nodes[^i].getRawLen() + i -= 1 + while state.centerqueue > 0: + buffer.renderNode(buffer.nodes[^state.centerqueue], state) + state.centerqueue -= 1 + state.docenter = false + + buffer.preAlignNode(node, state) + + node.assignCoords(state) + if isTextNode(node): + buffer.writeWrappedText(state, node.text.formattedText, node.text.text) + elif isElemNode(node): + buffer.writeWrappedText(state, node.element.formattedElem, node.element.rawElem) + + buffer.postAlignNode(node, state) + +iterator revItems*(n: XmlNode): XmlNode {.inline.} = + var i = n.len - 1 + while i >= 0: + yield n[i] + i -= 1 + +type + XmlHtmlNode* = ref XmlHtmlNodeObj + XmlHtmlNodeObj = object + xml*: XmlNode + html*: HtmlNode + +proc setLastHtmlLine(buffer: Buffer, state: RenderState) = + if buffer.text.len != buffer.lines[^1]: + state.atchar = buffer.text.len + 1 + state.atrawchar = buffer.rawtext.len + 1 + buffer.flushLine(state) + +proc renderHtml(buffer: Buffer) = + var stack: seq[XmlHtmlNode] + let first = XmlHtmlNode(xml: buffer.htmlSource, + html: getHtmlNode(buffer.htmlSource, none(HtmlElement))) + stack.add(first) + + var state = newRenderState() + while stack.len > 0: + let currElem = stack.pop() + if currElem.html.nodeType != NODE_COMMENT: + buffer.renderNode(currElem.html, state) + if currElem.html.isElemNode(): + if currElem.html.element.id != "": + eprint currElem.html.element.id + buffer.addNode(currElem.html) + var last = false + for item in currElem.xml.revItems: + let child = XmlHtmlNode(xml: item, + html: getHtmlNode(item, some(currElem.html.element))) + stack.add(child) + if not last and child.html.visibleNode(): + last = true + if currElem.html.element.display == DISPLAY_BLOCK: + stack[^1].html.closeblock = true + if last: + if currElem.html.element.display == DISPLAY_BLOCK: + stack[^1].html.openblock = true + buffer.setLastHtmlLine(state) + +proc drawHtml(buffer: Buffer) = + var state = newRenderState() + for node in buffer.nodes: + buffer.renderNode(node, state) + buffer.setLastHtmlLine(state) + +proc statusMsgForBuffer(buffer: Buffer) = + let msg = $buffer.cursorY & "/" & $buffer.lastLine() & " (" & + $buffer.atPercentOf() & "%) " & + "<" & buffer.title & buffer.hovertext + statusMsg(msg.maxString(buffer.width), buffer.height) + +proc cursorBufferPos(buffer: Buffer) = + var x = buffer.cursorX + if x > buffer.currentRawLineLength(): + x = buffer.currentRawLineLength() + var y = buffer.cursorY - 1 - buffer.fromY + termGoto(x, y) + +proc displayBuffer(buffer: Buffer) = + eraseScreen() + termGoto(0, 0) + + print(buffer.visibleText()) + +proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool = + var s = "" + var feedNext = false + while true: + cursorBufferPos(buffer) + if not feedNext: + s = "" + else: + feedNext = false + let c = getch() + s &= c + let action = getNormalAction(s) + var redraw = false + var reshape = false + case action + of ACTION_QUIT: + eraseScreen() + return false + of ACTION_CURSOR_LEFT: redraw = buffer.cursorLeft() + of ACTION_CURSOR_DOWN: redraw = buffer.cursorDown() + of ACTION_CURSOR_UP: redraw = buffer.cursorUp() + of ACTION_CURSOR_RIGHT: redraw = buffer.cursorRight() + of ACTION_CURSOR_LINEBEGIN: buffer.cursorLineBegin() + of ACTION_CURSOR_LINEEND: buffer.cursorLineEnd() + of ACTION_CURSOR_NEXT_WORD: redraw = buffer.cursorNextWord() + of ACTION_CURSOR_NEXT_NODE: redraw = buffer.cursorNextNode() + of ACTION_CURSOR_PREV_WORD: redraw = buffer.cursorPrevWord() + of ACTION_CURSOR_PREV_NODE: redraw = buffer.cursorPrevNode() + of ACTION_CURSOR_NEXT_LINK: redraw = buffer.cursorNextLink() + of ACTION_CURSOR_PREV_LINK: redraw = buffer.cursorPrevLink() + of ACTION_PAGE_DOWN: redraw = buffer.pageDown() + of ACTION_PAGE_UP: redraw = buffer.pageUp() + of ACTION_HALF_PAGE_DOWN: redraw = buffer.halfPageDown() + of ACTION_HALF_PAGE_UP: redraw = buffer.halfPageUp() + of ACTION_CURSOR_FIRST_LINE: redraw = buffer.cursorFirstLine() + of ACTION_CURSOR_LAST_LINE: redraw = buffer.cursorLastLine() + of ACTION_CURSOR_TOP: redraw = buffer.cursorTop() + of ACTION_CURSOR_MIDDLE: redraw = buffer.cursorMiddle() + of ACTION_CURSOR_BOTTOM: redraw = buffer.cursorBottom() + of ACTION_SCROLL_DOWN: redraw = buffer.scrollDown() + of ACTION_SCROLL_UP: redraw = buffer.scrollUp() + of ACTION_CLICK: + let selectedElem = buffer.findSelectedElement() + if selectedElem.isSome: + case selectedElem.get().htmlTag + of tagInput: + clearStatusMsg(buffer.height) + let status = readLine("TEXT:", selectedElem.get().value) + if status: + reshape = true + redraw = true + of tagA: + buffer.setLocation(parseUri(buffer.selectedlink.text.parent.href)) + return true + else: discard + of ACTION_CHANGE_LOCATION: + var url = $buffer.location + clearStatusMsg(buffer.height) + let status = readLine("URL:", url) + if status: + buffer.setLocation(parseUri(url)) + return true + of ACTION_FEED_NEXT: + feedNext = true + of ACTION_RELOAD: return true + of ACTION_RESHAPE: + reshape = true + redraw = true + of ACTION_REDRAW: redraw = true + else: discard + redraw = redraw or buffer.checkLinkSelection() + if reshape: + buffer.clearText() + buffer.drawHtml() + if redraw: + buffer.displayBuffer() + buffer.statusMsgForBuffer() + +proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool = + eraseScreen() + termGoto(0, 0) + #buffer.printwrite = true + discard buffer.gotoAnchor() + buffer.renderHtml() + buffer.displayBuffer() + buffer.statusMsgForBuffer() + return inputLoop(attrs, buffer) + diff --git a/htmlelement.nim b/htmlelement.nim new file mode 100644 index 00000000..4cf2e02e --- /dev/null +++ b/htmlelement.nim @@ -0,0 +1,340 @@ +import strutils +import re +import terminal +import options + +import fusion/htmlparser +import fusion/htmlparser/xmltree + +import twtstr +import twtio + +type + NodeType* = + enum + NODE_ELEMENT, NODE_TEXT, NODE_COMMENT + DisplayType* = + enum + DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_NONE + InputType* = + enum + INPUT_BUTTON, INPUT_CHECKBOX, INPUT_COLOR, INPUT_DATE, INPUT_DATETIME_LOCAL, + INPUT_EMAIL, INPUT_FILE, INPUT_HIDDEN, INPUT_IMAGE, INPUT_MONTH, + INPUT_NUMBER, INPUT_PASSWORD, INPUT_RADIO, INPUT_RANGE, INPUT_RESET, + INPUT_SEARCH, INPUT_SUBMIT, INPUT_TEL, INPUT_TEXT, INPUT_TIME, INPUT_URL, + INPUT_WEEK, INPUT_UNKNOWN + WhitespaceType* = + enum + WHITESPACE_NORMAL, WHITESPACE_NOWRAP, + WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP, + WHITESPACE_INITIAL, WHITESPACE_INHERIT + +type + HtmlText* = ref HtmlTextObj + HtmlTextObj = object + parent*: HtmlElement + text*: string + formattedText*: string + HtmlElement* = ref HtmlElementObj + HtmlElementObj = object + id*: string + name*: string + value*: string + centered*: bool + hidden*: bool + display*: DisplayType + innerText*: string + formattedElem*: string + rawElem*: string + textNodes*: int + margintop*: int + marginbottom*: int + marginleft*: int + marginright*: int + margin*: int + pad*: bool + bold*: bool + italic*: bool + underscore*: bool + parentElement*: HtmlElement + case htmlTag*: HtmlTag + of tagInput: + itype*: InputType + size*: int + of tagA: + href*: string + selected*: bool + else: + discard + +type + HtmlNode* = ref HtmlNodeObj + HtmlNodeObj = object + case nodeType*: NodeType + of NODE_ELEMENT: + element*: HtmlElement + of NODE_TEXT: + text*: HtmlText + of NODE_COMMENT: + comment*: string + x*: int + y*: int + width*: int + height*: int + openblock*: bool + closeblock*: bool + +func isTextNode*(node: HtmlNode): bool = + return node.nodeType == NODE_TEXT + +func isElemNode*(node: HtmlNode): bool = + return node.nodeType == NODE_ELEMENT + +func getFormattedLen*(htmlText: HtmlText): int = + return htmlText.formattedText.strip().len + +func getFormattedLen*(htmlElem: HtmlElement): int = + return htmlElem.formattedElem.len + +func getFormattedLen*(htmlNode: HtmlNode): int = + case htmlNode.nodeType + of NODE_TEXT: return htmlNode.text.getFormattedLen() + of NODE_ELEMENT: return htmlNode.element.getFormattedLen() + else: + assert(false) + return 0 + +func getRawLen*(htmlText: HtmlText): int = + return htmlText.text.len + +func getRawLen*(htmlElem: HtmlElement): int = + return htmlElem.rawElem.len + +func getRawLen*(htmlNode: HtmlNode): int = + case htmlNode.nodeType + of NODE_TEXT: return htmlNode.text.getRawLen() + of NODE_ELEMENT: return htmlNode.element.getRawLen() + else: + assert(false) + return 0 + +func visibleNode*(node: HtmlNode): bool = + case node.nodeType + of NODE_TEXT: return true + of NODE_ELEMENT: return true + else: return false + +func displayed*(elem: HtmlElement): bool = + return elem.display != DISPLAY_NONE and (elem.getFormattedLen() > 0 or elem.htmlTag == tagBr) and not elem.hidden + +func displayed*(node: HtmlNode): bool = + if node.isTextNode(): + return node.getRawLen() > 0 + elif node.isElemNode(): + return node.element.displayed() + +func empty*(elem: HtmlElement): bool = + return elem.textNodes == 0 or not elem.displayed() + +func toInputType*(str: string): InputType = + case str + of "button": INPUT_BUTTON + of "checkbox": INPUT_CHECKBOX + of "color": INPUT_COLOR + of "date": INPUT_DATE + of "datetime_local": INPUT_DATETIME_LOCAL + of "email": INPUT_EMAIL + of "file": INPUT_FILE + of "hidden": INPUT_HIDDEN + of "image": INPUT_IMAGE + of "month": INPUT_MONTH + of "number": INPUT_NUMBER + of "password": INPUT_PASSWORD + of "radio": INPUT_RADIO + of "range": INPUT_RANGE + of "reset": INPUT_RESET + of "search": INPUT_SEARCH + of "submit": INPUT_SUBMIT + of "tel": INPUT_TEL + of "text": INPUT_TEXT + of "time": INPUT_TIME + of "url": INPUT_URL + of "week": INPUT_WEEK + else: INPUT_UNKNOWN + +func toInputSize*(str: string): int = + if str.len == 0: + return 20 + return str.parseInt() + +func getInputElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement = + assert(htmlElement.htmlTag == tagInput) + htmlElement.itype = xmlElement.attr("type").toInputType() + htmlElement.size = xmlElement.attr("size").toInputSize() + htmlElement.value = xmlElement.attr("value") + htmlElement.pad = true + return htmlElement + +func getAnchorElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement = + assert(htmlElement.htmlTag == tagA) + htmlElement.href = xmlElement.attr("href") + return htmlElement + +func getSelectElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement = + assert(htmlElement.htmlTag == tagSelect) + for item in xmlElement.items: + if item.kind == xnElement: + if item.tag == "option" and item.attr("value") != "": + htmlElement.value = item.attr("value") + break + htmlElement.name = xmlElement.attr("name") + return htmlElement + +func getOptionElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement = + assert(htmlElement.htmlTag == tagOption) + htmlElement.value = xmlElement.attr("value") + if htmlElement.parentElement.value != htmlElement.value: + htmlElement.hidden = true + return htmlElement + +func getFormattedInput(htmlElement: HtmlElement): string = + case htmlElement.itype + of INPUT_TEXT, INPUT_SEARCH: + let valueFit = fitValueToSize(htmlElement.value, htmlElement.size) + return "[" & valueFit.addAnsiStyle(styleUnderscore).addAnsiFgColor(fgRed) & "]" + of INPUT_SUBMIT: return ("[" & htmlElement.value & "]").addAnsiFgColor(fgRed) + else: discard + +func getRawInput(htmlElement: HtmlElement): string = + case htmlElement.itype + of INPUT_TEXT, INPUT_SEARCH: + return "[" & htmlElement.value.fitValueToSize(htmlElement.size) & "]" + of INPUT_SUBMIT: return "[" & htmlElement.value & "]" + else: discard + +func getRawElem*(htmlElement: HtmlElement): string = + case htmlElement.htmlTag + of tagInput: return htmlElement.getRawInput() + of tagOption: return "[]" + else: return "" + +func getFormattedElem*(htmlElement: HtmlElement): string = + case htmlElement.htmlTag + of tagInput: return htmlElement.getFormattedInput() + else: return "" + +func getRawText*(htmlText: HtmlText): string = + if htmlText.parent.htmlTag != tagPre: + result = htmlText.text.replace(re"\n").strip() + else: + result = htmlText.text + + if htmlText.parent.htmlTag == tagOption: + result = "[" & result & "]" + +func getFormattedText*(htmlText: HtmlText): string = + result = htmlText.text + case htmlText.parent.htmlTag + of tagA: + result = result.addAnsiFgColor(fgBlue) + if htmlText.parent.selected: + result = result.addAnsiStyle(styleUnderscore) + of tagOption: result = result.addAnsiFgColor(fgRed) + else: discard + + if htmlText.parent.bold: + result = result.addAnsiStyle(styleBright) + if htmlText.parent.italic: + result = result.addAnsiStyle(styleItalic) + if htmlText.parent.underscore: + result = result.addAnsiStyle(styleUnderscore) + +proc newElemFromParent(elem: HtmlElement, parentOpt: Option[HtmlElement]): HtmlElement = + if parentOpt.isSome: + let parent = parentOpt.get() + elem.centered = parent.centered + elem.bold = parent.bold + elem.italic = parent.italic + elem.underscore = parent.underscore + elem.hidden = parent.hidden + elem.display = parent.display + #elem.margin = parent.margin + #elem.margintop = parent.margintop + #elem.marginbottom = parent.marginbottom + #elem.marginleft = parent.marginleft + #elem.marginright = parent.marginright + elem.parentElement = parent + elem.pad = false + + return elem + +proc getHtmlElement*(xmlElement: XmlNode, inherit: Option[HtmlElement]): HtmlElement = + assert kind(xmlElement) == xnElement + var htmlElement: HtmlElement + htmlElement = newElemFromParent(HtmlElement(htmlTag: htmlTag(xmlElement)), inherit) + htmlElement.id = xmlElement.attr("id") + + if htmlElement.htmlTag in InlineTags: + htmlElement.display = DISPLAY_INLINE + elif htmlElement.htmlTag in BlockTags: + htmlElement.display = DISPLAY_BLOCK + htmlElement.pad = true + elif htmlElement.htmlTag in SingleTags: + htmlElement.display = DISPLAY_SINGLE + else: + htmlElement.display = DISPLAY_NONE + + case htmlElement.htmlTag + of tagCenter: + htmlElement.centered = true + of tagB: + htmlElement.bold = true + of tagI: + htmlElement.italic = true + of tagU: + htmlElement.underscore = true + of tagHead: + htmlElement.hidden = true + of tagStyle: + htmlElement.hidden = true + of tagScript: + htmlElement.hidden = true + of tagInput: + htmlElement = getInputElement(xmlElement, htmlElement) + of tagA: + htmlElement = getAnchorElement(xmlElement, htmlElement) + of tagSelect: + htmlElement = getSelectElement(xmlElement, htmlElement) + of tagOption: + htmlElement = getOptionElement(xmlElement, htmlElement) + of tagPre, tagTd, tagTh: + htmlElement.margin = 1 + else: + discard + + for child in xmlElement.items: + if child.kind == xnText and child.text.strip().len > 0: + htmlElement.textNodes += 1 + + htmlElement.rawElem = htmlElement.getRawElem() + htmlElement.formattedElem = htmlElement.getFormattedElem() + return htmlElement + +proc getHtmlText*(text: string, parent: HtmlElement): HtmlText = + var textNode = HtmlText(parent: parent, text: text) + textNode.text = textNode.getRawText() + textNode.formattedText = textNode.getFormattedText() + return textNode + +proc getHtmlNode*(xmlElement: XmlNode, parent: Option[HtmlElement]): HtmlNode = + case kind(xmlElement) + of xnElement: + return HtmlNode(nodeType: NODE_ELEMENT, element: getHtmlElement(xmlElement, parent)) + of xnText: + assert(parent.isSome) + return HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(xmlElement.text, parent.get())) + of xnComment: + return HtmlNode(nodeType: NODE_COMMENT, comment: xmlElement.text) + of xnCData: + return HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(xmlElement.text, parent.get())) + else: assert(false) diff --git a/keymap b/keymap new file mode 100644 index 00000000..218cabd3 --- /dev/null +++ b/keymap @@ -0,0 +1,53 @@ +#"normal mode" keybindings +nmap q ACTION_QUIT +nmap h ACTION_CURSOR_LEFT +nmap j ACTION_CURSOR_DOWN +nmap k ACTION_CURSOR_UP +nmap l ACTION_CURSOR_RIGHT +nmap \e[D ACTION_CURSOR_LEFT +nmap \e[B ACTION_CURSOR_DOWN +nmap \e[A ACTION_CURSOR_UP +nmap \e[C ACTION_CURSOR_RIGHT +nmap ^ ACTION_CURSOR_LINEBEGIN +nmap $ ACTION_CURSOR_LINEEND +nmap b ACTION_CURSOR_PREV_WORD +nmap B ACTION_CURSOR_PREV_NODE +nmap w ACTION_CURSOR_NEXT_WORD +nmap W ACTION_CURSOR_NEXT_NODE +nmap [ ACTION_CURSOR_PREV_LINK +nmap ] ACTION_CURSOR_NEXT_LINK +nmap H ACTION_CURSOR_TOP +nmap M ACTION_CURSOR_MIDDLE +nmap L ACTION_CURSOR_BOTTOM +nmap C-d ACTION_HALF_PAGE_DOWN +nmap C-u ACTION_HALF_PAGE_UP +nmap C-f ACTION_PAGE_DOWN +nmap C-b ACTION_PAGE_UP +nmap \e[6~ ACTION_PAGE_DOWN +nmap \e[5~ ACTION_PAGE_UP +nmap C-e ACTION_SCROLL_DOWN +nmap C-y ACTION_SCROLL_UP +nmap C-m ACTION_CLICK +nmap C-j ACTION_CLICK +nmap C-l ACTION_CHANGE_LOCATION +nmap U ACTION_RELOAD +nmap r ACTION_RESHAPE +nmap R ACTION_REDRAW +nmap gg ACTION_CURSOR_FIRST_LINE +nmap G ACTION_CURSOR_LAST_LINE +nmap \e[H ACTION_CURSOR_FIRST_LINE +nmap \e[F ACTION_CURSOR_LAST_LINE + +#line editing keybindings +lemap C-m ACTION_LINED_SUBMIT +lemap C-j ACTION_LINED_SUBMIT +lemap C-h ACTION_LINED_BACKSPACE +lemap C-? ACTION_LINED_BACKSPACE +lemap C-c ACTION_LINED_CANCEL +lemap \eb ACTION_LINED_PREV_WORD +lemap \ef ACTION_LINED_NEXT_WORD +lemap C-b ACTION_LINED_BACK +lemap C-f ACTION_LINED_FORWARD +lemap C-u ACTION_LINED_CLEAR +lemap C-k ACTION_LINED_KILL +lemap C-w ACTION_LINED_KILL_WORD diff --git a/main.nim b/main.nim new file mode 100644 index 00000000..bbfffd58 --- /dev/null +++ b/main.nim @@ -0,0 +1,59 @@ +import httpClient +import uri +import os + +import fusion/htmlparser +import fusion/htmlparser/xmltree + +import display +import termattrs +import buffer +import twtio +import config + +proc loadRemotePage*(url: string): string = + return newHttpClient().getContent(url) + +proc loadLocalPage*(url: string): string = + return readFile(url) + +proc loadPageUri(uri: Uri, currentcontent: XmlNode): XmlNode = + var moduri = uri + var page: XmlNode + moduri.anchor = "" + if uri.scheme == "" and uri.path == "" and uri.anchor != "" and currentcontent != nil: + return currentcontent + elif uri.scheme == "" or uri.scheme == "file": + return parseHtml(loadLocalPage($moduri)) + else: + return parseHtml(loadRemotePage($moduri)) + +var buffers: seq[Buffer] + +proc main*() = + if paramCount() != 1: + eprint "Invalid parameters. Usage:\ntwt " + quit(1) + if not readKeymap("keymap"): + eprint "Failed to read keymap, falling back to default" + parseKeymap(keymapStr) + let attrs = getTermAttributes() + var buffer = newBuffer(attrs) + var url = parseUri(paramStr(1)) + buffers.add(buffer) + buffer.setLocation(uri) + buffer.htmlSource = loadPageUri(uri, buffer.htmlSource) + var lastUri = uri + while displayPage(attrs, buffer): + statusMsg("Loading...", buffer.height) + var newUri = buffer.location + lastUri.anchor = "" + newUri.anchor = "" + if $lastUri != $newUri: + buffer.htmlSource = loadPageUri(buffer.location, buffer.htmlSource) + buffer.clearBuffer() + else + lastUri = newUri + +#waitFor loadPage("https://lite.duckduckgo.com/lite/?q=hello%20world") +main() diff --git a/readme.md b/readme.md new file mode 100644 index 00000000..62c97ce6 --- /dev/null +++ b/readme.md @@ -0,0 +1,29 @@ +# twt - a web browser in your terminal + +## What is this? +A terminal web browser. It displays websites in your terminal. + +## Why make another web browser? +I've found other terminal web browsers insufficient for my needs. In fact, I started working on this after failing to add JavaScript support to w3m. + +## So what can this do? +Currently implemented features are: +* basic html rendering (WIP) +* custom keybindings + +Planned features: +* image (sixel/kitty) +* video (sixel/kitty) +* audio +* table +* cookie +* form +* JavaScript +* SOCKS proxy +* extension API (adblock support?) +* markdown? (with pandoc?) +* gopher? +* gemini? + +## How do I configure stuff? +Currently only keybindings can be configured. See the keymap file for the default (built-in) configuration. diff --git a/search.html b/search.html new file mode 100644 index 00000000..d4c3facd --- /dev/null +++ b/search.html @@ -0,0 +1,1586 @@ + + + + + + + + + rhetorische at DuckDuckGo + + + + + + + + + + +

 

+
DuckDuckGo
+

 

+ +
+ + + + + + + +
+ + + + + + + + +

 

+ + + +
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +

 

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1.  + + + Rhetorische Frage - Wikipedia + +
    + Die rhetorische Frage gilt als Stilmittel der Rhetorik. Rhetorische Fragen dienen nicht dem Informationsgewinn, sondern sind sprachliche Mittel der Beeinflussung. Semantisch stehen rhetorische Fragen den Behauptungen nahe. +
    + de.wikipedia.org/wiki/Rhetorische_Frage + +
  
2.  + + + Rhetorische Frage | Definition, Wirkung und Beispiel + +
    + Die rhetorische Frage ist ein Stilmittel der Rhetorik. Äußerlich betrachtet, unterscheidet sich eine rhetorische Wer eine rhetorische Frage stellt, fragt nicht nach Informationen, sondern versucht... +
    + wortwuchs.net/stilmittel/rhetorische-frage/ + +
  
3.  + + + Переводы «Rhetorische» (De-Ru) на ABBYY Lingvo Live + +
    + Jahrzehnte später, beim Klassentreffen erinnern sich Frauen selten an die rhetorische Gewandtheit, die Forschheit oder die Bereitschaft der Mitschülerinnen, ungestüm eigene Interessen zu vertreten. +
    + www.lingvolive.com/ru-ru/translate/de-ru/Rhetorische + +
  
4.  + + + Rhetorische Mittel + +
    + Rhetorische Mittel sind allgegenwärtig. Kaum ein Werbespruch kommt heute noch ohne eine rhetorische Figur aus. Auch wir selbst benutzen sie ständig, ohne uns dabei bewusst zu sein. +
    + www.abipedia.de/rhetorische-mittel.php + +
  
5.  + + + rhetorische - Translation into English - examples... | Reverso Context + +
    + Translations in context of "rhetorische" in German-English from Reverso Context: rhetorische Frage, rhetorische Figur. Das darf nicht nur eine rhetorische Verpflichtung bleiben. +
    + context.reverso.net/translation/german-english/rhetorische + +
  
6.  + + + rhetorische - translation - German-English Dictionary - Glosbe + +
    + translation and definition "rhetorische", German-English Dictionary online. Inflected form of rhetorisch. Automatic translation: rhetorische. +
    + en.glosbe.com/de/en/rhetorische + +
  
7.  + + + Rhetorische Figuren | Alltagsdeutsch - Podcast | DW | 08.11.2006 + +
    + Rhetorische Figuren sind doch keine Menschen! Diese rhetorische Figur zählt übrigens zu den grammatischen Figuren, die bewusst vom korrekten grammatischen Sprachgebrauch abweichen. +
    + www.dw.com/de/rhetorische-figuren/a-1606602 + +
  
8.  + + + Rhetorische Fragen: 25 Beispiele - und ihre Subbotschaften + +
    + Auf den ersten Blick ist die Frage völlig harmlos: „Wie geht es dir?" Oft handelt es sich dabei nur um eine Plattitüde, eine Art Eisbrecher beim Smalltalk, um ins Gespräch zu finden. +
    + karrierebibel.de/rhetorische-fragen/ + +
  
9.  + + + Rhetorische Frage: Definition, Wirkung & 16 typische Beispiele + +
    + Alles Wissenswerte über die rhetorische Frage: Definition, Wirkung, typische Beispiele und wie sie sich von der Suggestivfrage unterscheidet. +
    + www.schreiben.net/artikel/rhetorische-frage-3620/ + +
  
10.  + + + 40 wichtige rhetorische Mittel für deine nächste Textanalyse + +
    + ©iStockphoto.com/avosb. Rhetorische Mittel sind heute in jedermanns Sprachgebrauch zu finden, doch sind wir uns dessen selten bewusst. +
    + learnattack.de/journal/40-wichtige-rhetorische-mittel-textanalyse/ + +
  
11.  + + + rhetorische + +
    + rhetorische: 5 фраз в 2 тематиках. Лингвистика. +
    + www.multitran.com/m.exe?l1=3&l2=2&s=rhetorische + +
  
12.  + + + rhetorisch - это... Что такое rhetorisch? + +
    + Rhetorisch — Rhetorisch, 1) was nach Anleitung u. Zweck der Redekunst gefertigt ist od. in Verbindung mit derselben steht, z.B. Rhetorischer Ausdruck, Rhetorische Figuren (s.u. Figur 2) B); 2)... +
    + universal_de_ru.academic.ru/755953/rhetorisch + +
  
13.  + + + Rhetorische Perlen von AfD- und NPD-Anhängern und... | Facebook + +
    + See more of Rhetorische Perlen von AfD- und NPD-Anhängern und Verschwörungstheoretikern on Facebook. +
    + www.facebook.com/afdnpd/ + +
  
14.  + + + Rhetorische Antwort (@RhetorischeA) | Твиттер + +
    + Последние твиты от Rhetorische Antwort (@RhetorischeA). Es gibt keine dummen Antworten! #primatechangenotclimatechange. +
    + twitter.com/rhetorischea + +
  
15.  + + + Rhetorisch Übersetzung rhetorisch Definition auf TheFreeDictionary + +
    + rhetorisch nicht steig. geh. die Rhetorik betreffend rhetorische Fähigkeiten haben/erwerben, eine rhetorisch gute Rede halten eine rhetorische Frage eine Frage, auf die man keine ernsthafte Antwort... +
    + de.thefreedictionary.com/rhetorisch + +
  
16.  + + + Rhetorische Frage (Stilmittel) - Definition, Merkmale und Beispiele + +
    + Die rhetorische Frage gehört zu den ältesten und meist genutzten Stilmitteln der Rhetorik. Zu finden sind rhetorische Fragen in der Politik, der Literatur, in der Werbung und im ganz alltäglichen... +
    + www.inhaltsangabe.de/wissen/stilmittel/rhetorische-frage/ + +
  
17.  + + + Rhetorische-Mittel-Deutsch - Appar på Google Play + +
    + Rhetorische-Mittel-Deutsch. IT-KellerUtbildning. Ingen åldersgräns. +
    + play.google.com/store/apps/details?id=click.itkeller.lars.lernen&hl=sv + +
  
18.  + + + Rhetorik: Sprachliche Mittel und stilistische Figuren + +
    + Rhetorische Frage Eine rhetorische Frage ist eine Frage, deren Antwort schon bekannt ist und welche durch die Fragestellung schon hervorgegangen ist. Hier wird durch die Wirkung der Frage die... +
    + www.frustfrei-lernen.de/deutsch/rhetorik-sprachliche-mittel-stillistische-figuren.html + +
  
19.  + + + Rhetorische Stilmittel / sylistic devices of rhetoric on Vimeo + +
    + 1 Moderator. Related RSS Feeds. Rhetorische Stilmittel / sylistic devices of rhetoric. This is a Vimeo Group. Groups allow you to create mini communities around the things you like. +
    + vimeo.com/groups/156428 + +
  
20.  + + + Andreas Müller - Rhetorik | Rhetorische Figuren + +
    + Rhetorische Mittel leisten diese Zielsetzung bei richtigem Gebrauch. Der Inhalt kann durch rhetorische Stilmittel Man differenziert rhetorische Figuren weiterhin in Figuren und Bilder: Die... +
    + www.spektrum.de/astrowissen/rhetorik.html + +
  
21.  + + + Ethos Pathos Logos | Rhetorisches Dreieck | Überredendes Schreiben + +
    + Lernen Sie das rhetorische Dreieck von Ethos Pathos Logos mit lustigen und leicht verständlichen Storyboards. Ethos, Pathos und Logos sind wichtige Fähigkeiten für das Sprechen und... +
    + www.storyboardthat.com/de/articles/e/ethos-pathos-logos + +
  
22.  + + + Rhetorische Mittel | Übersicht | Erklärungen | Beispiele + +
    + Rhetorische Mittel: Erklärungen & Beispiele | Übersicht: die wichtigsten rhetorischen Übersichts-PDF rhetorische Mittel. Noch nicht genug? Hier stellen wir dir eine ausführlichere Liste mit rhetorischen... +
    + www.bachelorprint.at/wissenschaftliches-schreiben/rhetorische-mittel/ + +
  
23.  + + + Rhetorische Figuren/Stilmittel Flashcards | Quizlet + +
    + Start studying Rhetorische Figuren/Stilmittel. Learn vocabulary, terms and more with flashcards, games and other study tools. Rhetorische Figuren/Stilmittel. STUDY. Flashcards. +
    + quizlet.com/30344516/rhetorische-figurenstilmittel-flash-cards/ + +
  
24.  + + + rhetorische | Übersetzung Englisch-Deutsch + +
    + Deutsch-Englisch-Übersetzung für: rhetorische. ling. lit. rhetorical means. rhetorische Mittel {pl}. » Weitere 1 Übersetzungen für rhetorische innerhalb von Kommentaren. +
    + www.dict.cc/?s=rhetorische + +
  
25.  + + + Rhetorische Figuren 94 + +
    + Rhetorische Figuren 94. (4) die Aneignung der Rede durch Auswendiglernen (Memoria f), (5) die Kunst der rhetorische Frage: 1.im eigentlichen Sinn (als eine ↑ Immutatio syntactica) Aussage in... +
    + doclecture.net/1-48272.html + +
  
26.  + + + Rhetorische Kompetenz im Soft Skills Würfel (Rhetorik) + +
    + Rhetorische Kompetenz als Soft Skill ist die Summe von Redegewandtheit (Wortgewandtheit, Eloquenz) und Teilen der Soft Skills Präsentationskompetenz und Überzeugungsvermögen. +
    + www.soft-skills.com/rhetorische-kompetenz/ + +
  
+ + +
+ + + + + + + +      + + + + + + + + +
+ + +
+ +

 

+ +
+ + + + + + + + + + + +
+ +

 

+ + + + + + +
 
+ + + + + diff --git a/termattrs.nim b/termattrs.nim new file mode 100644 index 00000000..21fd3b04 --- /dev/null +++ b/termattrs.nim @@ -0,0 +1,12 @@ +import terminal + +type + TermAttributes* = object + termWidth*: int + termHeight*: int + +proc getTermAttributes*(): TermAttributes = + var t = TermAttributes() + t.termWidth = terminalWidth() + t.termHeight = terminalHeight() + return t diff --git a/twtio.nim b/twtio.nim new file mode 100644 index 00000000..cb59686c --- /dev/null +++ b/twtio.nim @@ -0,0 +1,125 @@ +import terminal +import tables +import strutils + +import twtstr +import config + +template print*(s: varargs[string, `$`]) = + for x in s: + stdout.write(x) + +template eprint*(s: varargs[string, `$`]) = + var a = false + for x in s: + if not a: + a = true + else: + stderr.write(' ') + stderr.write(x) + stderr.write('\n') + +proc termGoto*(x: int, y: int) = + setCursorPos(stdout, x, y) + +proc getNormalAction*(s: string): TwtAction = + if normalActionRemap.hasKey(s): + return normalActionRemap[s] + return NO_ACTION + +proc getLinedAction*(s: string): TwtAction = + if linedActionRemap.hasKey(s): + return linedActionRemap[s] + return NO_ACTION + +proc readLine*(prompt: string, current: var string): bool = + var new = current + print(prompt) + print(' ') + print(new) + var s = "" + var feedNext = false + var cursor = new.len + while true: + if not feedNext: + s = "" + else: + feedNext = false + let c = getch() + s &= c + let action = getLinedAction(s) + case action + of ACTION_LINED_CANCEL: + return false + of ACTION_LINED_SUBMIT: + current = new + return true + of ACTION_LINED_BACKSPACE: + if cursor > 0: + print(' '.repeat(new.len - cursor + 1)) + print('\b'.repeat(new.len - cursor + 1)) + print("\b \b") + new = new.substr(0, cursor - 2) & new.substr(cursor, new.len) + cursor -= 1 + print(new.substr(cursor, new.len)) + print('\b'.repeat(new.len - cursor)) + of ACTION_LINED_ESC: + new &= c + print("^[".addAnsiFgColor(fgBlue).addAnsiStyle(styleBright)) + of ACTION_LINED_CLEAR: + print(' '.repeat(new.len - cursor + 1)) + print('\b'.repeat(new.len - cursor + 1)) + print('\b'.repeat(cursor)) + print(' '.repeat(cursor)) + print('\b'.repeat(cursor)) + new = new.substr(cursor, new.len) + print(new) + print('\b'.repeat(new.len)) + cursor = 0 + of ACTION_LINED_KILL: + print(' '.repeat(new.len - cursor + 1)) + print('\b'.repeat(new.len - cursor + 1)) + new = new.substr(0, cursor - 1) + of ACTION_LINED_BACK: + if cursor > 0: + cursor -= 1 + print("\b") + of ACTION_LINED_FORWARD: + if cursor < new.len: + print(new[cursor]) + cursor += 1 + of ACTION_LINED_PREV_WORD: + while cursor > 0: + print('\b') + cursor -= 1 + if new[cursor] == ' ': + break + of ACTION_LINED_NEXT_WORD: + while cursor < new.len: + print(new[cursor]) + cursor += 1 + if cursor < new.len and new[cursor] == ' ': + break + of ACTION_LINED_KILL_WORD: + var chars = 0 + while cursor > chars: + chars += 1 + if new[cursor - chars] == ' ': + break + if chars > 0: + print(' '.repeat(new.len - cursor + 1)) + print('\b'.repeat(new.len - cursor + 1)) + print("\b \b".repeat(chars)) + new = new.substr(0, cursor - 1 - chars) & new.substr(cursor, new.len) + cursor -= chars + print(new.substr(cursor, new.len)) + print('\b'.repeat(new.len - cursor)) + of ACTION_FEED_NEXT: + feedNext = true + else: + print(' '.repeat(new.len - cursor + 1)) + print('\b'.repeat(new.len - cursor + 1)) + new = new.substr(0, cursor - 1) & c & new.substr(cursor, new.len) + print(new.substr(cursor, new.len)) + print('\b'.repeat(new.len - cursor - 1)) + cursor += 1 diff --git a/twtstr.nim b/twtstr.nim new file mode 100644 index 00000000..5a41f3e3 --- /dev/null +++ b/twtstr.nim @@ -0,0 +1,28 @@ +import terminal +import strutils + +func stripNewline*(str: string): string = + if str.len == 0: + result = str + return + + case str[^1] + of '\n': + result = str.substr(0, str.len - 2) + else: discard + +func addAnsiStyle*(str: string, style: Style): string = + return ansiStyleCode(style) & str & "\e[0m" + +func addAnsiFgColor*(str: string, color: ForegroundColor): string = + return ansiForegroundColorCode(color) & str & "\e[0m" + +func maxString*(str: string, max: int): string = + if max < str.len: + return str.substr(0, max - 2) & "$" + return str + +func fitValueToSize*(str: string, size: int): string = + if str.len < size: + return str & ' '.repeat(size - str.len) + return str.maxString(size) -- cgit 1.4.1-2-gfad0