diff options
author | bptato <nincsnevem662@gmail.com> | 2021-01-22 12:54:21 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2021-01-22 12:54:21 +0100 |
commit | 5d6af7f57a89239554a9cd51fe60f8227d7ce549 (patch) | |
tree | cc89b54443b227b4fc2140ee82ceee268d989f9f | |
parent | 6e1ef74bdca4c629bda2756a6e72279e038195f4 (diff) | |
download | chawan-5d6af7f57a89239554a9cd51fe60f8227d7ce549.tar.gz |
failed attempt for unicode
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | buffer.nim | 65 | ||||
-rw-r--r-- | config.nim | 41 | ||||
-rw-r--r-- | display.nim | 116 | ||||
-rw-r--r-- | enums.nim | 66 | ||||
-rw-r--r-- | htmlelement.nim | 460 | ||||
-rw-r--r-- | keymap | 2 | ||||
-rw-r--r-- | main.nim | 5 | ||||
-rw-r--r-- | parser.nim | 17 | ||||
-rw-r--r-- | readme.md | 28 | ||||
-rw-r--r-- | twtio.nim | 2 | ||||
-rw-r--r-- | twtstr.nim | 59 |
13 files changed, 531 insertions, 340 deletions
diff --git a/.gitignore b/.gitignore index 6197c0e1..7dab9430 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ a twt +dtwt +twt_opt diff --git a/Makefile b/Makefile index b0297753..5f71b0e1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ -all: build -build: - nim compile -d:ssl -o:twt main.nim +debug: + nim compile -d:ssl -o:dtwt main.nim release: nim compile -d:release -d:ssl -o:twt main.nim +release_opt: + nim compile -d:danger -d:ssl -o:twt_opt main.nim +all: debug release release_opt diff --git a/buffer.nim b/buffer.nim index cc0fafc9..df1f0612 100644 --- a/buffer.nim +++ b/buffer.nim @@ -1,13 +1,14 @@ import options import uri import tables +import strutils import fusion/htmlparser/xmltree -import fusion/htmlparser import termattrs import htmlelement import twtio +import enums type Buffer* = ref BufferObj @@ -29,19 +30,20 @@ type nodes*: seq[HtmlNode] links*: seq[HtmlNode] clickables*: seq[HtmlNode] - elements*: seq[HtmlNode] - idelements*: Table[string, HtmlNode] + elements*: seq[HtmlElement] + idelements*: Table[string, HtmlElement] selectedlink*: HtmlNode - location*: Uri printwrite*: bool attrs*: TermAttributes + document*: Document proc newBuffer*(attrs: TermAttributes): Buffer = return Buffer(lines: @[0], rawlines: @[0], width: attrs.termWidth, height: attrs.termHeight, - cursorY: 1) + cursorY: 1, + document: newDocument()) @@ -50,7 +52,7 @@ func lastLine*(buffer: Buffer): int = return buffer.lines.len - 1 func lastVisibleLine*(buffer: Buffer): int = - return min(buffer.fromY + buffer.height, buffer.lastLine() + 1) - 1 + return min(buffer.fromY + buffer.height - 2, buffer.lastLine()) #doesn't include newline func lineLength*(buffer: Buffer, line: int): int = @@ -85,7 +87,8 @@ 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] - 2) + result = buffer.text.substr(buffer.lines[buffer.fromY], buffer.lines[buffer.lastVisibleLine()]) + result.stripLineEnd() func lastNode*(buffer: Buffer): HtmlNode = return buffer.nodes[^1] @@ -98,15 +101,15 @@ func onSpace*(buffer: Buffer): bool = 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 + buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width func findSelectedElement*(buffer: Buffer): Option[HtmlElement] = - if buffer.selectedlink != nil: - return some(buffer.selectedlink.text.parent) + if buffer.selectedlink != nil and buffer.selectedLink.parentNode of HtmlElement: + return some(HtmlElement(buffer.selectedlink.parentNode)) for node in buffer.nodes: if node.isElemNode(): if node.getFmtLen() > 0: - if buffer.cursorOnNode(node): return some(node.element) + if buffer.cursorOnNode(node): return some(HtmlElement(node)) return none(HtmlElement) func cursorAt*(buffer: Buffer): int = @@ -118,7 +121,7 @@ func cursorChar*(buffer: Buffer): char = func canScroll*(buffer: Buffer): bool = return buffer.lastLine() > buffer.height -func getElementById*(buffer: Buffer, id: string): HtmlNode = +func getElementById*(buffer: Buffer, id: string): HtmlElement = if buffer.idelements.hasKey(id): return buffer.idelements[id] return nil @@ -133,24 +136,26 @@ proc findSelectedNode*(buffer: Buffer): Option[HtmlNode] = proc addNode*(buffer: Buffer, htmlNode: HtmlNode) = buffer.nodes.add(htmlNode) - if htmlNode.isTextNode() and htmlNode.text.parent.islink: + if htmlNode.isTextNode() and htmlNode.parentElement != nil and htmlNode.parentElement.islink: buffer.links.add(htmlNode) if htmlNode.isElemNode(): - case htmlNode.element.htmlTag - of tagInput, tagOption: - if not htmlNode.element.hidden: + case HtmlElement(htmlNode).tagType + of TAG_INPUT, TAG_OPTION: + if not HtmlElement(htmlNode).hidden: buffer.clickables.add(htmlNode) else: discard elif htmlNode.isTextNode(): - if htmlNode.text.parent.islink: - let anchor = htmlNode.text.parent.getParent(tagA) - buffer.clickables.add(anchor.node) + if htmlNode.parentElement != nil and htmlNode.parentElement.islink: + let anchor = htmlNode.getParent(TAG_A) + assert(anchor != nil) + buffer.clickables.add(anchor) if htmlNode.isElemNode(): - buffer.elements.add(htmlNode) - if htmlNode.element.id != "": - buffer.idelements[htmlNode.element.id] = htmlNode + let elem = HtmlElement(htmlNode) + buffer.elements.add(elem) + if elem.id != "" and not buffer.idelements.hasKey(elem.id): + buffer.idelements[elem.id] = elem proc writefmt*(buffer: Buffer, str: string) = buffer.text &= str @@ -279,7 +284,7 @@ proc cursorNextNode*(buffer: Buffer): bool = var res = buffer.cursorRight() if selectedNode.isNone: return res - while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get(): + while buffer.findSelectedNode().isNone or buffer.findSelectedNode().get() == selectedNode.get(): if buffer.cursorAtLineEnd(): return res res = buffer.cursorRight() @@ -314,7 +319,7 @@ proc cursorPrevNode*(buffer: Buffer): bool = var res = buffer.cursorLeft() if selectedNode.isNone: return res - while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get(): + while buffer.findSelectedNode().isNone or buffer.findSelectedNode().get() == selectedNode.get(): if buffer.cursorX == 0: return res res = res or buffer.cursorLeft() @@ -429,26 +434,26 @@ proc checkLinkSelection*(buffer: Buffer): bool = if buffer.cursorOnNode(buffer.selectedlink): return false else: - let anchor = buffer.selectedlink.text.parent.getParent(tagA) + let anchor = buffer.selectedlink.getParent(TAG_A) anchor.selected = false buffer.selectedlink = nil buffer.hovertext = "" for node in buffer.links: if buffer.cursorOnNode(node): buffer.selectedlink = node - let anchor = node.text.parent.getParent(tagA) + let anchor = node.getParent(TAG_A) assert(anchor != nil) anchor.selected = true - buffer.hovertext = anchor.href + buffer.hovertext = HtmlAnchorElement(anchor).href return true return false proc gotoAnchor*(buffer: Buffer): bool = - if buffer.location.anchor != "": - let node = buffer.getElementById(buffer.location.anchor) + if buffer.document.location.anchor != "": + let node = buffer.getElementById(buffer.document.location.anchor) if node != nil: return buffer.scrollTo(node.y) return false proc setLocation*(buffer: Buffer, uri: Uri) = - buffer.location = buffer.location.combine(uri) + buffer.document.location = buffer.document.location.combine(uri) diff --git a/config.nim b/config.nim index ca136b9e..df3d16d6 100644 --- a/config.nim +++ b/config.nim @@ -88,13 +88,46 @@ proc constructActionTable*(origTable: var Table[string, TwtAction]): Table[strin newTable[realk] = v return newTable -var keymapStr*: string macro staticReadKeymap(): untyped = var keymap = staticRead"keymap" + 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]) + + normalActionMap = constructActionTable(normalActionMap) + linedActionMap = constructActionTable(linedActionMap) + + let normalActionConstr = nnkTableConstr.newTree() + for k, v in normalActionMap: + let colonExpr = nnkExprColonExpr.newTree() + colonExpr.add(newLit(k)) + colonExpr.add(newLit(v)) + normalActionConstr.add(colonExpr) + + let normalActionAsgn = nnkAsgn.newTree() + normalActionAsgn.add(ident("normalActionRemap")) + normalActionAsgn.add(newCall(ident("toTable"), normalActionConstr)) + + let linedActionConstr = nnkTableConstr.newTree() + for k, v in linedActionMap: + let colonExpr = nnkExprColonExpr.newTree() + colonExpr.add(newLit(k)) + colonExpr.add(newLit(v)) + linedActionConstr.add(colonExpr) - let keymapLit = newLit(keymap) - result = quote do: - keymapStr = `keymapLit` + let linedActionAsgn = nnkAsgn.newTree() + linedActionAsgn.add(ident("linedActionRemap")) + linedActionAsgn.add(newCall(ident("toTable"), linedActionConstr)) + result = newStmtList() + result.add(normalActionAsgn) staticReadKeymap() diff --git a/display.nim b/display.nim index 898993dd..5ebf78f3 100644 --- a/display.nim +++ b/display.nim @@ -2,9 +2,9 @@ import terminal import options import uri import strutils +import unicode import fusion/htmlparser/xmltree -import fusion/htmlparser import buffer import termattrs @@ -12,6 +12,7 @@ import htmlelement import twtstr import twtio import config +import enums proc clearStatusMsg*(at: int) = setCursorPos(0, at) @@ -19,7 +20,7 @@ proc clearStatusMsg*(at: int) = proc statusMsg*(str: string, at: int) = clearStatusMsg(at) - print(str.addAnsiStyle(styleReverse)) + print(str.ansiStyle(styleReverse).ansiReset()) type RenderState = object @@ -63,26 +64,19 @@ proc addSpaces(buffer: Buffer, state: var RenderState, n: int) = state.atchar += n state.atrawchar += n -proc addSpace(buffer: Buffer, state: var RenderState) = - buffer.addSpaces(state, 1) - -proc addSpacePadding(buffer: Buffer, state: var RenderState) = - if not buffer.onSpace(): - buffer.addSpace(state) - -proc writeWrappedText(buffer: Buffer, state: var RenderState, fmttext: string, rawtext: string) = +proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) = state.lastwidth = 0 var n = 0 - var fmtword = "" - var rawword = "" + var fmtword: ustring = @[] + var rawword: ustring = @[] var prevl = false - for c in fmttext: - fmtword &= c + for r in node.fmttext: + fmtword &= r - if n >= rawtext.len or rawtext[n] != c: + if n >= node.rawtext.len or r != node.rawtext[n]: continue - rawword &= c + rawword &= r state.x += 1 if state.x > buffer.width: @@ -98,22 +92,22 @@ proc writeWrappedText(buffer: Buffer, state: var RenderState, fmttext: string, r else: state.lastwidth = max(state.lastwidth, state.x) - if c == ' ': - buffer.writefmt(fmtword) - buffer.writeraw(rawword) - state.atchar += fmtword.len - state.atrawchar += rawword.len + if r == runeSpace: + buffer.writefmt($fmtword) + buffer.writeraw($rawword) + state.atchar += ($fmtword).len + state.atrawchar += ($rawword).len if prevl: state.x += rawword.len prevl = false - fmtword = "" - rawword = "" + fmtword = @[] + rawword = @[] n += 1 - buffer.writefmt(fmtword) - buffer.writeraw(rawword) - state.atchar += fmtword.len - state.atrawchar += rawword.len + buffer.writefmt($fmtword) + buffer.writeraw($rawword) + state.atchar += ($fmtword).len + state.atrawchar += ($rawword).len state.lastwidth = max(state.lastwidth, state.x) proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = @@ -125,7 +119,6 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = while state.blanklines < max(elem.margin, elem.margintop): buffer.flushLine(state) if elem.display == DISPLAY_LIST_ITEM: - eprint "???" state.indent += 1 if not buffer.onNewLine() and state.blanklines == 0 and node.displayed(): @@ -140,11 +133,10 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = if elem.display == DISPLAY_LIST_ITEM and state.indent > 0: var listchar = "" - eprint "Display", listchar, elem.parent.htmlTag - case elem.parent.htmlTag - of tagUl: + case elem.parentElement.tagType + of TAG_UL: listchar = "*" - of tagOl: + of TAG_OL: state.listval += 1 listchar = $state.listval & ")" else: @@ -174,7 +166,7 @@ proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = if elem.display == DISPLAY_LIST_ITEM and node.isTextNode(): state.indent -= 1 - if elem.htmlTag == tagBr and not node.openblock: + if elem.tagType == TAG_BR and not node.openblock: buffer.flushLine(state) if elem.display == DISPLAY_LIST_ITEM and node.isElemNode(): @@ -184,16 +176,16 @@ proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = if not node.visibleNode(): return let elem = node.nodeAttr() - if elem.htmlTag == tagTitle: + if elem.tagType == TAG_TITLE: if node.isTextNode(): - buffer.title = node.rawtext + buffer.title = $node.rawtext return else: discard if elem.hidden: return if not state.docenter: if elem.centered: - if not node.closeblock and elem.htmlTag != tagBr: + if not node.closeblock and elem.tagType != TAG_BR: state.centerqueue += 1 return if state.centerqueue > 0: @@ -212,8 +204,11 @@ proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = node.x = state.x node.y = state.y - buffer.writeWrappedText(state, node.fmttext, node.rawtext) - node.width = state.lastwidth - node.x + 1 + buffer.writeWrappedText(state, node) + if state.x != node.x: + eprint node.x, node.y, state.x, state.y, node.nodeAttr().tagType + eprint "len", state.atrawchar + node.width = state.lastwidth - node.x node.height = state.y - node.y + 1 buffer.postAlignNode(node, state) @@ -240,7 +235,7 @@ proc setLastHtmlLine(buffer: Buffer, state: var RenderState) = proc renderHtml*(buffer: Buffer) = var stack: seq[XmlHtmlNode] let first = XmlHtmlNode(xml: buffer.htmlSource, - html: getHtmlNode(buffer.htmlSource, none(HtmlElement))) + html: getHtmlNode(buffer.htmlSource, buffer.document)) stack.add(first) var state = newRenderState() @@ -248,23 +243,20 @@ proc renderHtml*(buffer: Buffer) = let currElem = stack.pop() buffer.renderNode(currElem.html, state) buffer.addNode(currElem.html) - var last = false - var prev: HtmlNode = nil - for item in currElem.xml.revItems: - let child = XmlHtmlNode(xml: item, - html: getHtmlNode(item, some(currElem.html.element))) - stack.add(child) - child.html.prev = prev - prev = child.html - if not last and child.html.visibleNode(): - last = true - if currElem.html.element.display == DISPLAY_BLOCK: - stack[^1].html.closeblock = true - else: - child.html.next = stack[^1].html - if last: - if currElem.html.element.display == DISPLAY_BLOCK: - stack[^1].html.openblock = true + if currElem.xml.len > 0: + var last = false + for item in currElem.xml.revItems: + let child = XmlHtmlNode(xml: item, + html: getHtmlNode(item, currElem.html)) + stack.add(child) + currElem.html.childNodes.add(child.html) + if not last and child.html.visibleNode(): + last = true + if HtmlElement(currElem.html).display == DISPLAY_BLOCK: + stack[^1].html.closeblock = true + if last: + if HtmlElement(currElem.html).display == DISPLAY_BLOCK: + stack[^1].html.openblock = true buffer.setLastHtmlLine(state) proc drawHtml(buffer: Buffer) = @@ -338,20 +330,20 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool = of ACTION_CLICK: let selectedElem = buffer.findSelectedElement() if selectedElem.isSome: - case selectedElem.get().htmlTag - of tagInput: + case selectedElem.get().tagType + of TAG_INPUT: clearStatusMsg(buffer.height) - let status = readLine("TEXT:", selectedElem.get().value) + let status = readLine("TEXT:", HtmlInputElement(selectedElem.get()).value) if status: reshape = true redraw = true else: discard if selectedElem.get().islink: - let anchor = buffer.selectedlink.text.parent.getParent(tagA).href + let anchor = HtmlAnchorElement(buffer.selectedlink.getParent(TAG_A)).href buffer.setLocation(parseUri(anchor)) return true of ACTION_CHANGE_LOCATION: - var url = $buffer.location + var url = $buffer.document.location clearStatusMsg(buffer.height) let status = readLine("URL:", url) if status: @@ -374,8 +366,6 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool = buffer.statusMsgForBuffer() proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool = - eraseScreen() - termGoto(0, 0) #buffer.printwrite = true discard buffer.gotoAnchor() buffer.displayBuffer() diff --git a/enums.nim b/enums.nim new file mode 100644 index 00000000..e11059d0 --- /dev/null +++ b/enums.nim @@ -0,0 +1,66 @@ +type + NodeType* = + enum + NODE_UNKNOWN, NODE_ELEMENT, NODE_TEXT, NODE_COMMENT, NODE_CDATA, NODE_DOCUMENT + + DisplayType* = + enum + DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_LIST_ITEM, DISPLAY_NONE + + InputType* = + enum + INPUT_UNKNOWN, 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 + + WhitespaceType* = + enum + WHITESPACE_UNKNOWN, WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE, + WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP, WHITESPACE_INITIAL, + WHITESPACE_INHERIT + + TagType* = + enum + TAG_UNKNOWN, TAG_HTML, TAG_BASE, TAG_HEAD, TAG_LINK, TAG_META, TAG_STYLE, + TAG_TITLE, TAG_BODY, TAG_ADDRESS, TAG_ARTICLE, TAG_ASIDE, TAG_FOOTER, + TAG_HEADER, TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6, TAG_HGROUP, + TAG_MAIN, TAG_NAV, TAG_SECTION, TAG_BLOCKQUOTE, TAG_DD, TAG_DIV, TAG_DL, + TAG_DT, TAG_FIGCAPTION, TAG_FIGURE, TAG_HR, TAG_LI, TAG_OL, TAG_P, TAG_PRE, + TAG_UL, TAG_A, TAG_ABBR, TAG_B, TAG_BDI, TAG_BDO, TAG_BR, TAG_CITE, + TAG_CODE, TAG_DATA, TAG_DFN, TAG_EM, TAG_I, TAG_KBD, TAG_MARK, TAG_Q, + TAG_RB, TAG_RP, TAG_RT, TAG_RTC, TAG_RUBY, TAG_S, TAG_SAMP, TAG_SMALL, + TAG_SPAN, TAG_STRONG, TAG_SUB, TAG_SUP, TAG_TIME, TAG_U, TAG_VAR, TAG_WBR, + TAG_AREA, TAG_AUDIO, TAG_IMG, TAG_MAP, TAG_TRACK, TAG_VIDEO, TAG_EMBED, + TAG_IFRAME, TAG_OBJECT, TAG_PARAM, TAG_PICTURE, TAG_PORTAL, TAG_SOURCE, + TAG_CANVAS, TAG_NOSCRIPT, TAG_SCRIPT, TAG_DEL, TAG_INS, TAG_CAPTION, + TAG_COL, TAG_COLGROUP, TAG_TABLE, TAG_TBODY, TAG_TD, TAG_TFOOT, TAG_TH, + TAG_THEAD, TAG_TR, TAG_BUTTON, TAG_DATALIST, TAG_FIELDSET, TAG_FORM, + TAG_INPUT, TAG_LABEL, TAG_LEGEND, TAG_METER, TAG_OPTGROUP, TAG_OPTION, + TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT, TAG_TEXTAREA, TAG_DETAILS, + TAG_DIALOG, TAG_MENU, TAG_SUMMARY, TAG_BLINK, TAG_CENTER, TAG_COMMAND, + TAG_CONTENT, TAG_DIR, TAG_FONT, TAG_FRAME, TAG_NOFRAMES, TAG_FRAMESET, + TAG_STRIKE, TAG_TT + +const InlineTagTypes* = { + TAG_A, TAG_ABBR, TAG_B, TAG_BDO, TAG_BR, TAG_BUTTON, TAG_CITE, TAG_CODE, + TAG_DEL, TAG_DFN, TAG_EM, TAG_FONT, TAG_I, TAG_IMG, TAG_INS, TAG_INPUT, + TAG_IFRAME, TAG_KBD, TAG_LABEL, TAG_MAP, TAG_OBJECT, TAG_Q, TAG_SAMP, + TAG_SCRIPT, TAG_SELECT, TAG_SMALL, TAG_SPAN, TAG_STRONG, TAG_SUB, TAG_SUP, + TAG_TEXTAREA, TAG_TT, TAG_VAR, TAG_FONT, TAG_IFRAME, TAG_U, TAG_S, TAG_STRIKE, + TAG_WBR +} + +const BlockTagTypes* = { + TAG_ADDRESS, TAG_BLOCKQUOTE, TAG_CENTER, TAG_DEL, TAG_DIR, TAG_DIV, TAG_DL, + TAG_FIELDSET, TAG_FORM, TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6, + TAG_HR, TAG_INS, TAG_MENU, TAG_NOFRAMES, TAG_NOSCRIPT, TAG_OL, TAG_P, TAG_PRE, + TAG_TABLE, TAG_UL, TAG_CENTER, TAG_DIR, TAG_MENU, TAG_NOFRAMES +} + +const SingleTagTypes* = { + TAG_AREA, TAG_BASE, TAG_BR, TAG_COL, TAG_EMBED, TAG_FRAME, TAG_HR, TAG_IMG, + TAG_INPUT, TAG_SOURCE, TAG_TRACK, TAG_LINK, TAG_META, TAG_PARAM, TAG_WBR, + TAG_COMMAND +} diff --git a/htmlelement.nim b/htmlelement.nim index 35430f1e..c37b294e 100644 --- a/htmlelement.nim +++ b/htmlelement.nim @@ -1,49 +1,51 @@ import strutils -import re import terminal -import options +import uri +import unicode import fusion/htmlparser import fusion/htmlparser/xmltree import twtstr import twtio +import enums +import macros type - NodeType* = - enum - NODE_ELEMENT, NODE_TEXT, NODE_COMMENT - DisplayType* = - enum - DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_LIST_ITEM, 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 + HtmlNode* = ref HtmlNodeObj + HtmlNodeObj = object of RootObj + nodeType*: NodeType + childNodes*: seq[HtmlNode] + firstChild*: HtmlNode + isConnected*: bool + lastChild*: HtmlNode + nextSibling*: HtmlNode + previousSibling*: HtmlNode + parentNode*: HtmlNode + parentElement*: HtmlElement + + rawtext*: ustring + fmttext*: ustring + x*: int + y*: int + width*: int + height*: int + openblock*: bool + closeblock*: bool + hidden*: bool + + Document* = ref DocumentObj + DocumentObj = object of HtmlNodeObj + location*: Uri -type - HtmlText* = ref HtmlTextObj - HtmlTextObj = object - parent*: HtmlElement HtmlElement* = ref HtmlElementObj - HtmlElementObj = object - node*: HtmlNode + HtmlElementObj = object of HtmlNodeObj id*: string + tagType*: TagType name*: string - value*: string centered*: bool - hidden*: bool display*: DisplayType innerText*: string - textNodes*: int margintop*: int marginbottom*: int marginleft*: int @@ -53,40 +55,55 @@ type italic*: bool underscore*: bool islink*: bool - parent*: HtmlElement - case htmlTag*: HtmlTag - of tagInput: - itype*: InputType - size*: int - of tagA: - href*: string - selected*: bool - else: - discard - HtmlNode* = ref HtmlNodeObj - HtmlNodeObj = object - case nodeType*: NodeType - of NODE_ELEMENT: - element*: HtmlElement - of NODE_TEXT: - text*: HtmlText - of NODE_COMMENT: - comment*: string - rawtext*: string - fmttext*: string - x*: int - y*: int - width*: int - height*: int - openblock*: bool - closeblock*: bool - next*: HtmlNode - prev*: HtmlNode + selected*: bool + + HtmlInputElement* = ref HtmlInputElementObj + HtmlInputElementObj = object of HtmlElementObj + itype*: InputType + autofocus*: bool + required*: bool + value*: string + size*: int + + HtmlAnchorElement* = ref HtmlAnchorElementObj + HtmlAnchorElementObj = object of HtmlElementObj + href*: string + + HtmlSelectElement* = ref HtmlSelectElementObj + HtmlSelectElementObj = object of HtmlElementObj + value*: string + + HtmlOptionElement* = ref HtmlOptionElementObj + HtmlOptionElementObj = object of HtmlElementObj + value*: string + +#no I won't manually write all this down +#maybe todo to accept stuff other than tagtype (idk how useful that'd be) +macro genEnumCase(s: string): untyped = + let casestmt = nnkCaseStmt.newTree() + casestmt.add(ident("s")) + for i in low(TagType) .. high(TagType): + let ret = nnkReturnStmt.newTree() + ret.add(newLit(TagType(i))) + let branch = nnkOfBranch.newTree() + let enumname = $TagType(i) + let tagname = enumname.substr("TAG_".len, enumname.len - 1).tolower() + branch.add(newLit(tagname)) + branch.add(ret) + casestmt.add(branch) + let ret = nnkReturnStmt.newTree() + ret.add(newLit(TAG_UNKNOWN)) + let branch = nnkElse.newTree() + branch.add(ret) + casestmt.add(branch) + +func tagType*(s: string): TagType = + genEnumCase(s) func nodeAttr*(node: HtmlNode): HtmlElement = case node.nodeType - of NODE_TEXT: return node.text.parent - of NODE_ELEMENT: return node.element + of NODE_TEXT: return node.parentElement + of NODE_ELEMENT: return HtmlElement(node) else: assert(false) func displayed*(node: HtmlNode): bool = @@ -98,6 +115,15 @@ func isTextNode*(node: HtmlNode): bool = func isElemNode*(node: HtmlNode): bool = return node.nodeType == NODE_ELEMENT +func isComment*(node: HtmlNode): bool = + return node.nodeType == NODE_COMMENT + +func isCData*(node: HtmlNode): bool = + return node.nodeType == NODE_CDATA + +func isDocument*(node: HtmlNode): bool = + return node.nodeType == NODE_DOCUMENT + func getFmtLen*(htmlNode: HtmlNode): int = return htmlNode.fmttext.len @@ -139,195 +165,197 @@ func toInputType*(str: string): InputType = func toInputSize*(str: string): int = if str.len == 0: return 20 + for c in str: + if not c.isDigit: + return 20 return str.parseInt() -func getInputElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement = - assert(htmlElement.htmlTag == tagInput) - htmlElement.itype = xmlElement.attr("type").toInputType() - if htmlElement.itype == INPUT_HIDDEN: - htmlElement.hidden = true - htmlElement.size = xmlElement.attr("size").toInputSize() - htmlElement.value = xmlElement.attr("value") - return htmlElement - -func getAnchorElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement = - assert(htmlElement.htmlTag == tagA) - htmlElement.href = xmlElement.attr("href") - htmlElement.islink = true - 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": - 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.parent.value != htmlElement.value: - htmlElement.hidden = true - return htmlElement - -func getFormattedInput(htmlElement: HtmlElement): string = - case htmlElement.itype +func getFmtInput(inputElement: HtmlInputElement): ustring = + case inputElement.itype of INPUT_TEXT, INPUT_SEARCH: - let valueFit = fitValueToSize(htmlElement.value, htmlElement.size) - return valueFit.addAnsiStyle(styleUnderscore).buttonStr() + let valueFit = fitValueToSize(inputElement.value.toRunes(), inputElement.size) + return valueFit.ansiStyle(styleUnderscore).ansiReset().buttonFmt() of INPUT_SUBMIT: - return htmlElement.value.buttonStr() + return inputElement.value.toRunes().buttonFmt() else: discard -func getRawInput(htmlElement: HtmlElement): string = - case htmlElement.itype +func getRawInput(inputElement: HtmlInputElement): ustring = + case inputElement.itype of INPUT_TEXT, INPUT_SEARCH: - return "[" & htmlElement.value.fitValueToSize(htmlElement.size) & "]" + return inputElement.value.toRunes().fitValueToSize(inputElement.size).buttonFmt() of INPUT_SUBMIT: - return "[" & htmlElement.value & "]" + return inputElement.value.toRunes().buttonFmt() else: discard -func getParent*(htmlElement: HtmlElement, htmlTag: HtmlTag): HtmlElement = - result = htmlElement - while result != nil and result.htmlTag != htmlTag: - result = result.parent +func getParent*(htmlNode: HtmlNode, tagType: TagType): HtmlElement = + var pnode = htmlNode.parentElement + while pnode != nil and pnode.tagType != tagType: + pnode = pnode.parentElement + + return pnode -func getRawText*(htmlNode: HtmlNode): string = +proc getRawText*(htmlNode: HtmlNode): ustring = if htmlNode.isElemNode(): - case htmlNode.element.htmlTag - of tagInput: return htmlNode.element.getRawInput() - else: return "" + case HtmlElement(htmlNode).tagType + of TAG_INPUT: return HtmlInputElement(htmlNode).getRawInput() + else: return @[] elif htmlNode.isTextNode(): - if htmlNode.text.parent.htmlTag != tagPre: - result = htmlNode.rawtext.replace(re"\n") - if result.strip().len > 0: + if htmlNode.parentElement != nil and htmlNode.parentElement.tagType != TAG_PRE: + result = htmlNode.rawtext.remove(runeNewline) + if unicode.strip($result).toRunes().len > 0: if htmlNode.nodeAttr().display != DISPLAY_INLINE: - if htmlNode.prev == nil or htmlNode.prev.nodeAttr().display != DISPLAY_INLINE: - result = result.strip(true, false) - if htmlNode.next == nil or htmlNode.next.nodeAttr().display != DISPLAY_INLINE: - result = result.strip(false, true) + if htmlNode.previousSibling == nil or htmlNode.previousSibling.nodeAttr().display != DISPLAY_INLINE: + result = unicode.strip($result, true, false).toRunes() + if htmlNode.nextSibling == nil or htmlNode.nextSibling.nodeAttr().display != DISPLAY_INLINE: + result = unicode.strip($result, false, true).toRunes() else: - result = "" + result = @[] else: - result = htmlNode.rawtext.strip() - if htmlNode.text.parent.htmlTag == tagOption: - result = "[" & result & "]" + result = unicode.strip($htmlNode.rawtext).toRunes() + if htmlNode.parentElement != nil and htmlNode.parentElement.tagType == TAG_OPTION: + result = result.buttonRaw() else: assert(false) -func getFmtText*(htmlNode: HtmlNode): string = +func getFmtText*(htmlNode: HtmlNode): ustring = if htmlNode.isElemNode(): - case htmlNode.element.htmlTag - of tagInput: return htmlNode.element.getFormattedInput() - else: return "" + case HtmlElement(htmlNode).tagType + of TAG_INPUT: return HtmlInputElement(htmlNode).getFmtInput() + else: return @[] elif htmlNode.isTextNode(): result = htmlNode.rawtext - if htmlNode.text.parent.islink: - result = result.addAnsiFgColor(fgBlue) - let parent = htmlNode.text.parent.getParent(tagA) + if htmlNode.parentElement != nil and htmlNode.parentElement.islink: + result = result.ansiFgColor(fgBlue) + let parent = HtmlElement(htmlNode.parentNode).getParent(TAG_A) if parent != nil and parent.selected: - result = result.addAnsiStyle(styleUnderscore) - - if htmlNode.text.parent.htmlTag == tagOption: - result = result.addAnsiFgColor(fgRed) - - if htmlNode.text.parent.bold: - result = result.addAnsiStyle(styleBright) - if htmlNode.text.parent.italic: - result = result.addAnsiStyle(styleItalic) - if htmlNode.text.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.parent = parent - elem.islink = parent.islink - - 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 - elif htmlElement.htmlTag in SingleTags: - htmlElement.display = DISPLAY_SINGLE - elif htmlElement.htmlTag == tagLi: - htmlElement.display = DISPLAY_LIST_ITEM + result = result.ansiStyle(styleUnderscore).ansiReset() + + if HtmlElement(htmlNode.parentNode).tagType == TAG_OPTION: + result = result.ansiFgColor(fgRed).ansiReset() + + if HtmlElement(htmlNode.parentNode).bold: + result = result.ansiStyle(styleBright).ansiReset() + if HtmlElement(htmlNode.parentNode).italic: + result = result.ansiStyle(styleItalic).ansiReset() + if HtmlElement(htmlNode.parentNode).underscore: + result = result.ansiStyle(styleUnderscore).ansiReset() 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 + assert(false) + +proc getHtmlElement*(xmlElement: XmlNode, parentNode: HtmlNode): HtmlElement = + assert kind(xmlElement) == xnElement + let tagType = xmlElement.tag().tagType() + + case tagType + of TAG_INPUT: result = new(HtmlInputElement) + of TAG_A: result = new(HtmlAnchorElement) + else: new(result) + + result.tagType = tagType + result.parentNode = parentNode + if parentNode.isElemNode(): + result.parentElement = HtmlElement(parentNode) + + result.id = xmlElement.attr("id") + + if tagType in InlineTagTypes: + result.display = DISPLAY_INLINE + elif tagType in BlockTagTypes: + result.display = DISPLAY_BLOCK + elif tagType in SingleTagTypes: + result.display = DISPLAY_SINGLE + elif tagType == TAG_LI: + result.display = DISPLAY_LIST_ITEM else: - discard + result.display = DISPLAY_NONE + + case tagType + of TAG_CENTER: + result.centered = true + of TAG_B: + result.bold = true + of TAG_I: + result.italic = true + of TAG_U: + result.underscore = true + of TAG_HEAD: + result.hidden = true + of TAG_STYLE: + result.hidden = true + of TAG_SCRIPT: + result.hidden = true + of TAG_INPUT: + let inputElement = HtmlInputElement(result) + inputElement.itype = xmlElement.attr("type").toInputType() + if inputElement.itype == INPUT_HIDDEN: + inputElement.hidden = true + inputElement.size = xmlElement.attr("size").toInputSize() + inputElement.value = xmlElement.attr("value") + result = inputElement + of TAG_A: + let anchorElement = HtmlAnchorElement(result) + anchorElement.href = xmlElement.attr("href") + anchorElement.islink = true + result = anchorElement + of TAG_SELECT: + var selectElement = new(HtmlSelectElement) + for item in xmlElement.items: + if item.kind == xnElement: + if item.tag == "option": + selectElement.value = item.attr("value") + break + selectElement.name = xmlElement.attr("name") + result = selectElement + of TAG_OPTION: + var optionElement = new(HtmlOptionElement) + optionElement.value = xmlElement.attr("value") + if parentNode.isElemNode() and HtmlSelectElement(parentNode).value != optionElement.value: + optionElement.hidden = true + result = optionElement + of TAG_PRE, TAG_TD, TAG_TH: + result.margin = 1 + else: discard - for child in xmlElement.items: - if child.kind == xnText and child.text.strip().len > 0: - htmlElement.textNodes += 1 + if parentNode.isElemNode(): + let parent = HtmlElement(parentNode) + result.centered = result.centered or parent.centered + result.bold = result.bold or parent.bold + result.italic = result.italic or parent.italic + result.underscore = result.underscore or parent.underscore + result.hidden = result.hidden or parent.hidden + result.islink = result.islink or parent.islink - return htmlElement - -proc getHtmlText*(parent: HtmlElement): HtmlText = - return HtmlText(parent: parent) -proc getHtmlNode*(xmlElement: XmlNode, parent: Option[HtmlElement]): HtmlNode = +proc getHtmlNode*(xmlElement: XmlNode, parent: HtmlNode): HtmlNode = case kind(xmlElement) of xnElement: - result = HtmlNode(nodeType: NODE_ELEMENT, element: getHtmlElement(xmlElement, parent)) - result.element.node = result + result = getHtmlElement(xmlElement, parent) + result.nodeType = NODE_ELEMENT of xnText: - assert(parent.isSome) - result = HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(parent.get())) - result.rawtext = xmlElement.text + new(result) + result.nodeType = NODE_TEXT + result.rawtext = xmlElement.text.toRunes() of xnComment: - result = HtmlNode(nodeType: NODE_COMMENT, comment: xmlElement.text) + new(result) + result.nodeType = NODE_COMMENT + result.rawtext = xmlElement.text.toRunes() of xnCData: - result = HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(parent.get())) - result.rawtext = xmlElement.text + new(result) + result.nodeType = NODE_CDATA + result.rawtext = xmlElement.text.toRunes() else: assert(false) + + result.parentNode = parent + if parent.isElemNode(): + result.parentElement = HtmlElement(parent) + result.rawtext = result.getRawText() result.fmttext = result.getFmtText() + if parent.childNodes.len > 0: + result.previousSibling = parent.childNodes[^1] + result.previousSibling.nextSibling = result + parent.childNodes.add(result) + +func newDocument*(): Document = + new(result) + result.nodeType = NODE_DOCUMENT diff --git a/keymap b/keymap index 218cabd3..b63600f7 100644 --- a/keymap +++ b/keymap @@ -27,6 +27,8 @@ 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 J ACTION_SCROLL_DOWN +nmap K ACTION_SCROLL_UP nmap C-m ACTION_CLICK nmap C-j ACTION_CLICK nmap C-l ACTION_CHANGE_LOCATION diff --git a/main.nim b/main.nim index b0212511..c98f0db5 100644 --- a/main.nim +++ b/main.nim @@ -35,7 +35,6 @@ proc main*() = quit(1) if not readKeymap("keymap"): eprint "Failed to read keymap, falling back to default" - parseKeymap(keymapStr) let attrs = getTermAttributes() let buffer = newBuffer(attrs) let uri = parseUri(paramStr(1)) @@ -46,12 +45,12 @@ proc main*() = var lastUri = uri while displayPage(attrs, buffer): statusMsg("Loading...", buffer.height) - var newUri = buffer.location + var newUri = buffer.document.location lastUri.anchor = "" newUri.anchor = "" if $lastUri != $newUri: buffer.clearBuffer() - buffer.htmlSource = loadPageUri(buffer.location, buffer.htmlSource) + buffer.htmlSource = loadPageUri(buffer.document.location, buffer.htmlSource) buffer.renderHtml() lastUri = newUri diff --git a/parser.nim b/parser.nim new file mode 100644 index 00000000..591a43e6 --- /dev/null +++ b/parser.nim @@ -0,0 +1,17 @@ +import parsexml +import htmlelement +import streams + +func parseNextNode(str: string) = + return + +var s = "" +proc parseHtml*(inputStream: Stream) = + var x: XmlParser + x.open(inputStream, "") + while true: + x.next() + case x.kind + of xmlElementStart: discard + of xmlEof: break + else: discard diff --git a/readme.md b/readme.md index 62c97ce6..fe58b84d 100644 --- a/readme.md +++ b/readme.md @@ -1,27 +1,35 @@ # twt - a web browser in your terminal ## What is this? -A terminal web browser. It displays websites in your terminal. +A terminal web browser. It displays websites in your terminal and allows you to navigate on them. ## 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. +I've found other terminal web browsers insufficient for my needs, so I thought it'd be a fun excercise to write one myself. +I don't really want a standard-compliant browser, or one that displays pages perfectly - the only way you could do that in a terminal is to work like browsh, which kinda defeats the point of a terminal web browser. I want one that is good enough for daily use - something like lynx or w3m, but better. +So the aim is to implement HTML rendering, some degree of JS support, and a very limited subset of CSS. Plus some other things I'd add to w3m if it weren't 50k lines of incomprehensible ancient C code. ## So what can this do? Currently implemented features are: -* basic html rendering (WIP) + +* basic html rendering (very much WIP) * custom keybindings -Planned features: -* image (sixel/kitty) -* video (sixel/kitty) -* audio +Planned features (roughly in order of importance): + +* improved html rendering and parsing +* form * table * cookie -* form -* JavaScript * SOCKS proxy +* HTTP proxy +* image (sixel/kitty) +* audio +* JavaScript * extension API (adblock support?) -* markdown? (with pandoc?) +* video (sixel/kitty) +* custom charsets? +* async? +* markdown? (with pandoc or built-in parser?) * gopher? * gemini? diff --git a/twtio.nim b/twtio.nim index 4d2f1083..4c02cfb0 100644 --- a/twtio.nim +++ b/twtio.nim @@ -65,7 +65,7 @@ proc readLine*(prompt: string, current: var string): bool = print('\b'.repeat(new.len - cursor)) of ACTION_LINED_ESC: new &= c - print("^[".addAnsiFgColor(fgBlue).addAnsiStyle(styleBright)) + print("^[".ansiFgColor(fgBlue).ansiStyle(styleBright).ansiReset()) of ACTION_LINED_CLEAR: print(' '.repeat(new.len - cursor + 1)) print('\b'.repeat(new.len - cursor + 1)) diff --git a/twtstr.nim b/twtstr.nim index 983d58d8..50f0e4da 100644 --- a/twtstr.nim +++ b/twtstr.nim @@ -1,21 +1,60 @@ import terminal import strutils +import unicode -func addAnsiStyle*(str: string, style: Style): string = - return ansiStyleCode(style) & str & "\e[0m" +type ustring* = seq[Rune] -func addAnsiFgColor*(str: string, color: ForegroundColor): string = - return ansiForegroundColorCode(color) & str & ansiResetCode +const runeSpace*: Rune = " ".toRunes()[0] +const runeNewline*: Rune = "\n".toRunes()[0] +const runeReturn*: Rune = "\r".toRunes()[0] + +func isWhitespace(r: Rune): bool = + case r + of runeSpace, runeNewline, runeReturn: return true + else: return false + +func ansiStyle*(str: string, style: Style): string = + return ansiStyleCode(style) & str + +func ansiFgColor*(str: string, color: ForegroundColor): string = + return ansiForegroundColorCode(color) & str + +func ansiReset*(str: string): string = + return str & ansiResetCode + +func ansiStyle*(str: ustring, style: Style): ustring = + return ansiStyleCode(style).toRunes() & str + +func ansiFgColor*(str: ustring, color: ForegroundColor): ustring = + return ansiForegroundColorCode(color).toRunes & str + +func ansiReset*(str: ustring): ustring = + return str & ansiResetCode.toRunes() + +func maxString*(str: ustring, max: int): ustring = + result = str + if max < str.len: + result.setLen(max - 2) + result &= "$".toRunes() func maxString*(str: string, max: int): string = + result = str if max < str.len: - return str.substr(0, max - 2) & "$" - return str + result.setLen(max - 1) + result[max - 2] = '$' -func fitValueToSize*(str: string, size: int): string = +func fitValueToSize*(str: ustring, size: int): ustring = if str.len < size: - return str & ' '.repeat(size - str.len) + return str & ' '.repeat(size - str.len).toRunes() return str.maxString(size) -func buttonStr*(str: string): string = - return "[".addAnsiFgColor(fgRed) & str.addAnsiFgColor(fgRed) & "]".addAnsiFgColor(fgRed) +func buttonFmt*(str: ustring): ustring = + return "[".toRunes().ansiFgColor(fgRed).ansiReset() & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).toRunes().ansiReset() + +func buttonRaw*(str: ustring): ustring = + return "[".toRunes() & str & "]".toRunes() + +func remove*(s: ustring, r: Rune): ustring = + for c in s: + if c != r: + result.add(c) |