diff options
author | bptato <nincsnevem662@gmail.com> | 2022-11-15 23:42:20 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-11-19 14:32:54 +0100 |
commit | a6bbcd0dd3f77b0e98527c1fa9e510a40acd954e (patch) | |
tree | ca55cc9079afbe788a61986e42d1a8d9d0bc7c2f | |
parent | e75f62b34f7c7f3127bcde0c4a12cbb785342dd9 (diff) | |
download | chawan-a6bbcd0dd3f77b0e98527c1fa9e510a40acd954e.tar.gz |
Rewrite buffer/pager for multi-processing
-rw-r--r-- | res/config.toml | 41 | ||||
-rw-r--r-- | src/bindings/quickjs.nim | 2 | ||||
-rw-r--r-- | src/buffer/buffer.nim | 834 | ||||
-rw-r--r-- | src/buffer/cell.nim (renamed from src/io/cell.nim) | 46 | ||||
-rw-r--r-- | src/buffer/container.nim | 626 | ||||
-rw-r--r-- | src/config/config.nim | 73 | ||||
-rw-r--r-- | src/display/client.nim | 153 | ||||
-rw-r--r-- | src/display/pager.nim | 796 | ||||
-rw-r--r-- | src/io/buffer.nim | 1285 | ||||
-rw-r--r-- | src/io/lineedit.nim | 396 | ||||
-rw-r--r-- | src/io/loader.nim | 7 | ||||
-rw-r--r-- | src/io/serialize.nim | 79 | ||||
-rw-r--r-- | src/io/term.nim | 1 | ||||
-rw-r--r-- | src/js/javascript.nim | 94 | ||||
-rw-r--r-- | src/js/regex.nim | 25 | ||||
-rw-r--r-- | src/main.nim | 7 | ||||
-rw-r--r-- | src/render/renderdocument.nim | 2 | ||||
-rw-r--r-- | src/render/rendertext.nim | 2 | ||||
-rw-r--r-- | src/types/url.nim | 8 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 20 |
20 files changed, 2514 insertions, 1983 deletions
diff --git a/res/config.toml b/res/config.toml index fb43d065..7d69d8dc 100644 --- a/res/config.toml +++ b/res/config.toml @@ -58,25 +58,30 @@ D = 'pager.discardBuffer()' '.' = 'pager.nextBuffer()' M-c = 'commandMode()' C = 'command()' -'/' = 'pager.isearch()' -'?' = 'pager.isearchBack()' +'/' = 'pager.isearchForward()' +'?' = 'pager.isearchBackward()' n = 'pager.searchNext()' N = 'pager.searchPrev()' [line] -C-m = 'SUBMIT' -C-j = 'SUBMIT' -C-h = 'BACKSPACE' -'C-?' = 'BACKSPACE' -C-d = 'DELETE' -C-c = 'CANCEL' -M-b = 'PREV_WORD' -M-f = 'NEXT_WORD' -C-b = 'BACK' -C-f = 'FORWARD' -C-u = 'CLEAR' -C-k = 'KILL' -C-w = 'KILL_WORD' -C-a = 'BEGIN' -C-e = 'END' -C-v = 'ESC' +C-m = 'line.submit()' +C-j = 'line.submit()' +C-h = 'line.backspace()' +'C-?' = 'line.backspace()' +C-d = 'line.delete()' +C-c = 'line.cancel()' +M-b = 'line.prevWord()' +M-f = 'line.nextWord()' +C-b = 'line.backward()' +C-f = 'line.forward()' +C-u = 'line.clear()' +C-_ = 'line.clear()' +M-k = 'line.clear()' +C-k = 'line.kill()' +C-w = 'line.clearWord()' +M-C-h = 'line.clearWord()' +'M-C-?' = 'line.clearWord()' +M-d = 'line.killWord()' +C-a = 'line.begin()' +C-e = 'line.end()' +C-v = 'line.escape()' diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim index 290cbeca..6decbf31 100644 --- a/src/bindings/quickjs.nim +++ b/src/bindings/quickjs.nim @@ -1,6 +1,6 @@ import os -const javascriptDirs = ["/usr", "/lib", "/usr/lib", "/usr/local/lib", "/usr/local"] +const javascriptDirs = ["/usr/local/lib", "/usr/local", "/usr/lib", "/usr", "/lib"] const lib = (func(): string = when defined(posix): for dir in javascriptDirs: diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim new file mode 100644 index 00000000..6c2654dd --- /dev/null +++ b/src/buffer/buffer.nim @@ -0,0 +1,834 @@ +import macros +import options +import os +import streams +import tables +import unicode + +when defined(posix): + import posix + +import buffer/cell +import css/cascade +import css/cssparser +import css/mediaquery +import css/sheet +import css/stylednode +import config/config +import html/dom +import html/tags +import html/htmlparser +import io/loader +import io/process +import io/request +import io/serialize +import io/socketstream +import io/term +import js/regex +import layout/box +import render/renderdocument +import render/rendertext +import types/color +import types/url +import utils/twtstr + +type + BufferCommand* = enum + LOAD, RENDER, DRAW_BUFFER, WINDOW_CHANGE, GOTO_ANCHOR, READ_SUCCESS, + READ_CANCELED, CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, + FIND_PREV_MATCH, GET_SOURCE, GET_LINES, MOVE_CURSOR + + ContainerCommand* = enum + SET_LINES, SET_NEEDS_AUTH, SET_CONTENT_TYPE, SET_REDIRECT, SET_TITLE, + SET_HOVER, READ_LINE, LOAD_DONE, ANCHOR_FOUND, ANCHOR_FAIL, JUMP, OPEN, + SOURCE_READY, RESHAPE + + BufferSourceType* = enum + CLONE, LOAD_REQUEST, LOAD_PIPE + + BufferSource* = object + location*: URL + contenttype*: Option[string] # override + case t*: BufferSourceType + of CLONE: + clonepid*: Pid + of LOAD_REQUEST: + request*: Request + of LOAD_PIPE: + fd*: FileHandle + + BufferMatch* = object + success*: bool + x*: int + y*: int + str*: string + + Buffer* = ref object + input: HTMLInputElement + contenttype: string + lines: FlexibleGrid + rendered: bool + bsource: BufferSource + width: int + height: int + attrs: TermAttributes + document: Document + viewport: Viewport + prevstyled: StyledNode + reshape: bool + nostatus: bool + location: Url + istream: Stream + pistream: Stream # for input pipe + postream: Stream # for output pipe + streamclosed: bool + source: string + prevnode: StyledNode + userstyle: CSSStylesheet + loader: FileLoader + config: Config + +macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) = + result = newStmtList() + result.add(quote do: `buffer`.postream.swrite(`cmd`)) + for arg in args: + result.add(quote do: `buffer`.postream.swrite(`arg`)) + result.add(quote do: `buffer`.postream.flush()) + +func getLink(node: StyledNode): HTMLAnchorElement = + if node == nil: + return nil + if node.t == STYLED_ELEMENT and node.node != nil and Element(node.node).tagType == TAG_A: + return HTMLAnchorElement(node.node) + if node.node != nil: + return HTMLAnchorElement(node.node.findAncestor({TAG_A})) + #TODO ::before links? + +const ClickableElements = { + TAG_A, TAG_INPUT, TAG_OPTION +} + +func getClickable(styledNode: StyledNode): Element = + if styledNode == nil or styledNode.node == nil: + return nil + if styledNode.t == STYLED_ELEMENT: + let element = Element(styledNode.node) + if element.tagType in ClickableElements: + return element + styledNode.node.findAncestor(ClickableElements) + +func getCursorClickable(buffer: Buffer, cursorx, cursory: int): Element = + let i = buffer.lines[cursory].findFormatN(cursorx) - 1 + if i >= 0: + return buffer.lines[cursory].formats[i].node.getClickable() + +func cursorBytes(buffer: Buffer, y: int, cc: int): int = + let line = buffer.lines[y].str + var w = 0 + var i = 0 + while i < line.len and w < cc: + var r: Rune + fastRuneAt(line, i, r) + w += r.width() + return i + +func findNextLink(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] = + let line = buffer.lines[cursory] + var i = line.findFormatN(cursorx) - 1 + var link: Element = nil + if i >= 0: + link = line.formats[i].node.getClickable() + inc i + + while i < line.formats.len: + let format = line.formats[i] + let fl = format.node.getClickable() + if fl != nil and fl != link: + return (format.pos, cursory) + inc i + + for y in (cursory + 1)..(buffer.lines.len - 1): + let line = buffer.lines[y] + i = 0 + while i < line.formats.len: + let format = line.formats[i] + let fl = format.node.getClickable() + if fl != nil and fl != link: + return (format.pos, y) + inc i + return (-1, -1) + +func findPrevLink(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] = + let line = buffer.lines[cursory] + var i = line.findFormatN(cursorx) - 1 + var link: Element = nil + if i >= 0: + link = line.formats[i].node.getClickable() + dec i + + var ly = 0 #last y + var lx = 0 #last x + template link_beginning() = + #go to beginning of link + ly = y #last y + lx = format.pos #last x + + #on the current line + let line = buffer.lines[y] + while i >= 0: + let format = line.formats[i] + let nl = format.node.getClickable() + if nl == fl: + lx = format.pos + dec i + + #on previous lines + for iy in countdown(ly - 1, 0): + let line = buffer.lines[iy] + i = line.formats.len - 1 + while i >= 0: + let format = line.formats[i] + let nl = format.node.getClickable() + if nl == fl: + ly = iy + lx = format.pos + dec i + + while i >= 0: + let format = line.formats[i] + let fl = format.node.getClickable() + if fl != nil and fl != link: + let y = cursory + link_beginning + return (lx, ly) + dec i + + for y in countdown(cursory - 1, 0): + let line = buffer.lines[y] + i = line.formats.len - 1 + while i >= 0: + let format = line.formats[i] + let fl = format.node.getClickable() + if fl != nil and fl != link: + link_beginning + return (lx, ly) + dec i + return (-1, -1) + +proc findNextMatch(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch = + template return_if_match = + if res.success and res.captures.len > 0: + let cap = res.captures[0] + let x = buffer.lines[y].str.width(cap.s) + let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) + return BufferMatch(success: true, x: x, y: y, str: str) + var y = cursory + let b = buffer.cursorBytes(y, cursorx) + let b2 = if buffer.lines[y].str.len > b: b + buffer.lines[y].str.runeLenAt(b) else: b + let res = regex.exec(buffer.lines[y].str, b2, buffer.lines[y].str.len) + return_if_match + inc y + while true: + if y > buffer.lines.high: + if wrap: + y = 0 + else: + break + if y == cursory: + let res = regex.exec(buffer.lines[y].str, 0, b) + return_if_match + break + let res = regex.exec(buffer.lines[y].str) + return_if_match + inc y + +proc findPrevMatch(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch = + template return_if_match = + if res.success and res.captures.len > 0: + let cap = res.captures[^1] + let x = buffer.lines[y].str.width(cap.s) + let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) + return BufferMatch(success: true, x: x, y: y, str: str) + var y = cursory + let b = buffer.cursorBytes(y, cursorx) + let b2 = if b > 0: b - buffer.lines[y].str.lastRune(b)[1] else: 0 + let res = regex.exec(buffer.lines[y].str, 0, b2) + return_if_match + dec y + while true: + if y < 0: + if wrap: + y = buffer.lines.high + else: + break + if y == cursory: + let res = regex.exec(buffer.lines[y].str, b, buffer.lines[y].str.len) + return_if_match + break + let res = regex.exec(buffer.lines[y].str) + return_if_match + dec y + +proc gotoAnchor(buffer: Buffer) = + if buffer.document == nil: return + let anchor = buffer.document.getElementById(buffer.location.anchor) + if anchor == nil: return + for y in 0..<buffer.lines.len: + let line = buffer.lines[y] + var i = 0 + while i < line.formats.len: + let format = line.formats[i] + if format.node != nil and anchor in format.node.node: + buffer.writeCommand(JUMP, format.pos, y) + return + inc i + +proc windowChange(buffer: Buffer) = + buffer.width = buffer.attrs.width - 1 + buffer.height = buffer.attrs.height - 1 + buffer.reshape = true + +proc updateHover(buffer: Buffer, cursorx, cursory: int) = + var thisnode: StyledNode + let i = buffer.lines[cursory].findFormatN(cursorx) - 1 + if i >= 0: + thisnode = buffer.lines[cursory].formats[i].node + let prevnode = buffer.prevnode + + if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node): + for styledNode in thisnode.branch: + if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: + let elem = Element(styledNode.node) + if not elem.hover: + elem.hover = true + buffer.reshape = true + + let link = thisnode.getLink() + if link != nil: + buffer.writeCommand(SET_HOVER, link.href) + else: + buffer.writeCommand(SET_HOVER, "") + + for styledNode in prevnode.branch: + if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: + let elem = Element(styledNode.node) + if elem.hover: + elem.hover = false + buffer.reshape = true + + buffer.prevnode = thisnode + +proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) = + let url = parseUrl(elem.href, document.location.some) + if url.isSome: + let url = url.get + if url.scheme == buffer.location.scheme: + let media = elem.media + if media != "": + let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media))) + if not media.applies(): return + let fs = buffer.loader.doRequest(newRequest(url)) + if fs.body != nil and fs.contenttype == "text/css": + elem.sheet = parseStylesheet(fs.body) + +proc loadResources(buffer: Buffer, document: Document) = + var stack: seq[Element] + if document.html != nil: + stack.add(document.html) + while stack.len > 0: + let elem = stack.pop() + + if elem.tagType == TAG_LINK: + let elem = HTMLLinkElement(elem) + if elem.rel == "stylesheet": + buffer.loadResource(document, elem) + + for child in elem.children_rev: + stack.add(child) + +proc setupSource(buffer: Buffer): int = + let source = buffer.bsource + let setct = source.contenttype.isNone + if not setct: + buffer.contenttype = source.contenttype.get + buffer.location = source.location + case source.t + of CLONE: + buffer.istream = connectSocketStream(source.clonepid) + if setct: + buffer.contenttype = "text/plain" + of LOAD_PIPE: + var f: File + if not open(f, source.fd, fmRead): + return 1 + buffer.istream = newFileStream(f) + if setct: + buffer.contenttype = "text/plain" + of LOAD_REQUEST: + let request = source.request + let response = buffer.loader.doRequest(request) + if response.body == nil: + return response.res + if setct: + buffer.contenttype = response.contenttype + buffer.istream = response.body + if response.status == 401: # Unauthorized + buffer.writeCommand(SET_NEEDS_AUTH) + if response.redirect.isSome: + buffer.writeCommand(SET_REDIRECT, response.redirect.get) + if setct: + buffer.writeCommand(SET_CONTENT_TYPE, buffer.contenttype) + +proc load(buffer: Buffer) = + case buffer.contenttype + of "text/html": + if not buffer.streamclosed: + buffer.source = buffer.istream.readAll() + buffer.istream.close() + buffer.istream = newStringStream(buffer.source) + buffer.document = parseHTML5(buffer.istream) + buffer.streamclosed = true + else: + buffer.document = parseHTML5(newStringStream(buffer.source)) + buffer.writeCommand(SET_TITLE, buffer.document.title) + buffer.document.location = buffer.location + buffer.loadResources(buffer.document) + else: + if not buffer.streamclosed: + buffer.source = buffer.istream.readAll() + buffer.istream.close() + buffer.streamclosed = true + +proc render(buffer: Buffer) = + case buffer.contenttype + of "text/html": + if buffer.viewport == nil: + buffer.viewport = Viewport(term: buffer.attrs) + if buffer.userstyle == nil: + buffer.userstyle = buffer.config.stylesheet.parseStylesheet() + let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled) + buffer.lines = ret[0] + buffer.prevstyled = ret[1] + else: + if not buffer.rendered: + buffer.lines = renderPlainText(buffer.source) + buffer.rendered = true + +# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set +proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] = + if form.constructingentrylist: + return + form.constructingentrylist = true + + var entrylist: seq[tuple[name, value: string]] + for field in form.controls: + if field.findAncestor({TAG_DATALIST}) != nil or + field.attrb("disabled") or + field.isButton() and Element(field) != submitter: + continue + + if field.tagType == TAG_INPUT: + let field = HTMLInputElement(field) + if field.inputType == INPUT_IMAGE: + let name = if field.attr("name") != "": + field.attr("name") & '.' + else: + "" + entrylist.add((name & 'x', $field.xcoord)) + entrylist.add((name & 'y', $field.ycoord)) + continue + + #TODO custom elements + + let name = field.attr("name") + + if name == "": + continue + + if field.tagType == TAG_SELECT: + let field = HTMLSelectElement(field) + for option in field.options: + if option.selected or option.disabled: + entrylist.add((name, option.value)) + elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}: + let value = if field.attr("value") != "": + field.attr("value") + else: + "on" + entrylist.add((name, value)) + elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE: + #TODO file + discard + elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"): + let charset = if encoding != "": + encoding + else: + "UTF-8" + entrylist.add((name, charset)) + else: + if field.tagType == TAG_INPUT: + entrylist.add((name, HTMLInputElement(field).value)) + else: + assert false + if field.tagType == TAG_TEXTAREA or + field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}: + if field.attr("dirname") != "": + let dirname = field.attr("dirname") + let dir = "ltr" #TODO bidi + entrylist.add((dirname, dir)) + + form.constructingentrylist = false + return entrylist + +#https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm +proc makeCRLF(s: string): string = + result = newStringOfCap(s.len) + var i = 0 + while i < s.len - 1: + if s[i] == '\r' and s[i + 1] != '\n': + result &= '\r' + result &= '\n' + elif s[i] != '\r' and s[i + 1] == '\n': + result &= s[i] + result &= '\r' + result &= '\n' + inc i + else: + result &= s[i] + inc i + +proc serializeMultipartFormData(kvs: seq[(string, string)]): MimeData = + for it in kvs: + let name = makeCRLF(it[0]) + let value = makeCRLF(it[1]) + result[name] = value + +proc serializePlainTextFormData(kvs: seq[(string, string)]): string = + for it in kvs: + let (name, value) = it + result &= name + result &= '=' + result &= value + result &= "\r\n" + +proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = + let entrylist = form.constructEntryList(submitter) + + let action = if submitter.action() == "": + $form.document.location + else: + submitter.action() + + let url = parseUrl(action, submitter.document.baseUrl.some) + if url.isnone: + return none(Request) + + var parsedaction = url.get + let scheme = parsedaction.scheme + let enctype = submitter.enctype() + let formmethod = submitter.formmethod() + if formmethod == FORM_METHOD_DIALOG: + #TODO + return none(Request) + let httpmethod = if formmethod == FORM_METHOD_GET: + HTTP_GET + else: + assert formmethod == FORM_METHOD_POST + HTTP_POST + + #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): + # submitter.attr("formtarget") + #else: + # submitter.target() + #let noopener = true #TODO + + template mutateActionUrl() = + let query = serializeApplicationXWWWFormUrlEncoded(entrylist) + parsedaction.query = query.some + return newRequest(parsedaction, httpmethod).some + + template submitAsEntityBody() = + var mimetype: string + var body = none(string) + var multipart = none(MimeData) + case enctype + of FORM_ENCODING_TYPE_URLENCODED: + body = serializeApplicationXWWWFormUrlEncoded(entrylist).some + mimeType = $enctype + of FORM_ENCODING_TYPE_MULTIPART: + multipart = serializeMultipartFormData(entrylist).some + mimetype = $enctype + of FORM_ENCODING_TYPE_TEXT_PLAIN: + body = serializePlainTextFormData(entrylist).some + mimetype = $enctype + return newRequest(parsedaction, httpmethod, {"Content-Type": mimetype}, body, multipart).some + + template getActionUrl() = + return newRequest(parsedaction).some + + case scheme + of "http", "https": + if formmethod == FORM_METHOD_GET: + mutateActionUrl + else: + assert formmethod == FORM_METHOD_POST + submitAsEntityBody + of "ftp": + getActionUrl + of "data": + if formmethod == FORM_METHOD_GET: + mutateActionUrl + else: + assert formmethod == FORM_METHOD_POST + getActionUrl + +template set_focus(e: Element) = + if buffer.document.focus != e: + buffer.document.focus = e + buffer.reshape = true + +template restore_focus = + if buffer.document.focus != nil: + buffer.document.focus = nil + buffer.reshape = true + +proc lineInput(buffer: Buffer, s: string) = + if buffer.input != nil: + let input = buffer.input + case input.inputType + of INPUT_SEARCH: + input.value = s + input.invalid = true + buffer.reshape = true + if input.form != nil: + let submitaction = submitForm(input.form, input) + if submitaction.isSome: + buffer.writeCommand(OPEN, submitaction.get) + of INPUT_TEXT, INPUT_PASSWORD: + input.value = s + input.invalid = true + buffer.reshape = true + of INPUT_FILE: + let cdir = parseUrl("file://" & getCurrentDir() & DirSep) + let path = parseUrl(s, cdir) + if path.issome: + input.file = path + input.invalid = true + buffer.reshape = true + else: discard + buffer.input = nil + +proc click(buffer: Buffer, cursorx, cursory: int) = + let clickable = buffer.getCursorClickable(cursorx, cursory) + if clickable != nil: + case clickable.tagType + of TAG_SELECT: + set_focus clickable + of TAG_A: + restore_focus + let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some) + if url.issome: + buffer.writeCommand(OPEN, newRequest(url.get, HTTP_GET)) + of TAG_OPTION: + let option = HTMLOptionElement(clickable) + let select = option.select + if select != nil: + if buffer.document.focus == select: + # select option + if not select.attrb("multiple"): + for option in select.options: + option.selected = false + option.selected = true + restore_focus + else: + # focus on select + set_focus select + of TAG_INPUT: + restore_focus + let input = HTMLInputElement(clickable) + case input.inputType + of INPUT_SEARCH: + buffer.input = input + buffer.writeCommand(READ_LINE, "SEARCH: ", input.value, false) + of INPUT_TEXT, INPUT_PASSWORD: + buffer.input = input + buffer.writeCommand(READ_LINE, "TEXT: ", input.value, input.inputType == INPUT_PASSWORD) + of INPUT_FILE: + var path = if input.file.issome: + input.file.get.path.serialize_unicode() + else: + "" + buffer.writeCommand(READ_LINE, "Filename: ", path, false) + of INPUT_CHECKBOX: + input.checked = not input.checked + input.invalid = true + buffer.reshape = true + of INPUT_RADIO: + for radio in input.radiogroup: + radio.checked = false + radio.invalid = true + input.checked = true + input.invalid = true + buffer.reshape = true + of INPUT_RESET: + if input.form != nil: + input.form.reset() + buffer.reshape = true + of INPUT_SUBMIT, INPUT_BUTTON: + if input.form != nil: + let submitaction = submitForm(input.form, input) + if submitaction.isSome: + buffer.writeCommand(OPEN, submitaction.get) + else: + restore_focus + else: + restore_focus + +proc drawBuffer(buffer: Buffer, ostream: Stream) = + var format = newFormat() + for line in buffer.lines: + if line.formats.len == 0: + ostream.swrite(line.str & "\n") + else: + var x = 0 + var i = 0 + var s = "" + for f in line.formats: + var outstr = "" + #assert f.pos < line.str.width(), "fpos " & $f.pos & "\nstr" & line.str & "\n" + while x < f.pos: + var r: Rune + fastRuneAt(line.str, i, r) + outstr &= r + x += r.width() + s &= outstr + s &= format.processFormat(f.format) + s &= line.str.substr(i) & format.processFormat(newFormat()) & "\n" + ostream.swrite(s) + ostream.flush() + ostream.swrite("") + ostream.flush() + +proc runBuffer(buffer: Buffer, istream, ostream: Stream) = + buffer.pistream = istream + buffer.postream = ostream + while true: + var cmd: BufferCommand + try: + istream.sread(cmd) + #eprint "cmd", cmd + case cmd + of LOAD: + let code = buffer.setupSource() + buffer.load() + buffer.writeCommand(LOAD_DONE, code) + of GOTO_ANCHOR: + var anchor: string + istream.sread(anchor) + if buffer.document != nil and buffer.document.getElementById(anchor) != nil: + buffer.writeCommand(ANCHOR_FOUND) + else: + buffer.writeCommand(ANCHOR_FAIL) + of RENDER: + buffer.render() + buffer.gotoAnchor() + of GET_LINES: + var w: Slice[int] + istream.sread(w) + ostream.swrite(SET_LINES) + ostream.swrite(buffer.lines.len) + w.b = min(buffer.lines.high, w.b) + ostream.swrite(w) + for y in w: + ostream.swrite(buffer.lines[y]) + ostream.flush() + ostream.flush() + of DRAW_BUFFER: + buffer.drawBuffer(ostream) + of WINDOW_CHANGE: + istream.sread(buffer.attrs) + buffer.windowChange() + of FIND_PREV_LINK: + var cx, cy: int + istream.sread(cx) + istream.sread(cy) + let pl = buffer.findPrevLink(cx, cy) + buffer.writeCommand(JUMP, pl.x, pl.y) + of FIND_NEXT_LINK: + var cx, cy: int + istream.sread(cx) + istream.sread(cy) + let nl = buffer.findNextLink(cx, cy) + buffer.writeCommand(JUMP, nl.x, nl.y) + of FIND_PREV_MATCH: + var cx, cy: int + var regex: Regex + var wrap: bool + istream.sread(cx) + istream.sread(cy) + istream.sread(regex) + istream.sread(wrap) + let match = buffer.findPrevMatch(regex, cx, cy, wrap) + if match.success: + buffer.writeCommand(JUMP, match.x, match.y) + of FIND_NEXT_MATCH: + var cx, cy: int + var regex: Regex + var wrap: bool + istream.sread(cx) + istream.sread(cy) + istream.sread(regex) + istream.sread(wrap) + let match = buffer.findNextMatch(regex, cx, cy, wrap) + if match.success: + buffer.writeCommand(JUMP, match.x, match.y) + of READ_SUCCESS: + var s: string + istream.sread(s) + buffer.lineInput(s) + of READ_CANCELED: + buffer.input = nil + of CLICK: + var cx, cy: int + istream.sread(cx) + istream.sread(cy) + buffer.click(cx, cy) + of MOVE_CURSOR: + var cx, cy: int + istream.sread(cx) + istream.sread(cy) + buffer.updateHover(cx, cy) + of GET_SOURCE: + let ssock = initServerSocket(getpid()) + buffer.writeCommand(SOURCE_READY) + let stream = ssock.acceptSocketStream() + if not buffer.streamclosed: + buffer.source = buffer.istream.readAll() + buffer.streamclosed = true + stream.write(buffer.source) + stream.close() + ssock.close() + if buffer.reshape: + buffer.reshape = false + buffer.render() + buffer.writeCommand(RESHAPE) + except IOError: + break + istream.close() + ostream.close() + when defined(posix): + #TODO remove this + if buffer.loader != nil: + assert kill(buffer.loader.process, cint(SIGTERM)) == 0 + buffer.loader = nil + quit(0) + +proc launchBuffer*(config: Config, source: BufferSource, attrs: TermAttributes, + istream, ostream: Stream) = + let buffer = new Buffer + buffer.attrs = attrs + buffer.windowChange() + buffer.config = config + buffer.loader = newFileLoader() + buffer.bsource = source + buffer.runBuffer(istream, ostream) diff --git a/src/io/cell.nim b/src/buffer/cell.nim index 8c5333b1..84efef46 100644 --- a/src/io/cell.nim +++ b/src/buffer/cell.nim @@ -21,32 +21,35 @@ type Format* = object fgcolor*: CellColor bgcolor*: CellColor - flags: set[FormatFlags] - - Cell* = object of RootObj - format*: Format - node*: StyledNode + flags*: set[FormatFlags] # A FormatCell *starts* a new terminal formatting context. # If no FormatCell exists before a given cell, the default formatting is used. - FormatCell* = object of Cell + FormatCell* = object + format*: Format pos*: int computed*: ComputedFormat + node*: StyledNode + + SimpleFormatCell* = object + format*: Format + pos*: int FlexibleLine* = object str*: string formats*: seq[FormatCell] - Mark* = ref object - x*: int - y*: int - width*: int - format*: Format + SimpleFlexibleLine* = object + str*: string + formats*: seq[SimpleFormatCell] FlexibleGrid* = seq[FlexibleLine] - FixedCell* = object of Cell + SimpleFlexibleGrid* = seq[SimpleFlexibleLine] + + FixedCell* = object str*: string + format*: Format FixedGrid* = seq[FixedCell] @@ -74,8 +77,7 @@ template `blink=`*(f: var Format, b: bool) = flag_template f, b, FLAG_BLINK func `==`*(a: FixedCell, b: FixedCell): bool = return a.format == b.format and - a.str == b.str and - a.node == b.node + a.str == b.str func newFixedGrid*(w: int, h: int = 1): FixedGrid = return newSeq[FixedCell](w * h) @@ -90,7 +92,7 @@ func newFormat*(): Format = return Format(fgcolor: defaultColor, bgcolor: defaultColor) # Get the first format cell after pos, if any. -func findFormatN*(line: FlexibleLine, pos: int): int = +func findFormatN*(line: FlexibleLine|SimpleFlexibleLine, pos: int): int = var i = 0 while i < line.formats.len: if line.formats[i].pos > pos: @@ -105,6 +107,13 @@ func findFormat*(line: FlexibleLine, pos: int): FormatCell = else: result.pos = -1 +func findFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell = + let i = line.findFormatN(pos) - 1 + if i != -1: + result = line.formats[i] + else: + result.pos = -1 + func findNextFormat*(line: FlexibleLine, pos: int): FormatCell = let i = line.findFormatN(pos) if i < line.formats.len: @@ -112,6 +121,13 @@ func findNextFormat*(line: FlexibleLine, pos: int): FormatCell = else: result.pos = -1 +func findNextFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell = + let i = line.findFormatN(pos) + if i < line.formats.len: + result = line.formats[i] + else: + result.pos = -1 + proc addLine*(grid: var FlexibleGrid) = grid.add(FlexibleLine()) diff --git a/src/buffer/container.nim b/src/buffer/container.nim new file mode 100644 index 00000000..beb5fedc --- /dev/null +++ b/src/buffer/container.nim @@ -0,0 +1,626 @@ +import macros +import options +import streams +import strformat +import unicode + +when defined(posix): + import posix + +import buffer/buffer +import buffer/cell +import config/config +import io/request +import io/serialize +import io/term +import js/regex +import types/url +import utils/twtstr + +type + CursorPosition* = object + cursorx*: int + cursory*: int + xend*: int + fromx*: int + fromy*: int + setx: int + + ContainerEventType* = enum + NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE, + STATUS, JUMP, READ_LINE, OPEN + + ContainerEvent* = object + case t*: ContainerEventType + of READ_LINE: + prompt*: string + value*: string + password*: bool + of OPEN: + request*: Request + else: discard + + Container* = ref object + attrs*: TermAttributes + width*: int + height*: int + contenttype*: Option[string] + title*: string + hovertext*: string + source*: BufferSource + children*: seq[Container] + pos: CursorPosition + bpos: seq[CursorPosition] + parent*: Container + sourcepair*: Container + istream*: Stream + ostream*: Stream + ifd*: FileHandle + process: Pid + lines: SimpleFlexibleGrid + lineshift: int + numLines*: int + replace*: Container + code*: int + retry*: seq[URL] + redirect*: Option[URL] + ispipe: bool + jump: bool + pipeto: Container + tty: FileHandle + +proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {. + importc: "setvbuf", header: "<stdio.h>", tags: [].} + +proc newBuffer*(config: Config, source: BufferSource, tty: FileHandle, ispipe = false): Container = + let attrs = getTermAttributes(stdout) + when defined(posix): + var pipefd_in, pipefd_out: array[0..1, cint] + if pipe(pipefd_in) == -1: + raise newException(Defect, "Failed to open input pipe.") + if pipe(pipefd_out) == -1: + raise newException(Defect, "Failed to open output pipe.") + let pid = fork() + if pid == -1: + raise newException(Defect, "Failed to fork buffer process") + elif pid == 0: + discard close(tty) + discard close(stdout.getFileHandle()) + # child process + discard close(pipefd_in[1]) # close write + discard close(pipefd_out[0]) # close read + var readf, writef: File + if not open(readf, pipefd_in[0], fmRead): + raise newException(Defect, "Failed to open input handle") + if not open(writef, pipefd_out[1], fmWrite): + raise newException(Defect, "Failed to open output handle") + let istream = newFileStream(readf) + let ostream = newFileStream(writef) + launchBuffer(config, source, attrs, istream, ostream) + else: + discard close(pipefd_in[0]) # close read + discard close(pipefd_out[1]) # close write + var readf, writef: File + if not open(writef, pipefd_in[1], fmWrite): + raise newException(Defect, "Failed to open output handle") + if not open(readf, pipefd_out[0], fmRead): + raise newException(Defect, "Failed to open input handle") + let istream = newFileStream(readf) + # Disable buffering of the read end so epoll doesn't get stuck + discard c_setvbuf(readf, nil, IONBF, 0) + let ostream = newFileStream(writef) + result = Container(istream: istream, ostream: ostream, source: source, + ifd: pipefd_out[0], process: pid, attrs: attrs, + width: attrs.width - 1, height: attrs.height - 1, + contenttype: source.contenttype, ispipe: ispipe, + tty: tty) + result.pos.setx = -1 + +func lineLoaded(container: Container, y: int): bool = + return y - container.lineshift in 0..container.lines.high + +func getLine(container: Container, y: int): SimpleFlexibleLine = + if container.lineLoaded(y): + return container.lines[y - container.lineshift] + +iterator ilines*(container: Container, slice: Slice[int]): SimpleFlexibleLine {.inline.} = + for y in slice: + yield container.getLine(y) + +func cursorx*(container: Container): int {.inline.} = container.pos.cursorx +func cursory*(container: Container): int {.inline.} = container.pos.cursory +func fromx*(container: Container): int {.inline.} = container.pos.fromx +func fromy*(container: Container): int {.inline.} = container.pos.fromy +func xend*(container: Container): int {.inline.} = container.pos.xend +func lastVisibleLine*(container: Container): int = min(container.fromy + container.height, container.numLines) - 1 + +func acursorx*(container: Container): int = + max(0, container.cursorx - container.fromx) + +func acursory*(container: Container): int = + container.cursory - container.fromy + +func currentLine*(container: Container): string = + return container.getLine(container.cursory).str + +func cursorBytes(container: Container, y: int, cc = container.cursorx): int = + let line = container.getLine(y).str + var w = 0 + var i = 0 + while i < line.len and w < cc: + var r: Rune + fastRuneAt(line, i, r) + w += r.width() + return i + +func currentCursorBytes(container: Container, cc = container.cursorx): int = + return container.cursorBytes(container.cursory, cc) + +func prevWidth*(container: Container): int = + if container.numLines == 0: return 0 + let line = container.currentLine + if line.len == 0: return 0 + var w = 0 + var i = 0 + let cc = container.pos.fromx + container.pos.cursorx + var pr: Rune + var r: Rune + fastRuneAt(line, i, r) + while i < line.len and w < cc: + pr = r + fastRuneAt(line, i, r) + w += r.width() + return pr.width() + +func currentWidth*(container: Container): int = + if container.numLines == 0: return 0 + let line = container.currentLine + if line.len == 0: return 0 + var w = 0 + var i = 0 + let cc = container.cursorx + var r: Rune + fastRuneAt(line, i, r) + while i < line.len and w < cc: + fastRuneAt(line, i, r) + w += r.width() + return r.width() + +func maxScreenWidth(container: Container): int = + for line in container.ilines(container.fromy..container.lastVisibleLine): + result = max(line.str.width(), result) + +func getTitle*(container: Container): string = + if container.title != "": + return container.title + if container.ispipe: + return "*pipe*" + return container.source.location.serialize(excludepassword = true) + +func currentLineWidth*(container: Container): int = + if container.numLines == 0: return 0 + return container.currentLine.width() + +func maxfromy(container: Container): int = max(container.numLines - container.height, 0) + +func maxfromx(container: Container): int = max(container.currentLineWidth() - container.width, 0) + +func atPercentOf*(container: Container): int = + if container.numLines == 0: return 100 + return (100 * (container.cursory + 1)) div container.numLines + +func lineInfo*(container: Container): string = + fmt"line {container.cursory + 1}/{container.numLines} ({container.atPercentOf}%) col {container.cursorx + 1}/{container.currentLineWidth} (byte {container.currentCursorBytes})" + +func lineWindow(container: Container): Slice[int] = + if container.numLines == 0: # not loaded + return 0..container.height * 5 + let n = (container.height * 5) div 2 + var x = container.fromy - n + container.height div 2 + var y = container.fromy + n + container.height div 2 + if x < 0: + y += -x + x = 0 + if y >= container.numLines: + x -= y - container.numLines + y = container.numLines + return max(x, 0) .. min(y, container.numLines - 1) + +macro writeCommand(container: Container, cmd: BufferCommand, args: varargs[typed]) = + result = newStmtList() + result.add(quote do: `container`.ostream.swrite(`cmd`)) + for arg in args: + result.add(quote do: `container`.ostream.swrite(`arg`)) + result.add(quote do: `container`.ostream.flush()) + +proc setFromY*(container: Container, y: int) = + if container.pos.fromy != y: + container.pos.fromy = max(min(y, container.maxfromy), 0) + container.writeCommand(GET_LINES, container.lineWindow) + +proc setFromX*(container: Container, x: int) = + if container.pos.fromx != x: + container.pos.fromx = max(min(x, container.maxfromx), 0) + +proc setFromXY*(container: Container, x, y: int) = + container.setFromY(y) + container.setFromX(x) + +proc setCursorX*(container: Container, x: int, refresh = true, save = true) = + if not container.lineLoaded(container.cursory): + container.pos.setx = x + return + container.pos.setx = -1 + let cw = container.currentLineWidth() + let x = max(min(x, cw - 1), 0) + if (not refresh) or (container.fromx <= x and x < container.fromx + container.width): + container.pos.cursorx = x + else: + if refresh and container.fromx > container.cursorx: + container.setFromX(max(cw - 1, 0)) + container.pos.cursorx = container.fromx + elif x > container.cursorx: + container.setFromX(max(x - container.width + 1, 0)) + container.pos.cursorx = x + elif x < container.cursorx: + container.setFromX(x) + container.pos.cursorx = x + container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory) + if save: + container.pos.xend = container.cursorx + +proc restoreCursorX(container: Container) = + container.setCursorX(max(min(container.currentLineWidth() - 1, container.xend), 0), false, false) + +proc setCursorY*(container: Container, y: int) = + let y = max(min(y, container.numLines - 1), 0) + if container.cursory == y: return + if y - container.fromy >= 0 and y - container.height < container.fromy: + container.pos.cursory = y + else: + if y > container.cursory: + container.setFromY(y - container.height + 1) + else: + container.setFromY(y) + container.pos.cursory = y + container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory) + container.restoreCursorX() + +proc centerLine*(container: Container) = + container.setFromY(container.cursory - container.height div 2) + +proc setCursorXY*(container: Container, x, y: int) = + let fy = container.fromy + container.setCursorY(y) + container.setCursorX(x) + if fy != container.fromy: + container.centerLine() + +proc cursorDown*(container: Container) = + container.setCursorY(container.cursory + 1) + +proc cursorUp*(container: Container) = + container.setCursorY(container.cursory - 1) + +proc cursorLeft*(container: Container) = + container.setCursorX(container.cursorx - container.prevWidth()) + +proc cursorRight*(container: Container) = + container.setCursorX(container.cursorx + container.currentWidth()) + +proc cursorLineBegin*(container: Container) = + container.setCursorX(0) + +proc cursorLineEnd*(container: Container) = + container.setCursorX(container.currentLineWidth() - 1) + +proc cursorNextWord*(container: Container) = + if container.numLines == 0: return + var r: Rune + var b = container.currentCursorBytes() + var x = container.cursorx + while b < container.currentLine.len: + let pb = b + fastRuneAt(container.currentLine, b, r) + if r.breaksWord(): + b = pb + break + x += r.width() + + while b < container.currentLine.len: + let pb = b + fastRuneAt(container.currentLine, b, r) + if not r.breaksWord(): + b = pb + break + x += r.width() + + if b < container.currentLine.len: + container.setCursorX(x) + else: + if container.cursory < container.numLines - 1: + container.cursorDown() + container.cursorLineBegin() + else: + container.cursorLineEnd() + +proc cursorPrevWord*(container: Container) = + if container.numLines == 0: return + var b = container.currentCursorBytes() + var x = container.cursorx + if container.currentLine.len > 0: + b = min(b, container.currentLine.len - 1) + while b >= 0: + let (r, o) = lastRune(container.currentLine, b) + if r.breaksWord(): + break + b -= o + x -= r.width() + + while b >= 0: + let (r, o) = lastRune(container.currentLine, b) + if not r.breaksWord(): + break + b -= o + x -= r.width() + else: + b = -1 + + if b >= 0: + container.setCursorX(x) + else: + if container.cursory > 0: + container.cursorUp() + container.cursorLineEnd() + else: + container.cursorLineBegin() + +proc pageDown*(container: Container) = + container.setFromY(container.fromy + container.height) + container.setCursorY(container.cursory + container.height) + container.restoreCursorX() + +proc pageUp*(container: Container) = + container.setFromY(container.fromy - container.height) + container.setCursorY(container.cursory - container.height) + container.restoreCursorX() + +proc pageLeft*(container: Container) = + container.setFromX(container.fromx - container.width) + container.setCursorX(container.cursorx - container.width) + +proc pageRight*(container: Container) = + container.setFromX(container.fromx + container.width) + container.setCursorX(container.cursorx + container.width) + +proc halfPageUp*(container: Container) = + container.setFromY(container.fromy - container.height div 2 + 1) + container.setCursorY(container.cursory - container.height div 2 + 1) + container.restoreCursorX() + +proc halfPageDown*(container: Container) = + container.setFromY(container.fromy + container.height div 2 - 1) + container.setCursorY(container.cursory + container.height div 2 - 1) + container.restoreCursorX() + +proc cursorFirstLine*(container: Container) = + container.setCursorY(0) + +proc cursorLastLine*(container: Container) = + container.setCursorY(container.numLines - 1) + +proc cursorTop*(container: Container) = + container.setCursorY(container.fromy) + +proc cursorMiddle*(container: Container) = + container.setCursorY(container.fromy + (container.height - 2) div 2) + +proc cursorBottom*(container: Container) = + container.setCursorY(container.fromy + container.height - 1) + +proc cursorLeftEdge*(container: Container) = + container.setCursorX(container.fromx) + +proc cursorVertMiddle*(container: Container) = + container.setCursorX(container.fromx + (container.width - 2) div 2) + +proc cursorRightEdge*(container: Container) = + container.setCursorX(container.fromx + container.width - 1) + +proc scrollDown*(container: Container) = + if container.fromy + container.height < container.numLines: + container.setFromY(container.fromy + 1) + if container.fromy > container.cursory: + container.cursorDown() + else: + container.cursorDown() + +proc scrollUp*(container: Container) = + if container.fromy > 0: + container.setFromY(container.fromy - 1) + if container.fromy + container.height <= container.cursory: + container.cursorUp() + else: + container.cursorUp() + +proc scrollRight*(container: Container) = + if container.fromx + container.width < container.maxScreenWidth(): + container.setFromX(container.fromx + 1) + +proc scrollLeft*(container: Container) = + if container.fromx > 0: + container.setFromX(container.fromx - 1) + if container.cursorx < container.fromx: + container.setCursorX(container.currentLineWidth() - 1) + +proc updateCursor(container: Container) = + if container.pos.setx > -1: + container.setCursorX(container.pos.setx) + if container.fromy > container.lastVisibleLine: + container.setFromY(0) + container.setCursorY(container.lastVisibleLine) + if container.cursory >= container.numLines: + container.pos.cursory = max(0, container.numLines - 1) + if container.numLines == 0: + container.pos.cursory = 0 + +proc pushCursorPos*(container: Container) = + container.bpos.add(container.pos) + +proc popCursorPos*(container: Container) = + container.pos = container.bpos.pop() + container.updateCursor() + container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory) + container.writeCommand(GET_LINES, container.lineWindow) + +macro proxy(fun: typed) = + let name = fun[0] # sym + let params = fun[3] # formalparams + let retval = params[0] # sym + var body = newStmtList() + assert params.len >= 2 # return type, container + var x = name.strVal.toScreamingSnakeCase() + if x[^1] == '=': + x = "SET_" & x[0..^2] + let nup = ident(x) + let container = params[1][0] + body.add(quote do: + `container`.ostream.swrite(`nup`)) + for c in params[2..^1]: + let s = c[0] # sym e.g. url + body.add(quote do: + `container`.ostream.swrite(`s`)) + body.add(quote do: + `container`.ostream.flush()) + if retval.kind != nnkEmpty: + body.add(quote do: + `container`.istream.sread(result)) + var params2: seq[NimNode] + for x in params.children: params2.add(x) + result = newProc(name, params2, body) + +proc cursorNextLink*(container: Container) = + container.writeCommand(FIND_NEXT_LINK, container.cursorx, container.cursory) + container.jump = true + +proc cursorPrevLink*(container: Container) = + container.writeCommand(FIND_PREV_LINK, container.cursorx, container.cursory) + container.jump = true + +proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) = + container.writeCommand(FIND_NEXT_MATCH, container.cursorx, container.cursory, regex, wrap) + container.jump = true + +proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) = + container.writeCommand(FIND_PREV_MATCH, container.cursorx, container.cursory, regex, wrap) + container.jump = true + +proc load*(container: Container) {.proxy.} = discard +proc gotoAnchor*(container: Container, anchor: string) {.proxy.} = discard +proc readCanceled*(container: Container) {.proxy.} = discard +proc readSuccess*(container: Container, s: string) {.proxy.} = discard + +proc render*(container: Container) = + container.writeCommand(RENDER) + container.jump = true # may jump to anchor + container.writeCommand(GET_LINES, container.lineWindow) + +proc dupeBuffer*(container: Container, config: Config, location = none(URL), contenttype = none(string)): Container = + var pipefd: array[0..1, cint] + if pipe(pipefd) == -1: + raise newException(Defect, "Failed to open dupe pipe.") + let source = BufferSource( + t: CLONE, + location: location.get(container.source.location), + contenttype: if contenttype.isSome: contenttype else: container.contenttype, + clonepid: container.process, + ) + container.pipeto = newBuffer(config, source, container.tty, container.ispipe) + container.writeCommand(GET_SOURCE) + return container.pipeto + +proc click*(container: Container) = + container.writeCommand(CLICK, container.cursorx, container.cursory) + +proc drawBuffer*(container: Container) = + container.writeCommand(DRAW_BUFFER) + while true: + var s: string + container.istream.sread(s) + if s == "": break + try: + stdout.write(s) + except IOError: # couldn't write to stdout; it's probably just a broken pipe. + quit(1) + stdout.flushFile() + +proc windowChange*(container: Container, attrs: TermAttributes) = + container.attrs = attrs + container.width = attrs.width - 1 + container.height = attrs.height - 1 + container.writeCommand(WINDOW_CHANGE, attrs) + +proc handleEvent*(container: Container): ContainerEvent = + var cmd: ContainerCommand + container.istream.sread(cmd) + case cmd + of SET_LINES: + var w: Slice[int] + container.istream.sread(container.numLines) + container.istream.sread(w) + container.lines.setLen(w.len) + container.lineshift = w.a + for y in 0 ..< w.len: + container.istream.sread(container.lines[y]) + container.updateCursor() + let cw = container.fromy ..< container.fromy + container.height + if w.a in cw or w.b in cw or cw.a in w or cw.b in w: + return ContainerEvent(t: UPDATE) + of SET_NEEDS_AUTH: + return ContainerEvent(t: NEEDS_AUTH) + of SET_CONTENT_TYPE: + var ctype: string + container.istream.sread(ctype) + container.contenttype = some(ctype) + of SET_REDIRECT: + var redirect: URL + container.istream.sread(redirect) + container.redirect = some(redirect) + return ContainerEvent(t: REDIRECT) + of SET_TITLE: + container.istream.sread(container.title) + return ContainerEvent(t: STATUS) + of SET_HOVER: + container.istream.sread(container.hovertext) + return ContainerEvent(t: STATUS) + of LOAD_DONE: + container.istream.sread(container.code) + if container.code != 0: + return ContainerEvent(t: FAIL) + return ContainerEvent(t: SUCCESS) + of ANCHOR_FOUND: + return ContainerEvent(t: ANCHOR) + of ANCHOR_FAIL: + return ContainerEvent(t: FAIL) + of READ_LINE: + var prompt, str: string + var pwd: bool + container.istream.sread(prompt) + container.istream.sread(str) + container.istream.sread(pwd) + return ContainerEvent(t: READ_LINE, prompt: prompt, value: str, password: pwd) + of JUMP: + var x, y: int + container.istream.sread(x) + container.istream.sread(y) + if container.jump and x >= 0 and y >= 0: + container.setCursorXY(x, y) + container.jump = false + return ContainerEvent(t: UPDATE) + of OPEN: + return ContainerEvent(t: OPEN, request: container.istream.readRequest()) + of SOURCE_READY: + if container.pipeto != nil: + container.pipeto.load() + of RESHAPE: + container.writeCommand(GET_LINES, container.lineWindow) diff --git a/src/config/config.nim b/src/config/config.nim index 10e097bd..a3b121ba 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -1,6 +1,5 @@ import tables import os -import strutils import streams import config/toml @@ -8,41 +7,10 @@ import types/color import utils/twtstr 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_LINK, ACTION_CURSOR_PREV_LINK, - ACTION_PAGE_DOWN, ACTION_PAGE_UP, ACTION_PAGE_LEFT, ACTION_PAGE_RIGHT, - ACTION_HALF_PAGE_DOWN, ACTION_HALF_PAGE_UP, - ACTION_SCROLL_DOWN, ACTION_SCROLL_UP, ACTION_SCROLL_LEFT, ACTION_SCROLL_RIGHT, - ACTION_CLICK, - ACTION_CHANGE_LOCATION, ACTION_DUPE_BUFFER, - ACTION_PREV_BUFFER, ACTION_NEXT_BUFFER, ACTION_DISCARD_BUFFER, - ACTION_RELOAD, ACTION_RESHAPE, ACTION_REDRAW, ACTION_TOGGLE_SOURCE, - ACTION_CURSOR_FIRST_LINE, ACTION_CURSOR_LAST_LINE, - ACTION_CURSOR_TOP, ACTION_CURSOR_MIDDLE, ACTION_CURSOR_BOTTOM, - ACTION_CURSOR_RIGHT_EDGE, ACTION_CURSOR_VERT_MIDDLE, ACTION_CURSOR_LEFT_EDGE, - ACTION_CENTER_LINE, ACTION_LINE_INFO, - ACTION_COMMAND, - ACTION_SEARCH, ACTION_SEARCH_BACK, ACTION_ISEARCH, ACTION_ISEARCH_BACK, - ACTION_SEARCH_NEXT, ACTION_SEARCH_PREV, - ACTION_LINED_SUBMIT, ACTION_LINED_CANCEL, - ACTION_LINED_BACKSPACE, ACTION_LINED_DELETE, - 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_BEGIN, ACTION_LINED_END, - ACTION_LINED_ESC - - ActionMap = Table[string, TwtAction] + ActionMap = Table[string, string] Config* = ref ConfigObj ConfigObj = object - nmap*: Table[string, string] + nmap*: ActionMap lemap*: ActionMap stylesheet*: string startup*: string @@ -77,7 +45,6 @@ func getRealKey(key: string): string = else: if meta == 2: realk &= '\e' - realk &= c meta = 0 if control == 2: realk &= getControlChar(c) @@ -90,27 +57,7 @@ func getRealKey(key: string): string = realk &= 'M' return realk -func constructActionTable*(origTable: ActionMap): ActionMap = - var newTable: ActionMap - var strs: seq[string] - 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: - 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 - -func constructActionTable2*(origTable: Table[string, string]): Table[string, string] = +func constructActionTable*(origTable: Table[string, string]): Table[string, string] = var strs: seq[string] for k in origTable.keys: let realk = getRealKey(k) @@ -128,11 +75,6 @@ func constructActionTable2*(origTable: Table[string, string]): Table[string, str result[teststr] = "client.feedNext()" result[realk] = v -func getLineAction(s: string): TwtAction = - if s == "NULL": - return NO_ACTION - return parseEnum[TwtAction]("ACTION_LINED_" & s) - proc readUserStylesheet(dir, file: string): string = if file.len == 0: return "" @@ -154,11 +96,10 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) = config.ambiguous_double = general["double-width-ambiguous"].b if "page" in t: for k, v in t["page"].pairs: - #config.nmap[getRealKey(k)] = getAction(v.s) config.nmap[getRealKey(k)] = v.s if "line" in t: for k, v in t["line"].pairs: - config.lemap[getRealKey(k)] = getLineAction(v.s) + config.lemap[getRealKey(k)] = v.s if "css" in t: let css = t["css"] if "include" in css: @@ -184,7 +125,7 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) = of "magenta": config.markcolor = CellColor(rgb: false, color: 45u8) of "cyan": config.markcolor = CellColor(rgb: false, color: 46u8) of "white": config.markcolor = CellColor(rgb: false, color: 47u8) - of "terminal": config.markcolor = defaultColor + of "terminal": config.markcolor = CellColor(rgb: false, color: 0) proc parseConfig(config: Config, dir: string, stream: Stream) = config.parseConfig(dir, parseToml(stream)) @@ -209,10 +150,10 @@ proc getNormalAction*(config: Config, s: string): string = return config.nmap[s] return "" -proc getLinedAction*(config: Config, s: string): TwtAction = +proc getLinedAction*(config: Config, s: string): string = if config.lemap.hasKey(s): return config.lemap[s] - return NO_ACTION + return "" proc readConfig*(): Config = new(result) diff --git a/src/display/client.nim b/src/display/client.nim index 24f65f06..2f4771f8 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -9,13 +9,14 @@ when defined(posix): import posix import std/monotimes +import std/selectors +import buffer/container import css/sheet import config/config import display/pager import html/dom import html/htmlparser -import io/buffer import io/lineedit import io/loader import io/request @@ -36,6 +37,7 @@ type loader: FileLoader console {.jsget.}: Console pager {.jsget.}: Pager + line {.jsget.}: LineEdit config: Config jsrt: JSRuntime jsctx: JSContext @@ -49,7 +51,7 @@ type Console* = ref object err*: Stream - lastbuf*: Buffer + lastcontainer*: Container ibuf: string tty: File @@ -69,18 +71,6 @@ proc statusMode(client: Client) = print(HVP(client.attrs.height + 1, 1)) print(EL()) -proc readPipe(client: Client, ctype: string) = - let buffer = newBuffer(client.config, client.console.tty) - buffer.contenttype = if ctype != "": ctype else: "text/plain" - buffer.ispipe = true - buffer.istream = newFileStream(stdin) - buffer.location = newURL("file://-") - client.pager.addBuffer(buffer) - if client.console.tty != nil: - buffer.setupBuffer() - else: - buffer.load() - proc doRequest(client: Client, req: Request): Response {.jsfunc.} = client.loader.doRequest(req) @@ -99,7 +89,8 @@ proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = proc evalJS(client: Client, src, filename: string): JSObject = unblockStdin(client.console.tty.getFileHandle()) - return client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL) + result = client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL) + restoreStdin(client.console.tty.getFileHandle()) proc evalJSFree(client: Client, src, filename: string) = free(client.evalJS(src, filename)) @@ -126,69 +117,103 @@ proc command(client: Client, src: string) = let previ = client.console.err.getPosition() client.command0(src) client.console.err.setPosition(previ) - if client.console.lastbuf == nil: - let buffer = newBuffer(client.config, client.console.tty) - buffer.istream = newStringStream(client.console.err.readAll()) #TODO - buffer.contenttype = "text/plain" - buffer.location = parseUrl("javascript:void(0);").get - client.console.lastbuf = buffer - client.pager.addBuffer(buffer) - else: - client.console.lastbuf.istream = newStringStream(client.console.lastbuf.source & client.console.err.readAll()) - client.console.lastbuf.streamclosed = false - client.console.lastbuf.setupBuffer() - client.console.lastbuf.cursorLastLine() + #TODO yeah this won't work... + #if client.console.lastcontainer == nil: + # let source = BufferSource(t: STRING, slocation: parseUrl("javascript:void(0);").get, str: client.console.err.readAll(), contenttype: some("text/plain")) + # let container = newBuffer(client.config, client.console.tty, source) + # client.console.lastcontainer = container + # client.pager.addContainer(container) + #else: + # client.console.lastbuf.istream = newStringStream(client.console.lastbuf.source & client.console.err.readAll()) + # client.console.lastbuf.streamclosed = false + #client.console.lastbuf.setupBuffer() + #client.console.lastbuf.cursorLastLine() proc command(client: Client): bool {.jsfunc.} = - var iput: string client.statusMode() - let status = readLine("COMMAND: ", iput, client.attrs.width, config = client.config, tty = client.console.tty) - if status: - client.command(iput) - return status + client.pager.lineedit = some(readLine("COMMAND: ", client.attrs.width, config = client.config, tty = client.console.tty)) proc commandMode(client: Client) {.jsfunc.} = client.pager.commandMode = client.command() proc quit(client: Client, code = 0) {.jsfunc.} = if stdout.isatty(): - print(HVP(getTermAttributes(stdout).height, 0)) + print(HVP(getTermAttributes(stdout).height, 1)) + print('\n') print(EL()) + stdout.showCursor() when defined(posix): assert kill(client.loader.process, cint(SIGTERM)) == 0 - for buffer in client.pager.buffers: - if buffer.loader != nil: - assert kill(buffer.loader.process, cint(SIGTERM)) == 0 quit(code) proc feedNext(client: Client) {.jsfunc.} = client.feednext = true proc input(client: Client) = + restoreStdin(client.console.tty.getFileHandle()) if client.pager.commandMode: client.commandMode() return - if not client.feednext: - client.s = "" - else: - client.feednext = false - restoreStdin(client.console.tty.getFileHandle()) let c = client.console.readChar() client.s &= c + if client.pager.lineedit.isSome: + let edit = client.pager.lineedit.get + client.line = edit + if edit.escNext: + edit.escNext = false + if edit.write(client.s): + client.s = "" + else: + let action = getLinedAction(client.config, client.s) + if action == "": + if edit.write(client.s): + client.s = "" + else: + client.feedNext = true + elif not client.feedNext: + client.evalJSFree(action, "<command>") + client.pager.updateReadLine() + if client.pager.lineedit.isNone: + client.line = nil + else: + let action = getNormalAction(client.config, client.s) + client.evalJSFree(action, "<command>") + if not client.feedNext: + client.s = "" + else: + client.feedNext = false + if client.pager.container != nil: + if client.pager.lineedit.isNone: + client.pager.refreshStatusMsg() + client.pager.displayStatus() + client.pager.displayCursor() - let action = getNormalAction(client.config, client.s) - client.evalJSFree(action, "<command>") +proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {. + importc: "setvbuf", header: "<stdio.h>", tags: [].} proc inputLoop(client: Client) = + discard c_setvbuf(client.console.tty, nil, IONBF, 0) #??? I thought raw mode would take care of this + client.pager.selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil) while true: - restoreStdin(client.console.tty.getFileHandle()) - client.pager.displayPage() - client.pager.followRedirect() - if client.pager.container != nil: - client.pager.container.buffer.refreshBuffer() - if client.pager.container.needsauth: # Unauthorized - client.pager.checkAuth() - client.input() + let events = client.pager.selector.select(-1) + for event in events: + if event.fd == client.console.tty.getFileHandle(): + client.input() + stdout.flushFile() + else: + let container = client.pager.fdmap[FileHandle(event.fd)] + if not client.pager.handleEvent(container): + disableRawMode() + for msg in client.pager.status: + eprint msg + client.quit(1) + if client.pager.lineedit.isNone and client.pager.switched: + client.pager.refreshDisplay(client.pager.container) + client.pager.displayPage() + client.pager.switched = false + if client.pager.command != "": + client.command(client.pager.command) + client.pager.command = "" #TODO this is dumb proc readFile(client: Client, path: string): string {.jsfunc.} = @@ -283,7 +308,7 @@ proc jsEventLoop(client: Client) = if wait > 0: sleep(wait) -proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) = +proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], dump: bool) = if client.config.startup != "": let s = readFile(client.config.startup) client.console.err = newFileStream(stderr) @@ -293,26 +318,23 @@ proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool quit() client.userstyle = client.config.stylesheet.parseStylesheet() if not stdin.isatty: - client.readPipe(ctype) + client.pager.readPipe(ctype) else: client.console.tty = stdin for page in pages: - client.pager.loadURL(page, force = true, ctype = ctype) + client.pager.loadURL(page, ctype = ctype) if stdout.isatty and not dump: - if client.pager.container != nil: - when defined(posix): - enableRawMode(client.console.tty.getFileHandle()) - client.inputLoop() - else: - for msg in client.pager.status: - eprint msg + when defined(posix): + enableRawMode(client.console.tty.getFileHandle()) + client.inputLoop() else: for msg in client.pager.status: eprint msg - for buffer in client.pager.buffers: - buffer.drawBuffer() + for container in client.pager.containers: + container.render() + container.drawBuffer() stdout.close() client.quit() @@ -339,14 +361,14 @@ proc sleep(client: Client, millis: int) {.jsfunc.} = proc newClient*(config: Config): Client = new(result) result.config = config - result.loader = newFileLoader() result.console = newConsole() if stdin.isatty(): result.console.tty = stdin elif stdout.isatty(): discard open(result.console.tty, "/dev/tty", fmRead) result.attrs = getTermAttributes(stdout) - result.pager = newPager(config, result.attrs, result.loader, result.console.tty) + result.loader = newFileLoader() + result.pager = newPager(config, result.attrs, result.console.tty) let rt = newJSRuntime() rt.setInterruptHandler(interruptHandler, cast[pointer](result)) let ctx = rt.newJSContext() @@ -365,4 +387,5 @@ proc newClient*(config: Config): Client = ctx.addDOMModule() ctx.addHTMLModule() ctx.addRequestModule() + ctx.addLineEditModule() ctx.addPagerModule() diff --git a/src/display/pager.nim b/src/display/pager.nim index c7e994b7..38eed767 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -1,13 +1,17 @@ import options import os +import streams +import tables import terminal import unicode +import std/selectors + +import buffer/buffer +import buffer/cell +import buffer/container import config/config -import io/buffer -import io/cell import io/lineedit -import io/loader import io/request import io/term import js/javascript @@ -16,26 +20,32 @@ import types/url import utils/twtstr type - Container = ref object - buffer*: Buffer - children: seq[Container] - pos: CursorPosition - parent: Container - sourcepair: Container - needsauth*: bool #TODO move to buffer? - redirecturl: Option[URL] + LineMode* = enum + NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F, + SEARCH_B, ISEARCH_F, ISEARCH_B Pager* = ref object attrs: TermAttributes commandMode*: bool container*: Container + lineedit*: Option[LineEdit] + linemode*: LineMode + username: string + command*: string config: Config - loader: FileLoader regex: Option[Regex] + iregex: Option[Regex] reverseSearch: bool status*: seq[string] + statusmsg*: FixedGrid switched*: bool tty: File + selector*: Selector[Container] + fdmap*: Table[FileHandle, Container] + icpos: CursorPosition + display: FixedGrid + bheight*: int + bwidth*: int iterator containers*(pager: Pager): Container = if pager.container != nil: @@ -48,194 +58,252 @@ iterator containers*(pager: Pager): Container = for i in countdown(c.children.high, 0): stack.add(c.children[i]) -iterator buffers*(pager: Pager): Buffer = - for container in pager.containers: - yield container.buffer - proc setContainer*(pager: Pager, c: Container) = pager.container = c pager.switched = true -proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLeft() -proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorDown() -proc cursorUp(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorUp() -proc cursorRight(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorRight() -proc cursorLineBegin(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLineBegin() -proc cursorLineEnd(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLineEnd() -proc cursorNextWord(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorNextWord() -proc cursorPrevWord(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorPrevWord() -proc cursorNextLink(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorNextLink() -proc cursorPrevLink(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorPrevLink() -proc pageDown(pager: Pager) {.jsfunc.} = pager.container.buffer.pageDown() -proc pageUp(pager: Pager) {.jsfunc.} = pager.container.buffer.pageUp() -proc pageRight(pager: Pager) {.jsfunc.} = pager.container.buffer.pageRight() -proc pageLeft(pager: Pager) {.jsfunc.} = pager.container.buffer.pageLeft() -proc halfPageDown(pager: Pager) {.jsfunc.} = pager.container.buffer.halfPageDown() -proc halfPageUp(pager: Pager) {.jsfunc.} = pager.container.buffer.halfPageUp() -proc cursorFirstLine(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorFirstLine() -proc cursorLastLine(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLastLine() -proc cursorTop(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorTop() -proc cursorMiddle(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorMiddle() -proc cursorBottom(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorBottom() -proc cursorLeftEdge(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLeftEdge() -proc cursorVertMiddle(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorVertMiddle() -proc cursorRightEdge(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorRightEdge() -proc centerLine(pager: Pager) {.jsfunc.} = pager.container.buffer.centerLine() -proc scrollDown(pager: Pager) {.jsfunc.} = pager.container.buffer.scrollDown() -proc scrollUp(pager: Pager) {.jsfunc.} = pager.container.buffer.scrollUp() -proc scrollLeft(pager: Pager) {.jsfunc.} = pager.container.buffer.scrollLeft() -proc scrollRight(pager: Pager) {.jsfunc.} = pager.container.buffer.scrollRight() -proc lineInfo(pager: Pager) {.jsfunc.} = pager.container.buffer.lineInfo() -proc reshape(pager: Pager) {.jsfunc.} = pager.container.buffer.reshape = true -proc redraw(pager: Pager) {.jsfunc.} = pager.container.buffer.redraw = true +proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.cursorDown() +proc cursorUp(pager: Pager) {.jsfunc.} = pager.container.cursorUp() +proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.cursorLeft() +proc cursorRight(pager: Pager) {.jsfunc.} = pager.container.cursorRight() +proc cursorLineBegin(pager: Pager) {.jsfunc.} = pager.container.cursorLineBegin() +proc cursorLineEnd(pager: Pager) {.jsfunc.} = pager.container.cursorLineEnd() +proc cursorNextWord(pager: Pager) {.jsfunc.} = pager.container.cursorNextWord() +proc cursorPrevWord(pager: Pager) {.jsfunc.} = pager.container.cursorPrevWord() +proc cursorNextLink(pager: Pager) {.jsfunc.} = pager.container.cursorNextLink() +proc cursorPrevLink(pager: Pager) {.jsfunc.} = pager.container.cursorPrevLink() +proc pageUp(pager: Pager) {.jsfunc.} = pager.container.pageUp() +proc pageDown(pager: Pager) {.jsfunc.} = pager.container.pageDown() +proc pageRight(pager: Pager) {.jsfunc.} = pager.container.pageRight() +proc pageLeft(pager: Pager) {.jsfunc.} = pager.container.pageLeft() +proc halfPageDown(pager: Pager) {.jsfunc.} = pager.container.halfPageDown() +proc halfPageUp(pager: Pager) {.jsfunc.} = pager.container.halfPageUp() +proc cursorFirstLine(pager: Pager) {.jsfunc.} = pager.container.cursorFirstLine() +proc cursorLastLine(pager: Pager) {.jsfunc.} = pager.container.cursorLastLine() +proc cursorTop(pager: Pager) {.jsfunc.} = pager.container.cursorTop() +proc cursorMiddle(pager: Pager) {.jsfunc.} = pager.container.cursorMiddle() +proc cursorBottom(pager: Pager) {.jsfunc.} = pager.container.cursorBottom() +proc cursorLeftEdge(pager: Pager) {.jsfunc.} = pager.container.cursorLeftEdge() +proc cursorVertMiddle(pager: Pager) {.jsfunc.} = pager.container.cursorVertMiddle() +proc cursorRightEdge(pager: Pager) {.jsfunc.} = pager.container.cursorRightEdge() +proc centerLine(pager: Pager) {.jsfunc.} = pager.container.centerLine() +proc scrollDown(pager: Pager) {.jsfunc.} = pager.container.scrollDown() +proc scrollUp(pager: Pager) {.jsfunc.} = pager.container.scrollUp() +proc scrollLeft(pager: Pager) {.jsfunc.} = pager.container.scrollLeft() +proc scrollRight(pager: Pager) {.jsfunc.} = pager.container.scrollRight() +proc reshape(pager: Pager) {.jsfunc.} = pager.container.render() proc searchNext(pager: Pager) {.jsfunc.} = if pager.regex.issome: if not pager.reverseSearch: - discard pager.container.buffer.cursorNextMatch(pager.regex.get) + pager.container.cursorNextMatch(pager.regex.get, true) else: - discard pager.container.buffer.cursorPrevMatch(pager.regex.get) + pager.container.cursorPrevMatch(pager.regex.get, true) proc searchPrev(pager: Pager) {.jsfunc.} = if pager.regex.issome: if not pager.reverseSearch: - discard pager.container.buffer.cursorPrevMatch(pager.regex.get) + pager.container.cursorPrevMatch(pager.regex.get, true) else: - discard pager.container.buffer.cursorNextMatch(pager.regex.get) + pager.container.cursorNextMatch(pager.regex.get, true) proc statusMode(pager: Pager) = print(HVP(pager.attrs.height + 1, 1)) print(EL()) -proc search(pager: Pager) {.jsfunc.} = +proc setLineEdit(pager: Pager, edit: LineEdit, mode: LineMode) = pager.statusMode() - var iput: string - let status = readLine("/", iput, pager.attrs.width, config = pager.config, tty = pager.tty) - if status: - if iput.len != 0: - pager.regex = compileSearchRegex(iput) - pager.reverseSearch = false - pager.searchNext() - -proc searchBack(pager: Pager) {.jsfunc.} = - pager.statusMode() - var iput: string - let status = readLine("?", iput, pager.attrs.width, config = pager.config, tty = pager.tty) - if status: - if iput.len != 0: - pager.regex = compileSearchRegex(iput) - pager.reverseSearch = true - pager.searchNext() + edit.writeStart() + stdout.flushFile() + pager.lineedit = some(edit) + pager.linemode = mode -proc displayPage*(pager: Pager) = - let buffer = pager.container.buffer - if pager.switched or buffer.refreshBuffer(): - pager.switched = false - stdout.hideCursor() - print(buffer.generateFullOutput()) - stdout.showCursor() - -proc isearch(pager: Pager) {.jsfunc.} = - pager.statusMode() - var iput: string - let cpos = pager.container.buffer.cpos - var mark: Mark - template del_mark() = - if mark != nil: - pager.container.buffer.removeMark(mark) - - let status = readLine("/", iput, pager.attrs.width, {}, false, pager.config, pager.tty, (proc(state: var LineState): bool = - del_mark - let regex = compileSearchRegex($state.news) - pager.container.buffer.cpos = cpos - if regex.issome: - let match = pager.container.buffer.cursorNextMatch(regex.get) - if match.success: - mark = pager.container.buffer.addMark(match.x, match.y, match.str.width()) - pager.container.buffer.redraw = true - pager.container.buffer.refreshBuffer(true) - pager.displayPage() - print(HVP(pager.attrs.height + 1, 2)) - print(SGR()) - else: - del_mark - pager.container.buffer.redraw = true - pager.container.buffer.refreshBuffer(true) - pager.displayPage() - print(HVP(pager.attrs.height + 1, 2)) - print(SGR()) - return true - false - )) - - del_mark - pager.container.buffer.redraw = true - pager.container.buffer.refreshBuffer(true) - if status: - pager.regex = compileSearchRegex(iput) - else: - pager.container.buffer.cpos = cpos +proc clearLineEdit(pager: Pager) = + pager.lineedit = none(LineEdit) -proc isearchBack(pager: Pager) {.jsfunc.} = - pager.statusMode() - var iput: string - let cpos = pager.container.buffer.cpos - var mark: Mark - template del_mark() = - if mark != nil: - pager.container.buffer.removeMark(mark) - let status = readLine("?", iput, pager.container.buffer.width, {}, false, pager.config, pager.tty, (proc(state: var LineState): bool = - del_mark - let regex = compileSearchRegex($state.news) - pager.container.buffer.cpos = cpos - if regex.issome: - let match = pager.container.buffer.cursorPrevMatch(regex.get) - if match.success: - mark = pager.container.buffer.addMark(match.x, match.y, match.str.width()) - pager.container.buffer.redraw = true - pager.container.buffer.refreshBuffer(true) - pager.displayPage() - print(HVP(pager.attrs.height + 1, 2)) - print(SGR()) - else: - del_mark - pager.container.buffer.redraw = true - pager.container.buffer.refreshBuffer(true) - pager.displayPage() - print(HVP(pager.attrs.height + 1, 2)) - print(SGR()) - return true - false - )) - del_mark - pager.container.buffer.redraw = true - if status: - pager.regex = compileSearchRegex(iput) - else: - pager.container.buffer.cpos = cpos +proc searchForward(pager: Pager) {.jsfunc.} = + pager.setLineEdit(readLine("/", pager.attrs.width, config = pager.config, tty = pager.tty), SEARCH_F) -proc newContainer(buffer: Buffer, parent: Container): Container = - new(result) - result.buffer = buffer - result.parent = parent +proc searchBackward(pager: Pager) {.jsfunc.} = + pager.setLineEdit(readLine("?", pager.attrs.width, config = pager.config, tty = pager.tty), SEARCH_B) + +proc isearchForward(pager: Pager) {.jsfunc.} = + pager.container.pushCursorPos() + pager.setLineEdit(readLine("/", pager.attrs.width, config = pager.config, tty = pager.tty), ISEARCH_F) -proc newPager*(config: Config, attrs: TermAttributes, loader: FileLoader, tty: File): Pager = +proc isearchBackward(pager: Pager) {.jsfunc.} = + pager.container.pushCursorPos() + pager.setLineEdit(readLine("?", pager.attrs.width, config = pager.config, tty = pager.tty), ISEARCH_B) + +proc newPager*(config: Config, attrs: TermAttributes, tty: File): Pager = new(result) result.config = config result.attrs = attrs - result.loader = loader result.tty = tty + result.selector = newSelector[Container]() + result.bwidth = attrs.width - 1 # writing to the last column is a bad idea it seems + result.bheight = attrs.height - 1 + result.display = newFixedGrid(result.bwidth, result.bheight) + +proc clearDisplay(pager: Pager) = + pager.display = newFixedGrid(pager.bwidth, pager.bheight) + +proc refreshDisplay*(pager: Pager, container = pager.container) = + var r: Rune + var by = 0 + pager.clearDisplay() + for line in container.ilines(container.fromy ..< min(container.fromy + pager.bheight, container.numLines)): + var w = 0 # width of the row so far + var i = 0 # byte in line.str + # Skip cells till buffer.fromx. + while w < container.fromx and i < line.str.len: + fastRuneAt(line.str, i, r) + w += r.width() + let dls = by * container.width # starting position of row in display + # Fill in the gap in case we skipped more cells than fromx mandates (i.e. + # we encountered a double-width character.) + var k = 0 + if w > container.fromx: + while k < w - container.fromx: + pager.display[dls + k].str &= ' ' + inc k + var cf = line.findFormat(w) + var nf = line.findNextFormat(w) + let startw = w # save this for later + # Now fill in the visible part of the row. + while i < line.str.len: + let pw = w + fastRuneAt(line.str, i, r) + w += r.width() + if w > container.fromx + pager.bwidth: + break # die on exceeding the width limit + if nf.pos != -1 and nf.pos <= pw: + cf = nf + nf = line.findNextFormat(pw) + pager.display[dls + k].str &= r + if cf.pos != -1: + pager.display[dls + k].format = cf.format + let tk = k + r.width() + while k < tk and k < pager.bwidth - 1: + inc k + # Then, for each cell that has a mark, override its formatting with that + # specified by the mark. + #TODO honestly this was always broken anyways. not sure about how to re-implement it + #var l = 0 + #while l < pager.marks.len and buffer.marks[l].y < by: + # inc l # linear search to find the first applicable mark + #let aw = buffer.width - (startw - buffer.fromx) # actual width + #while l < buffer.marks.len and buffer.marks[l].y == by: + # let mark = buffer.marks[l] + # inc l + # if mark.x >= startw + aw or mark.x + mark.width < startw: continue + # for i in max(mark.x, startw)..<min(mark.x + mark.width, startw + aw): + # buffer.display[dls + i - startw].format = mark.format + inc by + +func generateStatusMessage*(pager: Pager): string = + var format = newFormat() + var w = 0 + for cell in pager.statusmsg: + result &= format.processFormat(cell.format) + result &= cell.str + w += cell.width() + if w < pager.bwidth: + result &= EL() + +proc clearStatusMessage(pager: Pager) = + pager.statusmsg = newFixedGrid(pager.bwidth) + +proc writeStatusMessage(pager: Pager, str: string, format: Format = Format()) = + pager.clearStatusMessage() + var i = 0 + for r in str.runes: + i += r.width() + if i >= pager.statusmsg.len: + pager.statusmsg[^1].str = "$" + break + pager.statusmsg[i].str &= r + pager.statusmsg[i].format = format + +proc refreshStatusMsg*(pager: Pager) = + let container = pager.container + if container != nil: + var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" & + $container.atPercentOf() & "%) " & "<" & container.getTitle() & ">" + if container.hovertext.len > 0: + msg &= " " & container.hovertext + var format: Format + format.reverse = true + pager.writeStatusMessage(msg, format) + +func generateStatusOutput(pager: Pager): string = + if pager.status.len > 0: + result = pager.status[0] & EL() + pager.status = pager.status[1..^1] + else: + return pager.generateStatusMessage() + +func generateFullOutput(pager: Pager): string = + var x = 0 + var w = 0 + var format = newFormat() + result &= HVP(1, 1) + for cell in pager.display: + if x >= pager.bwidth: + result &= EL() + result &= "\r\n" + x = 0 + w = 0 + result &= format.processFormat(cell.format) + result &= cell.str + w += cell.width() + inc x + result &= EL() + result &= "\r\n" + +proc displayCursor*(pager: Pager) = + if pager.container == nil: return + print(HVP(pager.container.acursory + 1, pager.container.acursorx + 1)) + stdout.flushFile() + +proc displayStatus*(pager: Pager) = + if pager.lineedit.isNone: + pager.statusMode() + print(pager.generateStatusOutput()) + stdout.flushFile() + +proc displayPage*(pager: Pager) = + stdout.hideCursor() + print(SGR()) + print(pager.generateFullOutput()) + pager.displayStatus() + pager.displayCursor() + stdout.showCursor() + if pager.lineedit.isSome: + pager.statusMode() + pager.lineedit.get.writePrompt() + pager.lineedit.get.fullRedraw() + stdout.flushFile() -proc addBuffer*(pager: Pager, buffer: Buffer) = - var ncontainer = newContainer(buffer, pager.container) +proc redraw(pager: Pager) {.jsfunc.} = + pager.refreshDisplay() + pager.refreshStatusMsg() + pager.displayPage() + +proc addContainer*(pager: Pager, container: Container) = + container.parent = pager.container if pager.container != nil: - pager.container.children.add(ncontainer) - pager.setContainer(ncontainer) + pager.container.children.add(container) + pager.setContainer(container) + assert int(container.ifd) != 0 + pager.fdmap[container.ifd] = container + pager.selector.registerHandle(int(container.ifd), {Read}, pager.container) + +proc dupeContainer(pager: Pager, container: Container, location: Option[URL]): Container = + return container.dupeBuffer(pager.config, location) proc dupeBuffer*(pager: Pager, location = none(URL)) {.jsfunc.} = - var clone: Buffer - clone = pager.container.buffer.dupeBuffer(location) - pager.addBuffer(clone) + pager.addContainer(pager.dupeContainer(pager.container, location)) # The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT # commands by traversing the container tree in a depth-first order. @@ -244,15 +312,13 @@ proc prevBuffer*(pager: Pager): bool {.jsfunc.} = return false if pager.container.parent == nil: return false - for i in 0..pager.container.parent.children.high: - let child = pager.container.parent.children[i] - if child == pager.container: - if i > 0: - pager.setContainer(pager.container.parent.children[i - 1]) - else: - pager.setContainer(pager.container.parent) - return true - assert false, "Container not a child of its parent" + let n = pager.container.parent.children.find(pager.container) + assert n != -1, "Container not a child of its parent" + if n > 0: + pager.setContainer(pager.container.parent.children[n - 1]) + else: + pager.setContainer(pager.container.parent) + return true proc nextBuffer*(pager: Pager): bool {.jsfunc.} = if pager.container == nil: @@ -262,95 +328,97 @@ proc nextBuffer*(pager: Pager): bool {.jsfunc.} = return true if pager.container.parent == nil: return false - for i in countdown(pager.container.parent.children.high, 0): - let child = pager.container.parent.children[i] - if child == pager.container: - if i < pager.container.parent.children.high: - pager.setContainer(pager.container.parent.children[i + 1]) - return true - return false - assert false, "Container not a child of its parent" + let n = pager.container.parent.children.find(pager.container) + assert n != -1, "Container not a child of its parent" + if n < pager.container.parent.children.high: + pager.setContainer(pager.container.parent.children[n + 1]) + return true + return false -#TODO we should have a separate status message stack for all buffers AND the -# pager. -proc setStatusMessage(pager: Pager, msg: string) = - if pager.container != nil: - pager.container.buffer.setStatusMessage(msg) +proc setStatusMessage*(pager: Pager, msg: string) = + pager.status.add(msg) + pager.refreshStatusMsg() + +proc lineInfo(pager: Pager) {.jsfunc.} = + pager.setStatusMessage(pager.container.lineInfo()) + +proc deleteContainer(pager: Pager, container: Container) = + if container.parent == nil and container.children.len == 0 and container != pager.container: + return + if container.parent != nil: + let parent = container.parent + let n = parent.children.find(container) + assert n != -1, "Container not a child of its parent" + for i in countdown(container.children.high, 0): + let child = container.children[i] + child.parent = container.parent + parent.children.insert(child, n + 1) + parent.children.delete(n) + if container == pager.container: + pager.setContainer(parent) + elif container.children.len > 0: + let parent = container.children[0] + parent.parent = nil + for i in 1..container.children.high: + container.children[i].parent = parent + parent.children.add(container.children[i]) + if container == pager.container: + pager.setContainer(parent) else: - pager.status.add(msg) + for child in container.children: + child.parent = nil + if container == pager.container: + pager.setContainer(nil) + container.parent = nil + container.children.setLen(0) + pager.fdmap.del(container.ifd) + pager.selector.unregister(int(container.ifd)) + container.istream.close() + container.ostream.close() proc discardBuffer*(pager: Pager) {.jsfunc.} = - if pager.container.parent == nil and pager.container.children.len == 0: + if pager.container == nil or pager.container.parent == nil and + pager.container.children.len == 0: pager.setStatusMessage("Cannot discard last buffer!") else: - if pager.container.parent != nil: - let parent = pager.container.parent - let n = parent.children.find(pager.container) - assert n != -1, "Container not a child of its parent" - for i in countdown(pager.container.children.high, 0): - let child = pager.container.children[i] - child.parent = pager.container.parent - parent.children.insert(child, n + 1) - parent.children.delete(n) - pager.setContainer(parent) - else: - pager.setContainer(pager.container.children[0]) - pager.container.parent = nil - -proc drawBuffer*(pager: Pager) {.jsfunc.} = - pager.container.buffer.drawBuffer() #TODO move this to pager + pager.deleteContainer(pager.container) proc toggleSource*(pager: Pager) {.jsfunc.} = if pager.container.sourcepair != nil: pager.setContainer(pager.container.sourcepair) else: - let buffer = newBuffer(pager.config, pager.tty) - buffer.source = pager.container.buffer.source - buffer.streamclosed = true - buffer.location = pager.container.buffer.location - buffer.ispipe = pager.container.buffer.ispipe - if pager.container.buffer.contenttype == "text/plain": - buffer.contenttype = "text/html" + let contenttype = if pager.container.contenttype.get("") == "text/html": + some("text/plain") else: - buffer.contenttype = "text/plain" - buffer.setupBuffer() - let container = newContainer(buffer, pager.container) + some("text/html") + let container = pager.container.dupeBuffer(pager.config, contenttype = contenttype) container.sourcepair = pager.container pager.container.sourcepair = container pager.container.children.add(container) # Load request in a new buffer. -proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), force = false, ctype = "", replace = false): bool {.discardable.} = - if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or +proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) = + if prevurl.isnone or not prevurl.get.equals(request.url, true) or request.url.hash == "" or request.httpmethod != HTTP_GET: # Basically, we want to reload the page *only* when - # a) force == true + # a) we force a reload (by setting prevurl to none) # b) or the new URL isn't just the old URL + an anchor # I think this makes navigation pretty natural, or at least very close to # what other browsers do. Still, it would be nice if we got some visual # feedback on what is actually going to happen when typing a URL; TODO. - let response = pager.loader.doRequest(request) - if response.body != nil: - let buffer = newBuffer(pager.config, pager.tty) - buffer.contenttype = if ctype != "": ctype else: response.contenttype - buffer.istream = response.body - buffer.location = request.url - buffer.setupBuffer() - if replace: - pager.discardBuffer() - pager.addBuffer(buffer) - pager.container.needsauth = response.status == 401 # Unauthorized - pager.container.redirecturl = response.redirect - else: - pager.setStatusMessage("Couldn't load " & $request.url & " (" & $response.res & ")") - return false + let source = BufferSource( + t: LOAD_REQUEST, + request: request, + contenttype: ctype, + location: request.url + ) + let container = newBuffer(pager.config, source, pager.tty.getFileHandle()) + container.replace = replace + pager.addContainer(container) + container.load() else: - if pager.container.buffer.hasAnchor(request.url.anchor): - pager.dupeBuffer(request.url.some) - else: - pager.setStatusMessage("Couldn't find anchor " & request.url.anchor) - return false - return true + pager.container.redirect = some(request.url) + pager.container.gotoAnchor(request.url.anchor) # When the user has passed a partial URL as an argument, they might've meant # either: @@ -358,90 +426,198 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), force = false # * https://<url> # So we attempt to load both, and see what works. # (TODO: make this optional) -proc loadURL*(pager: Pager, url: string, force = false, ctype = "") = +proc loadURL*(pager: Pager, url: string, ctype = none(string)) = let firstparse = parseURL(url) if firstparse.issome: let prev = if pager.container != nil: - some(pager.container.buffer.location) + some(pager.container.source.location) else: none(URL) - pager.gotoURL(newRequest(firstparse.get), prev, force, ctype) + pager.gotoURL(newRequest(firstparse.get), prev, ctype) return + var urls: seq[URL] + let pageurl = parseURL("https://" & url) + if pageurl.isSome: # attempt to load remote page + urls.add(pageurl.get) let cdir = parseURL("file://" & getCurrentDir() & DirSep) - let newurl = parseURL(url, cdir) - if newurl.isSome: - # attempt to load local file - if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype): - return - block: - let purl = percentEncode(url, LocalPathPercentEncodeSet) - if purl != url: - let newurl = parseURL(purl, cdir) - if newurl.isSome: - if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype): - pager.status.setLen(0) - return - block: - let newurl = parseURL("https://" & url) + let purl = percentEncode(url, LocalPathPercentEncodeSet) + if purl != url: + let newurl = parseURL(purl, cdir) if newurl.isSome: - # attempt to load remote page - if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype): - pager.status.setLen(0) - return - pager.setStatusMessage("Invalid URL " & url) + urls.add(newurl.get) + let localurl = parseURL(url, cdir) + if localurl.isSome: # attempt to load local file + urls.add(localurl.get) + if urls.len == 0: + pager.setStatusMessage("Invalid URL " & url) + else: + let prevc = pager.container + pager.gotoURL(newRequest(urls.pop()), ctype = ctype) + if pager.container != prevc: + pager.container.retry = urls + +proc readPipe*(pager: Pager, ctype: Option[string]) = + let source = BufferSource( + t: LOAD_PIPE, + fd: stdin.getFileHandle(), + contenttype: some(ctype.get("text/plain")), + location: parseUrl("file://-").get + ) + let container = newBuffer(pager.config, source, pager.tty.getFileHandle(), ispipe = true) + pager.addContainer(container) + container.load() + +proc updateReadLineISearch(pager: Pager, linemode: LineMode) = + let lineedit = pager.lineedit.get + case lineedit.state + of CANCEL: + pager.iregex = none(Regex) + pager.container.popCursorPos() + of EDIT: + let x = $lineedit.news + if x != "": pager.iregex = compileSearchRegex(x) + pager.container.popCursorPos() + if pager.iregex.isSome: + if linemode == ISEARCH_F: + pager.container.cursorNextMatch(pager.iregex.get, true) + else: + pager.container.cursorPrevMatch(pager.iregex.get, true) + pager.container.pushCursorPos() + pager.displayPage() + pager.statusMode() + pager.lineedit.get.fullRedraw() + of FINISH: + if pager.iregex.isSome: + pager.regex = pager.iregex + pager.reverseSearch = linemode == ISEARCH_B + +proc updateReadLine*(pager: Pager) = + let lineedit = pager.lineedit.get + template s: string = $lineedit.news + if pager.linemode in {ISEARCH_F, ISEARCH_B}: + pager.updateReadLineISearch(pager.linemode) + else: + case lineedit.state + of EDIT: return + of FINISH: + case pager.linemode + of LOCATION: pager.loadURL(s) + of USERNAME: + pager.username = s + pager.setLineEdit(readLine("Password: ", pager.attrs.width, hide = true, config = pager.config, tty = pager.tty), PASSWORD) + of PASSWORD: + let url = newURL(pager.container.source.location) + url.username = pager.username + url.password = s + pager.username = "" + pager.gotoURL(newRequest(url), some(pager.container.source.location), replace = pager.container) + of COMMAND: pager.command = s + of BUFFER: pager.container.readSuccess(s) + of SEARCH_F: + let x = s + if x != "": pager.regex = compileSearchRegex(x) + pager.reverseSearch = false + pager.searchNext() + of SEARCH_B: + let x = s + if x != "": pager.regex = compileSearchRegex(x) + pager.reverseSearch = true + pager.searchPrev() + else: discard + of CANCEL: + case pager.linemode + of USERNAME: pager.discardBuffer() + of PASSWORD: + pager.username = "" + pager.discardBuffer() + of BUFFER: pager.container.readCanceled() + else: discard + if lineedit.state in {CANCEL, FINISH}: + if pager.lineedit.get == lineedit: + pager.clearLineEdit() + print('\r') + print(EL()) + pager.displayPage() # Open a URL prompt and visit the specified URL. proc changeLocation(pager: Pager) {.jsfunc.} = - var url = pager.container.buffer.location.serialize() - pager.statusMode() - let status = readLine("URL: ", url, pager.attrs.width, config = pager.config, tty = pager.tty) - if status: - pager.loadURL(url) + var url = pager.container.source.location.serialize() + pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, config = pager.config, tty = pager.tty), LOCATION) # Reload the page in a new buffer, then kill the previous buffer. proc reloadPage(pager: Pager) {.jsfunc.} = - pager.gotoURL(newRequest(pager.container.buffer.location), none(URL), true, pager.container.buffer.contenttype, true) + pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container) proc click(pager: Pager) {.jsfunc.} = - #TODO this conflicts with the planned event loop - let req = pager.container.buffer.click() - if req.issome: - pager.gotoURL(req.get, pager.container.buffer.location.some) - -proc followRedirect*(pager: Pager) - -proc checkAuth*(pager: Pager) = - if pager.container != nil and pager.container.needsauth: - pager.container.buffer.refreshBuffer() - pager.statusMode() - var username = "" - let ustatus = readLine("Username: ", username, pager.attrs.width, config = pager.config, tty = pager.tty) - if not ustatus: - pager.container.needsauth = false - return - pager.statusMode() - var password = "" - let pstatus = readLine("Password: ", password, pager.attrs.width, hide = true, config = pager.config, tty = pager.tty) - if not pstatus: - pager.container.needsauth = false - return - var url = pager.container.buffer.location - url.username = username - url.password = password - pager.gotoURL(newRequest(url), prevurl = some(pager.container.buffer.location), replace = true) - pager.followRedirect() - -proc followRedirect*(pager: Pager) = - while pager.container != nil and pager.container.redirecturl.issome: - pager.statusMode() - print("Redirecting to ", $pager.container.redirecturl.get) - stdout.flushFile() - pager.container.buffer.refreshBuffer(true) - let redirecturl = pager.container.redirecturl.get - pager.container.redirecturl = none(URL) - pager.gotoURL(newRequest(redirecturl), prevurl = some(pager.container.buffer.location), replace = true) - if pager.container.needsauth: - pager.checkAuth() + pager.container.click() + +proc authorize*(pager: Pager) = + pager.setLineEdit(readLine("Username: ", pager.attrs.width, config = pager.config, tty = pager.tty), USERNAME) + +proc handleEvent*(pager: Pager, container: Container): bool = + let event = container.handleEvent() + case event.t + of FAIL: + pager.deleteContainer(container) + if container.retry.len > 0: + pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype) + else: + pager.setStatusMessage("Couldn't load " & $container.source.location & " (error code " & $container.code & ")") + pager.displayStatus() + pager.displayCursor() + if pager.container == nil: + return false + of SUCCESS: + container.render() + if container.replace != nil: + container.children.add(container.replace.children) + for child in container.children: + child.parent = container + container.replace.children.setLen(0) + if container.replace.parent != nil: + container.parent = container.replace.parent + let n = container.replace.parent.children.find(container.replace) + assert n != -1, "Container not a child of its parent" + container.parent.children[n] = container + if pager.container == container.replace: + pager.setContainer(container) + of NEEDS_AUTH: + if pager.container == container: + pager.authorize() + of REDIRECT: + let redirect = container.redirect.get + pager.setStatusMessage("Redirecting to " & $redirect) + pager.displayStatus() + pager.displayCursor() + pager.gotoURL(newRequest(redirect), some(pager.container.source.location), replace = pager.container) + of ANCHOR: + pager.addContainer(pager.dupeContainer(container, container.redirect)) + of NO_ANCHOR: + pager.setStatusMessage("Couldn't find anchor " & container.redirect.get.anchor) + pager.displayStatus() + pager.displayCursor() + of UPDATE: + if container == pager.container: + pager.refreshDisplay() + pager.refreshStatusMsg() + pager.displayPage() + of JUMP: + if container == pager.container: + pager.refreshStatusMsg() + pager.displayStatus() + pager.displayCursor() + of STATUS: + if container == pager.container: + pager.refreshStatusMsg() + pager.displayStatus() + pager.displayCursor() + of READ_LINE: + if container == pager.container: + pager.setLineEdit(readLine(event.prompt, pager.bwidth, current = event.value, hide = event.password, config = pager.config, tty = pager.tty), BUFFER) + of OPEN: + pager.gotoURL(event.request, some(container.source.location)) + of NO_EVENT: discard + return true proc addPagerModule*(ctx: JSContext) = ctx.registerType(Pager) diff --git a/src/io/buffer.nim b/src/io/buffer.nim deleted file mode 100644 index f4cce8c4..00000000 --- a/src/io/buffer.nim +++ /dev/null @@ -1,1285 +0,0 @@ -import algorithm -import options -import os -import streams -import tables -import terminal -import unicode - -import css/cascade -import css/cssparser -import css/mediaquery -import css/sheet -import css/stylednode -import config/config -import html/dom -import html/tags -import html/htmlparser -import io/cell -import io/lineedit -import io/loader -import io/request -import io/term -import js/regex -import layout/box -import render/renderdocument -import render/rendertext -import types/color -import types/url -import utils/twtstr - -type - CursorPosition* = object - cursorx*: int - cursory*: int - xend*: int - fromx*: int - fromy*: int - - BufferMatch* = object - success*: bool - x*: int - y*: int - str*: string - - Buffer* = ref object - contenttype*: string - title*: string - lines*: FlexibleGrid - display*: FixedGrid - prevdisplay*: FixedGrid - statusmsg*: FixedGrid - hovertext*: string - width*: int - height*: int - cpos*: CursorPosition - attrs*: TermAttributes - document*: Document - viewport*: Viewport - prevstyled*: StyledNode - redraw*: bool - reshape*: bool - nostatus*: bool - location*: Url - ispipe*: bool - istream*: Stream - streamclosed*: bool - source*: string - prevnode*: StyledNode - userstyle*: CSSStylesheet - loader*: FileLoader - marks*: seq[Mark] - config*: Config - tty: File - -proc newBuffer*(config: Config, tty: File): Buffer = - new(result) - result.attrs = getTermAttributes(stdout) - result.width = result.attrs.width - result.height = result.attrs.height - 1 - result.config = config - result.loader = newFileLoader() - - result.display = newFixedGrid(result.width, result.height) - result.prevdisplay = newFixedGrid(result.width, result.height) - result.statusmsg = newFixedGrid(result.width) - -func cursorx*(buffer: Buffer): int {.inline.} = buffer.cpos.cursorx -func cursory*(buffer: Buffer): int {.inline.} = buffer.cpos.cursory -func fromx*(buffer: Buffer): int {.inline.} = buffer.cpos.fromx -func fromy*(buffer: Buffer): int {.inline.} = buffer.cpos.fromy -func xend*(buffer: Buffer): int {.inline.} = buffer.cpos.xend - -func generateFullOutput*(buffer: Buffer): string = - var x = 0 - var w = 0 - var format = newFormat() - result &= HVP(1, 1) - - for cell in buffer.display: - if x >= buffer.width: - result &= EL() - result &= "\r\n" - x = 0 - w = 0 - - result &= format.processFormat(cell.format) - result &= cell.str - - w += cell.width() - inc x - - result &= EL() - result &= "\r\n" - -# generate a sequence of instructions to replace the previous frame with the -# current one. ideally should be used when small changes are made (e.g. hover -# changes underlining) -func generateSwapOutput(buffer: Buffer): string = - var format = newFormat() - let curr = buffer.display - let prev = buffer.prevdisplay - var i = 0 - var x = 0 - var y = 0 - var line = "" - var lr = false - while i < curr.len: - if x >= buffer.width: - if lr: - result &= HVP(y + 1, 1) - result &= EL() - result &= line - lr = false - x = 0 - inc y - line = "" - lr = lr or (curr[i] != prev[i]) - line &= format.processFormat(curr[i].format) - line &= curr[i].str - inc i - inc x - if lr: - result &= HVP(y + 1, 1) - result &= EL() - result &= line - lr = false - - #TODO maybe fix this - #var x = 0 - #var y = 0 - #var cx = -1 - #var cy = -1 - #var i = 0 - #var text = "" - #while i < max: - # if x >= buffer.width: - # x = 0 - # inc y - - # if curr[i] != prev[i]: - # let currwidth = curr[i].runes.width() - # let prevwidth = prev[i].runes.width() - # if (curr[i].runes.len > 0 or currwidth < prevwidth) and (x != cx or y != cy): - # if text.len > 0: - # result &= text - # text = "" - # result &= HVP(y + 1, x + 1) - # cx = x - # cy = y - - # text &= format.processFormat(curr[i].format) - - # text &= $curr[i].runes - # if currwidth < prevwidth: - # var j = 0 - # while j < prevwidth - currwidth: - # text &= ' ' - # inc j - # if text.len > 0: - # inc cx - - # inc x - # inc i - #if text.len > 0: - # result &= $text - -func generateStatusMessage*(buffer: Buffer): string = - var format = newFormat() - var w = 0 - for cell in buffer.statusmsg: - result &= format.processFormat(cell.format) - result &= cell.str - w += cell.width() - if w < buffer.width: - result &= EL() - -func numLines(buffer: Buffer): int = buffer.lines.len - -func lastVisibleLine(buffer: Buffer): int = min(buffer.fromy + buffer.height, buffer.numLines) - -func currentLineWidth(buffer: Buffer): int = - return buffer.lines[buffer.cursory].width() - -func maxfromy(buffer: Buffer): int = max(buffer.numLines - buffer.height, 0) - -func maxfromx(buffer: Buffer): int = max(buffer.currentLineWidth() - buffer.width, 0) - -func acursorx(buffer: Buffer): int = - return max(0, buffer.cursorx - buffer.fromx) - -func acursory(buffer: Buffer): int = - return buffer.cursory - buffer.fromy - -func cellOrigin(buffer: Buffer, x, y: int): int = - let row = y * buffer.width - var ox = x - while ox > 0 and buffer.display[row + ox].str.len == 0: - dec ox - return ox - -func currentCellOrigin(buffer: Buffer): int = - return buffer.cellOrigin(buffer.acursorx, buffer.acursory) - -func currentDisplayCell(buffer: Buffer): FixedCell = - let row = (buffer.cursory - buffer.fromy) * buffer.width - return buffer.display[row + buffer.currentCellOrigin()] - -func getLink(node: StyledNode): HTMLAnchorElement = - if node == nil: - return nil - if node.t == STYLED_ELEMENT and node.node != nil and Element(node.node).tagType == TAG_A: - return HTMLAnchorElement(node.node) - if node.node != nil: - return HTMLAnchorElement(node.node.findAncestor({TAG_A})) - #TODO ::before links? - -const ClickableElements = { - TAG_A, TAG_INPUT, TAG_OPTION -} - -func getClickable(styledNode: StyledNode): Element = - if styledNode == nil or styledNode.node == nil: - return nil - if styledNode.t == STYLED_ELEMENT: - let element = Element(styledNode.node) - if element.tagType in ClickableElements: - return element - styledNode.node.findAncestor(ClickableElements) - -func getCursorClickable(buffer: Buffer): Element = - return buffer.currentDisplayCell().node.getClickable() - -func currentLine(buffer: Buffer): string = - return buffer.lines[buffer.cursory].str - -func cursorBytes(buffer: Buffer, y: int, cc = buffer.fromx + buffer.cursorx): int = - assert y < buffer.lines.len - let line = buffer.lines[y].str - var w = 0 - var i = 0 - while i < line.len and w < cc: - var r: Rune - fastRuneAt(line, i, r) - w += r.width() - return i - -func currentCursorBytes(buffer: Buffer, cc = buffer.fromx + buffer.cursorx): int = - return buffer.cursorBytes(buffer.cursory, cc) - -func currentWidth(buffer: Buffer): int = - let line = buffer.currentLine - if line.len == 0: return 0 - var w = 0 - var i = 0 - let cc = buffer.fromx + buffer.cursorx - var r: Rune - fastRuneAt(line, i, r) - while i < line.len and w < cc: - fastRuneAt(line, i, r) - w += r.width() - return r.width() - -func prevWidth(buffer: Buffer): int = - let line = buffer.currentLine - if line.len == 0: return 0 - var w = 0 - var i = 0 - let cc = buffer.fromx + buffer.cursorx - var pr: Rune - var r: Rune - fastRuneAt(line, i, r) - while i < line.len and w < cc: - pr = r - fastRuneAt(line, i, r) - w += r.width() - return pr.width() - -func maxScreenWidth(buffer: Buffer): int = - for line in buffer.lines[buffer.fromy..buffer.lastVisibleLine - 1]: - result = max(line.width(), result) - -func atPercentOf(buffer: Buffer): int = - if buffer.lines.len == 0: return 100 - return (100 * (buffer.cursory + 1)) div buffer.numLines - -func hasAnchor*(buffer: Buffer, anchor: string): bool = - return buffer.document.getElementById(anchor) != nil - -func getTitle(buffer: Buffer): string = - if buffer.document != nil: - result = buffer.document.title - if result != "": return result - if buffer.ispipe: - return "*pipe*" - return buffer.location.serialize(excludepassword = true) - -proc clearDisplay(buffer: Buffer) = - buffer.prevdisplay = buffer.display - buffer.display = newFixedGrid(buffer.width, buffer.height) - -proc refreshDisplay(buffer: Buffer) = - var r: Rune - var y = 0 # y position on screen - buffer.clearDisplay() - - for by in buffer.fromy..buffer.lastVisibleLine - 1: - let line = buffer.lines[by] # by: y position in lines - var w = 0 # width of the row so far - var i = 0 # byte in line.str - - # Skip cells till buffer.fromx. - while w < buffer.fromx and i < line.str.len: - fastRuneAt(line.str, i, r) - w += r.width() - - let dls = y * buffer.width # starting position of row in display - - # Fill in the gap in case we skipped more cells than fromx mandates (i.e. - # we encountered a double-width character.) - var k = 0 - if w > buffer.fromx: - while k < w - buffer.fromx: - buffer.display[dls + k].str &= ' ' - inc k - - var cf = line.findFormat(w) - var nf = line.findNextFormat(w) - - let startw = w # save this for later - - # Now fill in the visible part of the row. - while i < line.str.len: - let pw = w - fastRuneAt(line.str, i, r) - w += r.width() - if w > buffer.fromx + buffer.width: - break # die on exceeding the width limit - if nf.pos != -1 and nf.pos <= pw: - cf = nf - nf = line.findNextFormat(pw) - buffer.display[dls + k].str &= r - if cf.pos != -1: - buffer.display[dls + k].format = cf.format - buffer.display[dls + k].node = cf.node - let tk = k + r.width() - while k < tk and k < buffer.width - 1: - inc k - - # Then, for each cell that has a mark, override its formatting with that - # specified by the mark. - var l = 0 - while l < buffer.marks.len and buffer.marks[l].y < by: - inc l # linear search to find the first applicable mark - let aw = buffer.width - (startw - buffer.fromx) # actual width - while l < buffer.marks.len and buffer.marks[l].y == by: - let mark = buffer.marks[l] - inc l - if mark.x >= startw + aw or mark.x + mark.width < startw: continue - for i in max(mark.x, startw)..<min(mark.x + mark.width, startw + aw): - buffer.display[dls + i - startw].format = mark.format - - inc y - -proc setCursorX(buffer: Buffer, x: int, refresh = true, save = true) = - if (not refresh) or (buffer.fromx <= x and x < buffer.fromx + buffer.width): - buffer.cpos.cursorx = x - else: - if refresh and buffer.fromx > buffer.cursorx: - buffer.cpos.fromx = max(buffer.currentLineWidth() - 1, 0) - buffer.cpos.cursorx = buffer.fromx - elif x > buffer.cursorx: - buffer.cpos.fromx = max(x - buffer.width + 1, 0) - buffer.cpos.cursorx = x - elif x < buffer.cursorx: - buffer.cpos.fromx = x - buffer.cpos.cursorx = x - buffer.redraw = true - if save: - buffer.cpos.xend = buffer.cursorx - -proc restoreCursorX(buffer: Buffer) = - buffer.setCursorX(max(min(buffer.currentLineWidth() - 1, buffer.xend), 0), false, false) - -proc setCursorY(buffer: Buffer, y: int) = - if buffer.cursory == y: - return - if y - buffer.fromy >= 0 and y - buffer.height < buffer.fromy: - buffer.cpos.cursory = y - else: - if y > buffer.cursory: - buffer.cpos.fromy = max(y - buffer.height + 1, 0) - else: - buffer.cpos.fromy = min(y, buffer.maxfromy) - buffer.cpos.cursory = y - buffer.redraw = true - buffer.restoreCursorX() - -proc centerLine*(buffer: Buffer) = - let ny = max(min(buffer.cursory - buffer.height div 2, buffer.numLines - buffer.height), 0) - if ny != buffer.fromy: - buffer.cpos.fromy = ny - buffer.redraw = true - -proc setCursorXY*(buffer: Buffer, x, y: int) = - let fy = buffer.fromy - buffer.setCursorY(max(min(y, buffer.numLines - 1), 0)) - buffer.setCursorX(max(min(buffer.currentLineWidth(), x), 0)) - if fy != buffer.fromy: - buffer.centerLine() - -proc setFromXY*(buffer: Buffer, x, y: int) = - buffer.cpos.fromy = max(min(y, buffer.maxfromy), 0) - buffer.cpos.fromx = max(min(x, buffer.maxfromx), 0) - -proc cursorDown*(buffer: Buffer) = - if buffer.cursory < buffer.numLines - 1: - buffer.setCursorY(buffer.cursory + 1) - -proc cursorUp*(buffer: Buffer) = - if buffer.cursory > 0: - buffer.setCursorY(buffer.cursory - 1) - -proc cursorRight*(buffer: Buffer) = - let cellwidth = buffer.currentWidth() - if buffer.cursorx + cellwidth < buffer.currentLineWidth(): - buffer.setCursorX(buffer.cursorx + cellwidth) - -proc cursorLeft*(buffer: Buffer) = - buffer.setCursorX(max(buffer.cursorx - buffer.prevWidth(), 0)) - -proc cursorLineBegin*(buffer: Buffer) = - buffer.setCursorX(0) - -proc cursorLineEnd*(buffer: Buffer) = - buffer.setCursorX(max(buffer.currentLineWidth() - 1, 0)) - -proc cursorNextWord*(buffer: Buffer) = - var r: Rune - var b = buffer.currentCursorBytes() - var x = buffer.cursorx - while b < buffer.currentLine.len: - let pb = b - fastRuneAt(buffer.currentLine, b, r) - if r.breaksWord(): - b = pb - break - x += r.width() - - while b < buffer.currentLine.len: - let pb = b - fastRuneAt(buffer.currentLine, b, r) - if not r.breaksWord(): - b = pb - break - x += r.width() - - if b < buffer.currentLine.len: - buffer.setCursorX(x) - else: - if buffer.cursory < buffer.numLines - 1: - buffer.cursorDown() - buffer.cursorLineBegin() - else: - buffer.cursorLineEnd() - -proc cursorPrevWord*(buffer: Buffer) = - var b = buffer.currentCursorBytes() - var x = buffer.cursorx - if buffer.currentLine.len > 0: - b = min(b, buffer.currentLine.len - 1) - while b >= 0: - let (r, o) = lastRune(buffer.currentLine, b) - if r.breaksWord(): - break - b -= o - x -= r.width() - - while b >= 0: - let (r, o) = lastRune(buffer.currentLine, b) - if not r.breaksWord(): - break - b -= o - x -= r.width() - else: - b = -1 - - if b >= 0: - buffer.setCursorX(x) - else: - if buffer.cursory > 0: - buffer.cursorUp() - buffer.cursorLineEnd() - else: - buffer.cursorLineBegin() - -proc cursorNextLink*(buffer: Buffer) = - let line = buffer.lines[buffer.cursory] - var i = line.findFormatN(buffer.cursorx) - 1 - var link: Element = nil - if i >= 0: - link = line.formats[i].node.getClickable() - inc i - - while i < line.formats.len: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - buffer.setCursorX(format.pos) - return - inc i - - for y in (buffer.cursory + 1)..(buffer.numLines - 1): - let line = buffer.lines[y] - i = 0 - while i < line.formats.len: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - buffer.setCursorXY(format.pos, y) - return - inc i - -proc cursorPrevLink*(buffer: Buffer) = - let line = buffer.lines[buffer.cursory] - var i = line.findFormatN(buffer.cursorx) - 1 - var link: Element = nil - if i >= 0: - link = line.formats[i].node.getClickable() - dec i - - var ly = 0 #last y - var lx = 0 #last x - template link_beginning() = - #go to beginning of link - ly = y #last y - lx = format.pos #last x - - #on the current line - let line = buffer.lines[y] - while i >= 0: - let format = line.formats[i] - let nl = format.node.getClickable() - if nl == fl: - lx = format.pos - dec i - - #on previous lines - for iy in countdown(ly - 1, 0): - let line = buffer.lines[iy] - i = line.formats.len - 1 - while i >= 0: - let format = line.formats[i] - let nl = format.node.getClickable() - if nl == fl: - ly = iy - lx = format.pos - dec i - - while i >= 0: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - let y = buffer.cursory - link_beginning - buffer.setCursorXY(lx, ly) - return - dec i - - for y in countdown(buffer.cursory - 1, 0): - let line = buffer.lines[y] - i = line.formats.len - 1 - while i >= 0: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - link_beginning - buffer.setCursorXY(lx, ly) - return - dec i - -proc cursorFirstLine*(buffer: Buffer) = - buffer.setCursorY(0) - -proc cursorLastLine*(buffer: Buffer) = - buffer.setCursorY(buffer.numLines - 1) - -proc cursorTop*(buffer: Buffer) = - buffer.setCursorY(buffer.fromy) - -proc cursorMiddle*(buffer: Buffer) = - buffer.setCursorY(min(buffer.fromy + (buffer.height - 2) div 2, buffer.numLines - 1)) - -proc cursorBottom*(buffer: Buffer) = - buffer.setCursorY(min(buffer.fromy + buffer.height - 1, buffer.numLines - 1)) - -proc cursorLeftEdge*(buffer: Buffer) = - buffer.setCursorX(buffer.fromx) - -proc cursorVertMiddle*(buffer: Buffer) = - buffer.setCursorX(min(buffer.fromx + (buffer.width - 2) div 2, buffer.currentLineWidth)) - -proc cursorRightEdge*(buffer: Buffer) = - buffer.setCursorX(min(buffer.fromx + buffer.width - 1, buffer.currentLineWidth)) - -proc halfPageUp*(buffer: Buffer) = - buffer.cpos.cursory = max(buffer.cursory - buffer.height div 2 + 1, 0) - let nfy = max(0, buffer.fromy - buffer.height div 2 + 1) - if nfy != buffer.fromy: - buffer.cpos.fromy = nfy - buffer.redraw = true - buffer.restoreCursorX() - -proc halfPageDown*(buffer: Buffer) = - buffer.cpos.cursory = min(buffer.cursory + buffer.height div 2 - 1, buffer.numLines - 1) - let nfy = min(max(buffer.numLines - buffer.height, 0), buffer.fromy + buffer.height div 2 - 1) - if nfy != buffer.fromy: - buffer.cpos.fromy = nfy - buffer.redraw = true - buffer.restoreCursorX() - -proc pageUp*(buffer: Buffer) = - buffer.cpos.cursory = max(buffer.cursory - buffer.height, 0) - let nfy = max(0, buffer.fromy - buffer.height) - if nfy != buffer.fromy: - buffer.cpos.fromy = nfy - buffer.redraw = true - buffer.restoreCursorX() - -proc pageDown*(buffer: Buffer) = - buffer.cpos.cursory = min(buffer.cursory + buffer.height, buffer.numLines - 1) - let nfy = min(buffer.fromy + buffer.height, max(buffer.numLines - buffer.height, 0)) - if nfy != buffer.fromy: - buffer.cpos.fromy = nfy - buffer.redraw = true - buffer.restoreCursorX() - -proc pageLeft*(buffer: Buffer) = - buffer.cpos.cursorx = max(buffer.cursorx - buffer.width, 0) - let nfx = max(0, buffer.fromx - buffer.width) - if nfx != buffer.fromx: - buffer.cpos.fromx = nfx - buffer.redraw = true - -proc pageRight*(buffer: Buffer) = - buffer.cpos.cursorx = min(buffer.fromx, buffer.currentLineWidth()) - let nfx = min(max(buffer.maxScreenWidth() - buffer.width, 0), buffer.fromx + buffer.width) - if nfx != buffer.fromx: - buffer.cpos.fromx = nfx - buffer.redraw = true - -proc scrollDown*(buffer: Buffer) = - if buffer.fromy + buffer.height < buffer.numLines: - inc buffer.cpos.fromy - if buffer.fromy > buffer.cursory: - buffer.cursorDown() - buffer.redraw = true - else: - buffer.cursorDown() - -proc scrollUp*(buffer: Buffer) = - if buffer.fromy > 0: - dec buffer.cpos.fromy - if buffer.fromy + buffer.height <= buffer.cursory: - buffer.cursorUp() - buffer.redraw = true - else: - buffer.cursorUp() - -proc scrollRight*(buffer: Buffer) = - if buffer.fromx + buffer.width < buffer.maxScreenWidth(): - inc buffer.cpos.fromx - buffer.redraw = true - -proc scrollLeft*(buffer: Buffer) = - if buffer.fromx > 0: - dec buffer.cpos.fromx - if buffer.cursorx < buffer.fromx: - buffer.setCursorX(max(buffer.currentLineWidth() - 1, 0)) - buffer.redraw = true - -proc gotoAnchor*(buffer: Buffer) = - if buffer.document == nil: return - let anchor = buffer.document.getElementById(buffer.location.anchor) - if anchor == nil: return - for y in 0..<buffer.numLines: - let line = buffer.lines[y] - var i = 0 - while i < line.formats.len: - let format = line.formats[i] - if format.node != nil and anchor in format.node.node: - buffer.setCursorY(y) - buffer.centerLine() - buffer.setCursorX(format.pos) - return - inc i - -proc addMark*(buffer: Buffer, x, y, width: int): Mark = - assert y < buffer.lines.len - var format = newFormat() - format.bgcolor = buffer.config.markcolor - result = Mark(x: x, y: y, width: width, format: format) - let previ = upperBound(buffer.marks, y, (proc(a: Mark, b: int): int = cmp(a.y, b))) - buffer.marks.insert(result, previ) - -proc removeMark*(buffer: Buffer, mark: Mark) = - let i = buffer.marks.find(mark) - if i != -1: - buffer.marks.delete(i) - -proc cursorNextMatch(buffer: Buffer, regex: Regex, sy, ey: int, wrap = false): BufferMatch = - for y in sy..ey: - let s = if y == buffer.cursory and not wrap: - buffer.currentCursorBytes(buffer.fromx + buffer.cursorx + 1) - else: - 0 - let res = regex.exec(buffer.lines[y].str, s) - if res.success and res.captures.len > 0: - let cap = res.captures[0] - let x = buffer.lines[y].str.width(cap.s) - buffer.setCursorXY(x, y) - result.success = true - result.y = y - result.x = x - result.str = buffer.lines[y].str.substr(cap.s, cap.e - 1) - return - -proc cursorNextMatch*(buffer: Buffer, regex: Regex, wrap = true): BufferMatch = - let s = buffer.currentCursorBytes(buffer.fromx + buffer.cursorx + 1) - var low = buffer.cursory - if s == buffer.lines.len: - low += 1 - if low > buffer.lines.high: - low = 0 - let ret = buffer.cursorNextMatch(regex, low, buffer.lines.high) - if ret.success: - return ret - if wrap: - return buffer.cursorNextMatch(regex, 0, low, true) - -proc cursorPrevMatch*(buffer: Buffer, regex: Regex, sy, ey: int, wrap = false): BufferMatch = - for y in countdown(sy, ey): - let e = if y == buffer.cursory and not wrap: - buffer.currentCursorBytes() - else: - buffer.lines[y].str.len + 1 - let res = regex.exec(buffer.lines[y].str) - if res.success: - for i in countdown(res.captures.high, 0): - let cap = res.captures[i] - if cap.s < e: - let x = buffer.lines[y].str.width(cap.s) - buffer.setCursorXY(x, y) - result.success = true - result.y = y - result.x = x - result.str = buffer.lines[y].str.substr(cap.s, cap.e - 1) - return - -proc cursorPrevMatch*(buffer: Buffer, regex: Regex, wrap = true): BufferMatch = - var high = buffer.cursory - if buffer.fromx + buffer.cursorx - 1 < 0: - high -= 1 - if high < 0: - high = buffer.lines.high - let ret = buffer.cursorPrevMatch(regex, high, 0) - if ret.success: - return ret - if wrap: - return buffer.cursorPrevMatch(regex, buffer.lines.high, high) - -proc refreshTermAttrs*(buffer: Buffer): bool = - let newAttrs = getTermAttributes(stdout) - if newAttrs != buffer.attrs: - buffer.attrs = newAttrs - buffer.width = newAttrs.width - buffer.height = newAttrs.height - 1 - return true - return false - -proc updateCursor(buffer: Buffer) = - if buffer.fromy > buffer.lastVisibleLine - 1: - buffer.cpos.fromy = 0 - buffer.cpos.cursory = buffer.lastVisibleLine - 1 - - if buffer.cursory >= buffer.lines.len: - buffer.cpos.cursory = max(0, buffer.lines.len - 1) - - if buffer.lines.len == 0: - buffer.cpos.cursory = 0 - -proc updateHover(buffer: Buffer) = - let thisnode = buffer.currentDisplayCell().node - let prevnode = buffer.prevnode - - if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node): - for styledNode in thisnode.branch: - if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: - let elem = Element(styledNode.node) - if not elem.hover: - elem.hover = true - buffer.reshape = true - - let link = thisnode.getLink() - if link != nil: - buffer.hovertext = link.href - else: - buffer.hovertext = "" - - for styledNode in prevnode.branch: - if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: - let elem = Element(styledNode.node) - if elem.hover: - elem.hover = false - buffer.reshape = true - - buffer.prevnode = thisnode - -proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) = - let url = parseUrl(elem.href, document.location.some) - if url.isSome: - let url = url.get - if url.scheme == buffer.location.scheme: - let media = elem.media - if media != "": - let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media))) - if not media.applies(): return - let fs = buffer.loader.doRequest(newRequest(url)) - if fs.body != nil and fs.contenttype == "text/css": - elem.sheet = parseStylesheet(fs.body) - -proc loadResources(buffer: Buffer, document: Document) = - var stack: seq[Element] - if document.html != nil: - stack.add(document.html) - while stack.len > 0: - let elem = stack.pop() - - if elem.tagType == TAG_LINK: - let elem = HTMLLinkElement(elem) - if elem.rel == "stylesheet": - buffer.loadResource(document, elem) - - for child in elem.children_rev: - stack.add(child) - -proc load*(buffer: Buffer) = - case buffer.contenttype - of "text/html": - if not buffer.streamclosed: - buffer.source = buffer.istream.readAll() - buffer.istream.close() - buffer.istream = newStringStream(buffer.source) - buffer.document = parseHTML5(buffer.istream) - buffer.streamclosed = true - else: - buffer.document = parseHTML5(newStringStream(buffer.source)) - buffer.document.location = buffer.location - buffer.loadResources(buffer.document) - else: - if not buffer.streamclosed: - buffer.source = buffer.istream.readAll() - buffer.istream.close() - buffer.streamclosed = true - buffer.lines = renderPlainText(buffer.source) - -proc render*(buffer: Buffer) = - case buffer.contenttype - of "text/html": - if buffer.viewport == nil: - buffer.viewport = Viewport(term: buffer.attrs) - if buffer.userstyle == nil: - buffer.userstyle = buffer.config.stylesheet.parseStylesheet() - let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled) - buffer.lines = ret[0] - buffer.prevstyled = ret[1] - else: discard - buffer.updateCursor() - -proc cursorBufferPos(buffer: Buffer) = - let x = buffer.acursorx - let y = buffer.acursory - print(HVP(y + 1, x + 1)) - -proc clearStatusMessage(buffer: Buffer) = - buffer.statusmsg = newFixedGrid(buffer.width) - -proc writeStatusMessage(buffer: Buffer, str: string, format: Format = Format()) = - buffer.clearStatusMessage() - var i = 0 - for r in str.runes: - i += r.width() - if i >= buffer.statusmsg.len: - buffer.statusmsg[^1].str = "$" - break - buffer.statusmsg[i].str &= r - buffer.statusmsg[i].format = format - -proc statusMsgForBuffer(buffer: Buffer) = - var msg = $(buffer.cursory + 1) & "/" & $buffer.numLines & " (" & - $buffer.atPercentOf() & "%) " & "<" & buffer.title & ">" - if buffer.hovertext.len > 0: - msg &= " " & buffer.hovertext - var format: Format - format.reverse = true - buffer.writeStatusMessage(msg, format) - -proc setStatusMessage*(buffer: Buffer, str: string) = - buffer.writeStatusMessage(str) - buffer.nostatus = true - -proc lineInfo*(buffer: Buffer) = - buffer.setStatusMessage("line " & $(buffer.cursory + 1) & "/" & $buffer.numLines & " col " & $(buffer.cursorx + 1) & "/" & $buffer.currentLineWidth() & " x: " & $buffer.currentCursorBytes()) - -proc displayBufferSwapOutput(buffer: Buffer) = - print(buffer.generateSwapOutput()) - -proc displayBuffer(buffer: Buffer) = - print(buffer.generateFullOutput()) - -proc displayStatusMessage*(buffer: Buffer) = - print(HVP(buffer.height + 1, 1)) - print(SGR()) - print(buffer.generateStatusMessage()) - print(SGR()) - -# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set -proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] = - if form.constructingentrylist: - return - form.constructingentrylist = true - - var entrylist: seq[tuple[name, value: string]] - for field in form.controls: - if field.findAncestor({TAG_DATALIST}) != nil or - field.attrb("disabled") or - field.isButton() and Element(field) != submitter: - continue - - if field.tagType == TAG_INPUT: - let field = HTMLInputElement(field) - if field.inputType == INPUT_IMAGE: - let name = if field.attr("name") != "": - field.attr("name") & '.' - else: - "" - entrylist.add((name & 'x', $field.xcoord)) - entrylist.add((name & 'y', $field.ycoord)) - continue - - #TODO custom elements - - let name = field.attr("name") - - if name == "": - continue - - if field.tagType == TAG_SELECT: - let field = HTMLSelectElement(field) - for option in field.options: - if option.selected or option.disabled: - entrylist.add((name, option.value)) - elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}: - let value = if field.attr("value") != "": - field.attr("value") - else: - "on" - entrylist.add((name, value)) - elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE: - #TODO file - discard - elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"): - let charset = if encoding != "": - encoding - else: - "UTF-8" - entrylist.add((name, charset)) - else: - if field.tagType == TAG_INPUT: - entrylist.add((name, HTMLInputElement(field).value)) - else: - assert false - if field.tagType == TAG_TEXTAREA or - field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}: - if field.attr("dirname") != "": - let dirname = field.attr("dirname") - let dir = "ltr" #TODO bidi - entrylist.add((dirname, dir)) - - form.constructingentrylist = false - return entrylist - -#https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm -proc makeCRLF(s: string): string = - result = newStringOfCap(s.len) - var i = 0 - while i < s.len - 1: - if s[i] == '\r' and s[i + 1] != '\n': - result &= '\r' - result &= '\n' - elif s[i] != '\r' and s[i + 1] == '\n': - result &= s[i] - result &= '\r' - result &= '\n' - inc i - else: - result &= s[i] - inc i - -proc serializeMultipartFormData(kvs: seq[(string, string)]): MimeData = - for it in kvs: - let name = makeCRLF(it[0]) - let value = makeCRLF(it[1]) - result[name] = value - -proc serializePlainTextFormData(kvs: seq[(string, string)]): string = - for it in kvs: - let (name, value) = it - result &= name - result &= '=' - result &= value - result &= "\r\n" - -proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = - let entrylist = form.constructEntryList(submitter) - - let action = if submitter.action() == "": - $form.document.location - else: - submitter.action() - - let url = parseUrl(action, submitter.document.baseUrl.some) - if url.isnone: - return none(Request) - - var parsedaction = url.get - let scheme = parsedaction.scheme - let enctype = submitter.enctype() - let formmethod = submitter.formmethod() - if formmethod == FORM_METHOD_DIALOG: - #TODO - return none(Request) - let httpmethod = if formmethod == FORM_METHOD_GET: - HTTP_GET - else: - assert formmethod == FORM_METHOD_POST - HTTP_POST - - #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): - # submitter.attr("formtarget") - #else: - # submitter.target() - #let noopener = true #TODO - - template mutateActionUrl() = - let query = serializeApplicationXWWWFormUrlEncoded(entrylist) - parsedaction.query = query.some - return newRequest(parsedaction, httpmethod).some - - template submitAsEntityBody() = - var mimetype: string - var body = none(string) - var multipart = none(MimeData) - case enctype - of FORM_ENCODING_TYPE_URLENCODED: - body = serializeApplicationXWWWFormUrlEncoded(entrylist).some - mimeType = $enctype - of FORM_ENCODING_TYPE_MULTIPART: - multipart = serializeMultipartFormData(entrylist).some - mimetype = $enctype - of FORM_ENCODING_TYPE_TEXT_PLAIN: - body = serializePlainTextFormData(entrylist).some - mimetype = $enctype - return newRequest(parsedaction, httpmethod, {"Content-Type": mimetype}, body, multipart).some - - template getActionUrl() = - return newRequest(parsedaction).some - - case scheme - of "http", "https": - if formmethod == FORM_METHOD_GET: - mutateActionUrl - else: - assert formmethod == FORM_METHOD_POST - submitAsEntityBody - of "ftp": - getActionUrl - of "data": - if formmethod == FORM_METHOD_GET: - mutateActionUrl - else: - assert formmethod == FORM_METHOD_POST - getActionUrl - -proc click*(buffer: Buffer): Option[Request] = - let clickable = buffer.getCursorClickable() - if clickable != nil: - template set_focus(e: Element) = - if buffer.document.focus != e: - buffer.document.focus = e - buffer.reshape = true - template restore_focus = - if buffer.document.focus != nil: - buffer.document.focus = nil - buffer.reshape = true - case clickable.tagType - of TAG_SELECT: - set_focus clickable - of TAG_A: - restore_focus - let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some) - if url.issome: - return newRequest(url.get, HTTP_GET).some - of TAG_OPTION: - let option = HTMLOptionElement(clickable) - let select = option.select - if select != nil: - if buffer.document.focus == select: - # select option - if not select.attrb("multiple"): - for option in select.options: - option.selected = false - option.selected = true - restore_focus - else: - # focus on select - set_focus select - of TAG_INPUT: - restore_focus - let input = HTMLInputElement(clickable) - case input.inputType - of INPUT_SEARCH: - var value = input.value - print(HVP(buffer.height + 1, 1)) - print(EL()) - let status = readLine("SEARCH: ", value, buffer.width, {'\r', '\n'}, config = buffer.config, tty = buffer.tty) - if status: - input.value = value - input.invalid = true - buffer.reshape = true - if input.form != nil: - let submitaction = submitForm(input.form, input) - return submitaction - of INPUT_TEXT, INPUT_PASSWORD: - var value = input.value - print(HVP(buffer.height + 1, 1)) - print(EL()) - let status = readLine("TEXT: ", value, buffer.width, {'\r', '\n'}, input.inputType == INPUT_PASSWORD, config = buffer.config, tty = buffer.tty) - if status: - input.value = value - input.invalid = true - buffer.reshape = true - of INPUT_FILE: - var path = if input.file.issome: - input.file.get.path.serialize_unicode() - else: - "" - print(HVP(buffer.height + 1, 1)) - print(EL()) - let status = readLine("Filename: ", path, buffer.width, {'\r', '\n'}, config = buffer.config, tty = buffer.tty) - if status: - let cdir = parseUrl("file://" & getCurrentDir() & DirSep) - let path = parseUrl(path, cdir) - if path.issome: - input.file = path - input.invalid = true - buffer.reshape = true - of INPUT_CHECKBOX: - input.checked = not input.checked - input.invalid = true - buffer.reshape = true - of INPUT_RADIO: - for radio in input.radiogroup: - radio.checked = false - radio.invalid = true - input.checked = true - input.invalid = true - buffer.reshape = true - of INPUT_RESET: - if input.form != nil: - input.form.reset() - buffer.reshape = true - of INPUT_SUBMIT, INPUT_BUTTON: - if input.form != nil: - let submitaction = submitForm(input.form, input) - return submitaction - else: - restore_focus - else: - restore_focus - -proc setupBuffer*(buffer: Buffer) = - buffer.load() - buffer.render() - buffer.gotoAnchor() - buffer.redraw = true - -proc dupeBuffer*(buffer: Buffer, location = none(URL)): Buffer = - let clone = newBuffer(buffer.config, buffer.tty) - clone.contenttype = buffer.contenttype - clone.ispipe = buffer.ispipe - if location.isSome: - clone.location = location.get - else: - clone.location = buffer.location - clone.istream = newStringStream(buffer.source) - clone.setupBuffer() - return clone - -proc drawBuffer*(buffer: Buffer) = - var format = newFormat() - for line in buffer.lines: - if line.formats.len == 0: - print(line.str & "\n") - else: - var x = 0 - var i = 0 - for f in line.formats: - var outstr = "" - #assert f.pos < line.str.width(), "fpos " & $f.pos & "\nstr" & line.str & "\n" - while x < f.pos: - var r: Rune - fastRuneAt(line.str, i, r) - outstr &= r - x += r.width() - print(outstr) - print(format.processFormat(f.format)) - print(line.str.substr(i)) - print(format.processFormat(newFormat())) - print("\n") - -proc refreshTitle*(buffer: Buffer) = - buffer.title = buffer.getTitle() - -proc refreshBuffer*(buffer: Buffer, peek = false): bool {.discardable.} = - buffer.refreshTitle() - stdout.hideCursor() - - if buffer.refreshTermAttrs(): - buffer.redraw = true - buffer.reshape = true - - if buffer.redraw: - buffer.refreshDisplay() - buffer.displayBuffer() - #result = true - buffer.redraw = false - - if not peek: - buffer.updateHover() - - if buffer.reshape: - buffer.render() - buffer.reshape = false - buffer.refreshDisplay() - buffer.displayBufferSwapOutput() - #result = true - - if not peek: - if not buffer.nostatus: - buffer.statusMsgForBuffer() - else: - buffer.nostatus = false - buffer.displayStatusMessage() - buffer.cursorBufferPos() - stdout.showCursor() diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim index 3336d7ac..2d7da512 100644 --- a/src/io/lineedit.nim +++ b/src/io/lineedit.nim @@ -4,27 +4,30 @@ import strutils import sequtils import sugar +import bindings/quickjs import config/config -import io/term +import js/javascript import utils/twtstr -type LineState* = object - news*: seq[Rune] - prompt*: string - current: string - s: string - feedNext: bool - escNext: bool - cursor: int - shift: int - minlen: int - maxlen: int - displen: int - disallowed: set[char] - hide: bool - config: Config #TODO get rid of this - tty: File - callback: proc(state: var LineState): bool +type + LineEditState* = enum + EDIT, FINISH, CANCEL + + LineEdit* = ref object + news*: seq[Rune] + prompt*: string + current: string + state*: LineEditState + escNext*: bool + cursor: int + shift: int + minlen: int + maxlen: int + displen: int + disallowed: set[char] + hide: bool + config: Config #TODO get rid of this + tty: File func lwidth(r: Rune): int = if r.isControlChar(): @@ -52,57 +55,55 @@ func lwidth(s: seq[Rune], min: int): int = result += lwidth(s[i]) inc i -template kill(state: LineState, i: int) = - state.space(i) - state.backward(i) +template kill0(edit: LineEdit, i: int) = + edit.space(i) + edit.backward0(i) -template kill(state: LineState) = - let w = min(state.news.lwidth(state.cursor), state.displen) - state.kill(w) +template kill0(edit: LineEdit) = + let w = min(edit.news.lwidth(edit.cursor), edit.displen) + edit.kill0(w) -proc backward(state: LineState, i: int) = +proc backward0(state: LineEdit, i: int) = if i > 0: if i == 1: print('\b') else: cursorBackward(i) -proc forward(state: LineState, i: int) = +proc forward0(state: LineEdit, i: int) = if i > 0: cursorForward(i) -proc begin(state: LineState) = +proc begin0(state: LineEdit) = print('\r') - state.forward(state.minlen) + state.forward0(state.minlen) -proc space(state: LineState, i: int) = +proc space(edit: LineEdit, i: int) = print(' '.repeat(i)) -proc redraw(state: var LineState) = +proc redraw(state: LineEdit) = var dispw = state.news.lwidth(state.shift, state.shift + state.displen) if state.shift + state.displen > state.news.len: state.displen = state.news.len - state.shift while dispw > state.maxlen - 1: dispw -= state.news[state.shift + state.displen - 1].lwidth() dec state.displen - - state.begin() + state.begin0() let os = state.news.substr(state.shift, state.shift + state.displen) if state.hide: printesc('*'.repeat(os.lwidth())) else: printesc($os) state.space(max(state.maxlen - state.minlen - os.lwidth(), 0)) + state.begin0() + state.forward0(state.news.lwidth(state.shift, state.cursor)) - state.begin() - state.forward(state.news.lwidth(state.shift, state.cursor)) - -proc zeroShiftRedraw(state: var LineState) = +proc zeroShiftRedraw(state: LineEdit) = state.shift = 0 state.displen = state.maxlen - 1 state.redraw() -proc fullRedraw(state: var LineState) = +proc fullRedraw*(state: LineEdit) = state.displen = state.maxlen - 1 if state.cursor > state.shift: var shiftw = state.news.lwidth(state.shift, state.cursor) @@ -111,10 +112,9 @@ proc fullRedraw(state: var LineState) = shiftw -= state.news[state.shift].lwidth() else: state.shift = max(state.cursor - 1, 0) - state.redraw() -proc insertCharseq(state: var LineState, cs: var seq[Rune], disallowed: set[char]) = +proc insertCharseq(state: LineEdit, cs: var seq[Rune], disallowed: set[char]) = let escNext = state.escNext cs.keepIf((r) => (escNext or not r.isControlChar) and not (r.isAscii and char(r) in disallowed)) state.escNext = false @@ -133,169 +133,165 @@ proc insertCharseq(state: var LineState, cs: var seq[Rune], disallowed: set[char state.cursor += cs.len state.fullRedraw() -proc readLine(state: var LineState): bool = - printesc(state.prompt) - if state.hide: - printesc('*'.repeat(state.current.lwidth())) - else: - printesc(state.current) +proc cancel*(edit: LineEdit) {.jsfunc.} = + edit.state = CANCEL - while true: - if not state.feedNext: - state.s = "" - else: - state.feedNext = false - - restoreStdin(state.tty.getFileHandle()) - let c = state.tty.readChar() - state.s &= c - - var action = getLinedAction(state.config, state.s) - if state.escNext: - action = NO_ACTION - case action - of ACTION_LINED_CANCEL: - return false - of ACTION_LINED_SUBMIT: - return true - of ACTION_LINED_BACKSPACE: - if state.cursor > 0: - let w = state.news[state.cursor - 1].lwidth() - state.news.delete(state.cursor - 1..state.cursor - 1) - dec state.cursor - if state.cursor == state.news.len and state.shift == 0: - state.backward(w) - state.kill(w) - else: - state.fullRedraw() - of ACTION_LINED_DELETE: - if state.cursor >= 0 and state.cursor < state.news.len: - let w = state.news[state.cursor].lwidth() - state.news.delete(state.cursor..state.cursor) - if state.cursor == state.news.len and state.shift == 0: - state.kill(w) - else: - state.fullRedraw() - of ACTION_LINED_ESC: - state.escNext = true - of ACTION_LINED_CLEAR: - if state.cursor > 0: - state.news.delete(0..state.cursor - 1) - state.cursor = 0 - state.zeroShiftRedraw() - of ACTION_LINED_KILL: - if state.cursor < state.news.len: - state.kill() - state.news.setLen(state.cursor) - of ACTION_LINED_BACK: - if state.cursor > 0: - dec state.cursor - if state.cursor > state.shift or state.shift == 0: - state.backward(state.news[state.cursor].lwidth()) - else: - state.fullRedraw() - of ACTION_LINED_FORWARD: - if state.cursor < state.news.len: - inc state.cursor - if state.news.lwidth(state.shift, state.cursor) < state.displen: - var n = 1 - if state.news.len > state.cursor: - n = state.news[state.cursor].lwidth() - state.forward(n) - else: - state.fullRedraw() - of ACTION_LINED_PREV_WORD: - let oc = state.cursor - while state.cursor > 0: - dec state.cursor - if state.news[state.cursor].breaksWord(): - break - if state.cursor != oc: - if state.cursor > state.shift or state.shift == 0: - state.backward(state.news.lwidth(state.cursor, oc)) - else: - state.fullRedraw() - of ACTION_LINED_NEXT_WORD: - let oc = state.cursor - while state.cursor < state.news.len: - inc state.cursor - if state.cursor < state.news.len: - if state.news[state.cursor].breaksWord(): - break - - if state.cursor != oc: - let dw = state.news.lwidth(oc, state.cursor) - if oc + dw - state.shift < state.displen: - state.forward(dw) - else: - state.fullRedraw() - of ACTION_LINED_KILL_WORD: - var chars = 0 - if state.cursor > chars: - inc chars - - while state.cursor > chars: - inc chars - if state.news[state.cursor - chars].breaksWord(): - dec chars - break - if chars > 0: - let w = state.news.lwidth(state.cursor - chars, state.cursor) - state.news.delete(state.cursor - chars..state.cursor - 1) - state.cursor -= chars - if state.cursor == state.news.len and state.shift == 0: - state.backward(w) - state.kill(w) - else: - state.fullRedraw() - of ACTION_LINED_BEGIN: - if state.cursor > 0: - if state.shift == 0: - state.backward(state.news.lwidth(0, state.cursor)) - else: - state.fullRedraw() - state.cursor = 0 - of ACTION_LINED_END: - if state.cursor < state.news.len: - if state.news.lwidth(state.shift, state.news.len) < state.maxlen: - state.forward(state.news.lwidth(state.cursor, state.news.len)) - else: - state.fullRedraw() - state.cursor = state.news.len - of ACTION_FEED_NEXT: - state.feedNext = true - elif validateUtf8(state.s) == -1: - var cs = state.s.toRunes() - state.insertCharseq(cs, state.disallowed) - if state.callback(state): - state.fullRedraw() +proc submit*(edit: LineEdit) {.jsfunc.} = + edit.state = FINISH + +proc backspace*(edit: LineEdit) {.jsfunc.} = + if edit.cursor > 0: + let w = edit.news[edit.cursor - 1].lwidth() + edit.news.delete(edit.cursor - 1..edit.cursor - 1) + dec edit.cursor + if edit.cursor == edit.news.len and edit.shift == 0: + edit.backward0(w) + edit.kill0(w) else: - state.feedNext = true - -proc readLine*(prompt: string, current: var string, termwidth: int, - disallowed: set[char], hide: bool, config: Config, - tty: File, callback: proc(state: var LineState): bool): bool = - var state: LineState - - state.prompt = prompt - state.current = current - state.news = current.toRunes() - state.cursor = state.news.len - state.minlen = prompt.lwidth() - state.maxlen = termwidth - prompt.len - state.displen = state.maxlen - 1 - state.disallowed = disallowed - state.callback = callback - state.hide = hide - state.config = config - state.tty = tty - - if state.readLine(): - current = $state.news + edit.fullRedraw() + +proc write*(edit: LineEdit, s: string): bool {.jsfunc.} = + if validateUtf8(s) == -1: + var cs = s.toRunes() + edit.insertCharseq(cs, edit.disallowed) return true - return false -proc readLine*(prompt: string, current: var string, termwidth: int, +proc delete*(edit: LineEdit) {.jsfunc.} = + if edit.cursor >= 0 and edit.cursor < edit.news.len: + let w = edit.news[edit.cursor].lwidth() + edit.news.delete(edit.cursor..edit.cursor) + if edit.cursor == edit.news.len and edit.shift == 0: + edit.kill0(w) + else: + edit.fullRedraw() + +proc escape*(edit: LineEdit) {.jsfunc.} = + edit.escNext = true + +proc clear*(edit: LineEdit) {.jsfunc.} = + if edit.cursor > 0: + edit.news.delete(0..edit.cursor - 1) + edit.cursor = 0 + edit.zeroShiftRedraw() + +proc kill*(edit: LineEdit) {.jsfunc.} = + if edit.cursor < edit.news.len: + edit.kill0() + edit.news.setLen(edit.cursor) + +proc backward*(edit: LineEdit) {.jsfunc.} = + if edit.cursor > 0: + dec edit.cursor + if edit.cursor > edit.shift or edit.shift == 0: + edit.backward0(edit.news[edit.cursor].lwidth()) + else: + edit.fullRedraw() + +proc forward*(edit: LineEdit) {.jsfunc.} = + if edit.cursor < edit.news.len: + inc edit.cursor + if edit.news.lwidth(edit.shift, edit.cursor) < edit.displen: + var n = 1 + if edit.news.len > edit.cursor: + n = edit.news[edit.cursor].lwidth() + edit.forward0(n) + else: + edit.fullRedraw() + +proc prevWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} = + let oc = edit.cursor + while edit.cursor > 0: + dec edit.cursor + if edit.news[edit.cursor].breaksWord(check): + break + if edit.cursor != oc: + if edit.cursor > edit.shift or edit.shift == 0: + edit.backward0(edit.news.lwidth(edit.cursor, oc)) + else: + edit.fullRedraw() + +proc nextWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} = + let oc = edit.cursor + while edit.cursor < edit.news.len: + inc edit.cursor + if edit.cursor < edit.news.len: + if edit.news[edit.cursor].breaksWord(check): + break + if edit.cursor != oc: + let dw = edit.news.lwidth(oc, edit.cursor) + if oc + dw - edit.shift < edit.displen: + edit.forward0(dw) + else: + edit.fullRedraw() + +proc clearWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} = + var i = edit.cursor + if i > 0: + # point to the previous character + dec i + while i > 0: + dec i + if edit.news[i].breaksWord(check): + inc i + break + if i != edit.cursor: + edit.news.delete(i..<edit.cursor) + edit.cursor = i + edit.fullRedraw() + +proc killWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} = + var i = edit.cursor + if i < edit.news.len and edit.news[i].breaksWord(check): + inc i + while i < edit.news.len: + if edit.news[i].breaksWord(check): + break + inc i + if i != edit.cursor: + edit.news.delete(edit.cursor..<i) + edit.fullRedraw() + +proc begin*(edit: LineEdit) {.jsfunc.} = + if edit.cursor > 0: + if edit.shift == 0: + edit.backward0(edit.news.lwidth(0, edit.cursor)) + else: + edit.fullRedraw() + edit.cursor = 0 + +proc `end`*(edit: LineEdit) {.jsfunc.} = + if edit.cursor < edit.news.len: + if edit.news.lwidth(edit.shift, edit.news.len) < edit.maxlen: + edit.forward0(edit.news.lwidth(edit.cursor, edit.news.len)) + else: + edit.fullRedraw() + edit.cursor = edit.news.len + +proc writePrompt*(lineedit: LineEdit) = + printesc(lineedit.prompt) + +proc writeStart*(lineedit: LineEdit) = + lineedit.writePrompt() + if lineedit.hide: + printesc('*'.repeat(lineedit.current.lwidth())) + else: + printesc(lineedit.current) + +proc readLine*(prompt: string, termwidth: int, current = "", disallowed: set[char] = {}, hide = false, config: Config, - tty: File): bool = - readLine(prompt, current, termwidth, disallowed, hide, config, tty, (proc(state: var LineState): bool = false)) + tty: File): LineEdit = + new(result) + result.prompt = prompt + result.current = current + result.news = current.toRunes() + result.cursor = result.news.len + result.minlen = prompt.lwidth() + result.maxlen = termwidth - prompt.len + result.displen = result.maxlen - 1 + result.disallowed = disallowed + result.hide = hide + result.config = config + result.tty = tty + +proc addLineEditModule*(ctx: JSContext) = + ctx.registerType(LineEdit) diff --git a/src/io/loader.nim b/src/io/loader.nim index 7e6faab4..a8d0b969 100644 --- a/src/io/loader.nim +++ b/src/io/loader.nim @@ -76,10 +76,11 @@ proc loadResource(loader: FileLoader, request: Request, ostream: Stream) = ostream.swrite(-1) # error ostream.flush() +var ssock: ServerSocket proc runFileLoader(loader: FileLoader, fd: cint) = if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK: raise newException(Defect, "Failed to initialize libcurl.") - let ssock = initServerSocket(getpid()) + ssock = initServerSocket(getpid()) # The server has been initialized, so the main process can resume execution. var writef: File if not open(writef, FileHandle(fd), fmWrite): @@ -88,6 +89,10 @@ proc runFileLoader(loader: FileLoader, fd: cint) = writef.flushFile() close(writef) discard close(fd) + onSignal SIGTERM, SIGINT: + curl_global_cleanup() + ssock.close() + quit(1) while true: let stream = ssock.acceptSocketStream() try: diff --git a/src/io/serialize.nim b/src/io/serialize.nim index 932b82df..2dde0649 100644 --- a/src/io/serialize.nim +++ b/src/io/serialize.nim @@ -4,7 +4,10 @@ import options import streams import tables +import buffer/cell import io/request +import js/regex +import types/color import types/url template swrite*[T](stream: Stream, o: T) = @@ -47,11 +50,9 @@ proc swrite*[T](stream: Stream, s: seq[T]) = stream.swrite(m) proc swrite*[T](stream: Stream, o: Option[T]) = + stream.swrite(o.issome) if o.issome: - stream.swrite(1u8) stream.swrite(o.get) - else: - stream.swrite(0u8) proc swrite*(stream: Stream, request: Request) = stream.swrite(request.httpmethod) @@ -60,6 +61,39 @@ proc swrite*(stream: Stream, request: Request) = stream.swrite(request.body) stream.swrite(request.multipart) +proc swrite*(stream: Stream, color: CellColor) = + stream.swrite(color.rgb) + if color.rgb: + stream.swrite(color.rgbcolor) + else: + stream.swrite(color.color) + +proc swrite*(stream: Stream, format: Format) = + stream.swrite(format.fgcolor) + stream.swrite(format.bgcolor) + stream.swrite(format.flags) + +proc swrite*(stream: Stream, cell: SimpleFormatCell) = + stream.swrite(cell.format) + stream.swrite(cell.pos) + +proc swrite*(stream: Stream, line: SimpleFlexibleLine) = + stream.swrite(line.str) + stream.swrite(line.formats) + +proc swrite*(stream: Stream, cell: FormatCell) = + stream.swrite(cell.format) + stream.swrite(cell.pos) + +proc swrite*(stream: Stream, line: FlexibleLine) = + stream.swrite(line.str) + stream.swrite(line.formats) + +proc swrite*(stream: Stream, regex: Regex) = + stream.swrite(regex.plen) + stream.writeData(regex.bytecode, regex.plen) + stream.swrite(regex.buf) + template sread*[T](stream: Stream, o: T) = stream.read(o) @@ -119,13 +153,13 @@ proc sread*[T](stream: Stream, s: var seq[T]) = stream.sread(s[i]) proc sread*[T](stream: Stream, o: var Option[T]) = - let c = uint8(stream.readChar()) - if c == 1u8: + var x: bool + stream.sread(x) + if x: var m: T stream.sread(m) o = some(m) else: - assert c == 0u8 o = none(T) proc sread*(stream: Stream, req: var RequestObj) = @@ -135,6 +169,39 @@ proc sread*(stream: Stream, req: var RequestObj) = stream.sread(req.body) stream.sread(req.multipart) +proc sread*(stream: Stream, color: var CellColor) = + var rgb: bool + stream.sread(rgb) + if rgb: + color = CellColor(rgb: true) + stream.sread(color.rgbcolor) + else: + color = CellColor(rgb: false) + stream.sread(color.color) + +proc sread*(stream: Stream, format: var Format) = + stream.sread(format.fgcolor) + stream.sread(format.bgcolor) + stream.sread(format.flags) + +proc sread*(stream: Stream, cell: var SimpleFormatCell) = + stream.sread(cell.format) + stream.sread(cell.pos) + +proc sread*(stream: Stream, line: var SimpleFlexibleLine) = + stream.sread(line.str) + stream.sread(line.formats) + +proc sread*(stream: Stream, regex: var Regex) = + assert regex.bytecode == nil + stream.sread(regex.plen) + regex.bytecode = cast[ptr uint8](alloc(regex.plen)) + regex.clone = true + let l = stream.readData(regex.bytecode, regex.plen) + stream.sread(regex.buf) + if l != regex.plen: + `=destroy`(regex) + proc readRequest*(stream: Stream): Request = new(result) stream.sread(result[]) diff --git a/src/io/term.nim b/src/io/term.nim index bc0f2ae9..1eacd237 100644 --- a/src/io/term.nim +++ b/src/io/term.nim @@ -23,7 +23,6 @@ when defined(posix): discard tcSetAttr(stdin_fileno, TCSAFLUSH, addr orig_termios) proc enableRawMode*(fileno: FileHandle) = - eprint "raw mode" stdin_fileno = fileno addExitProc(disableRawMode) discard tcGetAttr(fileno, addr orig_termios) diff --git a/src/js/javascript.nim b/src/js/javascript.nim index b453cb13..08639aa9 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -4,6 +4,7 @@ import streams import strformat import strutils import tables +import unicode import bindings/quickjs @@ -498,6 +499,44 @@ proc fromJSSeq[T](ctx: JSContext, val: JSValue): Option[seq[T]] = return none(seq[T]) result.get.add(genericRes.get) +proc fromJSSet[T](ctx: JSContext, val: JSValue): Option[set[T]] = + let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_iterator) + if JS_IsException(itprop): + return none(set[T]) + defer: JS_FreeValue(ctx, itprop) + let it = JS_Call(ctx, itprop, val, 0, nil) + if JS_IsException(it): + return none(set[T]) + defer: JS_FreeValue(ctx, it) + let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().next) + if JS_IsException(next_method): + return none(set[T]) + defer: JS_FreeValue(ctx, next_method) + var s: set[T] + result = some(s) + while true: + let next = JS_Call(ctx, next_method, it, 0, nil) + if JS_IsException(next): + return none(set[T]) + defer: JS_FreeValue(ctx, next) + let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done) + if JS_IsException(doneVal): + return none(set[T]) + defer: JS_FreeValue(ctx, doneVal) + let done = fromJS[bool](ctx, doneVal) + if done.isnone: # exception + return none(set[T]) + if done.get: + break + let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().value) + if JS_IsException(valueVal): + return none(set[T]) + defer: JS_FreeValue(ctx, valueVal) + let genericRes = fromJS[typeof(result.get.items)](ctx, valueVal) + if genericRes.isnone: # exception + return none(set[T]) + result.get.incl(genericRes.get) + proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] = var ptab: ptr JSPropertyEnum var plen: uint32 @@ -526,16 +565,67 @@ proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] = return none(Table[A, B]) result.get[kn.get] = vn.get +proc toJS*[T](ctx: JSContext, obj: T): JSValue + +# ew.... +proc fromJSFunction1[T, U](ctx: JSContext, val: JSValue): Option[proc(x: U): Option[T]] = + return some(proc(x: U): Option[T] = + var arg1 = toJS(ctx, x) + let ret = JS_Call(ctx, val, JS_UNDEFINED, 1, addr arg1) + return fromJS[T](ctx, ret) + ) + +macro unpackReturnType(f: typed) = + var x = f.getTypeImpl() + while x.kind == nnkBracketExpr and x.len == 2: + x = x[1].getTypeImpl() + let params = x.findChild(it.kind == nnkFormalParams) + let rv = params[0] + assert rv[0].strVal == "Option" + let rvv = rv[1] + result = quote do: `rvv` + +macro unpackArg0(f: typed) = + var x = f.getTypeImpl() + while x.kind == nnkBracketExpr and x.len == 2: + x = x[1].getTypeImpl() + let params = x.findChild(it.kind == nnkFormalParams) + let rv = params[1] + assert rv.kind == nnkIdentDefs + let rvv = rv[1] + result = quote do: `rvv` + proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] = when T is string: return toString(ctx, val) + elif T is char: + let s = toString(ctx, val) + if s.isNone: + return none(char) + if s.get.len > 1: + return none(char) + return some(s.get[0]) + elif T is Rune: + let s = toString(ctx, val) + if s.isNone: + return none(Rune) + var i = 0 + var r: Rune + fastRuneAt(s.get, i, r) + if i < s.get.len: + return none(Rune) + return some(r) + elif T is (proc): + return fromJSFunction1[typeof(unpackReturnType(T)), typeof(unpackArg0(T))](ctx, val) elif typeof(result.unsafeGet) is Option: # unwrap let res = fromJS[typeof(result.get.get)](ctx, val) if res.isnone: return none(T) return some(res) elif T is seq: - return fromJSSeq[typeof(result.get[0])](ctx, val) + return fromJSSeq[typeof(result.get.items)](ctx, val) + elif T is set: + return fromJSSet[typeof(result.get.items)](ctx, val) elif T is tuple: return fromJSTuple[T](ctx, val) elif T is bool: @@ -634,6 +724,8 @@ func toJSObject[T](ctx: JSContext, obj: T): JSValue = proc toJS*[T](ctx: JSContext, obj: T): JSValue = when T is string: return ctx.toJSString(obj) + elif T is Rune: + return ctx.toJSString($obj) elif T is SomeNumber: return ctx.toJSNumber(obj) elif T is bool: diff --git a/src/js/regex.nim b/src/js/regex.nim index dcaf1729..9f24b160 100644 --- a/src/js/regex.nim +++ b/src/js/regex.nim @@ -19,6 +19,9 @@ export type Regex* = object bytecode*: ptr uint8 + plen*: cint + clone*: bool + buf*: string RegexResult* = object success*: bool @@ -29,19 +32,20 @@ var dummyContext = dummyRuntime.newJSContextRaw() proc `=destroy`(regex: var Regex) = if regex.bytecode != nil: - dummyRuntime.js_free_rt(regex.bytecode) + if regex.clone: + dealloc(regex.bytecode) + else: + dummyRuntime.js_free_rt(regex.bytecode) regex.bytecode = nil proc compileRegex*(buf: string, flags: int): Option[Regex] = var regex: Regex - var len: cint var error_msg_size = 64 var error_msg = cast[cstring](alloc0(error_msg_size)) - let bytecode = lre_compile(addr len, error_msg, cint(error_msg_size), cstring(buf), csize_t(buf.len), cint(flags), dummyContext) - + let bytecode = lre_compile(addr regex.plen, error_msg, cint(error_msg_size), cstring(buf), csize_t(buf.len), cint(flags), dummyContext) + regex.buf = buf if error_msg != nil: #TODO error handling? - #eprint "err", error_msg dealloc(error_msg) error_msg = nil if bytecode == nil: @@ -80,8 +84,8 @@ proc compileSearchRegex*(str: string): Option[Regex] = else: assert false return compileRegex(str.substr(0, flagsi - 1), flags) -proc exec*(regex: Regex, str: string, start = 0): RegexResult = - assert 0 <= start and start <= str.len +proc exec*(regex: Regex, str: string, start = 0, length = str.len): RegexResult = + assert 0 <= start and start <= length, "Start: " & $start & ", length: " & $length & " str: " & $str let captureCount = lre_get_capture_count(regex.bytecode) @@ -97,12 +101,15 @@ proc exec*(regex: Regex, str: string, start = 0): RegexResult = break var ustr: string16 if not ascii: - ustr = toUTF16(str) + if start != 0 or length != str.len: + ustr = toUTF16(str.substr(start, length)) + else: + ustr = toUTF16(str) cstr = cstring(ustr) let ret = lre_exec(capture, regex.bytecode, cast[ptr uint8](cstr), cint(start), - cint(str.len), cint(not ascii), dummyContext) + cint(length), cint(not ascii), dummyContext) result.success = ret == 1 #TODO error handling? (-1) diff --git a/src/main.nim b/src/main.nim index 817ec190..09eb3f6b 100644 --- a/src/main.nim +++ b/src/main.nim @@ -1,3 +1,4 @@ +import options import os import terminal @@ -37,7 +38,7 @@ Options: quit(i) var i = 0 -var ctype = "" +var ctype = none(string) var pages: seq[string] var dump = false var escape_all = false @@ -54,7 +55,7 @@ while i < params.len: of "-T": inc i if i < params.len: - ctype = params[i] + ctype = some(params[i]) else: help(1) of "-": @@ -94,7 +95,7 @@ if pages.len == 0 and conf.startup == "": if stdin.isatty: help(1) -conf.nmap = constructActionTable2(conf.nmap) +conf.nmap = constructActionTable(conf.nmap) conf.lemap = constructActionTable(conf.lemap) width_table = makewidthtable(conf.ambiguous_double) diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim index 4fddddc7..44b369f7 100644 --- a/src/render/renderdocument.nim +++ b/src/render/renderdocument.nim @@ -1,12 +1,12 @@ import strutils import unicode +import buffer/cell import css/cascade import css/sheet import css/stylednode import css/values import html/dom -import io/cell import io/term import layout/box import layout/engine diff --git a/src/render/rendertext.nim b/src/render/rendertext.nim index b893d781..f164c4dc 100644 --- a/src/render/rendertext.nim +++ b/src/render/rendertext.nim @@ -1,6 +1,6 @@ import streams -import io/cell +import buffer/cell import utils/twtstr proc renderPlainText*(text: string): FlexibleGrid = diff --git a/src/types/url.nim b/src/types/url.nim index 8d274b57..fc9e20e8 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -947,6 +947,14 @@ proc set*(params: URLSearchParams, name: string, value: string) {.jsfunc.} = first = false params.list[i][1] = value +proc newURL*(url: URL): URL = + new(result) + result[] = url[] + if url.searchParams != nil: #TODO ideally this should never be false + result.searchParams = URLSearchParams() + result.searchParams[] = url.searchParams[] + result.searchParams.url = some(result) + #TODO add Option wrapper proc newURL*(s: string, base: Option[string] = none(string)): URL {.jserr, jsctor.} = if base.issome: diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 3132510e..3636da14 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -125,6 +125,16 @@ func toHeaderCase*(str: string): string = result[i] = result[i].toUpperAscii() flip = result[i] == '-' +func toScreamingSnakeCase*(str: string): string = # input is camel case + if str.len >= 1: result &= str[0].toUpperAscii() + for c in str[1..^1]: + if c in AsciiUpperAlpha: + result &= '_' + result &= c + else: + result &= c.toUpperAscii() + + func startsWithNoCase*(str, prefix: string): bool = if str.len < prefix.len: return false # prefix.len is always lower @@ -899,6 +909,16 @@ func width*(s: seq[Rune], min: int): int = func breaksWord*(r: Rune): bool = return not (r.isDigitAscii() or r.width() == 0 or r.isAlpha()) +type BoundaryFunction* = proc(x: Rune): Option[bool] + +proc breaksWord*(r: Rune, check: Option[BoundaryFunction]): bool = + if check.isSome: + let f = check.get() + let v = f(r) + if v.isSome: #TODO report error? + return v.get() + return r.breaksWord() + func padToWidth*(str: string, size: int, schar = '$'): string = if str.width() < size: return str & ' '.repeat(size - str.width()) |