diff options
author | bptato <nincsnevem662@gmail.com> | 2023-09-14 01:08:10 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-09-14 01:08:10 +0200 |
commit | db0798acccbedcef4b16737f6be0cf7388cc0528 (patch) | |
tree | 208616de68fe9729e09e4084557f27f5b7204387 /src/display | |
parent | c2004e4aba182473e79100759a2d58e1bd7d184c (diff) | |
download | chawan-db0798acccbedcef4b16737f6be0cf7388cc0528.tar.gz |
move some modules to local/
makes a bit more sense than the previous arrangement
Diffstat (limited to 'src/display')
-rw-r--r-- | src/display/client.nim | 643 | ||||
-rw-r--r-- | src/display/pager.nim | 1189 |
2 files changed, 0 insertions, 1832 deletions
diff --git a/src/display/client.nim b/src/display/client.nim deleted file mode 100644 index 3236732d..00000000 --- a/src/display/client.nim +++ /dev/null @@ -1,643 +0,0 @@ -import cstrutils -import nativesockets -import net -import options -import os -import selectors -import streams -import strutils -import tables -import terminal - -when defined(posix): - import posix - -import std/exitprocs - -import bindings/quickjs -import buffer/container -import config/config -import css/sheet -import display/pager -import display/term -import html/chadombuilder -import html/dom -import html/event -import io/headers -import io/lineedit -import io/loader -import io/posixstream -import io/promise -import io/request -import io/window -import ips/forkserver -import ips/socketstream -import js/base64 -import js/domexception -import js/error -import js/fromjs -import js/intl -import js/javascript -import js/module -import js/timeout -import js/tojs -import types/blob -import types/cookie -import types/url -import utils/opt -import utils/twtstr -import xhr/formdata -import xhr/xmlhttprequest - -import chakasu/charset - -type - Client* = ref object - alive: bool - attrs: WindowAttributes - config {.jsget.}: Config - console {.jsget.}: Console - errormessage: string - fd: int - fdmap: Table[int, Container] - feednext: bool - forkserver: ForkServer - notnum: bool # has a non-numeric character been input already? - jsctx: JSContext - jsrt: JSRuntime - line {.jsget.}: LineEdit - loader: FileLoader - mainproc: Pid - pager {.jsget.}: Pager - precnum: int32 # current number prefix (when vi-numeric-prefix is true) - s: string # current input buffer - selector: Selector[Container] - store {.jsget, jsset.}: Document - timeouts: TimeoutState[Container] - userstyle: CSSStylesheet - - Console = ref object - err: Stream - pager: Pager - container: Container - prev: Container - ibuf: string - tty: File - -jsDestructor(Client) -jsDestructor(Console) - -proc readChar(console: Console): char = - if console.ibuf == "": - try: - return console.tty.readChar() - except EOFError: - quit(1) - result = console.ibuf[0] - console.ibuf = console.ibuf.substr(1) - -proc finalize(client: Client) {.jsfin.} = - if client.jsctx != nil: - free(client.jsctx) - if client.jsrt != nil: - free(client.jsrt) - -proc doRequest(client: Client, req: Request): Response {.jsfunc.} = - return client.loader.doRequest(req) - -proc fetch[T: Request|string](client: Client, req: T, - init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} = - let req = ?newRequest(client.jsctx, req, init) - return ok(client.loader.fetch(req)) - -proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = - let client = cast[Client](opaque) - if client.console == nil or client.console.tty == nil: return - try: - let c = client.console.tty.readChar() - if c == char(3): #C-c - client.console.ibuf = "" - return 1 - else: - client.console.ibuf &= c - except IOError: - discard - return 0 - -proc runJSJobs(client: Client) = - client.jsrt.runJSJobs(client.console.err) - -proc evalJS(client: Client, src, filename: string, module = false): JSValue = - client.pager.term.unblockStdin() - let flags = if module: - JS_EVAL_TYPE_MODULE - else: - JS_EVAL_TYPE_GLOBAL - result = client.jsctx.eval(src, filename, flags) - client.runJSJobs() - client.pager.term.restoreStdin() - -proc evalJSFree(client: Client, src, filename: string) = - JS_FreeValue(client.jsctx, client.evalJS(src, filename)) - -proc command0(client: Client, src: string, filename = "<command>", - silence = false, module = false) = - let ret = client.evalJS(src, filename, module = module) - if JS_IsException(ret): - client.jsctx.writeException(client.console.err) - else: - if not silence: - let str = fromJS[string](client.jsctx, ret) - if str.isSome: - client.console.err.write(str.get & '\n') - client.console.err.flush() - JS_FreeValue(client.jsctx, ret) - -proc command(client: Client, src: string) = - client.command0(src) - client.console.container.requestLines().then(proc() = - client.console.container.cursorLastLine()) - -proc suspend(client: Client) {.jsfunc.} = - client.pager.term.quit() - discard kill(client.mainproc, cint(SIGSTOP)) - client.pager.term.restart() - -proc quit(client: Client, code = 0) {.jsfunc.} = - if client.alive: - client.alive = false - client.pager.quit() - let ctx = client.jsctx - var global = JS_GetGlobalObject(ctx) - JS_FreeValue(ctx, global) - if client.jsctx != nil: - free(client.jsctx) - #TODO - #if client.jsrt != nil: - # free(client.jsrt) - quit(code) - -proc feedNext(client: Client) {.jsfunc.} = - client.feednext = true - -proc alert(client: Client, msg: string) {.jsfunc.} = - client.pager.alert(msg) - -proc handlePagerEvents(client: Client) = - let container = client.pager.container - if container != nil: - client.pager.handleEvents(container) - -proc evalAction(client: Client, action: string, arg0: int32) = - let ret = client.evalJS(action, "<command>") - let ctx = client.jsctx - if JS_IsFunction(ctx, ret): - if arg0 != 0: - var arg0 = toJS(ctx, arg0) - JS_FreeValue(ctx, JS_Call(ctx, ret, JS_UNDEFINED, 1, addr arg0)) - JS_FreeValue(ctx, arg0) - else: # no precnum - JS_FreeValue(ctx, JS_Call(ctx, ret, JS_UNDEFINED, 0, nil)) - JS_FreeValue(ctx, ret) - -# The maximum number we are willing to accept. -# This should be fine for 32-bit signed ints (which precnum currently is). -# We can always increase it further (e.g. by switching to uint32, uint64...) if -# it proves to be too low. -const MaxPrecNum = 100000000 - -proc handleCommandInput(client: Client, c: char) = - if client.config.input.vi_numeric_prefix and not client.notnum: - if client.precnum != 0 and c == '0' or c in '1' .. '9': - if client.precnum < MaxPrecNum: # better ignore than eval... - client.precnum *= 10 - client.precnum += cast[int32](decValue(c)) - return - else: - client.notnum = true - client.s &= c - let action = getNormalAction(client.config, client.s) - client.evalAction(action, client.precnum) - if not client.feedNext: - client.precnum = 0 - client.notnum = false - client.handlePagerEvents() - client.pager.refreshStatusMsg() - -proc input(client: Client) = - client.pager.term.restoreStdin() - while true: - let c = client.console.readChar() - if client.pager.askpromise != nil: - if c == 'y': - client.pager.fulfillAsk(true) - client.runJSJobs() - elif c == 'n': - client.pager.fulfillAsk(false) - client.runJSJobs() - elif client.pager.lineedit.isSome: - client.s &= c - let edit = client.pager.lineedit.get - client.line = edit - if edit.escNext: - edit.escNext = false - if edit.write(client.s, client.pager.term.cs): - client.s = "" - else: - let action = getLinedAction(client.config, client.s) - if action == "": - if edit.write(client.s, client.pager.term.cs): - client.s = "" - else: - client.feedNext = true - elif not client.feednext: - client.evalAction(action, 0) - if client.pager.lineedit.isNone: - client.line = nil - if not client.feedNext: - client.pager.updateReadLine() - else: - client.handleCommandInput(c) - if not client.feednext: - client.s = "" - break - else: - client.feednext = false - client.s = "" - -proc setTimeout[T: JSValue|string](client: Client, handler: T, - timeout = 0i32): int32 {.jsfunc.} = - return client.timeouts.setTimeout(handler, timeout) - -proc setInterval[T: JSValue|string](client: Client, handler: T, - interval = 0i32): int32 {.jsfunc.} = - return client.timeouts.setInterval(handler, interval) - -proc clearTimeout(client: Client, id: int32) {.jsfunc.} = - client.timeouts.clearTimeout(id) - -proc clearInterval(client: Client, id: int32) {.jsfunc.} = - client.timeouts.clearInterval(id) - -let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint - -proc log(console: Console, ss: varargs[string]) {.jsfunc.} = - for i in 0..<ss.len: - console.err.write(ss[i]) - if i != ss.high: - console.err.write(' ') - console.err.write('\n') - console.err.flush() - -proc show(console: Console) {.jsfunc.} = - if console.pager.container != console.container: - console.prev = console.pager.container - console.pager.setContainer(console.container) - console.container.requestLines() - -proc hide(console: Console) {.jsfunc.} = - if console.pager.container == console.container: - console.pager.setContainer(console.prev) - -proc buffer(console: Console): Container {.jsfget.} = - return console.container - -proc acceptBuffers(client: Client) = - while client.pager.unreg.len > 0: - let (pid, stream) = client.pager.unreg.pop() - let fd = stream.source.getFd() - if int(fd) in client.fdmap: - client.selector.unregister(fd) - client.fdmap.del(int(fd)) - else: - client.pager.procmap.del(pid) - stream.close() - for pid, container in client.pager.procmap: - let stream = connectSocketStream(pid, buffered = false, blocking = true) - container.setStream(stream) - let fd = stream.source.getFd() - client.fdmap[int(fd)] = container - client.selector.registerHandle(fd, {Read}, nil) - client.pager.handleEvents(container) - client.pager.procmap.clear() - -proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {. - importc: "setvbuf", header: "<stdio.h>", tags: [].} - -proc handleRead(client: Client, fd: int) = - if client.console.tty != nil and fd == client.console.tty.getFileHandle(): - client.input() - client.handlePagerEvents() - elif fd == client.forkserver.estream.fd: - var nl = false - const prefix = "STDERR: " - var s = prefix - while true: - try: - let c = client.forkserver.estream.readChar() - if nl and s.len > prefix.len: - client.console.err.write(s) - s = prefix - nl = false - s &= c - nl = c == '\n' - except IOError: - break - if s.len > prefix.len: - client.console.err.write(s) - client.console.err.flush() - elif fd in client.loader.connecting: - client.loader.onConnected(fd) - client.runJSJobs() - elif fd in client.loader.ongoing: - client.loader.onRead(fd) - elif fd in client.loader.unregistered: - discard # ignore - else: - let container = client.fdmap[fd] - client.pager.handleEvent(container) - -proc flushConsole*(client: Client) {.jsfunc.} = - client.handleRead(client.forkserver.estream.fd) - -proc handleError(client: Client, fd: int) = - if client.console.tty != nil and fd == client.console.tty.getFileHandle(): - #TODO do something here... - stderr.write("Error in tty\n") - quit(1) - elif fd == client.forkserver.estream.fd: - #TODO do something here... - stderr.write("Fork server crashed :(\n") - quit(1) - elif fd in client.loader.connecting: - #TODO handle error? - discard - elif fd in client.loader.ongoing: - client.loader.onError(fd) - elif fd in client.loader.unregistered: - discard # already unregistered... - else: - if fd in client.fdmap: - let container = client.fdmap[fd] - if container != client.console.container: - client.console.log("Error in buffer", $container.location) - else: - client.console.container = nil - client.selector.unregister(fd) - client.fdmap.del(fd) - if client.console.container != nil: - client.console.show() - else: - doAssert false - -proc inputLoop(client: Client) = - let selector = client.selector - discard c_setvbuf(client.console.tty, nil, IONBF, 0) - selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil) - let sigwinch = selector.registerSignal(int(SIGWINCH), nil) - while true: - let events = client.selector.select(-1) - for event in events: - if Read in event.events: - client.handleRead(event.fd) - if Error in event.events: - client.handleError(event.fd) - if Signal in event.events: - assert event.fd == sigwinch - client.attrs = getWindowAttributes(client.console.tty) - client.pager.windowChange(client.attrs) - if selectors.Event.Timer in event.events: - assert client.timeouts.runTimeoutFd(event.fd) - client.runJSJobs() - client.console.container.requestLines().then(proc() = - client.console.container.cursorLastLine()) - client.loader.unregistered.setLen(0) - client.acceptBuffers() - if client.pager.scommand != "": - client.command(client.pager.scommand) - client.pager.scommand = "" - client.handlePagerEvents() - if client.pager.container == nil: - # No buffer to display. - quit(1) - client.pager.showAlerts() - client.pager.draw() - -func hasSelectFds(client: Client): bool = - return not client.timeouts.empty or - client.pager.numload > 0 or - client.loader.connecting.len > 0 or - client.loader.ongoing.len > 0 or - client.pager.procmap.len > 0 - -proc headlessLoop(client: Client) = - while client.hasSelectFds(): - let events = client.selector.select(-1) - for event in events: - if Read in event.events: - client.handleRead(event.fd) - if Error in event.events: - client.handleError(event.fd) - if selectors.Event.Timer in event.events: - assert client.timeouts.runTimeoutFd(event.fd) - client.runJSJobs() - client.loader.unregistered.setLen(0) - client.acceptBuffers() - -proc clientLoadJSModule(ctx: JSContext, module_name: cstring, - opaque: pointer): JSModuleDef {.cdecl.} = - let global = JS_GetGlobalObject(ctx) - JS_FreeValue(ctx, global) - var x: Option[URL] - if module_name.startsWith("/") or module_name.startsWith("./") or - module_name.startsWith("../"): - let cur = getCurrentDir() - x = parseURL($module_name, parseURL("file://" & cur & "/")) - else: - x = parseURL($module_name) - if x.isNone or x.get.scheme != "file": - JS_ThrowTypeError(ctx, "Invalid URL: %s", module_name) - return nil - try: - let f = readFile($x.get.path) - return finishLoadModule(ctx, f, module_name) - except IOError: - JS_ThrowTypeError(ctx, "Failed to open file %s", module_name) - return nil - -proc readBlob(client: Client, path: string): Option[WebFile] {.jsfunc.} = - try: - return some(newWebFile(path)) - except IOError: - discard - -#TODO this is dumb -proc readFile(client: Client, path: string): string {.jsfunc.} = - try: - return readFile(path) - except IOError: - discard - -#TODO ditto -proc writeFile(client: Client, path: string, content: string) {.jsfunc.} = - writeFile(path, content) - -proc newConsole(pager: Pager, tty: File): Console = - new(result) - if tty != nil: - var pipefd: array[0..1, cint] - if pipe(pipefd) == -1: - raise newException(Defect, "Failed to open console pipe.") - let url = newURL("javascript:console.show()") - result.container = pager.readPipe0(some("text/plain"), CHARSET_UNKNOWN, - pipefd[0], option(url.get(nil)), "Browser console") - var f: File - if not open(f, pipefd[1], fmWrite): - raise newException(Defect, "Failed to open file for console pipe.") - result.err = newFileStream(f) - result.err.writeLine("Type (M-c) console.hide() to return to buffer mode.") - result.err.flush() - result.pager = pager - result.tty = tty - pager.registerContainer(result.container) - else: - result.err = newFileStream(stderr) - -proc dumpBuffers(client: Client) = - client.headlessLoop() - let ostream = newFileStream(stdout) - for container in client.pager.containers: - try: - client.pager.drawBuffer(container, ostream) - client.pager.handleEvents(container) - except IOError: - client.console.log("Error in buffer", $container.location) - # check for errors - client.handleRead(client.forkserver.estream.fd) - quit(1) - stdout.close() - -proc launchClient*(client: Client, pages: seq[string], - contentType: Option[string], cs: Charset, dump: bool) = - var tty: File - var dump = dump - if not dump: - if stdin.isatty(): - tty = stdin - if stdout.isatty(): - if tty == nil: - dump = not open(tty, "/dev/tty", fmRead) - else: - dump = true - let selector = newSelector[Container]() - let efd = int(client.forkserver.estream.fd) - selector.registerHandle(efd, {Read}, nil) - client.loader.registerFun = proc(fd: int) = - selector.registerHandle(fd, {Read}, nil) - client.loader.unregisterFun = proc(fd: int) = - selector.unregister(fd) - client.selector = selector - client.pager.launchPager(tty) - client.console = newConsole(client.pager, tty) - #TODO passing console.err here makes it impossible to change it later. maybe - # better associate it with jsctx - client.timeouts = newTimeoutState(client.selector, client.jsctx, - client.console.err, proc(src, file: string) = client.evalJSFree(src, file)) - client.alive = true - addExitProc((proc() = client.quit())) - if client.config.start.startup_script != "": - let s = if fileExists(client.config.start.startup_script): - readFile(client.config.start.startup_script) - else: - client.config.start.startup_script - let ismodule = client.config.start.startup_script.endsWith(".mjs") - client.command0(s, client.config.start.startup_script, silence = true, - module = ismodule) - client.userstyle = client.config.css.stylesheet.parseStylesheet() - - if not stdin.isatty(): - client.pager.readPipe(contentType, cs, stdin.getFileHandle()) - - for page in pages: - client.pager.loadURL(page, ctype = contentType, cs = cs) - client.pager.showAlerts() - client.acceptBuffers() - if not dump: - client.inputLoop() - else: - client.dumpBuffers() - if client.config.start.headless: - client.headlessLoop() - client.quit() - -proc nimGCStats(client: Client): string {.jsfunc.} = - return GC_getStatistics() - -proc jsGCStats(client: Client): string {.jsfunc.} = - return client.jsrt.getMemoryUsage() - -proc nimCollect(client: Client) {.jsfunc.} = - GC_fullCollect() - -proc jsCollect(client: Client) {.jsfunc.} = - JS_RunGC(client.jsrt) - -proc sleep(client: Client, millis: int) {.jsfunc.} = - sleep millis - -proc atob(client: Client, data: string): DOMResult[string] {.jsfunc.} = - return atob(data) - -proc btoa(client: Client, data: string): DOMResult[string] {.jsfunc.} = - return btoa(data) - -proc addJSModules(client: Client, ctx: JSContext) = - ctx.addDOMExceptionModule() - ctx.addCookieModule() - ctx.addURLModule() - ctx.addEventModule() - ctx.addDOMModule() - ctx.addHTMLModule() - ctx.addIntlModule() - ctx.addBlobModule() - ctx.addFormDataModule() - ctx.addXMLHttpRequestModule() - ctx.addHeadersModule() - ctx.addRequestModule() - ctx.addResponseModule() - ctx.addLineEditModule() - ctx.addConfigModule() - ctx.addPagerModule() - ctx.addContainerModule() - -func getClient(client: Client): Client {.jsfget: "client".} = - return client - -proc newClient*(config: Config, forkserver: ForkServer, mainproc: Pid): Client = - setControlCHook(proc() {.noconv.} = quit(1)) - let jsrt = newJSRuntime() - JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil) - let jsctx = jsrt.newJSContext() - let attrs = getWindowAttributes(stdout) - let client = Client( - config: config, - forkserver: forkserver, - mainproc: mainproc, - attrs: attrs, - loader: forkserver.newFileLoader( - defaultHeaders = config.getDefaultHeaders(), - proxy = config.getProxy(), - acceptProxy = true - ), - jsrt: jsrt, - jsctx: jsctx, - pager: newPager(config, attrs, forkserver, mainproc, jsctx) - ) - jsrt.setInterruptHandler(interruptHandler, cast[pointer](client)) - var global = JS_GetGlobalObject(jsctx) - jsctx.registerType(Client, asglobal = true) - setGlobal(jsctx, global, client) - JS_FreeValue(jsctx, global) - jsctx.registerType(Console) - client.addJSModules(jsctx) - return client diff --git a/src/display/pager.nim b/src/display/pager.nim deleted file mode 100644 index 5c6d1852..00000000 --- a/src/display/pager.nim +++ /dev/null @@ -1,1189 +0,0 @@ -import deques -import net -import options -import os -import osproc -import streams -import tables -import unicode - -when defined(posix): - import posix - -import buffer/cell -import buffer/container -import buffer/select -import config/config -import config/mailcap -import config/mimetypes -import display/term -import extern/editor -import extern/runproc -import io/connecterror -import io/lineedit -import io/loader -import io/promise -import io/request -import io/tempfile -import io/window -import ips/forkserver -import ips/socketstream -import js/dict -import js/javascript -import js/regex -import js/tojs -import types/buffersource -import types/color -import types/cookie -import types/url -import utils/opt -import utils/twtstr - -import chakasu/charset - -type - LineMode* = enum - NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F, - SEARCH_B, ISEARCH_F, ISEARCH_B, GOTO_LINE - - Pager* = ref object - alerton: bool - alerts: seq[string] - askcursor: int - askpromise*: Promise[bool] - askprompt: string - commandMode* {.jsget.}: bool - config: Config - container*: Container - cookiejars: Table[string, CookieJar] - display: FixedGrid - forkserver: ForkServer - iregex: Result[Regex, string] - isearchpromise: EmptyPromise - lineedit*: Option[LineEdit] - linehist: array[LineMode, LineHistory] - linemode*: LineMode - mailcap: Mailcap - mainproc: Pid - mimeTypes: MimeTypes - numload*: int - omnirules: seq[OmniRule] - procmap*: Table[Pid, Container] - proxy: URL - redraw*: bool - regex: Opt[Regex] - reverseSearch: bool - scommand*: string - siteconf: seq[SiteConfig] - statusgrid*: FixedGrid - term*: Terminal - tty: File - unreg*: seq[(Pid, SocketStream)] - username: string - -jsDestructor(Pager) - -func attrs(pager: Pager): WindowAttributes = pager.term.attrs - -func getRoot(container: Container): Container = - var c = container - while c.parent != nil: c = c.parent - return c - -iterator all_children(parent: Container): Container {.inline.} = - var stack = newSeqOfCap[Container](parent.children.len) - for i in countdown(parent.children.high, 0): - stack.add(parent.children[i]) - while stack.len > 0: - let c = stack.pop() - yield c - for i in countdown(c.children.high, 0): - stack.add(c.children[i]) - -iterator containers*(pager: Pager): Container {.inline.} = - if pager.container != nil: - let root = getRoot(pager.container) - yield root - for c in root.all_children: - yield c - -proc setContainer*(pager: Pager, c: Container) {.jsfunc.} = - pager.container = c - pager.redraw = true - if c != nil: - pager.term.setTitle(c.getTitle()) - -proc hasprop(ctx: JSContext, pager: Pager, s: string): bool {.jshasprop.} = - if pager.container != nil: - let cval = toJS(ctx, pager.container) - let val = JS_GetPropertyStr(ctx, cval, s) - if val != JS_UNDEFINED: - result = true - JS_FreeValue(ctx, val) - -proc reflect(ctx: JSContext, this_val: JSValue, argc: cint, argv: ptr JSValue, - magic: cint, func_data: ptr JSValue): JSValue {.cdecl.} = - let fun = cast[ptr JSValue](cast[int](func_data) + sizeof(JSValue))[] - return JS_Call(ctx, fun, func_data[], argc, argv) - -proc getter(ctx: JSContext, pager: Pager, s: string): Option[JSValue] - {.jsgetprop.} = - if pager.container != nil: - let cval = toJS(ctx, pager.container) - let val = JS_GetPropertyStr(ctx, cval, s) - if val != JS_UNDEFINED: - if JS_IsFunction(ctx, val): - var func_data = @[cval, val] - let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2, addr func_data[0]) - return some(fun) - return some(val) - -proc searchNext(pager: Pager) {.jsfunc.} = - if pager.regex.isSome: - let wrap = pager.config.search.wrap - if not pager.reverseSearch: - pager.container.cursorNextMatch(pager.regex.get, wrap, true) - else: - pager.container.cursorPrevMatch(pager.regex.get, wrap, true) - -proc searchPrev(pager: Pager) {.jsfunc.} = - if pager.regex.isSome: - let wrap = pager.config.search.wrap - if not pager.reverseSearch: - pager.container.cursorPrevMatch(pager.regex.get, wrap, true) - else: - pager.container.cursorNextMatch(pager.regex.get, wrap, true) - -proc getLineHist(pager: Pager, mode: LineMode): LineHistory = - if pager.linehist[mode] == nil: - pager.linehist[mode] = newLineHistory() - return pager.linehist[mode] - -proc setLineEdit(pager: Pager, prompt: string, mode: LineMode, current = "", hide = false) = - pager.lineedit = some(readLine(prompt, pager.attrs.width, current = current, term = pager.term, hide = hide, hist = pager.getLineHist(mode))) - pager.linemode = mode - -proc clearLineEdit(pager: Pager) = - pager.lineedit = none(LineEdit) - -proc searchForward(pager: Pager) {.jsfunc.} = - pager.setLineEdit("/", SEARCH_F) - -proc searchBackward(pager: Pager) {.jsfunc.} = - pager.setLineEdit("?", SEARCH_B) - -proc isearchForward(pager: Pager) {.jsfunc.} = - pager.container.pushCursorPos() - pager.isearchpromise = newResolvedPromise() - pager.setLineEdit("/", ISEARCH_F) - -proc isearchBackward(pager: Pager) {.jsfunc.} = - pager.container.pushCursorPos() - pager.isearchpromise = newResolvedPromise() - pager.setLineEdit("?", ISEARCH_B) - -proc gotoLine[T: string|int](pager: Pager, s: T = "") {.jsfunc.} = - when s is string: - if s == "": - pager.setLineEdit("Goto line: ", GOTO_LINE) - return - pager.container.gotoLine(s) - -proc alert*(pager: Pager, msg: string) - -proc newPager*(config: Config, attrs: WindowAttributes, - forkserver: ForkServer, mainproc: Pid, ctx: JSContext): Pager = - let pager = Pager( - config: config, - display: newFixedGrid(attrs.width, attrs.height - 1), - forkserver: forkserver, - mainproc: mainproc, - omnirules: config.getOmniRules(ctx), - proxy: config.getProxy(), - siteconf: config.getSiteConfig(ctx), - statusgrid: newFixedGrid(attrs.width), - term: newTerminal(stdout, config, attrs), - mimeTypes: config.getMimeTypes() - ) - let (mcap, errs) = config.getMailcap() - pager.mailcap = mcap - for err in errs: - pager.alert("Error reading mailcap: " & err) - return pager - -proc launchPager*(pager: Pager, tty: File) = - pager.tty = tty - pager.term.start(tty) - -proc dumpAlerts*(pager: Pager) = - for msg in pager.alerts: - stderr.write("cha: " & msg & '\n') - -proc quit*(pager: Pager, code = 0) = - pager.term.quit() - pager.dumpAlerts() - -proc clearDisplay(pager: Pager) = - pager.display = newFixedGrid(pager.display.width, pager.display.height) - -proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container - -proc refreshDisplay(pager: Pager, container = pager.container) = - pager.clearDisplay() - container.drawLines(pager.display, - cellColor(pager.config.display.highlight_color)) - -# Note: this function doesn't work if start < i of last written char -proc writeStatusMessage(pager: Pager, str: string, - format: Format = newFormat(), start = 0, - maxwidth = -1, clip = '$'): int {.discardable.} = - var maxwidth = maxwidth - if maxwidth == -1: - maxwidth = pager.statusgrid.len - var i = start - let e = min(start + maxwidth, pager.statusgrid.width) - if i >= e: - return i - for r in str.runes: - let pi = i - i += r.twidth(i) - if i >= e: - if i >= pager.statusgrid.width: - i = pi - pager.statusgrid[i].format = format - pager.statusgrid[i].str = $clip - inc i - break - if r.isControlChar(): - pager.statusgrid[pi].str = "^" & getControlLetter(char(r)) - else: - pager.statusgrid[pi].str = $r - pager.statusgrid[pi].format = format - result = i - var def = newFormat() - while i < e: - pager.statusgrid[i].str = "" - pager.statusgrid[i].format = def - inc i - -# Note: should only be called directly after user interaction. -proc refreshStatusMsg*(pager: Pager) = - let container = pager.container - if container == nil: return - if pager.tty == nil: return - if pager.askpromise != nil: return - if container.loadinfo != "": - pager.alerton = false - pager.writeStatusMessage(container.loadinfo) - elif pager.alerts.len > 0: - pager.alerton = true - pager.writeStatusMessage(pager.alerts[0]) - pager.alerts.delete(0) - else: - var format = newFormat() - format.reverse = true - pager.alerton = false - container.clearHover() - var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" & - $container.atPercentOf() & "%)" - let mw = pager.writeStatusMessage(msg, format) - let title = " <" & container.getTitle() & ">" - let hover = container.getHoverText() - if hover.len == 0: - pager.writeStatusMessage(title, format, mw) - else: - let hover2 = " " & hover - let maxwidth = pager.statusgrid.width - hover2.width() - mw - let tw = pager.writeStatusMessage(title, format, mw, maxwidth, '>') - pager.writeStatusMessage(hover2, format, tw) - -# Call refreshStatusMsg if no alert is being displayed on the screen. -proc showAlerts*(pager: Pager) = - if not pager.alerton: - pager.refreshStatusMsg() - -proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) = - var format = newFormat() - container.readLines(proc(line: SimpleFlexibleLine) = - if line.formats.len == 0: - ostream.write(line.str & "\n") - else: - var x = 0 - var w = 0 - var i = 0 - var s = "" - for f in line.formats: - var outstr = "" - while x < f.pos: - var r: Rune - fastRuneAt(line.str, i, r) - outstr &= r - x += r.width() - s &= pager.term.processOutputString(outstr, w) - s &= pager.term.processFormat(format, f.format) - if i < line.str.len: - s &= pager.term.processOutputString(line.str.substr(i), w) - s &= pager.term.processFormat(format, newFormat()) & "\n" - ostream.write(s)) - ostream.flush() - -proc redraw(pager: Pager) {.jsfunc.} = - pager.redraw = true - pager.term.clearCanvas() - -proc draw*(pager: Pager) = - let container = pager.container - if container == nil: return - pager.term.hideCursor() - if pager.redraw: - pager.refreshDisplay() - pager.term.writeGrid(pager.display) - if container.select.open and container.select.redraw: - container.select.drawSelect(pager.display) - pager.term.writeGrid(pager.display) - if pager.askpromise != nil: - discard - elif pager.lineedit.isSome: - if pager.lineedit.get.isnew: - #TODO hack - # make term notice that it must redraw when status is restored - let x = newFixedGrid(pager.attrs.width) - pager.term.writeGrid(x, 0, pager.attrs.height - 1) - else: - pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) - pager.term.outputGrid() - if pager.askpromise != nil: - pager.term.setCursor(pager.askcursor, pager.attrs.height - 1) - elif pager.lineedit.isSome: - if pager.lineedit.get.isnew: - #TODO hack - pager.term.setCursor(0, pager.attrs.height - 1) - pager.lineedit.get.drawPrompt() - pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1) - pager.lineedit.get.fullRedraw() - pager.lineedit.get.isnew = false - pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1) - elif container.select.open: - pager.term.setCursor(container.select.getCursorX(), - container.select.getCursorY()) - else: - pager.term.setCursor(pager.container.acursorx, pager.container.acursory) - pager.term.showCursor() - pager.term.flush() - pager.redraw = false - -proc writeAskPrompt(pager: Pager) = - let yn = " (y/n)" - let maxwidth = pager.statusgrid.width - yn.len - let i = pager.writeStatusMessage(pager.askprompt, maxwidth = maxwidth) - pager.askcursor = pager.writeStatusMessage(yn, start = i) - pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) - -proc ask(pager: Pager, prompt: string): Promise[bool] {.jsfunc.} = - pager.askprompt = prompt - pager.writeAskPrompt() - pager.askpromise = Promise[bool]() - return pager.askpromise - -proc fulfillAsk*(pager: Pager, y: bool) = - pager.askpromise.resolve(y) - pager.askpromise = nil - pager.askprompt = "" - -proc registerContainer*(pager: Pager, container: Container) = - pager.procmap[container.process] = container - -proc addContainer*(pager: Pager, container: Container) = - container.parent = pager.container - if pager.container != nil: - pager.container.children.insert(container, 0) - pager.registerContainer(container) - pager.setContainer(container) - -proc newBuffer(pager: Pager, bufferConfig: BufferConfig, source: BufferSource, - title = "", redirectdepth = 0): Container = - return newBuffer( - pager.forkserver, - pager.mainproc, - bufferConfig, - source, - title, - redirectdepth - ) - -proc dupeBuffer(pager: Pager, container: Container, location: URL, - contentType = ""): Container = - let contentType = if contentType != "": - some(contentType) - else: - container.contenttype - let location = if location != nil: - location - else: - container.source.location - let source = BufferSource( - t: CLONE, - location: location, - contenttype: contentType, - clonepid: container.process, - ) - let pipeTo = pager.newBuffer(container.config, source, container.title) - container.pipeBuffer(pipeTo) - return pipeTo - -proc dupeBuffer(pager: Pager, location: URL = nil) {.jsfunc.} = - pager.addContainer(pager.dupeBuffer(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. -proc prevBuffer(pager: Pager): bool {.jsfunc.} = - if pager.container == nil: - return false - if pager.container.parent == nil: - return false - let n = pager.container.parent.children.find(pager.container) - assert n != -1, "Container not a child of its parent" - if n > 0: - var container = pager.container.parent.children[n - 1] - while container.children.len > 0: - container = container.children[^1] - pager.setContainer(container) - else: - pager.setContainer(pager.container.parent) - return true - -proc nextBuffer(pager: Pager): bool {.jsfunc.} = - if pager.container == nil: - return false - if pager.container.children.len > 0: - pager.setContainer(pager.container.children[0]) - return true - var container = pager.container - while container.parent != nil: - let n = container.parent.children.find(container) - assert n != -1, "Container not a child of its parent" - if n < container.parent.children.high: - pager.setContainer(container.parent.children[n + 1]) - return true - container = container.parent - return false - -proc parentBuffer(pager: Pager): bool {.jsfunc.} = - if pager.container == nil: - return false - if pager.container.parent == nil: - return false - pager.setContainer(pager.container.parent) - return true - -proc prevSiblingBuffer(pager: Pager): bool {.jsfunc.} = - if pager.container == nil: - return false - if pager.container.parent == nil: - return false - var n = pager.container.parent.children.find(pager.container) - assert n != -1, "Container not a child of its parent" - if n == 0: - n = pager.container.parent.children.len - pager.setContainer(pager.container.parent.children[n - 1]) - return true - -proc nextSiblingBuffer(pager: Pager): bool {.jsfunc.} = - if pager.container == nil: - return false - if pager.container.parent == nil: - return false - var 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: - n = -1 - pager.setContainer(pager.container.parent.children[n + 1]) - return true - -proc alert*(pager: Pager, msg: string) {.jsfunc.} = - pager.alerts.add(msg) - -proc deleteContainer(pager: Pager, container: Container) = - container.cancel() - if container.sourcepair != nil: - container.sourcepair.sourcepair = nil - container.sourcepair = nil - 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: - if n == 0: - pager.setContainer(parent) - else: - pager.setContainer(parent.children[n - 1]) - 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: - for child in container.children: - child.parent = nil - if container == pager.container: - pager.setContainer(nil) - container.parent = nil - container.children.setLen(0) - pager.unreg.add((container.process, SocketStream(container.iface.stream))) - pager.forkserver.removeChild(container.process) - -proc discardBuffer(pager: Pager, container = none(Container)) {.jsfunc.} = - let c = container.get(pager.container) - if c == nil or c.parent == nil and c.children.len == 0: - pager.alert("Cannot discard last buffer!") - else: - pager.deleteContainer(c) - -proc discardTree(pager: Pager, container = none(Container)) {.jsfunc.} = - let container = container.get(pager.container) - if container != nil: - for c in container.all_children: - pager.deleteContainer(c) - else: - pager.alert("Buffer has no children!") - -proc toggleSource(pager: Pager) {.jsfunc.} = - if pager.container.sourcepair != nil: - pager.setContainer(pager.container.sourcepair) - else: - let contenttype = if pager.container.contentType.get("") == "text/html": - "text/plain" - else: - "text/html" - let container = pager.dupeBuffer(pager.container, nil, contenttype) - container.sourcepair = pager.container - pager.container.sourcepair = container - pager.addContainer(container) - -proc windowChange*(pager: Pager, attrs: WindowAttributes) = - pager.term.windowChange(attrs) - pager.display = newFixedGrid(attrs.width, attrs.height - 1) - pager.statusgrid = newFixedGrid(attrs.width) - for container in pager.containers: - container.windowChange(attrs) - if pager.askprompt != "": - pager.writeAskPrompt() - pager.showAlerts() - -# Apply siteconf settings to a request. -# Note that this may modify the URL passed. -proc applySiteconf(pager: Pager, url: var URL): BufferConfig = - let host = url.host - var referer_from: bool - var cookiejar: CookieJar - var headers = pager.config.getDefaultHeaders() - var scripting: bool - var images: bool - var charsets = pager.config.encoding.document_charset - var userstyle = pager.config.css.stylesheet - var proxy = pager.proxy - let mimeTypes = pager.mimeTypes - for sc in pager.siteconf: - if sc.url.isSome and not sc.url.get.match($url): - continue - elif sc.host.isSome and not sc.host.get.match(host): - continue - if sc.rewrite_url != nil: - let s = sc.rewrite_url(url) - if s.isSome and s.get != nil: - url = s.get - if sc.cookie.isSome: - if sc.cookie.get: - # host/url might have changed by now - let jarid = sc.share_cookiejar.get(url.host) - if jarid notin pager.cookiejars: - pager.cookiejars[jarid] = newCookieJar(url, - sc.third_party_cookie) - cookiejar = pager.cookiejars[jarid] - else: - cookiejar = nil # override - if sc.scripting.isSome: - scripting = sc.scripting.get - if sc.referer_from.isSome: - referer_from = sc.referer_from.get - if sc.document_charset.len > 0: - charsets = sc.document_charset - if sc.images.isSome: - images = sc.images.get - if sc.stylesheet.isSome: - userstyle &= "\n" - userstyle &= sc.stylesheet.get - if sc.proxy.isSome: - proxy = sc.proxy.get - return pager.config.getBufferConfig(url, cookiejar, headers, referer_from, - scripting, charsets, images, userstyle, proxy, mimeTypes) - -# Load request in a new buffer. -proc gotoURL(pager: Pager, request: Request, prevurl = none(URL), - ctype = none(string), cs = CHARSET_UNKNOWN, replace: Container = nil, - redirectdepth = 0, referrer: Container = nil) = - if referrer != nil and referrer.config.referer_from: - request.referer = referrer.source.location - var bufferconfig = pager.applySiteconf(request.url) - 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) 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 source = BufferSource( - t: LOAD_REQUEST, - request: request, - contenttype: ctype, - charset: cs, - location: request.url - ) - if referrer != nil: - bufferconfig.referrerpolicy = referrer.config.referrerpolicy - let container = pager.newBuffer( - bufferconfig, - source, - redirectdepth = redirectdepth - ) - if replace != nil: - container.replace = replace - container.copyCursorPos(container.replace) - pager.addContainer(container) - inc pager.numload - else: - pager.container.findAnchor(request.url.anchor) - -proc omniRewrite(pager: Pager, s: string): string = - for rule in pager.omnirules: - if rule.match.match(s): - let sub = rule.substitute_url(s) - if sub.isSome: - return sub.get - else: - pager.alert("Error in substitution of rule " & rule.match.buf & " for " & s) - return s - -# When the user has passed a partial URL as an argument, they might've meant -# either: -# * file://$PWD/<file> -# * https://<url> -# So we attempt to load both, and see what works. -proc loadURL*(pager: Pager, url: string, ctype = none(string), - cs = CHARSET_UNKNOWN) = - let url0 = pager.omniRewrite(url) - let url = if url[0] == '~': expandPath(url0) else: url0 - let firstparse = parseURL(url) - if firstparse.issome: - let prev = if pager.container != nil: - some(pager.container.source.location) - else: - none(URL) - pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs) - return - var urls: seq[URL] - if pager.config.network.prepend_https and url[0] != '/': - let pageurl = parseURL("https://" & url) - if pageurl.isSome: # attempt to load remote page - urls.add(pageurl.get) - let cdir = parseURL("file://" & percentEncode(getCurrentDir(), LocalPathPercentEncodeSet) & DirSep) - let localurl = percentEncode(url, LocalPathPercentEncodeSet) - let newurl = parseURL(localurl, cdir) - if newurl.isSome: - urls.add(newurl.get) # attempt to load local file - if urls.len == 0: - pager.alert("Invalid URL " & url) - else: - let prevc = pager.container - pager.gotoURL(newRequest(urls.pop()), ctype = ctype, cs = cs) - if pager.container != prevc: - pager.container.retry = urls - -proc readPipe0*(pager: Pager, ctype: Option[string], cs: Charset, - fd: FileHandle, location: Option[URL], title: string): Container = - var location = location.get(newURL("file://-").get) - let bufferconfig = pager.applySiteconf(location) - let source = BufferSource( - t: LOAD_PIPE, - fd: fd, - contenttype: some(ctype.get("text/plain")), - charset: cs, - location: location - ) - return pager.newBuffer(bufferconfig, source, title = title) - -proc readPipe*(pager: Pager, ctype: Option[string], cs: Charset, - fd: FileHandle) = - let container = pager.readPipe0(ctype, cs, fd, none(URL), "*pipe*") - pager.addContainer(container) - -proc command(pager: Pager) {.jsfunc.} = - pager.setLineEdit("COMMAND: ", COMMAND) - -proc commandMode(pager: Pager, val: bool) {.jsfset.} = - pager.commandMode = val - if val: - pager.command() - -proc checkRegex(pager: Pager, regex: Result[Regex, string]): Opt[Regex] = - if regex.isErr: - pager.alert("Invalid regex: " & regex.error) - return err() - return ok(regex.get) - -proc updateReadLineISearch(pager: Pager, linemode: LineMode) = - let lineedit = pager.lineedit.get - pager.isearchpromise = pager.isearchpromise.then(proc(): EmptyPromise = - case lineedit.state - of CANCEL: - pager.iregex.err() - pager.container.popCursorPos() - pager.container.clearSearchHighlights() - pager.redraw = true - pager.isearchpromise = nil - of EDIT: - let x = $lineedit.news - if x != "": pager.iregex = compileSearchRegex(x) - pager.container.popCursorPos(true) - pager.container.pushCursorPos() - if pager.iregex.isSome: - pager.container.hlon = true - let wrap = pager.config.search.wrap - return if linemode == ISEARCH_F: - pager.container.cursorNextMatch(pager.iregex.get, wrap, false) - else: - pager.container.cursorPrevMatch(pager.iregex.get, wrap, false) - of FINISH: - pager.regex = pager.checkRegex(pager.iregex) - pager.reverseSearch = linemode == ISEARCH_B - pager.container.clearSearchHighlights() - pager.container.sendCursorPosition() - pager.redraw = true - pager.isearchpromise = nil - ) - -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("Password: ", PASSWORD, hide = true) - 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, referrer = pager.container) - of COMMAND: - pager.scommand = s - if pager.commandMode: - pager.command() - of BUFFER: pager.container.readSuccess(s) - of SEARCH_F, SEARCH_B: - let x = s - if x != "": - pager.regex = pager.checkRegex(compileSearchRegex(x)) - pager.reverseSearch = pager.linemode == SEARCH_B - pager.searchNext() - of GOTO_LINE: - pager.container.gotoLine(s) - else: discard - of CANCEL: - case pager.linemode - of USERNAME: pager.discardBuffer() - of PASSWORD: - pager.username = "" - pager.discardBuffer() - of BUFFER: pager.container.readCanceled() - of COMMAND: pager.commandMode = false - else: discard - if lineedit.state in {CANCEL, FINISH}: - if pager.lineedit.get == lineedit: - pager.clearLineEdit() - -# Open a URL prompt and visit the specified URL. -proc load(pager: Pager, s = "") {.jsfunc.} = - if s.len > 0 and s[^1] == '\n': - pager.loadURL(s[0..^2]) - else: - var url = s - if url == "": - url = pager.container.source.location.serialize() - pager.setLineEdit("URL: ", LOCATION, url) - -# 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, replace = pager.container) - -proc setEnvVars(pager: Pager) {.jsfunc.} = - try: - putEnv("CHA_URL", $pager.container.location) - putEnv("CHA_CHARSET", $pager.container.charset) - except OSError: - pager.alert("Warning: failed to set some environment variables") - -#TODO use default values instead... -type ExternDict = object of JSDict - setenv: Opt[bool] - suspend: Opt[bool] - wait: bool - -#TODO this could be handled much better. -# * suspend, setenv, wait as dict flags -# * retval as int? -proc extern(pager: Pager, cmd: string, t = ExternDict()): bool {.jsfunc.} = - if t.setenv.get(true): - pager.setEnvVars() - if t.suspend.get(true): - return runProcess(pager.term, cmd, t.wait) - else: - return runProcess(cmd) - -proc authorize(pager: Pager) = - pager.setLineEdit("Username: ", USERNAME) - -# Pipe input into the mailcap command, then read its output into a buffer. -# needsterminal is ignored. -proc runMailcapReadPipe(pager: Pager, container: Container, - entry: MailcapEntry, cmd: string): (EmptyPromise, bool) = - var pipefd_in: array[2, cint] - if pipe(pipefd_in) == -1: - raise newException(Defect, "Failed to open pipe.") - var pipefd_out: array[2, cint] - if pipe(pipefd_out) == -1: - raise newException(Defect, "Failed to open pipe.") - let pid = fork() - if pid == -1: - return (nil, false) - elif pid == 0: - # child process - discard close(pipefd_in[1]) - discard close(pipefd_out[0]) - stdout.flushFile() - discard dup2(pipefd_in[0], stdin.getFileHandle()) - discard dup2(pipefd_out[1], stdout.getFileHandle()) - let devnull = open("/dev/null", O_WRONLY) - discard dup2(devnull, stderr.getFileHandle()) - discard close(devnull) - discard close(pipefd_in[0]) - discard close(pipefd_out[1]) - discard execCmd(cmd) - discard close(stdin.getFileHandle()) - discard close(stdout.getFileHandle()) - quit(0) - # parent - discard close(pipefd_in[0]) - discard close(pipefd_out[1]) - let fdin = pipefd_in[1] - let fdout = pipefd_out[0] - let p = container.redirectToFd(fdin, wait = false) - let p2 = p.then(proc(): auto = - discard close(fdin) - let ishtml = HTMLOUTPUT in entry.flags - if ishtml: - #TODO this is a hack for dupe buffer and should be reconsidered. - container.source.contenttype = some("text/html") - return container.readFromFd(fdout, ishtml) - ).then(proc() = - discard close(fdout) - ) - return (p2, true) - -# Pipe input into the mailcap command, and discard its output. -# If needsterminal, leave stderr and stdout open and wait for the process. -proc runMailcapWritePipe(pager: Pager, container: Container, - entry: MailcapEntry, cmd: string): (EmptyPromise, bool) = - let needsterminal = NEEDSTERMINAL in entry.flags - var pipefd: array[2, cint] - if pipe(pipefd) == -1: - raise newException(Defect, "Failed to open pipe.") - if needsterminal: - pager.term.quit() - let pid = fork() - if pid == -1: - return (nil, false) - elif pid == 0: - # child process - discard close(pipefd[1]) - discard dup2(pipefd[0], stdin.getFileHandle()) - if not needsterminal: - let devnull = open("/dev/null", O_WRONLY) - discard dup2(devnull, stdout.getFileHandle()) - discard dup2(devnull, stderr.getFileHandle()) - discard close(devnull) - discard close(pipefd[0]) - discard execCmd(cmd) - discard close(stdin.getFileHandle()) - quit(0) - else: - # parent - discard close(pipefd[0]) - let fd = pipefd[1] - let p = container.redirectToFd(fd, wait = false) - discard close(fd) - if needsterminal: - var x: cint - discard waitpid(pid, x, 0) - pager.term.restart() - return (p, false) - -# Save input in a file, run the command, and redirect its output to a -# new buffer. -# needsterminal is ignored. -proc runMailcapReadFile(pager: Pager, container: Container, - entry: MailcapEntry, cmd, outpath: string): (EmptyPromise, bool) = - let fd = open(outpath, O_WRONLY or O_CREAT, 0o666) - if fd == -1: - return (nil, false) - let p = container.redirectToFd(fd, wait = true).then(proc(): auto = - var pipefd: array[2, cint] # redirect stdout here - if pipe(pipefd) == -1: - raise newException(Defect, "Failed to open pipe.") - let pid = fork() - if pid == 0: - # child process - discard close(pipefd[0]) - discard dup2(pipefd[1], stdout.getFileHandle()) - discard close(pipefd[1]) - let devnull = open("/dev/null", O_WRONLY) - discard dup2(devnull, stderr.getFileHandle()) - discard close(devnull) - discard execCmd(cmd) - discard tryRemoveFile(outpath) - quit(0) - # parent - discard close(pipefd[1]) - let fdout = pipefd[0] - let ishtml = HTMLOUTPUT in entry.flags - if ishtml: - #TODO this is a hack for dupe buffer and should be reconsidered. - container.source.contenttype = some("text/html") - return container.readFromFd(fdout, ishtml).then(proc() = - discard close(fdout) - ) - ) - return (p, true) - -# Save input in a file, run the command, and discard its output. -# If needsterminal, leave stderr and stdout open and wait for the process. -proc runMailcapWriteFile(pager: Pager, container: Container, - entry: MailcapEntry, cmd, outpath: string): (EmptyPromise, bool) = - let needsterminal = NEEDSTERMINAL in entry.flags - let fd = open(outpath, O_WRONLY or O_CREAT, 0o666) - if fd == -1: - return (nil, false) - let p = container.redirectToFd(fd, wait = true).then(proc() = - if needsterminal: - pager.term.quit() - discard execCmd(cmd) - discard tryRemoveFile(outpath) - pager.term.restart() - else: - # don't block - let pid = fork() - if pid == 0: - # child process - let devnull = open("/dev/null", O_WRONLY) - discard dup2(devnull, stdin.getFileHandle()) - discard dup2(devnull, stdout.getFileHandle()) - discard dup2(devnull, stderr.getFileHandle()) - discard close(devnull) - discard execCmd(cmd) - discard tryRemoveFile(outpath) - quit(0) - ) - return (p, false) - -# Search for a mailcap entry, and if found, execute the specified command -# and pipeline the input and output appropriately. -# There is four possible outcomes: -# * pipe stdin, discard stdout -# * pipe stdin, read stdout -# * write to file, run, discard stdout -# * write to file, run, read stdout -# If needsterminal is specified, and stdout is not being read, then the -# pager is suspended until the command exits. -#TODO add support for edit/compose, better error handling (use Promise[bool] -# instead of tuple[EmptyPromise, bool]) -proc checkMailcap(pager: Pager, container: Container): (EmptyPromise, bool) = - if container.contenttype.isNone: - return (nil, true) - if container.source.t == CLONE: - return (nil, true) # clone cannot use mailcap - let contentType = container.contenttype.get - if contentType == "text/html": - # We support HTML natively, so it would make little sense to execute - # mailcap filters for it. - return (nil, true) - elif contentType == "text/plain": - # This could potentially be useful. Unfortunately, many mailcaps include - # a text/plain entry with less by default, so it's probably better to - # ignore this. - return (nil, true) - #TODO callback for outpath or something - let url = container.location - let cs = container.source.charset - let entry = pager.mailcap.getMailcapEntry(contentType, "", url, cs) - if entry != nil: - let tmpdir = pager.config.external.tmpdir - let ext = container.location.pathname.afterLast('.') - let tempfile = getTempfile(tmpdir, ext) - let outpath = if entry.nametemplate != "": - unquoteCommand(entry.nametemplate, contentType, tempfile, url, cs) - else: - tempfile - var canpipe = true - let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, cs, canpipe) - if {COPIOUSOUTPUT, HTMLOUTPUT} * entry.flags == {}: - # no output. - if canpipe: - return pager.runMailcapWritePipe(container, entry[], cmd) - else: - return pager.runMailcapWriteFile(container, entry[], cmd, outpath) - else: - if canpipe: - return pager.runMailcapReadPipe(container, entry[], cmd) - else: - return pager.runMailcapReadFile(container, entry[], cmd, outpath) - return (nil, true) - -proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool = - case event.t - of FAIL: - dec pager.numload - pager.deleteContainer(container) - if container.retry.len > 0: - pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype) - else: - let errorMessage = getLoaderErrorMessage(container.code) - pager.alert("Can't load " & $container.source.location & " (" & - errorMessage & ")") - return false - of SUCCESS: - if container.replace != nil: - let n = container.replace.children.find(container) - if n != -1: - container.replace.children.delete(n) - container.parent = nil - let n2 = container.children.find(container.replace) - if n2 != -1: - container.children.delete(n2) - container.replace.parent = 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 - container.replace.parent = nil - if pager.container == container.replace: - pager.setContainer(container) - pager.deleteContainer(container.replace) - container.replace = nil - of LOADED: - dec pager.numload - of NEEDS_AUTH: - if pager.container == container: - pager.authorize() - of REDIRECT: - if container.redirectdepth < pager.config.network.max_redirect: - pager.alert("Redirecting to " & $event.request.url) - pager.gotoURL(event.request, some(container.source.location), - replace = container, redirectdepth = container.redirectdepth + 1, - referrer = pager.container) - else: - pager.alert("Error: maximum redirection depth reached") - pager.deleteContainer(container) - return false - of ANCHOR: - var url2 = newURL(container.source.location) - url2.setHash(event.anchor) - pager.addContainer(pager.dupeBuffer(container, url2)) - of NO_ANCHOR: - pager.alert("Couldn't find anchor " & event.anchor) - of UPDATE: - if container == pager.container: - pager.redraw = true - if event.force: pager.term.clearCanvas() - of READ_LINE: - if container == pager.container: - pager.setLineEdit("(BUFFER) " & event.prompt, BUFFER, event.value, hide = event.password) - of READ_AREA: - if container == pager.container: - var s = event.tvalue - if openInEditor(pager.term, pager.config, s): - pager.container.readSuccess(s) - else: - pager.container.readCanceled() - pager.redraw = true - of OPEN: - if pager.container == nil or not pager.container.isHoverURL(event.request.url): - pager.ask("Open pop-up? " & $event.request.url).then(proc(x: bool) = - if x: - pager.gotoURL(event.request, some(container.source.location), referrer = pager.container)) - else: - pager.gotoURL(event.request, some(container.source.location), referrer = pager.container) - of INVALID_COMMAND: discard - of STATUS: - if pager.container == container: - pager.showAlerts() - of TITLE: - if pager.container == container: - pager.showAlerts() - pager.term.setTitle(container.getTitle()) - of ALERT: - if pager.container == container: - pager.alert(event.msg) - of CHECK_MAILCAP: - var (cm, connect) = pager.checkMailcap(container) - if cm == nil: - cm = container.connect2() - if connect: - cm.then(proc() = - container.startload()) - else: - cm.then(proc(): auto = - container.quit()) - of QUIT: - dec pager.numload - pager.deleteContainer(container) - return false - of NO_EVENT: discard - return true - -proc handleEvents*(pager: Pager, container: Container) = - while container.events.len > 0: - let event = container.events.popFirst() - if not pager.handleEvent0(container, event): - break - -proc handleEvent*(pager: Pager, container: Container) = - try: - container.handleEvent() - pager.handleEvents(container) - except IOError: - discard - -proc addPagerModule*(ctx: JSContext) = - ctx.registerType(Pager) |