about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--res/config.toml1
-rw-r--r--src/bindings/quickjs.nim129
-rw-r--r--src/client.nim79
-rw-r--r--src/config/config.nim1
-rw-r--r--src/js/javascript.nim108
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)