diff options
author | bptato <nincsnevem662@gmail.com> | 2022-09-12 00:30:21 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-09-12 00:30:21 +0200 |
commit | 51ea622d58bfca19212fac1800cfb033bb85ec39 (patch) | |
tree | b75891690f67b190c60584751f2a30c96f342fdc /src/display/client.nim | |
parent | e38402dfa1bbc33db6b9d9736517eb45533d595c (diff) | |
download | chawan-51ea622d58bfca19212fac1800cfb033bb85ec39.tar.gz |
Add JS binding generation
Diffstat (limited to 'src/display/client.nim')
-rw-r--r-- | src/display/client.nim | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/src/display/client.nim b/src/display/client.nim new file mode 100644 index 00000000..6e3c8c45 --- /dev/null +++ b/src/display/client.nim @@ -0,0 +1,591 @@ +import options +import os +import streams +import terminal +import unicode + +import css/sheet +import config/config +import io/buffer +import io/cell +import io/lineedit +import io/loader +import io/request +import io/term +import js/javascript +import js/regex +import types/url +import utils/twtstr + +type + Client* = ref ClientObj + ClientObj* = object + buffer*: Buffer + feednext: bool + s: string + iserror: bool + errormessage: string + userstyle: CSSStylesheet + loader: FileLoader + console: Console + jsrt: JSRuntime + jsctx: JSContext + regex: Option[Regex] + revsearch: bool + needsauth: bool + redirecturl: Option[Url] + cmdmode: bool + + Console* = ref object + err*: Stream + lastbuf*: Buffer + ibuf: string + + ActionError* = object of IOError + LoadError* = object of ActionError + InterruptError* = object of LoadError + +proc readChar(console: Console): char = + if console.ibuf == "": + return stdin.readChar() + result = console.ibuf[0] + console.ibuf = console.ibuf.substr(1) + +proc `=destroy`(client: var ClientObj) = + if client.jsctx != nil: + free(client.jsctx) + if client.jsrt != nil: + free(client.jsrt) + +proc statusMode(client: Client) = + print(HVP(client.buffer.height + 1, 1)) + print(EL()) + +proc loadError(s: string) = + raise newException(LoadError, s) + +proc actionError(s: string) = + raise newException(ActionError, s) + +proc addBuffer(client: Client) = + if client.buffer == nil: + client.buffer = newBuffer() + else: + let oldnext = client.buffer.next + client.buffer.next = newBuffer() + if oldnext != nil: + oldnext.prev = client.buffer.next + client.buffer.next.prev = client.buffer + client.buffer.next.next = oldnext + client.buffer = client.buffer.next + client.buffer.loader = client.loader + client.buffer.userstyle = client.userstyle + client.buffer.markcolor = gconfig.markcolor + +proc prevBuffer(client: Client) {.jsfunc.} = + if client.buffer.prev != nil: + client.buffer = client.buffer.prev + client.buffer.redraw = true + +proc nextBuffer(client: Client) {.jsfunc.} = + if client.buffer.next != nil: + client.buffer = client.buffer.next + client.buffer.redraw = true + +proc discardBuffer(buffer: Buffer) = + if buffer.next == nil and buffer.prev == nil: + actionError("Cannot discard last buffer!") + if buffer.sourcepair != nil: + buffer.sourcepair.sourcepair = nil + if buffer.next != nil: + buffer.next.prev = buffer.prev + if buffer.prev != nil: + buffer.prev.next = buffer.next + buffer.sourcepair = nil + buffer.next = nil + buffer.prev = nil + +proc discardBuffer(client: Client) {.jsfunc.} = + let old = client.buffer + if old.next != nil: + client.buffer = old.next + elif old.prev != nil: + client.buffer = old.prev + else: + actionError("Cannot discard last buffer!") + discardBuffer(old) + client.buffer.redraw = true + +proc setupBuffer(client: Client) = + let buffer = client.buffer + buffer.load() + buffer.render() + buffer.gotoAnchor() + buffer.redraw = true + +proc dupeBuffer(client: Client, location = none(URL)) {.jsfunc.} = + let prev = client.buffer + client.addBuffer() + client.buffer.contenttype = prev.contenttype + client.buffer.ispipe = prev.ispipe + client.buffer.istream = newStringStream(prev.source) + if location.issome: + client.buffer.location = location.get + else: + client.buffer.location = prev.location + client.buffer.document = prev.document + client.setupBuffer() + +proc readPipe(client: Client, ctype: string) = + client.addBuffer() + client.buffer.contenttype = if ctype != "": ctype else: "text/plain" + client.buffer.ispipe = true + client.buffer.istream = newFileStream(stdin) + client.buffer.location = newURL("file://-") + client.buffer.load() + #TODO is this portable at all? + if reopen(stdin, "/dev/tty", fmReadWrite): + client.setupBuffer() + else: + client.buffer.drawBuffer() + +# Load request in a new buffer. +var g_client: Client +proc gotoUrl(client: Client, request: Request, prevurl = none(URL), force = false, ctype = "") = + if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or + prevurl.get.equals(request.url) or request.httpmethod != HTTP_GET: + let page = client.loader.doRequest(request) + client.needsauth = page.status == 401 # Unauthorized + client.redirecturl = page.redirect + if page.body != nil: + client.addBuffer() + g_client = client + client.buffer.contenttype = if ctype != "": ctype else: page.contenttype + client.buffer.istream = page.body + client.buffer.location = request.url + client.setupBuffer() + else: + loadError("Couldn't load " & $request.url & " (" & $page.res & ")") + elif client.buffer != nil and prevurl.issome and prevurl.get.equals(request.url, true): + if client.buffer.hasAnchor(request.url.anchor): + client.dupeBuffer(request.url.some) + else: + loadError("Couldn't find anchor " & request.url.anchor) + +# Relative gotoUrl: either to prevurl, or if that's none, client.buffer.url. +proc gotoUrl(client: Client, url: string, prevurl = none(URL), force = false, ctype = "") = + var prevurl = prevurl + if prevurl.isnone and client.buffer != nil: + prevurl = client.buffer.location.some + let newurl = parseUrl(url, prevurl) + if newurl.isnone: + loadError("Invalid URL " & url) + client.gotoUrl(newRequest(newurl.get), prevurl, force, ctype) + +# 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. +# (TODO: make this optional) +proc loadUrl(client: Client, url: string, ctype = "") = + let firstparse = parseUrl(url) + if firstparse.issome: + client.gotoUrl(newRequest(firstparse.get), none(Url), true, ctype) + else: + let cdir = parseUrl("file://" & getCurrentDir() & DirSep) + try: + # attempt to load local file + client.gotoUrl(url, cdir, true, ctype) + except LoadError: + try: + # attempt to load local file (this time percent encoded) + client.gotoUrl(percentEncode(url, LocalPathPercentEncodeSet), cdir, true, ctype) + except LoadError: + # attempt to load remote page + client.gotoUrl("https://" & url, none(Url), true, ctype) + +# Reload the page in a new buffer, then kill the previous buffer. +proc reloadPage(client: Client) {.jsfunc.} = + let buf = client.buffer + client.gotoUrl(newRequest(client.buffer.location), none(URL), true, client.buffer.contenttype) + discardBuffer(buf) + +# Open a URL prompt and visit the specified URL. +proc changeLocation(client: Client) {.jsfunc.} = + let buffer = client.buffer + var url = buffer.location.serialize(true) + client.statusMode() + let status = readLine("URL: ", url, buffer.width) + if status: + client.loadUrl(url) + +proc click(client: Client) {.jsfunc.} = + let req = client.buffer.click() + if req.issome: + client.gotoUrl(req.get, client.buffer.location.some) + +proc toggleSource*(client: Client) {.jsfunc.} = + let buffer = client.buffer + if buffer.sourcepair != nil: + client.buffer = buffer.sourcepair + client.buffer.redraw = true + else: + client.addBuffer() + client.buffer.sourcepair = client.buffer.prev + client.buffer.sourcepair.sourcepair = client.buffer + client.buffer.source = client.buffer.prev.source + client.buffer.streamclosed = true + client.buffer.location = client.buffer.sourcepair.location + client.buffer.ispipe = client.buffer.sourcepair.ispipe + let prevtype = client.buffer.prev.contenttype + if prevtype == "text/html": + client.buffer.contenttype = "text/plain" + else: + client.buffer.contenttype = "text/html" + client.setupBuffer() + +proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = + let client = cast[Client](opaque) + try: + let c = stdin.readChar() + if c == char(3): #C-c + client.console.ibuf = "" + return 1 + else: + client.console.ibuf &= c + except IOError: + discard + return 0 + +proc evalJS(client: Client, src, filename: string): JSObject = + unblockStdin() + return client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL) + +proc command(client: Client, src: string) = + restoreStdin() + let previ = client.console.err.getPosition() + let ret = client.evalJS(src, "<command>") + if ret.isException(): + let ex = client.jsctx.getException() + let str = ex.toString() + if str.issome: + client.console.err.write(str.get & '\n') + var stack = ex.getProperty("stack") + if not stack.isUndefined(): + let str = stack.toString() + if str.issome: + client.console.err.write(str.get) + free(stack) + free(ex) + else: + let str = ret.toString() + if str.issome: + client.console.err.write(str.get & '\n') + free(ret) + g_client = client + client.console.err.setPosition(previ) + if client.console.lastbuf == nil or client.console.lastbuf != client.buffer: + client.addBuffer() + client.buffer.istream = newStringStream(client.console.err.readAll()) #TODO + client.buffer.contenttype = "text/plain" + client.buffer.location = parseUrl("javascript:void(0);").get + client.console.lastbuf = client.buffer + else: + client.buffer.istream = newStringStream(client.buffer.source & client.console.err.readAll()) + client.buffer.streamclosed = false + client.setupBuffer() + client.buffer.cursorLastLine() + +proc command(client: Client): bool {.jsfunc.} = + var iput: string + client.statusMode() + let status = readLine("COMMAND: ", iput, client.buffer.width) + if status: + client.command(iput) + return status + +proc commandMode(client: Client) {.jsfunc.} = + client.cmdmode = client.command() + +proc searchNext(client: Client) {.jsfunc.} = + if client.regex.issome: + if not client.revsearch: + discard client.buffer.cursorNextMatch(client.regex.get) + else: + discard client.buffer.cursorPrevMatch(client.regex.get) + +proc searchPrev(client: Client) {.jsfunc.} = + if client.regex.issome: + if not client.revsearch: + discard client.buffer.cursorPrevMatch(client.regex.get) + else: + discard client.buffer.cursorNextMatch(client.regex.get) + +proc search(client: Client) {.jsfunc.} = + client.statusMode() + var iput: string + let status = readLine("/", iput, client.buffer.width) + if status: + if iput.len != 0: + client.regex = compileSearchRegex(iput) + client.revsearch = false + client.searchNext() + +proc searchBack(client: Client) {.jsfunc.} = + client.statusMode() + var iput: string + let status = readLine("?", iput, client.buffer.width) + if status: + if iput.len != 0: + client.regex = compileSearchRegex(iput) + client.revsearch = true + client.searchNext() + +proc isearch(client: Client) {.jsfunc.} = + client.statusMode() + var iput: string + let cpos = client.buffer.cpos + var mark: Mark + template del_mark() = + if mark != nil: + client.buffer.removeMark(mark) + + let status = readLine("/", iput, client.buffer.width, {}, false, (proc(state: var LineState): bool = + del_mark + let regex = compileSearchRegex($state.news) + client.buffer.cpos = cpos + if regex.issome: + let match = client.buffer.cursorNextMatch(regex.get) + if match.success: + mark = client.buffer.addMark(match.x, match.y, match.str.width()) + client.buffer.redraw = true + client.buffer.refreshBuffer(true) + print(HVP(client.buffer.height + 1, 2)) + print(SGR()) + else: + del_mark + client.buffer.redraw = true + client.buffer.refreshBuffer(true) + print(HVP(client.buffer.height + 1, 2)) + print(SGR()) + return true + false + )) + + del_mark + client.buffer.redraw = true + client.buffer.refreshBuffer(true) + if status: + client.regex = compileSearchRegex(iput) + else: + client.buffer.cpos = cpos + +proc isearchBack(client: Client) {.jsfunc.} = + client.statusMode() + var iput: string + let cpos = client.buffer.cpos + var mark: Mark + template del_mark() = + if mark != nil: + client.buffer.removeMark(mark) + let status = readLine("?", iput, client.buffer.width, {}, false, (proc(state: var LineState): bool = + del_mark + let regex = compileSearchRegex($state.news) + client.buffer.cpos = cpos + if regex.issome: + let match = client.buffer.cursorPrevMatch(regex.get) + if match.success: + mark = client.buffer.addMark(match.x, match.y, match.str.width()) + client.buffer.redraw = true + client.buffer.refreshBuffer(true) + print(HVP(client.buffer.height + 1, 2)) + print(SGR()) + else: + del_mark + client.buffer.redraw = true + client.buffer.refreshBuffer(true) + print(HVP(client.buffer.height + 1, 2)) + print(SGR()) + return true + false + )) + del_mark + client.buffer.redraw = true + if status: + client.regex = compileSearchRegex(iput) + else: + client.buffer.cpos = cpos + +proc quit(client: Client) {.jsfunc.} = + eraseScreen() + print(HVP(0, 0)) + quit(0) + +proc feedNext(client: Client) {.jsfunc.} = + client.feednext = true + +#TODO move this to a pager module or something +proc cursorLeft(client: Client) {.jsfunc.} = client.buffer.cursorLeft() +proc cursorDown(client: Client) {.jsfunc.} = client.buffer.cursorDown() +proc cursorUp(client: Client) {.jsfunc.} = client.buffer.cursorUp() +proc cursorRight(client: Client) {.jsfunc.} = client.buffer.cursorRight() +proc cursorLineBegin(client: Client) {.jsfunc.} = client.buffer.cursorLineBegin() +proc cursorLineEnd(client: Client) {.jsfunc.} = client.buffer.cursorLineEnd() +proc cursorNextWord(client: Client) {.jsfunc.} = client.buffer.cursorNextWord() +proc cursorPrevWord(client: Client) {.jsfunc.} = client.buffer.cursorPrevWord() +proc cursorNextLink(client: Client) {.jsfunc.} = client.buffer.cursorNextLink() +proc cursorPrevLink(client: Client) {.jsfunc.} = client.buffer.cursorPrevLink() +proc pageDown(client: Client) {.jsfunc.} = client.buffer.pageDown() +proc pageUp(client: Client) {.jsfunc.} = client.buffer.pageUp() +proc pageRight(client: Client) {.jsfunc.} = client.buffer.pageRight() +proc pageLeft(client: Client) {.jsfunc.} = client.buffer.pageLeft() +proc halfPageDown(client: Client) {.jsfunc.} = client.buffer.halfPageDown() +proc halfPageUp(client: Client) {.jsfunc.} = client.buffer.halfPageUp() +proc cursorFirstLine(client: Client) {.jsfunc.} = client.buffer.cursorFirstLine() +proc cursorLastLine(client: Client) {.jsfunc.} = client.buffer.cursorLastLine() +proc cursorTop(client: Client) {.jsfunc.} = client.buffer.cursorTop() +proc cursorMiddle(client: Client) {.jsfunc.} = client.buffer.cursorMiddle() +proc cursorBottom(client: Client) {.jsfunc.} = client.buffer.cursorBottom() +proc cursorLeftEdge(client: Client) {.jsfunc.} = client.buffer.cursorLeftEdge() +proc cursorVertMiddle(client: Client) {.jsfunc.} = client.buffer.cursorVertMiddle() +proc cursorRightEdge(client: Client) {.jsfunc.} = client.buffer.cursorRightEdge() +proc centerLine(client: Client) {.jsfunc.} = client.buffer.centerLine() +proc scrollDown(client: Client) {.jsfunc.} = client.buffer.scrollDown() +proc scrollUp(client: Client) {.jsfunc.} = client.buffer.scrollUp() +proc scrollLeft(client: Client) {.jsfunc.} = client.buffer.scrollLeft() +proc scrollRight(client: Client) {.jsfunc.} = client.buffer.scrollRight() +proc lineInfo(client: Client) {.jsfunc.} = client.buffer.lineInfo() +proc reshape(client: Client) {.jsfunc.} = client.buffer.reshape = true +proc redraw(client: Client) {.jsfunc.} = client.buffer.redraw = true + +proc input(client: Client) = + if client.cmdmode: + client.commandMode() + return + if not client.feednext: + client.s = "" + else: + client.feednext = false + restoreStdin() + let c = client.console.readChar() + client.s &= c + + let action = getNormalAction(client.s) + discard client.evalJS(action, "<command>") + +proc followRedirect(client: Client) + +proc checkAuth(client: Client) = + if client.needsauth: + client.buffer.refreshBuffer() + client.statusMode() + var username = "" + let ustatus = readLine("Username: ", username, client.buffer.width) + if not ustatus: + client.needsauth = false + return + client.statusMode() + var password = "" + let pstatus = readLine("Password: ", password, client.buffer.width, hide = true) + if not pstatus: + client.needsauth = false + return + var url = client.buffer.location + url.username = username + url.password = password + var buf = client.buffer + client.gotoUrl(newRequest(url), prevurl = some(client.buffer.location)) + discardBuffer(buf) + client.followRedirect() + +proc followRedirect(client: Client) = + while client.redirecturl.issome: + client.statusMode() + print("Redirecting to ", $client.redirecturl.get) + stdout.flushFile() + client.buffer.refreshBuffer(true) + var buf = client.buffer + let redirecturl = client.redirecturl.get + client.redirecturl = none(Url) + client.gotoUrl(newRequest(redirecturl), prevurl = some(client.buffer.location)) + discardBuffer(buf) + if client.needsauth: + client.checkAuth() + +proc inputLoop(client: Client) = + while true: + g_client = client + restoreStdin() + client.followRedirect() + client.checkAuth() + client.buffer.refreshBuffer() + if client.needsauth: # Unauthorized + client.checkAuth() + try: + client.input() + except ActionError as e: + client.buffer.setStatusMessage(e.msg) + +proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) = + client.userstyle = gconfig.stylesheet.parseStylesheet() + if not stdin.isatty: + client.readPipe(ctype) + try: + for page in pages: + client.loadUrl(page, ctype) + except LoadError as e: + eprint e.msg + quit(1) + + if stdout.isatty and not dump: + when defined(posix): + enableRawMode() + client.inputLoop() + else: + var buffer = client.buffer + while buffer.next != nil: + buffer = buffer.next + + buffer.drawBuffer() + while buffer.prev != nil: + buffer = buffer.prev + buffer.drawBuffer() + +proc nimGCStats(client: Client): string {.jsfunc.} = + return GC_getStatistics() + +proc jsGCStats(client: Client): string {.jsfunc.} = + return client.jsrt.getMemoryUsage() + +func newConsole(): Console = + new(result) + result.err = newStringStream() + +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') + +proc newClient*(): Client = + new(result) + result.loader = newFileLoader() + result.console = newConsole() + let rt = newJSRuntime() + rt.setInterruptHandler(interruptHandler, cast[pointer](result)) + let ctx = rt.newJSContext() + result.jsrt = rt + result.jsctx = ctx + var global = ctx.getGlobalObject() + discard ctx.registerType(Client, asglobal = true, addto = some(global)) + global.setOpaque(result) + global.setProperty("client", global) + + let consoleClassId = ctx.registerType(Console) + let jsConsole = ctx.newJSObject(consoleClassId) + jsConsole.setOpaque(result.console) + global.setProperty("console", jsConsole) + + ctx.addUrlModule() |