diff options
-rw-r--r-- | res/config.toml | 3 | ||||
-rw-r--r-- | src/buffer/buffer.nim | 201 | ||||
-rw-r--r-- | src/buffer/cell.nim | 44 | ||||
-rw-r--r-- | src/buffer/container.nim | 138 | ||||
-rw-r--r-- | src/display/client.nim | 37 | ||||
-rw-r--r-- | src/display/pager.nim | 46 | ||||
-rw-r--r-- | src/ips/socketstream.nim | 9 | ||||
-rw-r--r-- | src/layout/engine.nim | 8 | ||||
-rw-r--r-- | src/main.nim | 4 | ||||
-rw-r--r-- | src/render/rendertext.nim | 52 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 13 |
11 files changed, 302 insertions, 253 deletions
diff --git a/res/config.toml b/res/config.toml index 5fb2dfdf..b9a37d75 100644 --- a/res/config.toml +++ b/res/config.toml @@ -44,11 +44,12 @@ K = 'pager.scrollUp()' ')' = 'pager.scrollRight()' C-m = 'pager.click()' C-j = 'pager.click()' -C-l = 'pager.changeLocation()' M-u = 'pager.dupeBuffer()' +C-l = 'pager.load()' U = 'pager.reload()' r = 'pager.redraw()' R = 'pager.reshape()' +C-cC-c = 'pager.cancel()' g = 'pager.cursorFirstLine()' G = 'pager.cursorLastLine()' z = 'pager.centerLine()' diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index 458b5b17..a2860952 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -44,9 +44,6 @@ type CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH, GET_SOURCE, GET_LINES, UPDATE_HOVER, PASS_FD, CONNECT, GOTO_ANCHOR - ContainerCommand* = enum - BUFFER_READY, RESHAPE, FULFILL_PROMISE - BufferMatch* = object success*: bool x*: int @@ -59,14 +56,13 @@ type contenttype: string lines: FlexibleGrid rendered: bool - bsource: BufferSource + source: BufferSource width: int height: int attrs: WindowAttributes document: Document viewport: Viewport prevstyled: StyledNode - reshape: bool location: Url selector: Selector[int] istream: Stream @@ -74,9 +70,9 @@ type available: int pistream: Stream # for input pipe postream: Stream # for output pipe + srenderer: StreamRenderer streamclosed: bool loaded: bool - source: string prevnode: StyledNode loader: FileLoader config: BufferConfig @@ -115,18 +111,24 @@ proc fulfill*(iface: BufferInterface, packetid, len: int) = promise.cb() promise = promise.next +proc hasPromises*(iface: BufferInterface): bool = + return iface.promises.len > 0 + proc then*(promise: EmptyPromise, cb: (proc())): EmptyPromise {.discardable.} = + if promise == nil: return promise.cb = cb promise.next = EmptyPromise() return promise.next proc then*[T](promise: Promise[T], cb: (proc(x: T))): EmptyPromise {.discardable.} = + if promise == nil: return return promise.then(proc() = if promise.stream != nil: promise.stream.sread(promise.res) cb(promise.res)) proc then*[T, U](promise: Promise[T], cb: (proc(x: T): Promise[U])): Promise[U] {.discardable.} = + if promise == nil: return let next = Promise[U]() promise.then(proc(x: T) = let p2 = cb(x) @@ -136,30 +138,6 @@ proc then*[T, U](promise: Promise[T], cb: (proc(x: T): Promise[U])): Promise[U] next.cb())) return next -macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) = - let cmdblock = newStmtList() - var idlist: seq[NimNode] - var i = 0 - for arg in args: - let id = ident("arg_" & $i) - idlist.add(id) - cmdblock.add(quote do: - let `id` = `arg`) - inc i - let lens = ident("lens") - cmdblock.add(quote do: - if `cmd` != BUFFER_READY: return - var `lens` = slen(`cmd`)) - for id in idlist: - cmdblock.add(quote do: `lens` += slen(`id`)) - cmdblock.add(quote do: `buffer`.postream.swrite(`lens`)) - cmdblock.add(quote do: `buffer`.postream.swrite(`cmd`)) - for id in idlist: - cmdblock.add(quote do: - `buffer`.postream.swrite(`id`)) - cmdblock.add(quote do: `buffer`.postream.flush()) - return newBlockStmt(cmdblock) - proc buildInterfaceProc(fun: NimNode): tuple[fun, name: NimNode] = let name = fun[0] # sym let params = fun[3] # formalparams @@ -172,6 +150,7 @@ proc buildInterfaceProc(fun: NimNode): tuple[fun, name: NimNode] = let nup = ident(x) # add this to enums let this2 = newIdentDefs(ident("iface"), ident("BufferInterface")) let thisval = this2[0] + let n = name.strVal body.add(quote do: `thisval`.stream.swrite(BufferCommand.`nup`) `thisval`.stream.swrite(`thisval`.packetid)) @@ -430,6 +409,7 @@ proc gotoAnchor*(buffer: Buffer): tuple[x, y: int] {.proxy.} = let format = line.formats[i] if format.node != nil and anchor in format.node.node: return (format.pos, y) + return (-1, -1) proc do_reshape(buffer: Buffer) = case buffer.contenttype @@ -440,7 +420,8 @@ proc do_reshape(buffer: Buffer) = buffer.lines = ret[0] buffer.prevstyled = ret[1] else: - buffer.lines = renderPlainText(buffer.source) + buffer.lines.renderStream(buffer.srenderer, buffer.available) + buffer.available = 0 proc windowChange*(buffer: Buffer, attrs: WindowAttributes) {.proxy.} = buffer.attrs = attrs @@ -453,6 +434,7 @@ type UpdateHoverResult* = object repaint*: bool proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.proxy.} = + if buffer.lines.len == 0: return var thisnode: StyledNode let i = buffer.lines[cursory].findFormatN(cursorx) - 1 if i >= 0: @@ -465,7 +447,6 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr let elem = Element(styledNode.node) if not elem.hover: elem.hover = true - buffer.reshape = true result.repaint = true let link = thisnode.getLink() @@ -479,8 +460,9 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr let elem = Element(styledNode.node) if elem.hover: elem.hover = false - buffer.reshape = true result.repaint = true + if result.repaint: + buffer.do_reshape() buffer.prevnode = thisnode @@ -517,13 +499,13 @@ proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {. func getFd(buffer: Buffer): int = if buffer.streamclosed: return -1 - let source = buffer.bsource + let source = buffer.source case source.t of CLONE, LOAD_REQUEST: let istream = SocketStream(buffer.istream) return cast[FileHandle](istream.source.getFd()) of LOAD_PIPE: - return buffer.bsource.fd + return buffer.source.fd type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string] @@ -531,7 +513,7 @@ proc setupSource(buffer: Buffer): ConnectResult = if buffer.loaded: result.code = -2 return - let source = buffer.bsource + let source = buffer.source let setct = source.contenttype.isNone if not setct: buffer.contenttype = source.contenttype.get @@ -562,6 +544,7 @@ proc setupSource(buffer: Buffer): ConnectResult = if setct: buffer.contenttype = response.contenttype buffer.istream = response.body + SocketStream(buffer.istream).recvw = true result.needsAuth = response.status == 401 # Unauthorized result.redirect = response.redirect if setct: @@ -569,56 +552,55 @@ proc setupSource(buffer: Buffer): ConnectResult = buffer.selector.registerHandle(cast[int](buffer.getFd()), {Read}, 1) buffer.loaded = true -proc load0(buffer: Buffer): auto = - if buffer.contenttype == "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) - return (true, buffer.document.title) - return (false, "") - proc connect*(buffer: Buffer): ConnectResult {.proxy.} = let code = buffer.setupSource() return code -proc load*(buffer: Buffer): tuple[success: bool, title: string] {.proxy.} = - return buffer.load0() +const BufferSize = 4096 + +proc load*(buffer: Buffer): tuple[atend: bool, lines, bytes: int] {.proxy.} = + var bytes = -1 + if buffer.streamclosed: return (true, buffer.lines.len, bytes) + let op = buffer.sstream.getPosition() + let s = buffer.istream.readStr(BufferSize) + buffer.sstream.setPosition(op + buffer.available) + buffer.sstream.write(s) + buffer.sstream.setPosition(op) + buffer.available += s.len + case buffer.contenttype + of "text/html": + bytes = buffer.available + else: + buffer.do_reshape() + return (buffer.istream.atEnd, buffer.lines.len, bytes) proc render*(buffer: Buffer): int {.proxy.} = buffer.do_reshape() return buffer.lines.len -proc load2(buffer: Buffer) = - case buffer.contenttype - of "text/html": - #assert false, "Not implemented yet..." - discard - else: - # This is incredibly stupid but it works so whatever. - # (We're basically recv'ing single bytes, but nim std/net does buffering - # for us so we should be ok?) - if not buffer.streamclosed: - let c = buffer.istream.readChar() - buffer.source &= c - buffer.do_reshape() - proc finishLoad(buffer: Buffer) = - if buffer.contenttype != "text/html" and not buffer.streamclosed: - if not buffer.istream.atEnd: - let a = buffer.istream.readAll() + if buffer.streamclosed: return + if not buffer.istream.atEnd: + let op = buffer.sstream.getPosition() + buffer.sstream.setPosition(op + buffer.available) + while not buffer.istream.atEnd: + let a = buffer.istream.readStr(BufferSize) buffer.sstream.write(a) buffer.available += a.len - buffer.reshape = true - buffer.selector.unregister(int(buffer.getFd())) - buffer.istream.close() - buffer.streamclosed = true + buffer.sstream.setPosition(op) + case buffer.contenttype + of "text/html": + buffer.sstream.setPosition(0) + buffer.available = 0 + buffer.document = parseHTML5(buffer.sstream) + buffer.document.location = buffer.location + buffer.loadResources(buffer.document) + buffer.do_reshape() + else: + buffer.do_reshape() + buffer.selector.unregister(int(buffer.getFd())) + buffer.istream.close() + buffer.streamclosed = 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]] = @@ -788,17 +770,19 @@ proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = assert formmethod == FORM_METHOD_POST getActionUrl -template set_focus(e: Element) = +template set_focus(buffer: Buffer, e: Element) = if buffer.document.focus != e: buffer.document.focus = e - buffer.reshape = true + buffer.do_reshape() -template restore_focus = +template restore_focus(buffer: Buffer) = if buffer.document.focus != nil: buffer.document.focus = nil - buffer.reshape = true + buffer.do_reshape() -type ReadSuccessResult* = tuple[open: Option[Request], reshape: bool] +type ReadSuccessResult* = object + open*: Option[Request] + repaint*: bool proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} = if buffer.input != nil: @@ -807,8 +791,8 @@ proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} = of INPUT_SEARCH: input.value = s input.invalid = true - result.reshape = true - buffer.reshape = true + buffer.do_reshape() + result.repaint = true if input.form != nil: let submitaction = submitForm(input.form, input) if submitaction.isSome: @@ -816,16 +800,16 @@ proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} = of INPUT_TEXT, INPUT_PASSWORD: input.value = s input.invalid = true - result.reshape = true - buffer.reshape = true + buffer.do_reshape() + result.repaint = true of INPUT_FILE: let cdir = parseUrl("file://" & getCurrentDir() & DirSep) let path = parseUrl(s, cdir) if path.issome: input.file = path input.invalid = true - result.reshape = true - buffer.reshape = true + buffer.do_reshape() + result.repaint = true else: discard buffer.input = nil @@ -844,9 +828,9 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = if clickable != nil: case clickable.tagType of TAG_SELECT: - set_focus clickable + buffer.set_focus clickable of TAG_A: - restore_focus + buffer.restore_focus let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some) if url.issome: result.open = some(newRequest(url.get, HTTP_GET)) @@ -860,12 +844,12 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = for option in select.options: option.selected = false option.selected = true - restore_focus + buffer.restore_focus else: # focus on select - set_focus select + buffer.set_focus select of TAG_INPUT: - restore_focus + buffer.restore_focus let input = HTMLInputElement(clickable) case input.inputType of INPUT_SEARCH: @@ -894,7 +878,7 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = input.checked = not input.checked input.invalid = true result.repaint = true - buffer.reshape = true + buffer.do_reshape() of INPUT_RADIO: for radio in input.radiogroup: radio.checked = false @@ -902,19 +886,19 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = input.checked = true input.invalid = true result.repaint = true - buffer.reshape = true + buffer.do_reshape() of INPUT_RESET: if input.form != nil: input.form.reset() result.repaint = true - buffer.reshape = true + buffer.do_reshape() of INPUT_SUBMIT, INPUT_BUTTON: if input.form != nil: result.open = submitForm(input.form, input) else: - restore_focus + buffer.restore_focus else: - restore_focus + buffer.restore_focus proc readCanceled*(buffer: Buffer) {.proxy.} = buffer.input = nil @@ -935,15 +919,14 @@ proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.} proc passFd*(buffer: Buffer) {.proxy.} = let fd = SocketStream(buffer.pistream).recvFileHandle() - buffer.bsource.fd = fd + buffer.source.fd = fd proc getSource*(buffer: Buffer) {.proxy.} = let ssock = initServerSocket() let stream = ssock.acceptSocketStream() - if not buffer.streamclosed: - buffer.source = buffer.istream.readAll() - buffer.streamclosed = true - stream.write(buffer.source) + buffer.finishLoad() + buffer.sstream.setPosition(0) + stream.write(buffer.sstream.readAll()) stream.close() ssock.close() @@ -973,16 +956,14 @@ macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, cmd: BufferCommand let `rval` = `call`) if rval == nil: stmts.add(quote do: - let len = slen(FULFILL_PROMISE) + slen(`packetid`) + let len = slen(`packetid`) buffer.postream.swrite(len) - buffer.postream.swrite(FULFILL_PROMISE) buffer.postream.swrite(`packetid`) buffer.postream.flush()) else: stmts.add(quote do: - let len = slen(FULFILL_PROMISE) + slen(`packetid`) + slen(`rval`) + let len = slen(`packetid`) + slen(`rval`) buffer.postream.swrite(len) - buffer.postream.swrite(FULFILL_PROMISE) buffer.postream.swrite(`packetid`) buffer.postream.swrite(`rval`) buffer.postream.flush()) @@ -1009,18 +990,11 @@ proc runBuffer(buffer: Buffer, rfd: int) = buffer.readCommand() except IOError: break loop - else: - buffer.load2() if Error in event.events: if event.fd == rfd: break loop elif event.fd == buffer.getFd(): buffer.finishLoad() - if not buffer.alive: - break loop - if buffer.reshape and buffer.document != nil: #TODO null check shouldn't be needed? - buffer.reshape = false - buffer.do_reshape() buffer.pistream.close() buffer.postream.close() buffer.loader.quit() @@ -1035,18 +1009,19 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource, attrs: attrs, config: config, loader: loader, - bsource: source, + source: source, sstream: newStringStream(), viewport: Viewport(window: attrs), width: attrs.width, height: attrs.height - 1 ) buffer.selector = newSelector[int]() + buffer.sstream = newStringStream() + buffer.srenderer = newStreamRenderer(buffer.sstream) let sstream = connectSocketStream(mainproc, false) sstream.swrite(getpid()) buffer.pistream = sstream buffer.postream = sstream - buffer.writeCommand(BUFFER_READY) let rfd = int(sstream.source.getFd()) buffer.selector.registerHandle(rfd, {Read}, 0) buffer.runBuffer(rfd) diff --git a/src/buffer/cell.nim b/src/buffer/cell.nim index 7c6ec00a..d7de064f 100644 --- a/src/buffer/cell.nim +++ b/src/buffer/cell.nim @@ -270,6 +270,50 @@ proc parseAnsiCode*(format: var Format, buf: string, fi: int): int = return i +type + AnsiCodeParseState* = enum + PARSE_START, PARSE_PARAMS, PARSE_INTERM, PARSE_FINAL, PARSE_DONE + + AnsiCodeParser* = object + state*: AnsiCodeParseState + params: string + +proc reset*(parser: var AnsiCodeParser) = + parser.state = PARSE_START + parser.params = "" + +proc parseAnsiCode*(parser: var AnsiCodeParser, format: var Format, c: char): bool = + case parser.state + of PARSE_START: + if 0x40 <= int(c) and int(c) <= 0x5F: + if c != '[': + #C1, TODO? + parser.state = PARSE_DONE + else: + parser.state = PARSE_PARAMS + else: + parser.state = PARSE_DONE + return true + of PARSE_PARAMS: + if 0x30 <= int(c) and int(c) <= 0x3F: + parser.params &= c + else: + parser.state = PARSE_INTERM + return parser.parseAnsiCode(format, c) + of PARSE_INTERM: + if 0x20 <= int(c) and int(c) <= 0x2F: + discard + else: + parser.state = PARSE_FINAL + return parser.parseAnsiCode(format, c) + of PARSE_FINAL: + parser.state = PARSE_DONE + if 0x40 <= int(c) and int(c) <= 0x7E: + format.handleAnsiCode(c, parser.params) + else: + return true + of PARSE_DONE: discard + proc parseAnsiCode*(format: var Format, stream: Stream) = if stream.atEnd(): return var c = stream.readChar() diff --git a/src/buffer/container.nim b/src/buffer/container.nim index f17267c3..58f93f42 100644 --- a/src/buffer/container.nim +++ b/src/buffer/container.nim @@ -80,8 +80,8 @@ type sourcepair*: Container pipeto: Container redraw*: bool - cmdvalid: array[ContainerCommand, bool] needslines*: bool + canceled: bool events*: seq[ContainerEvent] proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, title = ""): Container = @@ -99,7 +99,6 @@ proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, ti height: attrs.height - 1, contenttype: source.contenttype, title: title ) - result.cmdvalid[BUFFER_READY] = true istream.sread(result.process) result.pos.setx = -1 @@ -566,34 +565,45 @@ proc setLoadInfo(container: Container, msg: string) = container.triggerEvent(STATUS) proc load*(container: Container) = - container.loadinfo = "Connecting to " & $container.source.location + container.setLoadInfo("Connecting to " & $container.source.location & "...") + var onload: (proc(res: tuple[atend: bool, lines, bytes: int])) + onload = (proc(res: tuple[atend: bool, lines, bytes: int]) = + if res.bytes == -1: + container.setLoadInfo("") + elif not res.atend: + container.setLoadInfo(convert_size(res.bytes) & " loaded") + if res.lines > container.numLines: + container.numLines = res.lines + container.triggerEvent(STATUS) + container.requestLines() + if not res.atend and not container.canceled: + discard container.iface.load().then(onload) + elif not container.canceled: + container.iface.gotoAnchor().then(proc(res: tuple[x, y: int]) = + if res.x != -1 and res.y != -1: + container.setCursorXY(res.x, res.y) + ) + ) container.iface.connect().then(proc(res: ConnectResult): auto = - container.code = res.code if res.code != -2: + container.code = res.code if res.code == 0: + container.setLoadInfo("Connected to " & $container.source.location & ". Downloading...") if res.needsAuth: container.triggerEvent(NEEDS_AUTH) if res.redirect.isSome: + container.redirect = res.redirect container.triggerEvent(REDIRECT) if res.contentType != "": container.contenttype = some(res.contentType) - container.setLoadInfo("Downloading " & $container.source.location) return container.iface.load() else: + container.setLoadInfo("") container.triggerEvent(FAIL) - ).then(proc(res: tuple[success: bool, title: string]): auto = - if res.success: - container.title = res.title - container.setLoadInfo("Rendering " & $container.source.location) - return container.iface.render() - ).then(proc(lines: int): auto = - container.numLines = lines - container.setLoadInfo("") - container.needslines = true - return container.iface.gotoAnchor() - ).then(proc(res: tuple[x, y: int]) = - container.setCursorXY(res.x, res.y) - ) + ).then(onload) + +proc cancel*(container: Container) = + container.canceled = true proc findAnchor*(container: Container, anchor: string) = container.iface.findAnchor(anchor).then(proc(found: bool) = @@ -607,14 +617,15 @@ proc readCanceled*(container: Container) = proc readSuccess*(container: Container, s: string) = container.iface.readSuccess(s).then(proc(res: ReadSuccessResult) = - if res.reshape: + if res.repaint: container.needslines = true if res.open.isSome: container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get))) proc reshape*(container: Container, noreq = false) {.jsfunc.} = container.iface.render().then(proc(lines: int) = - container.numLines = lines) + container.numLines = lines + container.updateCursor()) if not noreq: container.needslines = true @@ -654,69 +665,46 @@ proc windowChange*(container: Container, attrs: WindowAttributes) = container.iface.windowChange(attrs).then(proc() = container.needslines = true) -proc handleCommand(container: Container, cmd: ContainerCommand, len: int): ContainerEvent = - if not container.cmdvalid[cmd] and false or cmd notin {FULFILL_PROMISE, BUFFER_READY}: - let len = len - sizeof(cmd) - #TODO TODO TODO - for i in 0 ..< len: - discard container.istream.readChar() - if cmd != RESHAPE: - return ContainerEvent(t: INVALID_COMMAND) - container.cmdvalid[cmd] = false - case cmd - of BUFFER_READY: - if container.source.t == LOAD_PIPE: - container.iface.passFd() - let s = SocketStream(container.ostream) - s.sendFileHandle(container.source.fd) - discard close(container.source.fd) - container.ostream.flush() - container.load() - of RESHAPE: - container.needslines = true - of FULFILL_PROMISE: - var packetid: int - container.istream.sread(packetid) - container.iface.fulfill(packetid, len - slen(packetid) - slen(FULFILL_PROMISE)) - if container.needslines: - container.requestLines() - container.needslines = false +proc handleCommand(container: Container) = + var packetid, len: int + container.istream.sread(len) + container.istream.sread(packetid) + container.iface.fulfill(packetid, len - slen(packetid)) proc setStream*(container: Container, stream: Stream) = container.istream = stream container.ostream = stream container.iface = newBufferInterface(stream) + if container.source.t == LOAD_PIPE: + container.iface.passFd() + let s = SocketStream(container.ostream) + s.sendFileHandle(container.source.fd) + discard close(container.source.fd) + container.ostream.flush() + container.load() # Synchronously read all lines in the buffer. iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} = - var cmd: ContainerCommand - container.requestLines(0 .. -1) - var len: int - container.istream.sread(len) - container.istream.sread(cmd) - #TODO TODO TODO - #while cmd != SET_LINES: - # discard container.handleCommand(cmd, len) - # container.istream.sread(len) - # container.istream.sread(cmd) - #assert cmd == SET_LINES - var w: Slice[int] - container.istream.sread(container.numLines) - container.istream.sread(w) - var line: SimpleFlexibleLine - for y in 0 ..< w.len: - container.istream.sread(line) - yield line - -proc handleEvent*(container: Container): ContainerEvent = - var len: int - container.istream.sread(len) - var cmd: ContainerCommand - container.istream.sread(cmd) - if cmd > high(ContainerCommand): - return ContainerEvent(t: INVALID_COMMAND) - else: - return container.handleCommand(cmd, len) + while container.iface.hasPromises: + # Spin event loop till container has been loaded + container.handleCommand() + if container.code == 0: + # load succeded + discard container.iface.getLines(0 .. -1) + var plen, len, packetid: int + container.istream.sread(plen) + container.istream.sread(packetid) + container.istream.sread(len) + var line: SimpleFlexibleLine + for y in 0 ..< len: + container.istream.sread(line) + yield line + +proc handleEvent*(container: Container) = + container.handleCommand() + if container.needslines: + container.requestLines() + container.needslines = false proc addContainerModule*(ctx: JSContext) = ctx.registerType(Container, name = "Buffer") diff --git a/src/display/client.nim b/src/display/client.nim index 24ea8183..3b56373e 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -47,6 +47,7 @@ type console {.jsget.}: Console pager {.jsget.}: Pager line {.jsget.}: LineEdit + sevent: seq[Container] config: Config jsrt: JSRuntime jsctx: JSContext @@ -84,6 +85,7 @@ proc doRequest(client: Client, req: Request): Response {.jsfunc.} = proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = let client = cast[Client](opaque) + if client.console.tty == nil: return try: let c = client.console.tty.readChar() if c == char(3): #C-c @@ -96,9 +98,11 @@ proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = return 0 proc evalJS(client: Client, src, filename: string): JSObject = - unblockStdin(client.console.tty.getFileHandle()) + if client.console.tty != nil: + unblockStdin(client.console.tty.getFileHandle()) result = client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL) - restoreStdin(client.console.tty.getFileHandle()) + if client.console.tty != nil: + restoreStdin(client.console.tty.getFileHandle()) proc evalJSFree(client: Client, src, filename: string) = free(client.evalJS(src, filename)) @@ -235,8 +239,7 @@ proc acceptBuffers(client: Client) = else: client.pager.procmap.del(pid) stream.close() - var i = 0 - while i < client.pager.procmap.len: + while client.pager.procmap.len > 0: let stream = client.ssock.acceptSocketStream() var pid: Pid stream.sread(pid) @@ -247,9 +250,10 @@ proc acceptBuffers(client: Client) = let fd = stream.source.getFd() client.fdmap[int(fd)] = container client.selector.registerHandle(fd, {Read}, nil) - inc i + client.sevent.add(container) else: - #TODO print an error? + #TODO uh what? + eprint "???" stream.close() proc log(console: Console, ss: varargs[string]) {.jsfunc.} = @@ -264,7 +268,7 @@ proc inputLoop(client: Client) = let selector = client.selector selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil) let sigwinch = selector.registerSignal(int(SIGWINCH), nil) - client.acceptBuffers() + let redrawtimer = client.selector.registerTimer(1000, false, nil) while true: let events = client.selector.select(-1) for event in events: @@ -286,9 +290,12 @@ proc inputLoop(client: Client) = client.pager.windowChange(client.attrs) else: assert false if Event.Timer in event.events: - if event.fd in client.interval_fdis: + if event.fd == redrawtimer: + if client.pager.container != nil: + client.pager.container.requestLines() + elif event.fd in client.interval_fdis: client.intervals[client.interval_fdis[event.fd]].handler() - if event.fd in client.timeout_fdis: + elif event.fd in client.timeout_fdis: let id = client.timeout_fdis[event.fd] let timeout = client.timeouts[id] timeout.handler() @@ -313,8 +320,7 @@ proc writeFile(client: Client, path: string, content: string) {.jsfunc.} = proc newConsole(pager: Pager, tty: File): Console = new(result) - result.tty = tty - if tty != nil and false: + if tty != nil: var pipefd: array[0..1, cint] if pipe(pipefd) == -1: raise newException(Defect, "Failed to open console pipe.") @@ -332,14 +338,10 @@ proc newConsole(pager: Pager, tty: File): Console = result.err = newFileStream(stderr) proc dumpBuffers(client: Client) = - client.acceptBuffers() - for container in client.pager.containers: - container.load() let ostream = newFileStream(stdout) for container in client.pager.containers: - container.reshape(true) client.pager.drawBuffer(container, ostream) - client.pager.dumpAlerts() + discard client.pager.handleEvents(container) stdout.close() proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], dump: bool) = @@ -372,7 +374,8 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du for page in pages: client.pager.loadURL(page, ctype = ctype) - + client.acceptBuffers() + client.pager.refreshStatusMsg() if not dump: client.inputLoop() else: diff --git a/src/display/pager.nim b/src/display/pager.nim index 7bd50e6f..a50eb9a8 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -59,7 +59,8 @@ iterator containers*(pager: Pager): Container = var stack: seq[Container] stack.add(c) while stack.len > 0: - yield stack.pop() + c = stack.pop() + yield c for i in countdown(c.children.high, 0): stack.add(c.children[i]) @@ -152,7 +153,7 @@ proc launchPager*(pager: Pager, tty: File) = proc dumpAlerts*(pager: Pager) = for msg in pager.alerts: - eprint msg + eprint "cha: " & msg proc quit*(pager: Pager, code = 0) = pager.term.quit() @@ -231,6 +232,7 @@ proc writeStatusMessage(pager: Pager, str: string, format: Format = Format()) = proc refreshStatusMsg*(pager: Pager) = let container = pager.container if container == nil: return + if pager.tty == nil: return if container.loadinfo != "": pager.writeStatusMessage(container.loadinfo) elif pager.alerts.len > 0: @@ -343,10 +345,7 @@ proc lineInfo(pager: Pager) {.jsfunc.} = pager.alert(pager.container.lineInfo()) proc deleteContainer(pager: Pager, container: Container) = - if container.parent == nil and - container.children.len == 0 and - container != pager.container: - return + container.cancel() if container.sourcepair != nil: container.sourcepair.sourcepair = nil container.sourcepair = nil @@ -565,28 +564,37 @@ proc updateReadLine*(pager: Pager) = pager.clearLineEdit() # Open a URL prompt and visit the specified URL. -proc changeLocation(pager: Pager) {.jsfunc.} = - var url = pager.container.source.location.serialize() - pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, term = pager.term), LOCATION) +proc load(pager: Pager, s = "") {.jsfunc.} = + if s.len > 0 and s[^1] == '\n': + pager.loadURL(s) + else: + var url = s + if url == "": + url = pager.container.source.location.serialize() + pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, term = pager.term), LOCATION) # Reload the page in a new buffer, then kill the previous buffer. proc reload(pager: Pager) {.jsfunc.} = pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container) +# Cancel loading current page (if exists). +proc cancel(pager: Pager) {.jsfunc.} = + pager.container.cancel() + proc click(pager: Pager) {.jsfunc.} = pager.container.click() proc authorize*(pager: Pager) = pager.setLineEdit(readLine("Username: ", pager.attrs.width, term = pager.term), USERNAME) -proc handleEvent0*(pager: Pager, container: Container, event: ContainerEvent): bool = +proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool = case event.t of FAIL: pager.deleteContainer(container) if container.retry.len > 0: pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype) else: - pager.alert("Couldn't load " & $container.source.location & " (error code " & $container.code & ")") + pager.alert("Can't load " & $container.source.location & " (error code " & $container.code & ")") pager.refreshStatusMsg() if pager.container == nil: return false @@ -631,19 +639,19 @@ proc handleEvent0*(pager: Pager, container: Container, event: ContainerEvent): b of NO_EVENT: discard return true -proc handleEvent*(pager: Pager, container: Container): bool = - var event: ContainerEvent - try: - event = container.handleEvent() - except IOError: - return false - if not pager.handleEvent0(container, event): - return false +proc handleEvents*(pager: Pager, container: Container): bool = while container.events.len > 0: let event = container.events.pop() if not pager.handleEvent0(container, event): return false return true +proc handleEvent*(pager: Pager, container: Container): bool = + try: + container.handleEvent() + except IOError: + return false + return pager.handleEvents(container) + proc addPagerModule*(ctx: JSContext) = ctx.registerType(Pager) diff --git a/src/ips/socketstream.nim b/src/ips/socketstream.nim index 6ce9bd88..6df9f09b 100644 --- a/src/ips/socketstream.nim +++ b/src/ips/socketstream.nim @@ -10,11 +10,18 @@ import ips/serversocket type SocketStream* = ref object of Stream source*: Socket + recvw*: bool isend: bool proc sockReadData(s: Stream, buffer: pointer, len: int): int = let s = SocketStream(s) - result = s.source.recv(buffer, len) + try: + if s.recvw: + result = s.source.recv(buffer, len, 100) + else: + result = s.source.recv(buffer, len) + except TimeoutError: + return if result < 0: raise newException(IOError, "Failed to read data (code " & $osLastError() & ")") elif result < len: diff --git a/src/layout/engine.nim b/src/layout/engine.nim index d1ce9f27..408c3b70 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -83,7 +83,7 @@ proc horizontalAlignLine(ictx: InlineContext, line: LineBox, computed: CSSComput let x = max(maxwidth, line.width) - line.width for atom in line.atoms: atom.offset.x += x - of TEXT_ALIGN_CENTER: + of TEXT_ALIGN_CENTER, TEXT_ALIGN_CHA_CENTER: let x = max((max(maxwidth - line.offset.x, line.width)) div 2 - line.width div 2, 0) for atom in line.atoms: atom.offset.x += x @@ -107,8 +107,6 @@ proc horizontalAlignLine(ictx: InlineContext, line: LineBox, computed: CSSComput let atom = InlineSpacing(atom) atom.width = spacingwidth line.width += atom.width - else: - discard # Align atoms (inline boxes, text, etc.) vertically inside the line. proc verticalAlignLine(ictx: InlineContext) = @@ -686,7 +684,6 @@ iterator rows(builder: TableBoxBuilder): BoxBuilder {.inline.} = var body: seq[TableRowBoxBuilder] var footer: seq[TableRowBoxBuilder] var caption: TableCaptionBoxBuilder - #TODO this should be done for child in builder.children: assert child.computed{"display"} in ProperTableChild, $child.computed{"display"} case child.computed{"display"} @@ -708,7 +705,8 @@ iterator rows(builder: TableBoxBuilder): BoxBuilder {.inline.} = if caption == nil: caption = TableCaptionBoxBuilder(child) else: discard - yield caption + if caption != nil: + yield caption for child in header: yield child for child in body: diff --git a/src/main.nim b/src/main.nim index 63c52947..ad978fa6 100644 --- a/src/main.nim +++ b/src/main.nim @@ -40,6 +40,7 @@ Options: -T, --type <type> Specify content mime type -v, --version Print version information -h, --help Print this page + -r, --run <script/file> Run passed script or file -- Interpret all following arguments as URLs""" if i == 0: echo s @@ -90,6 +91,7 @@ while i < params.len: inc i if i < params.len: conf.startup = params[i] + conf.headless = true dump = true else: help(1) @@ -102,7 +104,7 @@ while i < params.len: pages.add(param) inc i -if pages.len == 0 and conf.startup == "": +if pages.len == 0 and not conf.headless: if stdin.isatty: help(1) diff --git a/src/render/rendertext.nim b/src/render/rendertext.nim index d9cc4dfc..675c4ddb 100644 --- a/src/render/rendertext.nim +++ b/src/render/rendertext.nim @@ -3,6 +3,7 @@ import streams import buffer/cell import utils/twtstr +const tabwidth = 8 proc renderPlainText*(text: string): FlexibleGrid = var format = newFormat() template add_format() = @@ -11,7 +12,6 @@ proc renderPlainText*(text: string): FlexibleGrid = result[result.high].addFormat(result[^1].str.len, format) result.addLine() - const tabwidth = 8 var spaces = 0 var i = 0 var af = false @@ -46,20 +46,34 @@ proc renderPlainText*(text: string): FlexibleGrid = if result.len > 1 and result[^1].str.len == 0 and result[^1].formats.len == 0: discard result.pop() -proc renderStream*(grid: var FlexibleGrid, stream: Stream, len: int) = - var format = newFormat() +type StreamRenderer* = object + spaces: int + stream*: Stream + ansiparser: AnsiCodeParser + format: Format + af: bool + +proc newStreamRenderer*(stream: Stream): StreamRenderer = + result.format = newFormat() + result.ansiparser.state = PARSE_DONE + result.stream = stream + +proc renderStream*(grid: var FlexibleGrid, renderer: var StreamRenderer, len: int) = template add_format() = - if af: - af = false - grid[grid.high].addFormat(grid[^1].str.len, format) + if renderer.af: + renderer.af = false + grid[grid.high].addFormat(grid[^1].str.len, renderer.format) if grid.len == 0: grid.addLine() - const tabwidth = 8 - var spaces = 0 - var af = false var i = 0 - while i < len: - let c = stream.readChar() + while i < len and not renderer.stream.atEnd: + let c = renderer.stream.readChar() + if renderer.ansiparser.state != PARSE_DONE: + let cancel = renderer.ansiparser.parseAnsiCode(renderer.format, c) + if not cancel: + if renderer.ansiparser.state == PARSE_DONE: + renderer.af = true + continue case c of '\n': add_format @@ -67,18 +81,17 @@ proc renderStream*(grid: var FlexibleGrid, stream: Stream, len: int) = of '\r': discard of '\t': add_format - for i in 0 ..< tabwidth - spaces: + for i in 0 ..< tabwidth - renderer.spaces: grid[^1].str &= ' ' - spaces = 0 + renderer.spaces = 0 of ' ': add_format grid[^1].str &= c - inc spaces - if spaces == 8: - spaces = 0 + inc renderer.spaces + if renderer.spaces == 8: + renderer.spaces = 0 of '\e': - format.parseAnsiCode(stream) - af = true + renderer.ansiparser.reset() elif c.isControlChar(): add_format grid[^1].str &= '^' & c.getControlLetter() @@ -86,6 +99,3 @@ proc renderStream*(grid: var FlexibleGrid, stream: Stream, len: int) = add_format grid[^1].str &= c inc i - - #if grid.len > 1 and grid[^1].str.len == 0 and grid[^1].formats.len == 0: - # discard grid.pop() diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 2123daeb..3d33f8d1 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -224,6 +224,19 @@ func skipBlanks*(buf: string, at: int): int = while result < buf.len and buf[result].isWhitespace(): inc result +# From w3m +const SizeUnit = [ + "b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Bb", "Yb" +] +func convert_size*(len: int): string = + var sizepos = 0 + var csize = float(len) + while csize >= 999.495 and sizepos < SizeUnit.len: + csize = csize / 1024.0 + inc sizepos + result = $(floor(csize * 100 + 0.5) / 100) + result &= SizeUnit[sizepos] + func until*(s: string, c: set[char]): string = var i = 0 while i < s.len: |