diff options
-rw-r--r-- | res/config.toml | 1 | ||||
-rw-r--r-- | src/bindings/quickjs.nim | 129 | ||||
-rw-r--r-- | src/client.nim | 79 | ||||
-rw-r--r-- | src/config/config.nim | 1 | ||||
-rw-r--r-- | src/js/javascript.nim | 108 |
5 files changed, 314 insertions, 4 deletions
diff --git a/res/config.toml b/res/config.toml index 372a9f60..21005b5d 100644 --- a/res/config.toml +++ b/res/config.toml @@ -55,6 +55,7 @@ v = 'TOGGLE_SOURCE' D = 'DISCARD_BUFFER' ',' = 'PREV_BUFFER' '.' = 'NEXT_BUFFER' +M-c = 'COMMAND' [line] C-m = 'SUBMIT' diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim new file mode 100644 index 00000000..76cd74cd --- /dev/null +++ b/src/bindings/quickjs.nim @@ -0,0 +1,129 @@ +import os + +const javascriptDirs = ["/usr", "/lib", "/usr/lib", "/usr/local/lib", "/usr/local"] +const lib = (func(): string = + when defined(posix): + for dir in javascriptDirs: + for dir in @[dir, dir / "quickjs"]: + if fileExists(dir / "libquickjs.a"): + return dir +)() +const hlib = (func(): string = + when defined(posix): + for dir in javascriptDirs: + for dir in @[dir / "include", dir / "include" / "quickjs"]: + if fileExists(dir / "quickjs.h"): + return dir +)() +const qjsheader = "<quickjs/quickjs.h>" + +when lib != "": + {.passL: "-L" & lib.} +when hlib != "": + {.passC: "-I" & hlib.} +{.passL: "-lquickjs -lm -lpthread".} + +when sizeof(int) < sizeof(int64): + {.passC: "-DJS_NAN_BOXING".} + type + JSValue* {.importc: "JSValue", header: qjsheader.} = uint64 + # uh this won't compile you're on your own +else: + type + JSValueUnion {.union.} = object + int32: int32 + float64: float64 + `ptr`: pointer + JSValue* {.importc: "JSValue", header: qjsheader.} = object + u: JSValueUnion + tag: int64 + +type + JSRuntime* = ptr object + JSContext* = ptr object + JSCFunction* = proc (ctx: JSContext, this_val: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.} + JSCFunctionEnum* {.size: sizeof(cint).} = enum + JS_CFUNC_generic, JS_CFUNC_generic_magic, JS_CFUNC_constructor, + JS_CFUNC_constructor_magic, JS_CFUNC_constructor_or_func, + JS_CFUNC_constructor_or_func_magic, JS_CFUNC_f_f, JS_CFUNC_f_f_f, + JS_CFUNC_getter, JS_CFUNC_setter, JS_CFUNC_getter_magic, + JS_CFUNC_setter_magic, JS_CFUNC_iterator_next + +const ## all tags with a reference count are negative + JS_TAG_FIRST* = -10 ## first negative tag + JS_TAG_BIG_INT* = -10 + JS_TAG_BIG_FLOAT* = -9 + JS_TAG_SYMBOL* = -8 + JS_TAG_STRING* = -7 + JS_TAG_SHAPE* = -6 ## used internally during GC + JS_TAG_ASYNC_FUNCTION* = -5 ## used internally during GC + JS_TAG_VAR_REF* = -4 ## used internally during GC + JS_TAG_MODULE* = -3 ## used internally + JS_TAG_FUNCTION_BYTECODE* = -2 ## used internally + JS_TAG_OBJECT* = -1 + JS_TAG_INT* = 0 + JS_TAG_BOOL* = 1 + JS_TAG_NULL* = 2 + JS_TAG_UNDEFINED* = 3 + JS_TAG_UNINITIALIZED* = 4 + JS_TAG_CATCH_OFFSET* = 5 + JS_TAG_EXCEPTION* = 6 + JS_TAG_FLOAT64* = 7 ## any larger tag is FLOAT64 if JS_NAN_BOXING + +template JS_MKVAL*(t, val: untyped): JSValue = + JSValue(u: JSValueUnion(`int32`: val), tag: t) + +const + JS_NULL* = JS_MKVAL(JS_TAG_NULL, 0) + JS_UNDEFINED* = JS_MKVAL(JS_TAG_UNDEFINED, 0) + JS_FALSE* = JS_MKVAL(JS_TAG_BOOL, 0) + JS_TRUE* = JS_MKVAL(JS_TAG_BOOL, 1) + JS_EXCEPTION* = JS_MKVAL(JS_TAG_EXCEPTION, 0) + JS_UNINITIALIZED* = JS_MKVAL(JS_TAG_UNINITIALIZED, 0) + +const + JS_EVAL_TYPE_GLOBAL* = (0 shl 0) ## global code (default) + JS_EVAL_TYPE_MODULE* = (1 shl 0) ## module code + JS_EVAL_TYPE_DIRECT* = (2 shl 0) ## direct call (internal use) + JS_EVAL_TYPE_INDIRECT* = (3 shl 0) ## indirect call (internal use) + JS_EVAL_TYPE_MASK* = (3 shl 0) + JS_EVAL_FLAG_SHEBANG* = (1 shl 2) ## skip first line beginning with '#!' + JS_EVAL_FLAG_STRICT* = (1 shl 3) ## force 'strict' mode + JS_EVAL_FLAG_STRIP* = (1 shl 4) ## force 'strip' mode + JS_EVAL_FLAG_COMPILE_ONLY* = (1 shl 5) ## internal use + +proc JS_NewRuntime*(): JSRuntime {.importc: "JS_NewRuntime", header: qjsheader.} +proc JS_FreeRuntime*(rt: JSRuntime) {.importc: "JS_FreeRuntime", header: qjsheader.} + +proc JS_NewContext*(rt: JSRuntime): JSContext {.importc: "JS_NewContext", header: qjsheader.} +proc JS_FreeContext*(ctx: JSContext) {.importc: "JS_FreeContext", header: qjsheader.} + +proc JS_GetGlobalObject*(ctx: JSContext): JSValue {.importc: "JS_GetGlobalObject", header: qjsheader.} + +proc JS_NewObject*(ctx: JSContext): JSValue {.importc: "JS_NewObject", header: qjsheader.} + +proc JS_NewCFunction2*(ctx: JSContext, cfunc: JSCFunction, name: cstring, length: int, proto: JSCFunctionEnum, magic: int): JSValue {.importc: "JS_NewCFunction2", header: qjsheader.} +proc JS_NewCFunction*(ctx: JSContext, cfunc: JSCFunction, name: cstring, length: int): JSValue {.importc: "JS_NewCFunction", header: qjsheader.} + +proc JS_SetPropertyStr*(ctx: JSContext, this_obj: JSValue, prop: cstring, val: JSValue): int + {.importc: "JS_SetPropertyStr", header: qjsheader.} +proc JS_GetPropertyStr*(ctx: JSContext, this_obj: JSValue, prop: cstring): JSValue + {.importc: "JS_GetPropertyStr", header: qjsheader.} + +proc JS_FreeValue*(ctx: JSContext, v: JSValue) {.importc: "JS_FreeValue", header: qjsheader.} + +# use toString if possible +proc JS_ToCStringLen*(ctx: JSContext, plen: ptr int, val1: JSValue): cstring {.importc: "JS_ToCStringLen", header: qjsheader.} +proc JS_ToCString*(ctx: JSContext, val1: JSValue): cstring {.importc: "JS_ToCString", header: qjsheader.} +proc JS_FreeCString*(ctx: JSContext, `ptr`: cstring) {.importc: "JS_FreeCString", header: qjsheader.} + +proc JS_Eval*(ctx: JSContext, input: cstring, input_len: int, filename: cstring, eval_flags: int): JSValue {.importc: "JS_Eval", header: qjsheader.} + +proc JS_IsException*(v: JSValue): bool {.importc: "JS_IsException", header: qjsheader.} +proc JS_IsError*(v: JSValue): bool {.importc: "JS_IsError", header: qjsheader.} +proc JS_IsUndefined*(v: JSValue): bool {.importc: "JS_IsUndefined", header: qjsheader.} + +proc JS_GetException*(ctx: JSContext): JSValue {.importc: "JS_GetException", header: qjsheader.} + +proc JS_SetContextOpaque*(ctx: JSContext, opaque: pointer) {.importc: "JS_SetContextOpaque", header: qjsheader.} +proc JS_GetContextOpaque*(ctx: JSContext): pointer {.importc: "JS_GetContextOpaque", header: qjsheader.} diff --git a/src/client.nim b/src/client.nim index 66cd0c41..16e30b00 100644 --- a/src/client.nim +++ b/src/client.nim @@ -1,18 +1,20 @@ -import streams -import terminal import options import os +import streams +import terminal import css/sheet import config/config import io/buffer import io/lineedit import io/loader +import js/javascript import types/url import utils/twtstr type - Client* = ref object + Client* = ref ClientObj + ClientObj = object buffer: Buffer feednext: bool s: string @@ -20,14 +22,48 @@ type errormessage: string userstyle: CSSStylesheet loader: FileLoader + jsrt: JSRuntime + jsctx: JSContext ActionError = object of IOError LoadError = object of ActionError InterruptError = object of LoadError +proc js_console_log(ctx: JSContext, this: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.} = + let opaque = ctx.getOpaque() + for i in 0..<argc: + let arg = getJSObject(ctx, argv, i) + if i != 0: + opaque.err &= ' ' + let str = arg.toString() + if str.isnone: + return JS_EXCEPTION + opaque.err &= str.get + opaque.err &= '\n' + return JS_UNDEFINED + +# Probably never called, but just in case... +proc `=destroy`(client: var ClientObj) = + if client.jsctx != nil: + let opaque = client.jsctx.getOpaque() + if opaque != nil: + dealloc(opaque) + free(client.jsctx) + if client.jsrt != nil: + free(client.jsrt) + proc newClient*(): Client = new(result) result.loader = newFileLoader() + let rt = newJSRuntime() + let ctx = rt.newJSContext() + result.jsrt = rt + result.jsctx = ctx + let global = ctx.getGlobalObject() + let console = newJSObject(result.jsctx) + console.setFunctionProperty("log", js_console_log) + console.setProperty("console", console) + free(global) proc loadError(s: string) = raise newException(LoadError, s) @@ -118,7 +154,6 @@ proc gotoUrl(client: Client, url: Url, click = none(ClickAction), prevurl = none interruptError()) client.buffer.istream = page.s client.buffer.contenttype = if ctype != "": ctype else: page.contenttype - client.buffer.streamclosed = false else: loadError("Couldn't load " & $url) except IOError, OSError: @@ -192,6 +227,41 @@ proc toggleSource*(client: Client) = client.buffer.contenttype = "text/html" client.setupBuffer() +proc command(client: Client) = + var iput: string + print(HVP(client.buffer.height + 1, 1)) + print(EL()) + let status = readLine("COMMAND: ", iput, client.buffer.width) + if status and iput.len > 0: + let ret = client.jsctx.eval(iput, "<stdin>", JS_EVAL_TYPE_GLOBAL) + let opaque = client.jsctx.getOpaque() + if ret.isException(): + let ex = client.jsctx.getException() + let str = ex.toString() + if str.issome: + opaque.err &= str.get & '\n' + let stack = ex.getProperty("stack") + if not stack.isUndefined(): + let str = stack.toString() + if str.issome: + opaque.err &= str.get & '\n' + free(stack) + free(ex) + else: + let str = ret.toString() + if str.issome: + opaque.err &= str.get & '\n' + free(ret) + client.addBuffer() + g_client = client + setControlCHook(proc() {.noconv.} = + if g_client.buffer.prev != nil or g_client.buffer.next != nil: + g_client.discardBuffer() + interruptError()) + client.buffer.istream = newStringStream(opaque.err) + client.buffer.contenttype = "text/plain" + client.setupBuffer() + proc quit(client: Client) = eraseScreen() print(HVP(0, 0)) @@ -248,6 +318,7 @@ proc input(client: Client) = of ACTION_PREV_BUFFER: client.prevBuffer() of ACTION_NEXT_BUFFER: client.nextBuffer() of ACTION_DISCARD_BUFFER: client.discardBuffer() + of ACTION_COMMAND: client.command() else: discard proc inputLoop(client: Client) = diff --git a/src/config/config.nim b/src/config/config.nim index 543236cf..5d04f5e5 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -27,6 +27,7 @@ type ACTION_CURSOR_TOP, ACTION_CURSOR_MIDDLE, ACTION_CURSOR_BOTTOM, ACTION_CURSOR_RIGHT_EDGE, ACTION_CURSOR_VERT_MIDDLE, ACTION_CURSOR_LEFT_EDGE, ACTION_CENTER_LINE, ACTION_LINE_INFO, + ACTION_COMMAND, ACTION_LINED_SUBMIT, ACTION_LINED_CANCEL, ACTION_LINED_BACKSPACE, ACTION_LINED_DELETE, ACTION_LINED_CLEAR, ACTION_LINED_KILL, ACTION_LINED_KILL_WORD, diff --git a/src/js/javascript.nim b/src/js/javascript.nim new file mode 100644 index 00000000..2518f252 --- /dev/null +++ b/src/js/javascript.nim @@ -0,0 +1,108 @@ +import options + +import bindings/quickjs + +export + JS_NULL, JS_UNDEFINED, JS_FALSE, JS_TRUE, JS_EXCEPTION, JS_UNINITIALIZED + +export + JS_EVAL_TYPE_GLOBAL, + JS_EVAL_TYPE_MODULE, + JS_EVAL_TYPE_DIRECT, + JS_EVAL_TYPE_INDIRECT, + JS_EVAL_TYPE_MASK, + JS_EVAL_FLAG_SHEBANG, + JS_EVAL_FLAG_STRICT, + JS_EVAL_FLAG_STRIP, + JS_EVAL_FLAG_COMPILE_ONLY + +export JSRuntime, JSContext, JSValue + +type + JSObject* = object + ctx*: JSContext + val*: JSValue + + JSContextOpaque* = object + err*: string + +func newJSRuntime*(): JSRuntime = + result = JS_NewRuntime() + +proc newJSContext*(rt: JSRuntime): JSContext = + result = JS_NewContext(rt) + let opaque = cast[ptr JSContextOpaque](alloc0(sizeof(JSContextOpaque))) + opaque.err = "" + JS_SetContextOpaque(result, opaque) + +func getJSObject*(ctx: JSContext, v: JSValue): JSObject = + result.ctx = ctx + result.val = v + +func getJSObject*(ctx: JSContext, argv: ptr JSValue, i: int): JSObject = + getJSObject(ctx, cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[]) + +func newJSObject*(ctx: JSContext): JSObject = + result.ctx = ctx + result.val = JS_NewObject(ctx) + +func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int): JSObject = + result.ctx = ctx + result.val = JS_NewCFunction(ctx, fun, cstring(name), argc) + +func getGlobalObject*(ctx: JSContext): JSObject = + result.ctx = ctx + result.val = JS_GetGlobalObject(ctx) + +func getException*(ctx: JSContext): JSObject = + result.ctx = ctx + result.val = JS_GetException(ctx) + +func getProperty*(obj: JSObject, s: string): JSObject = + result.ctx = obj.ctx + result.val = JS_GetPropertyStr(obj.ctx, obj.val, cstring(s)); + +func getOpaque*(ctx: JSContext): ptr JSContextOpaque = + return cast[ptr JSContextOpaque](JS_GetContextOpaque(ctx)) + +func toString*(obj: JSObject): Option[string] = + var plen: int + let outp = JS_ToCStringLen(obj.ctx, addr plen, obj.val) # cstring + if outp != nil: + var ret = newString(plen) + for i in 0..<plen: + ret[i] = outp[i] + result = some(ret) + JS_FreeCString(obj.ctx, outp) # refc + +func `$`*(obj: JSObject): string = + return obj.toString().get("") + +func isUndefined*(obj: JSObject): bool = JS_IsUndefined(obj.val) +func isException*(obj: JSObject): bool = JS_IsException(obj.val) + +proc setProperty*(obj: JSObject, name: string, prop: JSObject) = + discard JS_SetPropertyStr(obj.ctx, obj.val, cstring(name), prop.val) + +proc setFunctionProperty*(obj: JSObject, name: string, fun: JSCFunction) = + obj.setProperty(name, obj.ctx.newJSCFunction(name, fun, 1)) + # (proc(ctx: JSContext, obj: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.} = + # var invoc: seq[JSValue] + # for i in 0..<argc: + # let arg = cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[] + # invoc.add(arg) + # return fun(JSObject(ctx: ctx, qjs: obj), invoc) + #), cstring(name), 1)) + +proc free*(ctx: JSContext) = + JS_FreeContext(ctx) + +proc free*(rt: JSRuntime) = + JS_FreeRuntime(rt) + +proc free*(obj: JSObject) = + JS_FreeValue(obj.ctx, obj.val) + +proc eval*(ctx: JSContext, s: string, file: string, eval_flags: int): JSObject = + result.ctx = ctx + result.val = JS_Eval(ctx, cstring(s), s.len, cstring(file), eval_flags) |