diff options
author | bptato <nincsnevem662@gmail.com> | 2021-01-20 13:26:00 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2021-01-20 13:26:00 +0100 |
commit | 5268aada71dd554dca29c5bc273702d39d14bb5c (patch) | |
tree | 18b4a41ffaf464a8c0df0222a248f29bc570e369 | |
download | chawan-5268aada71dd554dca29c5bc273702d39d14bb5c.tar.gz |
Some things work, most things don't
-rw-r--r-- | Makefile | 3 | ||||
-rw-r--r-- | buffer.nim | 444 | ||||
-rw-r--r-- | config.nim | 138 | ||||
-rw-r--r-- | display.nim | 365 | ||||
-rw-r--r-- | htmlelement.nim | 340 | ||||
-rw-r--r-- | keymap | 53 | ||||
-rw-r--r-- | main.nim | 59 | ||||
-rw-r--r-- | readme.md | 29 | ||||
-rw-r--r-- | search.html | 1586 | ||||
-rw-r--r-- | termattrs.nim | 12 | ||||
-rw-r--r-- | twtio.nim | 125 | ||||
-rw-r--r-- | twtstr.nim | 28 |
12 files changed, 3182 insertions, 0 deletions
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 <url>" + 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 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0, user-scalable=1;"> + <meta name="referrer" content="origin"> + <meta name="HandheldFriendly" content="true" /> + <meta name="robots" content="noindex, nofollow" /> + <title>rhetorische at DuckDuckGo</title> + <link title="DuckDuckGo (Lite)" type="application/opensearchdescription+xml" rel="search" href="//duckduckgo.com/opensearch_lite_v2.xml"> + <link href="//duckduckgo.com/favicon.ico" rel="shortcut icon" /> + <link rel="icon" href="//duckduckgo.com/favicon.ico" type="image/x-icon" /> + <link rel="apple-touch-icon" href="//duckduckgo.com/assets/logo_icon128.v101.png"> + <link rel="image_src" href="//duckduckgo.com/assets/logo_homepage.normal.v101.png"/> + + <style type="text/css"> + html { + font-family: 'Helvetica Neue','Segoe UI', Arial, sans-serif; + } + .header { + font-size: 32px; + font-weight: bold; + color: #dc5e47; + height: 32px; + line-height: 32px; + } + .extra { + margin-top: 0px; + margin-bottom: 0px; + height: 1.3em; + } + #end-spacer { + height: 10em; + } + body { + padding-left: 20px; + width: 90%; + margin-top: 0; + } + table { + border-collapse: collapse; + border-spacing: 0; + } + th, td { + margin: 0; + padding: 0; + font-size: 107.1%; + } + + span.no-results { + font-size: 110%; + } + .query { + height: 28px; + border-color: #dc5e47; + border-style: solid solid solid solid; + border-width: 1px 1px 1px 1px; + -moz-border-radius: 3px; + border-radius: 3px; + font-size: 20px; + padding: 5px 6px; + text-align: left; + width: 55%; + max-width: 600px; + } + .submit { + padding-left: 7px; + padding-right: 7px; + height: 40px; + font-size: 16px; + /* + color: white; + background-color: #019801; + border-style: none none none; + -moz-border-radius: 3px; + border-radius: 3px; + padding: 5px 6px; + */ + cursor: pointer; + } + .link-text { + color: #662200; + font-size: 77.4%; + } + .result-link { + font-size: 107.1%; + } + .result-snippet { + font-size: 77.4%; + } + + .result-sponsored:hover { + background: #eae3ad !important; + } + + .result-sponsored { + background: #fff7d0; + border: 1px solid #fef9eb !important; + } + + .did-you-mean { + color: #EE4444; + font-size: 107.1%; + } + a { + text-decoration: none; + color: #1168CC; + } + a:hover { + text-decoration: underline; + } + a:visited { + color: #6830BB; + } + .timestamp { + color:#555555; + font-size: 77.4%; + } + .navbutton { + background-color:transparent; + border: 0px none; + cursor:pointer; + font-size:16px; + font-weight:bold; + text-decoration: underline; + color: #1168CC; + border-bottom: 1px solid transparent; + padding: 0px; + } + + .results--powered { + color: #888; + display: inline; + margin-left: 10px; + position: absolute; + bottom: 10px; + right: 10px; + } + .results--powered a { + color: #888; + font-size: 13px; + } + .results--powered__badge { + vertical-align: baseline; + } + .badge--yahoo { + background-image: url("//duckduckgo.com/assets/attribution/yahoo.v103.png"); + background-size: 55px 13px; + width: 55px; + height: 13px; + } + .badge, .results--powered__badge { + text-indent: -999999px; + display: inline-block; + vertical-align: middle; + position: relative; + background-position: 50% 50%; + background-repeat: no-repeat; + } + + .next_form { + float: left; + } + .prev_form { + float: left; + } + + .filters { + margin-top: 10px; + } + + @media only screen and (max-device-width: 700px) { + body { + width: 95%; + margin-left: 10px; + padding: 0; + } + .query { + width: 180px; + font-size: 12px; + } + .submit { + font-size: 12px; + } + } + + + @media only screen and (max-device-width: 701px) and (orientation:landscape) + { + .query { + width: 273px; + } + } + + </style> +</head> + +<body> +<p class='extra'> </p> +<div class="header">DuckDuckGo</div> +<p class='extra'> </p> + +<form action="/lite/" method="post"> + <input class='query' type="text" size="40" name="q" value="rhetorische" > + <input class='submit' type="submit" value="Search" > + + + + + +<div class="filters"> + +<select class="submit" name="kl"> + + <option value="" >All Regions</option> + + <option value="ar-es" >Argentina</option> + + <option value="au-en" >Australia</option> + + <option value="at-de" >Austria</option> + + <option value="be-fr" >Belgium (fr)</option> + + <option value="be-nl" >Belgium (nl)</option> + + <option value="br-pt" >Brazil</option> + + <option value="bg-bg" >Bulgaria</option> + + <option value="ca-en" >Canada (en)</option> + + <option value="ca-fr" >Canada (fr)</option> + + <option value="ct-ca" >Catalonia</option> + + <option value="cl-es" >Chile</option> + + <option value="cn-zh" >China</option> + + <option value="co-es" >Colombia</option> + + <option value="hr-hr" >Croatia</option> + + <option value="cz-cs" >Czech Republic</option> + + <option value="dk-da" >Denmark</option> + + <option value="ee-et" >Estonia</option> + + <option value="fi-fi" >Finland</option> + + <option value="fr-fr" >France</option> + + <option value="de-de" >Germany</option> + + <option value="gr-el" >Greece</option> + + <option value="hk-tzh" >Hong Kong</option> + + <option value="hu-hu" >Hungary</option> + + <option value="in-en" >India (en)</option> + + <option value="id-en" >Indonesia (en)</option> + + <option value="ie-en" >Ireland</option> + + <option value="il-en" >Israel (en)</option> + + <option value="it-it" >Italy</option> + + <option value="jp-jp" >Japan</option> + + <option value="kr-kr" >Korea</option> + + <option value="lv-lv" >Latvia</option> + + <option value="lt-lt" >Lithuania</option> + + <option value="my-en" >Malaysia (en)</option> + + <option value="mx-es" >Mexico</option> + + <option value="nl-nl" >Netherlands</option> + + <option value="nz-en" >New Zealand</option> + + <option value="no-no" >Norway</option> + + <option value="pk-en" >Pakistan (en)</option> + + <option value="pe-es" >Peru</option> + + <option value="ph-en" >Philippines (en)</option> + + <option value="pl-pl" >Poland</option> + + <option value="pt-pt" >Portugal</option> + + <option value="ro-ro" >Romania</option> + + <option value="ru-ru" >Russia</option> + + <option value="xa-ar" >Saudi Arabia</option> + + <option value="sg-en" >Singapore</option> + + <option value="sk-sk" >Slovakia</option> + + <option value="sl-sl" >Slovenia</option> + + <option value="za-en" >South Africa</option> + + <option value="es-ca" >Spain (ca)</option> + + <option value="es-es" >Spain (es)</option> + + <option value="se-sv" >Sweden</option> + + <option value="ch-de" >Switzerland (de)</option> + + <option value="ch-fr" >Switzerland (fr)</option> + + <option value="tw-tzh" >Taiwan</option> + + <option value="th-en" >Thailand (en)</option> + + <option value="tr-tr" >Turkey</option> + + <option value="us-en" >US (English)</option> + + <option value="us-es" >US (Spanish)</option> + + <option value="ua-uk" >Ukraine</option> + + <option value="uk-en" >United Kingdom</option> + + <option value="vn-en" >Vietnam (en)</option> + +</select> + +<select class="submit" name="df"> + + <option value="" selected>Any Time</option> + + <option value="d" >Past Day</option> + + <option value="w" >Past Week</option> + + <option value="m" >Past Month</option> + + <option value="y" >Past Year</option> + +</select> + +</form> + + + <p class="extra"> </p> + <table border="0"><tr> + <td> + + </td> + <td> + + <form action="/lite/" method="post"> + <!-- <a rel="next" href="/lite/?q=rhetorische&v=l&l=us-en&p=-1&s=30&ex=-1&o=json&dl=en&ct=AT&ss_mkt=us&sp=0&vqd=3-317185285141429630887821832006479517976-203666168757858907862519566843911872030&dc=27&api=%2Fd.js">Next Page ></a> //--> + <input type="submit" class='navbutton' value="Next Page >"> + <input type="hidden" name="q" value="rhetorische"> + <input type="hidden" name="s" value="30"> + <input type="hidden" name="nextParams" value=""> + <input type="hidden" name="v" value="l"> + <input type="hidden" name="o" value="json"> + <input type="hidden" name="dc" value="27"> + <input type="hidden" name="api" value="/d.js"> + <input type="hidden" name="vqd" value="3-317185285141429630887821832006479517976-203666168757858907862519566843911872030"> + + + + <input name="kl" value="wt-wt" type="hidden"> + + + + + </form> + + </td> + </tr></table> + + + + +<p class='extra'> </p> + +<table border="0"> + +</table> + +<table border="0"> + +<!-- Web results are present --> + + + + + + <tr> + + <td valign="top">1. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fde.wikipedia.org%2Fwiki%2FRhetorische_Frage" class='result-link'><b>Rhetorische</b> Frage - Wikipedia</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Die <b>rhetorische</b> Frage gilt als Stilmittel der Rhetorik. <b>Rhetorische</b> Fragen dienen nicht dem Informationsgewinn, sondern sind sprachliche Mittel der Beeinflussung. Semantisch stehen <b>rhetorische</b> Fragen den Behauptungen nahe. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>de.wikipedia.org/wiki/Rhetorische_Frage</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">2. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwortwuchs.net%2Fstilmittel%2Frhetorische%2Dfrage%2F" class='result-link'><b>Rhetorische</b> Frage | Definition, Wirkung und Beispiel</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Die <b>rhetorische</b> Frage ist ein Stilmittel der Rhetorik. Äußerlich betrachtet, unterscheidet sich eine <b>rhetorische</b> Wer eine <b>rhetorische</b> Frage stellt, fragt nicht nach Informationen, sondern versucht... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>wortwuchs.net/stilmittel/rhetorische-frage/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">3. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.lingvolive.com%2Fru%2Dru%2Ftranslate%2Fde%2Dru%2FRhetorische" class='result-link'>Переводы «<b>Rhetorische</b>» (De-Ru) на ABBYY Lingvo Live</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Jahrzehnte später, beim Klassentreffen erinnern sich Frauen selten an die <b>rhetorische</b> Gewandtheit, die Forschheit oder die Bereitschaft der Mitschülerinnen, ungestüm eigene Interessen zu vertreten. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.lingvolive.com/ru-ru/translate/de-ru/Rhetorische</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">4. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=http%3A%2F%2Fwww.abipedia.de%2Frhetorische%2Dmittel.php" class='result-link'><b>Rhetorische</b> Mittel</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Mittel sind allgegenwärtig. Kaum ein Werbespruch kommt heute noch ohne eine <b>rhetorische</b> Figur aus. Auch wir selbst benutzen sie ständig, ohne uns dabei bewusst zu sein. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.abipedia.de/rhetorische-mittel.php</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">5. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fcontext.reverso.net%2Ftranslation%2Fgerman%2Denglish%2Frhetorische" class='result-link'><b>rhetorische</b> - Translation into English - examples... | Reverso Context</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Translations in context of "<b>rhetorische</b>" in German-English from Reverso Context: <b>rhetorische</b> Frage, <b>rhetorische</b> Figur. Das darf nicht nur eine <b>rhetorische</b> Verpflichtung bleiben. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>context.reverso.net/translation/german-english/rhetorische</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">6. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fen.glosbe.com%2Fde%2Fen%2Frhetorische" class='result-link'><b>rhetorische</b> - translation - German-English Dictionary - Glosbe</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + translation and definition "<b>rhetorische</b>", German-English Dictionary online. Inflected form of rhetorisch. Automatic translation: <b>rhetorische</b>. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>en.glosbe.com/de/en/rhetorische</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">7. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.dw.com%2Fde%2Frhetorische%2Dfiguren%2Fa%2D1606602" class='result-link'><b>Rhetorische</b> Figuren | Alltagsdeutsch - Podcast | DW | 08.11.2006</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Figuren sind doch keine Menschen! Diese <b>rhetorische</b> Figur zählt übrigens zu den grammatischen Figuren, die bewusst vom korrekten grammatischen Sprachgebrauch abweichen. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.dw.com/de/rhetorische-figuren/a-1606602</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">8. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fkarrierebibel.de%2Frhetorische%2Dfragen%2F" class='result-link'><b>Rhetorische</b> Fragen: 25 Beispiele - und ihre Subbotschaften</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + 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. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>karrierebibel.de/rhetorische-fragen/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">9. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.schreiben.net%2Fartikel%2Frhetorische%2Dfrage%2D3620%2F" class='result-link'><b>Rhetorische</b> Frage: Definition, Wirkung & 16 typische Beispiele</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Alles Wissenswerte über die <b>rhetorische</b> Frage: Definition, Wirkung, typische Beispiele und wie sie sich von der Suggestivfrage unterscheidet. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.schreiben.net/artikel/rhetorische-frage-3620/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">10. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Flearnattack.de%2Fjournal%2F40%2Dwichtige%2Drhetorische%2Dmittel%2Dtextanalyse%2F" class='result-link'>40 wichtige <b>rhetorische</b> Mittel für deine nächste Textanalyse</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + ©iStockphoto.com/avosb. <b>Rhetorische</b> Mittel sind heute in jedermanns Sprachgebrauch zu finden, doch sind wir uns dessen selten bewusst. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>learnattack.de/journal/40-wichtige-rhetorische-mittel-textanalyse/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">11. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.multitran.com%2Fm.exe%3Fl1%3D3%26l2%3D2%26s%3Drhetorische" class='result-link'><b>rhetorische</b></a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>rhetorische</b>: 5 фраз в 2 тематиках. Лингвистика. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.multitran.com/m.exe?l1=3&l2=2&s=rhetorische</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">12. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Funiversal_de_ru.academic.ru%2F755953%2Frhetorisch" class='result-link'>rhetorisch - это... Что такое rhetorisch?</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Rhetorisch — Rhetorisch, 1) was nach Anleitung u. Zweck der Redekunst gefertigt ist od. in Verbindung mit derselben steht, z.B. Rhetorischer Ausdruck, <b>Rhetorische</b> Figuren (s.u. Figur 2) B); 2)... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>universal_de_ru.academic.ru/755953/rhetorisch</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">13. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.facebook.com%2Fafdnpd%2F" class='result-link'><b>Rhetorische</b> Perlen von AfD- und NPD-Anhängern und... | Facebook</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + See more of <b>Rhetorische</b> Perlen von AfD- und NPD-Anhängern und Verschwörungstheoretikern on Facebook. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.facebook.com/afdnpd/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">14. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Ftwitter.com%2Frhetorischea" class='result-link'><b>Rhetorische</b> Antwort (@RhetorischeA) | Твиттер</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Последние твиты от <b>Rhetorische</b> Antwort (@RhetorischeA). Es gibt keine dummen Antworten! #primatechangenotclimatechange. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>twitter.com/rhetorischea</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">15. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fde.thefreedictionary.com%2Frhetorisch" class='result-link'>Rhetorisch Übersetzung rhetorisch Definition auf TheFreeDictionary</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + rhetorisch nicht steig. geh. die Rhetorik betreffend <b>rhetorische</b> Fähigkeiten haben/erwerben, eine rhetorisch gute Rede halten eine <b>rhetorische</b> Frage eine Frage, auf die man keine ernsthafte Antwort... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>de.thefreedictionary.com/rhetorisch</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">16. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.inhaltsangabe.de%2Fwissen%2Fstilmittel%2Frhetorische%2Dfrage%2F" class='result-link'><b>Rhetorische</b> Frage (Stilmittel) - Definition, Merkmale und Beispiele</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Die <b>rhetorische</b> Frage gehört zu den ältesten und meist genutzten Stilmitteln der Rhetorik. Zu finden sind <b>rhetorische</b> Fragen in der Politik, der Literatur, in der Werbung und im ganz alltäglichen... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.inhaltsangabe.de/wissen/stilmittel/rhetorische-frage/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">17. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dclick.itkeller.lars.lernen%26hl%3Dsv" class='result-link'><b>Rhetorische</b>-Mittel-Deutsch - Appar på Google Play</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b>-Mittel-Deutsch. IT-KellerUtbildning. Ingen åldersgräns. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>play.google.com/store/apps/details?id=click.itkeller.lars.lernen&hl=sv</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">18. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.frustfrei%2Dlernen.de%2Fdeutsch%2Frhetorik%2Dsprachliche%2Dmittel%2Dstillistische%2Dfiguren.html" class='result-link'>Rhetorik: Sprachliche Mittel und stilistische Figuren</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Frage Eine <b>rhetorische</b> 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... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.frustfrei-lernen.de/deutsch/rhetorik-sprachliche-mittel-stillistische-figuren.html</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">19. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fvimeo.com%2Fgroups%2F156428" class='result-link'><b>Rhetorische</b> Stilmittel / sylistic devices of rhetoric on Vimeo</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + 1 Moderator. Related RSS Feeds. <b>Rhetorische</b> Stilmittel / sylistic devices of rhetoric. This is a Vimeo Group. Groups allow you to create mini communities around the things you like. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>vimeo.com/groups/156428</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">20. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.spektrum.de%2Fastrowissen%2Frhetorik.html" class='result-link'>Andreas Müller - Rhetorik | <b>Rhetorische</b> Figuren</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Mittel leisten diese Zielsetzung bei richtigem Gebrauch. Der Inhalt kann durch <b>rhetorische</b> Stilmittel Man differenziert <b>rhetorische</b> Figuren weiterhin in Figuren und Bilder: Die... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.spektrum.de/astrowissen/rhetorik.html</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">21. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.storyboardthat.com%2Fde%2Farticles%2Fe%2Fethos%2Dpathos%2Dlogos" class='result-link'>Ethos Pathos Logos | <b>Rhetorisches</b> Dreieck | Überredendes Schreiben</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Lernen Sie das <b>rhetorische</b> 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... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.storyboardthat.com/de/articles/e/ethos-pathos-logos</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">22. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.bachelorprint.at%2Fwissenschaftliches%2Dschreiben%2Frhetorische%2Dmittel%2F" class='result-link'><b>Rhetorische</b> Mittel | Übersicht | Erklärungen | Beispiele</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Mittel: Erklärungen & Beispiele | Übersicht: die wichtigsten rhetorischen Übersichts-PDF <b>rhetorische</b> Mittel. Noch nicht genug? Hier stellen wir dir eine ausführlichere Liste mit rhetorischen... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.bachelorprint.at/wissenschaftliches-schreiben/rhetorische-mittel/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">23. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fquizlet.com%2F30344516%2Frhetorische%2Dfigurenstilmittel%2Dflash%2Dcards%2F" class='result-link'><b>Rhetorische</b> Figuren/Stilmittel Flashcards | Quizlet</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Start studying <b>Rhetorische</b> Figuren/Stilmittel. Learn vocabulary, terms and more with flashcards, games and other study tools. <b>Rhetorische</b> Figuren/Stilmittel. STUDY. Flashcards. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>quizlet.com/30344516/rhetorische-figurenstilmittel-flash-cards/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">24. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.dict.cc%2F%3Fs%3Drhetorische" class='result-link'><b>rhetorische</b> | Übersetzung Englisch-Deutsch</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + Deutsch-Englisch-Übersetzung für: <b>rhetorische</b>. ling. lit. rhetorical means. <b>rhetorische</b> Mittel {pl}. » Weitere 1 Übersetzungen für <b>rhetorische</b> innerhalb von Kommentaren. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.dict.cc/?s=rhetorische</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">25. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fdoclecture.net%2F1%2D48272.html" class='result-link'><b>Rhetorische</b> Figuren 94</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Figuren 94. (4) die Aneignung der Rede durch Auswendiglernen (Memoria f), (5) die Kunst der <b>rhetorische</b> Frage: 1.im eigentlichen Sinn (als eine ↑ Immutatio syntactica) Aussage in... + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>doclecture.net/1-48272.html</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + <tr> + + <td valign="top">26. </td> + <td> + + + <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.soft%2Dskills.com%2Frhetorische%2Dkompetenz%2F" class='result-link'><b>Rhetorische</b> Kompetenz im Soft Skills Würfel (Rhetorik)</a> + + </td> + </tr> + + + <tr> + <td> </td> + <td class='result-snippet'> + <b>Rhetorische</b> Kompetenz als Soft Skill ist die Summe von Redegewandtheit (Wortgewandtheit, Eloquenz) und Teilen der Soft Skills Präsentationskompetenz und Überzeugungsvermögen. + </td> + </tr> + + + + <tr> + <td> </td> + <td> + <span class='link-text'>www.soft-skills.com/rhetorische-kompetenz/</span> + + </td> + </tr> + + + <tr> + <td> </td> + <td> </td> + </tr> + + + + + + + + + + + <tr> + <td colspan=2> + + + <form class="next_form" action="/lite/" method="post"> + <!-- <a rel="next" href="/lite/?q=rhetorische&v=l&l=us-en&p=-1&s=30&ex=-1&o=json&dl=en&ct=AT&ss_mkt=us&sp=0&vqd=3-317185285141429630887821832006479517976-203666168757858907862519566843911872030&dc=27&api=%2Fd.js">Next Page ></a> //--> + <input type="submit" class='navbutton' value="Next Page >"> + <input type="hidden" name="q" value="rhetorische"> + <input type="hidden" name="s" value="30"> + <input type="hidden" name="o" value="json"> + <input type="hidden" name="dc" value="27"> + <input type="hidden" name="api" value="/d.js"> + + + + + <input name="kl" value="wt-wt" type="hidden"> + + + + + </form> + + + </td> + </tr> + +</table> + +<p class='extra'> </p> + +<form action="/lite/" method="post"> + <input class='query' type="text" size="40" name="q" value="rhetorische" > + <input class='submit' type="submit" value="Search" > + + + + + + + <input name="kl" value="wt-wt" type="hidden"> + + +</form> + +<p class='extra'> </p> + +<!-- +<a href="#top">Top</a> +//--> + + + <img src="//duckduckgo.com/t/sl_l"/> + +<div id='end-spacer'> </div> + + + +</body> +</html> 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) |