diff options
author | bptato <nincsnevem662@gmail.com> | 2023-09-14 01:41:47 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-09-14 02:01:21 +0200 |
commit | c1b8338045716b25d664c0b8dd91eac0cb76480e (patch) | |
tree | a9c0a6763f180c2b6dd380aa880253ffc7685d85 /src/io | |
parent | db0798acccbedcef4b16737f6be0cf7388cc0528 (diff) | |
download | chawan-c1b8338045716b25d664c0b8dd91eac0cb76480e.tar.gz |
move around more modules
* ips -> io/ * loader related stuff -> loader/ * tempfile -> extern/ * buffer, forkserver -> server/ * lineedit, window -> display/ * cell -> types/ * opt -> types/
Diffstat (limited to 'src/io')
-rw-r--r-- | src/io/about.nim | 28 | ||||
-rw-r--r-- | src/io/connecterror.nim | 18 | ||||
-rw-r--r-- | src/io/data.nim | 43 | ||||
-rw-r--r-- | src/io/file.nim | 107 | ||||
-rw-r--r-- | src/io/headers.nim | 100 | ||||
-rw-r--r-- | src/io/http.nim | 142 | ||||
-rw-r--r-- | src/io/lineedit.nim | 369 | ||||
-rw-r--r-- | src/io/loader.nim | 394 | ||||
-rw-r--r-- | src/io/loaderhandle.nim | 73 | ||||
-rw-r--r-- | src/io/promise.nim | 2 | ||||
-rw-r--r-- | src/io/request.nim | 332 | ||||
-rw-r--r-- | src/io/response.nim | 74 | ||||
-rw-r--r-- | src/io/serialize.nim | 444 | ||||
-rw-r--r-- | src/io/serversocket.nim | 28 | ||||
-rw-r--r-- | src/io/socketstream.nim | 147 | ||||
-rw-r--r-- | src/io/tempfile.nim | 18 | ||||
-rw-r--r-- | src/io/window.nim | 54 |
17 files changed, 620 insertions, 1753 deletions
diff --git a/src/io/about.nim b/src/io/about.nim deleted file mode 100644 index 737a291b..00000000 --- a/src/io/about.nim +++ /dev/null @@ -1,28 +0,0 @@ -import tables - -import io/connecterror -import io/headers -import io/loaderhandle -import io/request -import types/url - -const chawan = staticRead"res/chawan.html" -const HeaderTable = { - "Content-Type": "text/html" -}.toTable() - -proc loadAbout*(handle: LoaderHandle, request: Request) = - template t(body: untyped) = - if not body: - return - if request.url.pathname == "blank": - t handle.sendResult(0) - t handle.sendStatus(200) # ok - t handle.sendHeaders(newHeaders(HeaderTable)) - elif request.url.pathname == "chawan": - t handle.sendResult(0) - t handle.sendStatus(200) # ok - t handle.sendHeaders(newHeaders(HeaderTable)) - t handle.sendData(chawan) - else: - t handle.sendResult(ERROR_ABOUT_PAGE_NOT_FOUND) diff --git a/src/io/connecterror.nim b/src/io/connecterror.nim deleted file mode 100644 index d2af5762..00000000 --- a/src/io/connecterror.nim +++ /dev/null @@ -1,18 +0,0 @@ -import bindings/curl - -type ConnectErrorCode* = enum - ERROR_INVALID_DATA_URL = (-7, "invalid data URL") - ERROR_ABOUT_PAGE_NOT_FOUND = (-6, "about page not found") - ERROR_FILE_NOT_FOUND = (-5, "file not found") - ERROR_SOURCE_NOT_FOUND = (-4, "clone source could not be found"), - ERROR_LOADER_KILLED = (-3, "loader killed during transfer"), - ERROR_DISALLOWED_URL = (-2, "url not allowed by filter"), - ERROR_UNKNOWN_SCHEME = (-1, "unknown scheme") - -converter toInt*(code: ConnectErrorCode): int = - return int(code) - -func getLoaderErrorMessage*(code: int): string = - if code < 0: - return $ConnectErrorCode(code) - return $curl_easy_strerror(CURLcode(cint(code))) diff --git a/src/io/data.nim b/src/io/data.nim deleted file mode 100644 index 3afe58f0..00000000 --- a/src/io/data.nim +++ /dev/null @@ -1,43 +0,0 @@ -import base64 -import strutils -import tables - -import io/connecterror -import io/headers -import io/loaderhandle -import io/request -import types/url - -proc loadData*(handle: LoaderHandle, request: Request) = - template t(body: untyped) = - if not body: - return - var str = $request.url - let si = "data:".len # start index - var ct = "" - for i in si ..< str.len: - if str[i] == ',': - break - ct &= str[i] - let sd = si + ct.len + 1 # data start - if ct.endsWith(";base64"): - try: - let d = base64.decode(str[sd .. ^1]) # decode from ct end + 1 - t handle.sendResult(0) - t handle.sendStatus(200) - ct.setLen(ct.len - ";base64".len) # remove base64 indicator - t handle.sendHeaders(newHeaders({ - "Content-Type": ct - }.toTable())) - if d.len > 0: - t handle.sendData(d) - except ValueError: - discard handle.sendResult(ERROR_INVALID_DATA_URL) - else: - t handle.sendResult(0) - t handle.sendStatus(200) - t handle.sendHeaders(newHeaders({ - "Content-Type": ct - }.toTable())) - if ct.len + 1 < str.len: - t handle.sendData(addr str[sd], str.len - sd) diff --git a/src/io/file.nim b/src/io/file.nim deleted file mode 100644 index fe732d6c..00000000 --- a/src/io/file.nim +++ /dev/null @@ -1,107 +0,0 @@ -import algorithm -import os -import streams -import tables - -import io/connecterror -import io/headers -import io/loaderhandle -import types/url - -proc loadDir(handle: LoaderHandle, url: URL, path: string) = - template t(body: untyped) = - if not body: - return - var path = path - if path[^1] != '/': #TODO dos/windows - path &= '/' - var base = $url - if base[^1] != '/': #TODO dos/windows - base &= '/' - t handle.sendResult(0) - t handle.sendStatus(200) # ok - t handle.sendHeaders(newHeaders({"Content-Type": "text/html"}.toTable())) - t handle.sendData(""" -<HTML> -<HEAD> -<BASE HREF="""" & base & """"> -<TITLE>Directory list of """ & path & """</TITLE> -</HEAD> -<BODY> -<H1>Directory list of """ & path & """</H1> -[DIR] <A HREF="../">../</A></br> -""") - var fs: seq[(PathComponent, string)] - for pc, file in walkDir(path, relative = true): - fs.add((pc, file)) - fs.sort(cmp = proc(a, b: (PathComponent, string)): int = cmp(a[1], b[1])) - for (pc, file) in fs: - case pc - of pcDir: - t handle.sendData("[DIR] ") - of pcFile: - t handle.sendData("[FILE] ") - of pcLinkToDir, pcLinkToFile: - t handle.sendData("[LINK] ") - var fn = file - if pc == pcDir: - fn &= '/' - t handle.sendData("<A HREF=\"" & fn & "\">" & fn & "</A>") - if pc in {pcLinkToDir, pcLinkToFile}: - discard handle.sendData(" -> " & expandSymlink(path / file)) - t handle.sendData("<br>") - t handle.sendData(""" -</BODY> -</HTML>""") - -proc loadSymlink(handle: LoaderHandle, path: string) = - template t(body: untyped) = - if not body: - return - t handle.sendResult(0) - t handle.sendStatus(200) # ok - t handle.sendHeaders(newHeaders({"Content-Type": "text/html"}.toTable())) - let sl = expandSymlink(path) - t handle.sendData(""" -<HTML> -<HEAD> -<TITLE>Symlink view<TITLE> -</HEAD> -<BODY> -Symbolic link to <A HREF="""" & sl & """">""" & sl & """</A></br> -</BODY> -</HTML>""") - -proc loadFile(handle: LoaderHandle, istream: Stream) = - template t(body: untyped) = - if not body: - return - t handle.sendResult(0) - t handle.sendStatus(200) # ok - t handle.sendHeaders(newHeaders()) - while not istream.atEnd: - const bufferSize = 4096 - var buffer {.noinit.}: array[bufferSize, char] - while true: - let n = readData(istream, addr buffer[0], bufferSize) - if n == 0: - break - t handle.sendData(addr buffer[0], n) - if n < bufferSize: - break - -proc loadFilePath*(handle: LoaderHandle, url: URL) = - when defined(windows) or defined(OS2) or defined(DOS): - let path = url.path.serialize_unicode_dos() - else: - let path = url.path.serialize_unicode() - let istream = newFileStream(path, fmRead) - if istream == nil: - if dirExists(path): - handle.loadDir(url, path) - elif symlinkExists(path): - handle.loadSymlink(path) - else: - discard handle.sendResult(ERROR_FILE_NOT_FOUND) - else: - handle.loadFile(istream) diff --git a/src/io/headers.nim b/src/io/headers.nim deleted file mode 100644 index b02f30df..00000000 --- a/src/io/headers.nim +++ /dev/null @@ -1,100 +0,0 @@ -import tables - -import bindings/quickjs -import js/error -import js/fromjs -import js/javascript -import utils/twtstr - -type - Headers* = ref object - table* {.jsget.}: Table[string, seq[string]] - - HeadersInitType = enum - HEADERS_INIT_SEQUENCE, HEADERS_INIT_TABLE - - HeadersInit* = object - case t: HeadersInitType - of HEADERS_INIT_SEQUENCE: - s: seq[(string, string)] - of HEADERS_INIT_TABLE: - tab: Table[string, string] - -jsDestructor(Headers) - -proc fromJS2*(ctx: JSContext, val: JSValue, res: var JSResult[HeadersInit]) = - if JS_IsUndefined(val) or JS_IsNull(val): - res.err(nil) - return - if isSequence(ctx, val): - let x = fromJS[seq[(string, string)]](ctx, val) - if x.isSome: - res.ok(HeadersInit(t: HEADERS_INIT_SEQUENCE, s: x.get)) - else: - let x = fromJS[Table[string, string]](ctx, val) - if x.isSome: - res.ok(HeadersInit(t: HEADERS_INIT_TABLE, tab: x.get)) - -proc fill*(headers: Headers, s: seq[(string, string)]) = - for (k, v) in s: - if k in headers.table: - headers.table[k].add(v) - else: - headers.table[k] = @[v] - -proc fill*(headers: Headers, tab: Table[string, string]) = - for k, v in tab: - if k in headers.table: - headers.table[k].add(v) - else: - headers.table[k] = @[v] - -proc fill*(headers: Headers, init: HeadersInit) = - if init.t == HEADERS_INIT_SEQUENCE: - headers.fill(init.s) - else: # table - headers.fill(init.tab) - -func newHeaders*(): Headers = - return Headers() - -func newHeaders(obj = none(HeadersInit)): Headers {.jsctor.} = - let headers = Headers() - if obj.isSome: - headers.fill(obj.get) - return headers - -func newHeaders*(table: Table[string, string]): Headers = - let headers = Headers() - for k, v in table: - let k = k.toHeaderCase() - if k in headers.table: - headers.table[k].add(v) - else: - headers.table[k] = @[v] - return headers - -func clone*(headers: Headers): Headers = - return Headers( - table: headers.table - ) - -proc add*(headers: var Headers, k, v: string) = - let k = k.toHeaderCase() - if k notin headers.table: - headers.table[k] = @[v] - else: - headers.table[k].add(v) - -proc `[]=`*(headers: var Headers, k, v: string) = - headers.table[k.toHeaderCase()] = @[v] - -func getOrDefault*(headers: Headers, k: string, default = ""): string = - let k = k.toHeaderCase() - if k in headers.table: - headers.table[k][0] - else: - default - -proc addHeadersModule*(ctx: JSContext) = - ctx.registerType(Headers) diff --git a/src/io/http.nim b/src/io/http.nim deleted file mode 100644 index 0a5a6d79..00000000 --- a/src/io/http.nim +++ /dev/null @@ -1,142 +0,0 @@ -import options -import strutils - -import bindings/curl -import io/headers -import io/loaderhandle -import io/request -import types/blob -import types/formdata -import types/url -import utils/opt -import utils/twtstr - -type - CurlHandle* = ref CurlHandleObj - CurlHandleObj = object - curl*: CURL - statusline: bool - headers: Headers - request: Request - handle*: LoaderHandle - mime: curl_mime - slist: curl_slist - -func newCurlHandle(curl: CURL, request: Request, handle: LoaderHandle): - CurlHandle = - return CurlHandle( - headers: newHeaders(), - curl: curl, - handle: handle, - request: request - ) - -proc cleanup*(handleData: CurlHandle) = - handleData.handle.close() - if handleData.mime != nil: - curl_mime_free(handleData.mime) - if handleData.slist != nil: - curl_slist_free_all(handleData.slist) - curl_easy_cleanup(handleData.curl) - -template setopt(curl: CURL, opt: CURLoption, arg: typed) = - discard curl_easy_setopt(curl, opt, arg) - -template setopt(curl: CURL, opt: CURLoption, arg: string) = - discard curl_easy_setopt(curl, opt, cstring(arg)) - -template getinfo(curl: CURL, info: CURLINFO, arg: typed) = - discard curl_easy_getinfo(curl, info, arg) - -proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, - userdata: pointer): csize_t {.cdecl.} = - var line = newString(nitems) - for i in 0..<nitems: - line[i] = p[i] - - let op = cast[CurlHandle](userdata) - if not op.statusline: - op.statusline = true - if not op.handle.sendResult(int(CURLE_OK)): - return 0 - var status: clong - op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status) - if not op.handle.sendStatus(cast[int](status)): - return 0 - return nitems - - let k = line.until(':') - - if k.len == line.len: - # empty line (last, before body) or invalid (=> error) - if not op.handle.sendHeaders(op.headers): - return 0 - return nitems - - let v = line.substr(k.len + 1).strip() - op.headers.add(k, v) - return nitems - -# From the documentation: size is always 1. -proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t, - userdata: pointer): csize_t {.cdecl.} = - let handleData = cast[CurlHandle](userdata) - if nmemb > 0: - if not handleData.handle.sendData(p, int(nmemb)): - return 0 - return nmemb - -proc applyPostBody(curl: CURL, request: Request, handleData: CurlHandle) = - if request.multipart.isOk: - handleData.mime = curl_mime_init(curl) - doAssert handleData.mime != nil - for entry in request.multipart.get: - let part = curl_mime_addpart(handleData.mime) - doAssert part != nil - curl_mime_name(part, cstring(entry.name)) - if entry.isstr: - curl_mime_data(part, cstring(entry.svalue), csize_t(entry.svalue.len)) - else: - let blob = entry.value - if blob.isfile: #TODO ? - curl_mime_filedata(part, cstring(WebFile(blob).path)) - else: - curl_mime_data(part, blob.buffer, csize_t(blob.size)) - # may be overridden by curl_mime_filedata, so set it here - curl_mime_filename(part, cstring(entry.filename)) - curl.setopt(CURLOPT_MIMEPOST, handleData.mime) - elif request.body.issome: - curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get)) - curl.setopt(CURLOPT_POSTFIELDSIZE, request.body.get.len) - -proc loadHttp*(handle: LoaderHandle, curlm: CURLM, - request: Request): CurlHandle = - let curl = curl_easy_init() - doAssert curl != nil - let surl = request.url.serialize() - curl.setopt(CURLOPT_URL, surl) - let handleData = curl.newCurlHandle(request, handle) - curl.setopt(CURLOPT_WRITEDATA, handleData) - curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody) - curl.setopt(CURLOPT_HEADERDATA, handleData) - curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader) - if request.proxy != nil: - let purl = request.proxy.serialize() - curl.setopt(CURLOPT_PROXY, purl) - case request.httpmethod - of HTTP_GET: - curl.setopt(CURLOPT_HTTPGET, 1) - of HTTP_POST: - curl.setopt(CURLOPT_POST, 1) - curl.applyPostBody(request, handleData) - else: discard #TODO - for k, v in request.headers: - let header = k & ": " & v - handleData.slist = curl_slist_append(handleData.slist, cstring(header)) - if handleData.slist != nil: - curl.setopt(CURLOPT_HTTPHEADER, handleData.slist) - let res = curl_multi_add_handle(curlm, curl) - if res != CURLM_OK: - discard handle.sendResult(int(res)) - return nil - return handleData diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim deleted file mode 100644 index 1c1d273c..00000000 --- a/src/io/lineedit.nim +++ /dev/null @@ -1,369 +0,0 @@ -import sequtils -import streams -import strutils -import unicode - -import bindings/quickjs -import buffer/cell -import display/term -import js/javascript -import types/color -import utils/opt -import utils/twtstr - -import chakasu/charset -import chakasu/decoderstream -import chakasu/encoderstream - -type - LineEditState* = enum - EDIT, FINISH, CANCEL - - LineHistory* = ref object - lines: seq[string] - - LineEdit* = ref object - isnew*: bool #TODO hack - news*: seq[Rune] - prompt*: string - promptw: int - current: string - state*: LineEditState - escNext*: bool - cursor: int - shift: int - minlen: int - maxwidth: int - displen: int - disallowed: set[char] - hide: bool - term: Terminal - hist: LineHistory - histindex: int - histtmp: string - -jsDestructor(LineEdit) - -func newLineHistory*(): LineHistory = - return LineHistory() - -proc printesc(edit: LineEdit, rs: seq[Rune]) = - var dummy = 0 - edit.term.write(edit.term.processOutputString0(rs.items, true, dummy)) - -proc print(edit: LineEdit, s: string) = - var dummy = 0 - edit.term.write(edit.term.processOutputString(s, dummy)) - -template kill0(edit: LineEdit, i: int) = - edit.space(i) - edit.backward0(i) - -template kill0(edit: LineEdit) = - let w = min(edit.news.width(edit.cursor), edit.displen) - edit.kill0(w) - -proc backward0(state: LineEdit, i: int) = - state.term.cursorBackward(i) - -proc forward0(state: LineEdit, i: int) = - state.term.cursorForward(i) - -proc begin0(edit: LineEdit) = - edit.term.cursorBegin() - edit.forward0(edit.minlen) - -proc space(edit: LineEdit, i: int) = - edit.term.write(' '.repeat(i)) - -#TODO this is broken (e.g. it doesn't account for shift, but for other -# reasons too) -proc generateOutput*(edit: LineEdit): FixedGrid = - result = newFixedGrid(edit.promptw + edit.maxwidth) - var x = 0 - for r in edit.prompt.runes(): - result[x].str &= $r - x += r.width() - if edit.hide: - for r in edit.news: - let w = r.width() - result[x].str = '*'.repeat(w) - x += w - if x >= result.width: break - else: - for r in edit.news: - result[x].str &= $r - x += r.width() - if x >= result.width: break - var s = "" - for c in result: - s &= c.str - -proc getCursorX*(edit: LineEdit): int = - return edit.promptw + edit.news.width(edit.shift, edit.cursor) - -proc redraw(state: LineEdit) = - if state.shift + state.displen > state.news.len: - state.displen = state.news.len - state.shift - var dispw = state.news.width(state.shift, state.shift + state.displen) - while dispw > state.maxwidth - 1: - dispw -= state.news[state.shift + state.displen - 1].width() - dec state.displen - state.begin0() - let os = state.news.substr(state.shift, state.shift + state.displen) - if state.hide: - state.print('*'.repeat(os.width())) - else: - state.printesc(os) - state.space(max(state.maxwidth - state.minlen - os.width(), 0)) - state.begin0() - state.forward0(state.news.width(state.shift, state.cursor)) - -proc zeroShiftRedraw(state: LineEdit) = - state.shift = 0 - state.displen = state.news.len - state.redraw() - -proc fullRedraw*(state: LineEdit) = - state.displen = state.news.len - if state.cursor > state.shift: - var shiftw = state.news.width(state.shift, state.cursor) - while shiftw > state.maxwidth - 1: - inc state.shift - shiftw -= state.news[state.shift].width() - else: - state.shift = max(state.cursor - 1, 0) - state.redraw() - -proc drawPrompt*(edit: LineEdit) = - edit.term.write(edit.prompt) - -proc insertCharseq(edit: LineEdit, cs: var seq[Rune]) = - let escNext = edit.escNext - var i = 0 - for j in 0 ..< cs.len: - if cs[i].isAscii(): - let c = cast[char](cs[i]) - if not escNext and c in Controls or c in edit.disallowed: - continue - if i != j: - cs[i] = cs[j] - inc i - - edit.escNext = false - if cs.len == 0: - return - - if edit.cursor >= edit.news.len and edit.news.width(edit.shift, edit.cursor) + cs.width() < edit.maxwidth: - edit.news &= cs - edit.cursor += cs.len - if edit.hide: - edit.print('*'.repeat(cs.width())) - else: - edit.printesc(cs) - else: - edit.news.insert(cs, edit.cursor) - edit.cursor += cs.len - edit.fullRedraw() - -proc cancel(edit: LineEdit) {.jsfunc.} = - edit.state = CANCEL - -proc submit(edit: LineEdit) {.jsfunc.} = - let s = $edit.news - if edit.hist.lines.len == 0 or s != edit.hist.lines[^1]: - edit.hist.lines.add(s) - edit.state = FINISH - -proc backspace(edit: LineEdit) {.jsfunc.} = - if edit.cursor > 0: - let w = edit.news[edit.cursor - 1].width() - edit.news.delete(edit.cursor - 1..edit.cursor - 1) - dec edit.cursor - if edit.cursor == edit.news.len and edit.shift == 0: - edit.backward0(w) - edit.kill0(w) - else: - edit.fullRedraw() - -const buflen = 128 -var buf {.threadVar.}: array[buflen, uint32] -proc write*(edit: LineEdit, s: string, cs: Charset): bool = - let ss = newStringStream(s) - let ds = newDecoderStream(ss, cs = cs, buflen = buflen, - errormode = DECODER_ERROR_MODE_FATAL) - var cseq: seq[Rune] - while not ds.atEnd: - let n = ds.readData(buf) - for i in 0 ..< n div 4: - let r = cast[Rune](buf[i]) - cseq.add(r) - if ds.failed: - return false - edit.insertCharseq(cseq) - return true - -proc write(edit: LineEdit, s: string): bool {.jsfunc.} = - edit.write(s, CHARSET_UTF_8) - -proc delete(edit: LineEdit) {.jsfunc.} = - if edit.cursor >= 0 and edit.cursor < edit.news.len: - let w = edit.news[edit.cursor].width() - edit.news.delete(edit.cursor..edit.cursor) - if edit.cursor == edit.news.len and edit.shift == 0: - edit.kill0(w) - else: - edit.fullRedraw() - -proc escape(edit: LineEdit) {.jsfunc.} = - edit.escNext = true - -proc clear(edit: LineEdit) {.jsfunc.} = - if edit.cursor > 0: - edit.news.delete(0..edit.cursor - 1) - edit.cursor = 0 - edit.zeroShiftRedraw() - -proc kill(edit: LineEdit) {.jsfunc.} = - if edit.cursor < edit.news.len: - edit.kill0() - edit.news.setLen(edit.cursor) - -proc backward(edit: LineEdit) {.jsfunc.} = - if edit.cursor > 0: - dec edit.cursor - if edit.cursor > edit.shift or edit.shift == 0: - edit.backward0(edit.news[edit.cursor].width()) - else: - edit.fullRedraw() - -proc forward(edit: LineEdit) {.jsfunc.} = - if edit.cursor < edit.news.len: - inc edit.cursor - if edit.news.width(edit.shift, edit.cursor) < edit.maxwidth: - var n = 1 - if edit.news.len > edit.cursor: - n = edit.news[edit.cursor].width() - edit.forward0(n) - else: - edit.fullRedraw() - -proc prevWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} = - let oc = edit.cursor - while edit.cursor > 0: - dec edit.cursor - if edit.news[edit.cursor].breaksWord(check): - break - if edit.cursor != oc: - if edit.cursor > edit.shift or edit.shift == 0: - edit.backward0(edit.news.width(edit.cursor, oc)) - else: - edit.fullRedraw() - -proc nextWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} = - let oc = edit.cursor - let ow = edit.news.width(edit.shift, edit.cursor) - while edit.cursor < edit.news.len: - inc edit.cursor - if edit.cursor < edit.news.len: - if edit.news[edit.cursor].breaksWord(check): - break - if edit.cursor != oc: - let dw = edit.news.width(oc, edit.cursor) - if ow + dw < edit.maxwidth: - edit.forward0(dw) - else: - edit.fullRedraw() - -proc clearWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} = - var i = edit.cursor - if i > 0: - # point to the previous character - dec i - while i > 0: - dec i - if edit.news[i].breaksWord(check): - inc i - break - if i != edit.cursor: - edit.news.delete(i..<edit.cursor) - edit.cursor = i - edit.fullRedraw() - -proc killWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} = - var i = edit.cursor - if i < edit.news.len and edit.news[i].breaksWord(check): - inc i - while i < edit.news.len: - if edit.news[i].breaksWord(check): - break - inc i - if i != edit.cursor: - edit.news.delete(edit.cursor..<i) - edit.fullRedraw() - -proc begin(edit: LineEdit) {.jsfunc.} = - if edit.cursor > 0: - if edit.shift == 0: - edit.backward0(edit.news.width(0, edit.cursor)) - edit.cursor = 0 - else: - edit.cursor = 0 - edit.fullRedraw() - -proc `end`(edit: LineEdit) {.jsfunc.} = - if edit.cursor < edit.news.len: - if edit.news.width(edit.shift, edit.news.len) < edit.maxwidth: - edit.forward0(edit.news.width(edit.cursor, edit.news.len)) - edit.cursor = edit.news.len - else: - edit.cursor = edit.news.len - edit.fullRedraw() - -proc prevHist(edit: LineEdit) {.jsfunc.} = - if edit.histindex > 0: - if edit.news.len > 0: - edit.histtmp = $edit.news - dec edit.histindex - edit.news = edit.hist.lines[edit.histindex].toRunes() - edit.begin() - edit.end() - edit.fullRedraw() - -proc nextHist(edit: LineEdit) {.jsfunc.} = - if edit.histindex + 1 < edit.hist.lines.len: - inc edit.histindex - edit.news = edit.hist.lines[edit.histindex].toRunes() - edit.begin() - edit.end() - edit.fullRedraw() - elif edit.histindex < edit.hist.lines.len: - inc edit.histindex - edit.news = edit.histtmp.toRunes() - edit.begin() - edit.end() - edit.fullRedraw() - edit.histtmp = "" - -proc readLine*(prompt: string, termwidth: int, current = "", - disallowed: set[char] = {}, hide = false, - term: Terminal, hist: LineHistory): LineEdit = - result = LineEdit( - prompt: prompt, - promptw: prompt.width(), - current: current, - news: current.toRunes(), - minlen: prompt.width(), - disallowed: disallowed, - hide: hide, - term: term, - isnew: true - ) - result.cursor = result.news.width() - result.maxwidth = termwidth - result.promptw - result.displen = result.cursor - result.hist = hist - result.histindex = result.hist.lines.len - -proc addLineEditModule*(ctx: JSContext) = - ctx.registerType(LineEdit) diff --git a/src/io/loader.nim b/src/io/loader.nim deleted file mode 100644 index a6be3f6d..00000000 --- a/src/io/loader.nim +++ /dev/null @@ -1,394 +0,0 @@ -# A file loader server (?) -# The idea here is that we receive requests with a socket, then respond to each -# with a response (ideally a document.) -# For now, the protocol looks like: -# C: Request -# S: res (0 => success, _ => error) -# if success: -# S: status code -# S: headers -# S: response body -# -# The body is passed to the stream as-is, so effectively nothing can follow it. - -import nativesockets -import net -import options -import posix -import streams -import strutils -import tables - -import bindings/curl -import io/about -import io/connecterror -import io/data -import io/file -import io/headers -import io/http -import io/loaderhandle -import io/posixstream -import io/promise -import io/request -import io/response -import io/urlfilter -import ips/serialize -import ips/serversocket -import ips/socketstream -import js/error -import js/javascript -import types/cookie -import types/referer -import types/url -import utils/mimeguess -import utils/twtstr - -import chakasu/charset - -export request -export response - -type - FileLoader* = ref object - process*: Pid - connecting*: Table[int, ConnectData] - ongoing*: Table[int, OngoingData] - unregistered*: seq[int] - registerFun*: proc(fd: int) - unregisterFun*: proc(fd: int) - - ConnectData = object - promise: Promise[JSResult[Response]] - stream: Stream - request: Request - - OngoingData = object - buf: string - readbufsize: int - response: Response - bodyRead: Promise[string] - - LoaderCommand = enum - LOAD - QUIT - - LoaderContext = ref object - ssock: ServerSocket - alive: bool - curlm: CURLM - config: LoaderConfig - extra_fds: seq[curl_waitfd] - handleList: seq[CurlHandle] - - LoaderConfig* = object - defaultheaders*: Headers - filter*: URLFilter - cookiejar*: CookieJar - referrerpolicy*: ReferrerPolicy - proxy*: URL - # When set to false, requests with a proxy URL are overridden by the - # loader proxy. - acceptProxy*: bool - - FetchPromise* = Promise[JSResult[Response]] - -proc addFd(ctx: LoaderContext, fd: int, flags: int) = - ctx.extra_fds.add(curl_waitfd( - fd: cast[cint](fd), - events: cast[cshort](flags) - )) - -proc loadResource(ctx: LoaderContext, request: Request, handle: LoaderHandle) = - case request.url.scheme - of "file": - handle.loadFilePath(request.url) - handle.close() - of "http", "https": - let handleData = handle.loadHttp(ctx.curlm, request) - if handleData != nil: - ctx.handleList.add(handleData) - of "about": - handle.loadAbout(request) - handle.close() - of "data": - handle.loadData(request) - handle.close() - else: - discard handle.sendResult(ERROR_UNKNOWN_SCHEME) - handle.close() - -proc onLoad(ctx: LoaderContext, stream: Stream) = - var request: Request - stream.sread(request) - if not ctx.config.filter.match(request.url): - stream.swrite(ERROR_DISALLOWED_URL) - stream.close() - else: - let handle = newLoaderHandle(stream, request.canredir) - for k, v in ctx.config.defaultHeaders.table: - if k notin request.headers.table: - request.headers.table[k] = v - if ctx.config.cookiejar != nil and ctx.config.cookiejar.cookies.len > 0: - if "Cookie" notin request.headers.table: - let cookie = ctx.config.cookiejar.serialize(request.url) - if cookie != "": - request.headers["Cookie"] = cookie - if request.referer != nil and "Referer" notin request.headers.table: - let r = getReferer(request.referer, request.url, ctx.config.referrerpolicy) - if r != "": - request.headers["Referer"] = r - if request.proxy == nil or not ctx.config.acceptProxy: - request.proxy = ctx.config.proxy - ctx.loadResource(request, handle) - -proc acceptConnection(ctx: LoaderContext) = - #TODO TODO TODO acceptSocketStream should be non-blocking here, - # otherwise the client disconnecting between poll and accept could - # block this indefinitely. - let stream = ctx.ssock.acceptSocketStream() - try: - var cmd: LoaderCommand - stream.sread(cmd) - case cmd - of LOAD: - ctx.onLoad(stream) - of QUIT: - ctx.alive = false - stream.close() - except IOError: - # End-of-file, broken pipe, or something else. For now we just - # ignore it and pray nothing breaks. - # (TODO: this is probably not a very good idea.) - stream.close() - -proc finishCurlTransfer(ctx: LoaderContext, handleData: CurlHandle, res: int) = - if res != int(CURLE_OK): - discard handleData.handle.sendResult(int(res)) - discard curl_multi_remove_handle(ctx.curlm, handleData.curl) - handleData.cleanup() - -proc exitLoader(ctx: LoaderContext) = - for handleData in ctx.handleList: - ctx.finishCurlTransfer(handleData, ERROR_LOADER_KILLED) - discard curl_multi_cleanup(ctx.curlm) - curl_global_cleanup() - ctx.ssock.close() - quit(0) - -var gctx: LoaderContext -proc initLoaderContext(fd: cint, config: LoaderConfig): LoaderContext = - if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK: - raise newException(Defect, "Failed to initialize libcurl.") - let curlm = curl_multi_init() - if curlm == nil: - raise newException(Defect, "Failed to initialize multi handle.") - var ctx = LoaderContext( - alive: true, - curlm: curlm, - config: config - ) - gctx = ctx - #TODO ideally, buffered would be true. Unfortunately this conflicts with - # sendFileHandle/recvFileHandle. - ctx.ssock = initServerSocket(buffered = false) - # The server has been initialized, so the main process can resume execution. - var writef: File - if not open(writef, FileHandle(fd), fmWrite): - raise newException(Defect, "Failed to open input handle.") - writef.write(char(0u8)) - writef.flushFile() - close(writef) - discard close(fd) - onSignal SIGTERM, SIGINT: - discard sig - gctx.exitLoader() - ctx.addFd(int(ctx.ssock.sock.getFd()), CURL_WAIT_POLLIN) - return ctx - -proc runFileLoader*(fd: cint, config: LoaderConfig) = - var ctx = initLoaderContext(fd, config) - while ctx.alive: - var numfds: cint = 0 - #TODO do not discard - discard curl_multi_poll(ctx.curlm, addr ctx.extra_fds[0], - cuint(ctx.extra_fds.len), 30_000, addr numfds) - discard curl_multi_perform(ctx.curlm, addr numfds) - for extra_fd in ctx.extra_fds.mitems: - # For now, this is always ssock.sock.getFd(). - if extra_fd.events == extra_fd.revents: - ctx.acceptConnection() - extra_fd.revents = 0 - var msgs_left: cint = 1 - while msgs_left > 0: - let msg = curl_multi_info_read(ctx.curlm, addr msgs_left) - if msg == nil: - break - if msg.msg == CURLMSG_DONE: # the only possible value atm - var idx = -1 - for i in 0 ..< ctx.handleList.len: - if ctx.handleList[i].curl == msg.easy_handle: - idx = i - break - assert idx != -1 - ctx.finishCurlTransfer(ctx.handleList[idx], int(msg.data.result)) - ctx.handleList.del(idx) - ctx.exitLoader() - -proc getAttribute(contentType, attrname: string): string = - let kvs = contentType.after(';') - var i = kvs.find(attrname) - var s = "" - if i != -1 and kvs.len > i + attrname.len and - kvs[i + attrname.len] == '=': - i += attrname.len + 1 - while i < kvs.len and kvs[i] in AsciiWhitespace: - inc i - var q = false - for j in i ..< kvs.len: - if q: - s &= kvs[j] - else: - if kvs[j] == '\\': - q = true - elif kvs[j] == ';' or kvs[j] in AsciiWhitespace: - break - else: - s &= kvs[j] - return s - -proc applyHeaders(loader: FileLoader, request: Request, response: Response) = - if "Content-Type" in response.headers.table: - #TODO this is inefficient and broken on several levels. (In particular, - # it breaks mailcap named attributes other than charset.) - # Ideally, contentType would be a separate object type. - let header = response.headers.table["Content-Type"][0].toLowerAscii() - response.contenttype = header.until(';').strip().toLowerAscii() - response.charset = getCharset(header.getAttribute("charset")) - else: - response.contenttype = guessContentType($response.url.path, - "application/octet-stream", DefaultGuess) - if "Location" in response.headers.table: - if response.status in 301u16..303u16 or response.status in 307u16..308u16: - let location = response.headers.table["Location"][0] - let url = parseUrl(location, option(request.url)) - if url.isSome: - if (response.status == 303 and - request.httpmethod notin {HTTP_GET, HTTP_HEAD}) or - (response.status == 301 or response.status == 302 and - request.httpmethod == HTTP_POST): - response.redirect = newRequest(url.get, HTTP_GET, - mode = request.mode, credentialsMode = request.credentialsMode, - destination = request.destination) - else: - response.redirect = newRequest(url.get, request.httpmethod, - body = request.body, multipart = request.multipart, - mode = request.mode, credentialsMode = request.credentialsMode, - destination = request.destination) - -#TODO: add init -proc fetch*(loader: FileLoader, input: Request): FetchPromise = - let stream = connectSocketStream(loader.process, false, blocking = true) - stream.swrite(LOAD) - stream.swrite(input) - stream.flush() - let fd = int(stream.source.getFd()) - loader.registerFun(fd) - let promise = FetchPromise() - loader.connecting[fd] = ConnectData( - promise: promise, - request: input, - stream: stream - ) - return promise - -const BufferSize = 4096 - -proc handleHeaders(loader: FileLoader, request: Request, response: Response, - stream: Stream): bool = - var status: int - stream.sread(status) - response.status = cast[uint16](status) - response.headers = newHeaders() - stream.sread(response.headers) - loader.applyHeaders(request, response) - # Only a stream of the response body may arrive after this point. - response.body = stream - return true # success - -proc onConnected*(loader: FileLoader, fd: int) = - let connectData = loader.connecting[fd] - let stream = connectData.stream - let promise = connectData.promise - let request = connectData.request - var res: int - stream.sread(res) - let response = newResponse(res, request, fd, stream) - if res == 0 and loader.handleHeaders(request, response, stream): - assert loader.unregisterFun != nil - let realCloseImpl = stream.closeImpl - stream.closeImpl = nil - response.unregisterFun = proc() = - loader.ongoing.del(fd) - loader.unregistered.add(fd) - loader.unregisterFun(fd) - realCloseImpl(stream) - loader.ongoing[fd] = OngoingData( - response: response, - readbufsize: BufferSize, - bodyRead: response.bodyRead - ) - SocketStream(stream).source.getFd().setBlocking(false) - promise.resolve(JSResult[Response].ok(response)) - else: - loader.unregisterFun(fd) - loader.unregistered.add(fd) - let err = newTypeError("NetworkError when attempting to fetch resource") - promise.resolve(JSResult[Response].err(err)) - loader.connecting.del(fd) - -proc onRead*(loader: FileLoader, fd: int) = - loader.ongoing.withValue(fd, buffer): - let response = buffer[].response - while true: - let olen = buffer[].buf.len - buffer[].buf.setLen(olen + buffer.readbufsize) - try: - let n = response.body.readData(addr buffer[].buf[olen], - buffer.readbufsize) - if n != 0: - if buffer[].readbufsize < BufferSize: - buffer[].readbufsize = min(BufferSize, buffer[].readbufsize * 2) - buffer[].buf.setLen(olen + n) - if response.body.atEnd(): - buffer[].bodyRead.resolve(buffer[].buf) - buffer[].bodyRead = nil - buffer[].buf = "" - response.unregisterFun() - break - except ErrorAgain, ErrorWouldBlock: - assert buffer.readbufsize > 1 - buffer.readbufsize = buffer.readbufsize div 2 - -proc onError*(loader: FileLoader, fd: int) = - loader.onRead(fd) - -proc doRequest*(loader: FileLoader, request: Request, blocking = true, - canredir = false): Response = - let response = Response(url: request.url) - let stream = connectSocketStream(loader.process, false, blocking = true) - if canredir: - request.canredir = true #TODO set this somewhere else? - stream.swrite(LOAD) - stream.swrite(request) - stream.flush() - stream.sread(response.res) - if response.res == 0: - if loader.handleHeaders(request, response, stream): - if not blocking: - stream.source.getFd().setBlocking(blocking) - return response - -proc quit*(loader: FileLoader) = - let stream = connectSocketStream(loader.process) - if stream != nil: - stream.swrite(QUIT) diff --git a/src/io/loaderhandle.nim b/src/io/loaderhandle.nim deleted file mode 100644 index 077b1a2a..00000000 --- a/src/io/loaderhandle.nim +++ /dev/null @@ -1,73 +0,0 @@ -import net -import streams - -import io/posixstream -import io/headers -import ips/serialize -import ips/socketstream - -type LoaderHandle* = ref object - ostream: Stream - # Only the first handle can be redirected, because a) mailcap can only - # redirect the first handle and b) async redirects would result in race - # conditions that would be difficult to untangle. - canredir: bool - sostream: Stream # saved ostream when redirected - -# Create a new loader handle, with the output stream ostream. -proc newLoaderHandle*(ostream: Stream, canredir: bool): LoaderHandle = - return LoaderHandle(ostream: ostream, canredir: canredir) - -proc getFd*(handle: LoaderHandle): int = - return int(SocketStream(handle.ostream).source.getFd()) - -proc sendResult*(handle: LoaderHandle, res: int): bool = - try: - handle.ostream.swrite(res) - return true - except IOError: # broken pipe - return false - -proc sendStatus*(handle: LoaderHandle, status: int): bool = - try: - handle.ostream.swrite(status) - return true - except IOError: # broken pipe - return false - -proc sendHeaders*(handle: LoaderHandle, headers: Headers): bool = - try: - handle.ostream.swrite(headers) - if handle.canredir: - var redir: bool - handle.ostream.sread(redir) - if redir: - let fd = SocketStream(handle.ostream).recvFileHandle() - handle.sostream = handle.ostream - let stream = newPosixStream(fd) - handle.ostream = stream - return true - except IOError: # broken pipe - return false - -proc sendData*(handle: LoaderHandle, p: pointer, nmemb: int): bool = - try: - handle.ostream.writeData(p, nmemb) - return true - except IOError: # broken pipe - return false - -proc sendData*(handle: LoaderHandle, s: string): bool = - if s.len > 0: - return handle.sendData(unsafeAddr s[0], s.len) - return true - -proc close*(handle: LoaderHandle) = - if handle.sostream != nil: - try: - handle.sostream.swrite(true) - except IOError: - # ignore error, that just means the buffer has already closed the stream - discard - handle.sostream.close() - handle.ostream.close() diff --git a/src/io/promise.nim b/src/io/promise.nim index ee4f0654..549b878e 100644 --- a/src/io/promise.nim +++ b/src/io/promise.nim @@ -1,6 +1,6 @@ import tables -import utils/opt +import types/opt type PromiseState* = enum diff --git a/src/io/request.nim b/src/io/request.nim deleted file mode 100644 index 675ea048..00000000 --- a/src/io/request.nim +++ /dev/null @@ -1,332 +0,0 @@ -import options -import streams -import strutils -import tables - -import bindings/quickjs -import io/headers -import js/dict -import js/error -import js/fromjs -import js/javascript -import types/blob -import types/formdata -import types/referer -import types/url - -type - HttpMethod* = enum - HTTP_GET = "GET" - HTTP_CONNECT = "CONNECT" - HTTP_DELETE = "DELETE" - HTTP_HEAD = "HEAD" - HTTP_OPTIONS = "OPTIONS" - HTTP_PATCH = "PATCH" - HTTP_POST = "POST" - HTTP_PUT = "PUT" - HTTP_TRACE = "TRACE" - - RequestMode* = enum - NO_CORS = "no-cors" - SAME_ORIGIN = "same-origin" - CORS = "cors" - NAVIGATE = "navigate" - WEBSOCKET = "websocket" - - RequestDestination* = enum - NO_DESTINATION = "" - AUDIO = "audio" - AUDIOWORKLET = "audioworklet" - DOCUMENT = "document" - EMBED = "embed" - FONT = "font" - FRAME = "frame" - IFRAME = "iframe" - IMAGE = "image" - MANIFEST = "manifest" - OBJECT = "object" - PAINTWORKLET = "paintworklet" - REPORT = "report" - SCRIPT = "script" - SERVICEWORKER = "serviceworker" - SHAREDWORKER = "sharedworker" - STYLE = "style" - TRACK = "track" - WORKER = "worker" - XSLT = "xslt" - - CredentialsMode* = enum - SAME_ORIGIN = "same-origin" - OMIT = "omit" - INCLUDE = "include" - - CORSAttribute* = enum - NO_CORS = "no-cors" - ANONYMOUS = "anonymous" - USE_CREDENTIALS = "use-credentials" - -type - Request* = ref RequestObj - RequestObj* = object - httpmethod*: HttpMethod - url*: Url - headers* {.jsget.}: Headers - body*: Opt[string] - multipart*: Opt[FormData] - referer*: URL - mode* {.jsget.}: RequestMode - destination* {.jsget.}: RequestDestination - credentialsMode* {.jsget.}: CredentialsMode - proxy*: URL #TODO do something with this - canredir*: bool - - ReadableStream* = ref object of Stream - isource*: Stream - buf: string - isend: bool - -jsDestructor(Request) - -proc js_url(this: Request): string {.jsfget: "url".} = - return $this.url - -#TODO pretty sure this is incorrect -proc js_referrer(this: Request): string {.jsfget: "referrer".} = - if this.referer != nil: - return $this.referer - return "" - -iterator pairs*(headers: Headers): (string, string) = - for k, vs in headers.table: - for v in vs: - yield (k, v) - -proc rsReadData(s: Stream, buffer: pointer, bufLen: int): int = - var s = ReadableStream(s) - if s.atEnd: - return 0 - while s.buf.len < bufLen: - var len: int - s.isource.read(len) - if len == 0: - result = s.buf.len - copyMem(buffer, addr(s.buf[0]), result) - s.buf = s.buf.substr(result) - s.isend = true - return - var nbuf: string - s.isource.readStr(len, nbuf) - s.buf &= nbuf - assert s.buf.len >= bufLen - result = bufLen - copyMem(buffer, addr(s.buf[0]), result) - s.buf = s.buf.substr(result) - if s.buf.len == 0: - var len: int - s.isource.read(len) - if len == 0: - s.isend = true - else: - s.isource.readStr(len, s.buf) - -proc rsAtEnd(s: Stream): bool = - ReadableStream(s).isend - -proc rsClose(s: Stream) = {.cast(tags: [WriteIOEffect]).}: #TODO TODO TODO ew. - var s = ReadableStream(s) - if s.isend: return - s.buf = "" - while true: - var len: int - s.isource.read(len) - if len == 0: - s.isend = true - break - s.isource.setPosition(s.isource.getPosition() + len) - -proc newReadableStream*(isource: Stream): ReadableStream = - new(result) - result.isource = isource - result.readDataImpl = rsReadData - result.atEndImpl = rsAtEnd - result.closeImpl = rsClose - var len: int - result.isource.read(len) - if len == 0: - result.isend = true - else: - result.isource.readStr(len, result.buf) - -func newRequest*(url: URL, httpmethod = HTTP_GET, headers = newHeaders(), - body = opt(string), multipart = opt(FormData), mode = RequestMode.NO_CORS, - credentialsMode = CredentialsMode.SAME_ORIGIN, - destination = RequestDestination.NO_DESTINATION, proxy: URL = nil, - canredir = false): Request = - return Request( - url: url, - httpmethod: httpmethod, - headers: headers, - body: body, - multipart: multipart, - mode: mode, - credentialsMode: credentialsMode, - destination: destination, - proxy: proxy - ) - -func newRequest*(url: URL, httpmethod = HTTP_GET, - headers: seq[(string, string)] = @[], body = opt(string), - multipart = opt(FormData), mode = RequestMode.NO_CORS, proxy: URL = nil, - canredir = false): - Request = - let hl = newHeaders() - for pair in headers: - let (k, v) = pair - hl.table[k] = @[v] - return newRequest(url, httpmethod, hl, body, multipart, mode, proxy = proxy) - -func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors: CORSAttribute, fallbackFlag = false): Request = - var mode = if cors == NO_CORS: - RequestMode.NO_CORS - else: - RequestMode.CORS - if fallbackFlag and mode == NO_CORS: - mode = SAME_ORIGIN - let credentialsMode = if cors == ANONYMOUS: - CredentialsMode.SAME_ORIGIN - else: CredentialsMode.INCLUDE - return newRequest(url, destination = destination, mode = mode, credentialsMode = credentialsMode) - -type - BodyInitType = enum - BODY_INIT_BLOB, BODY_INIT_FORM_DATA, BODY_INIT_URL_SEARCH_PARAMS, - BODY_INIT_STRING - - BodyInit = object - #TODO ReadableStream, BufferSource - case t: BodyInitType - of BODY_INIT_BLOB: - blob: Blob - of BODY_INIT_FORM_DATA: - formData: FormData - of BODY_INIT_URL_SEARCH_PARAMS: - searchParams: URLSearchParams - of BODY_INIT_STRING: - str: string - - RequestInit* = object of JSDict - #TODO aliasing in dicts - `method`: HttpMethod # default: GET - headers: Opt[HeadersInit] - body: Opt[BodyInit] - referrer: Opt[string] - referrerPolicy: Opt[ReferrerPolicy] - credentials: Opt[CredentialsMode] - proxyUrl: URL - mode: Opt[RequestMode] - -proc fromJS2*(ctx: JSContext, val: JSValue, res: var JSResult[BodyInit]) = - if JS_IsUndefined(val) or JS_IsNull(val): - res.err(nil) - return - if not JS_IsObject(val): - res.err(newTypeError("Not an object")) - return - block formData: - let x = fromJS[FormData](ctx, val) - if x.isSome: - res.ok(BodyInit(t: BODY_INIT_FORM_DATA, formData: x.get)) - return - block blob: - let x = fromJS[Blob](ctx, val) - if x.isSome: - res.ok(BodyInit(t: BODY_INIT_BLOB, blob: x.get)) - return - block searchParams: - let x = fromJS[URLSearchParams](ctx, val) - if x.isSome: - res.ok(BodyInit(t: BODY_INIT_URL_SEARCH_PARAMS, searchParams: x.get)) - return - block str: - let x = fromJS[string](ctx, val) - if x.isSome: - res.ok(BodyInit(t: BODY_INIT_STRING, str: x.get)) - return - res.err(newTypeError("Invalid body init type")) - -func newRequest*[T: string|Request](ctx: JSContext, resource: T, - init = none(RequestInit)): JSResult[Request] {.jsctor.} = - when T is string: - let url = ?newURL(resource) - if url.username != "" or url.password != "": - return err(newTypeError("Input URL contains a username or password")) - var httpMethod = HTTP_GET - var headers = newHeaders() - let referer: URL = nil - var credentials = CredentialsMode.SAME_ORIGIN - var body: Opt[string] - var multipart: Opt[FormData] - var proxyUrl: URL #TODO? - let fallbackMode = opt(RequestMode.CORS) - else: - let url = resource.url - var httpMethod = resource.httpMethod - var headers = resource.headers.clone() - let referer = resource.referer - var credentials = resource.credentialsMode - var body = resource.body - var multipart = resource.multipart - var proxyUrl = resource.proxy #TODO? - let fallbackMode = opt(RequestMode) - #TODO window - var mode = fallbackMode.get(RequestMode.NO_CORS) - let destination = NO_DESTINATION - #TODO origin, window - if init.isSome: - if mode == RequestMode.NAVIGATE: - mode = RequestMode.SAME_ORIGIN - #TODO flags? - #TODO referrer - let init = init.get - httpMethod = init.`method` - if init.body.isSome: - let ibody = init.body.get - case ibody.t - of BODY_INIT_FORM_DATA: - multipart = opt(ibody.formData) - of BODY_INIT_STRING: - body = opt(ibody.str) - else: - discard #TODO - if httpMethod in {HTTP_GET, HTTP_HEAD}: - return err(newTypeError("HEAD or GET Request cannot have a body.")) - if init.headers.isSome: - headers.fill(init.headers.get) - if init.credentials.isSome: - credentials = init.credentials.get - if init.mode.isSome: - mode = init.mode.get - #TODO find a standard compatible way to implement this - proxyUrl = init.proxyUrl - return ok(Request( - url: url, - httpmethod: httpmethod, - headers: headers, - body: body, - multipart: multipart, - mode: mode, - credentialsMode: credentials, - destination: destination, - proxy: proxyUrl, - referer: referer - )) - -func credentialsMode*(attribute: CORSAttribute): CredentialsMode = - case attribute - of NO_CORS, ANONYMOUS: - return SAME_ORIGIN - of USE_CREDENTIALS: - return INCLUDE - -proc addRequestModule*(ctx: JSContext) = - ctx.registerType(Request) diff --git a/src/io/response.nim b/src/io/response.nim deleted file mode 100644 index 6d4db42f..00000000 --- a/src/io/response.nim +++ /dev/null @@ -1,74 +0,0 @@ -import streams - -import bindings/quickjs -import io/headers -import io/promise -import io/request -import js/error -import js/javascript -import types/url - -import chakasu/charset - -type - Response* = ref object - res*: int - fd*: int - body*: Stream - bodyUsed* {.jsget.}: bool - contenttype* {.jsget.}: string - status* {.jsget.}: uint16 - headers* {.jsget.}: Headers - redirect*: Request - url*: URL #TODO should be urllist? - unregisterFun*: proc() - bodyRead*: Promise[string] - charset*: Charset - -jsDestructor(Response) - -proc newResponse*(res: int, request: Request, fd = -1, stream: Stream = nil): - Response = - return Response( - res: res, - url: request.url, - body: stream, - bodyRead: Promise[string](), - fd: fd - ) - -func sok(response: Response): bool {.jsfget: "ok".} = - return response.status in 200u16 .. 299u16 - -func surl(response: Response): string {.jsfget: "url".} = - return $response.url - -#TODO: this should be a property of body -proc close*(response: Response) {.jsfunc.} = - response.bodyUsed = true - if response.unregisterFun != nil: - response.unregisterFun() - if response.body != nil: - response.body.close() - -proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} = - if response.bodyRead == nil: - let p = newPromise[JSResult[string]]() - let err = JSResult[string] - .err(newTypeError("Body has already been consumed")) - p.resolve(err) - return p - let bodyRead = response.bodyRead - response.bodyRead = nil - return bodyRead.then(proc(s: string): JSResult[string] = - ok(s)) - -proc json(ctx: JSContext, this: Response): Promise[JSResult[JSValue]] - {.jsfunc.} = - return this.text().then(proc(s: JSResult[string]): JSResult[JSValue] = - let s = ?s - return ok(JS_ParseJSON(ctx, cstring(s), cast[csize_t](s.len), - cstring"<input>"))) - -proc addResponseModule*(ctx: JSContext) = - ctx.registerType(Response) diff --git a/src/io/serialize.nim b/src/io/serialize.nim new file mode 100644 index 00000000..fd558ba9 --- /dev/null +++ b/src/io/serialize.nim @@ -0,0 +1,444 @@ +# Write data to streams. + +import options +import sets +import streams +import tables + +import js/regex +import loader/request +import types/blob +import types/buffersource +import types/formdata +import types/url +import types/opt + +proc swrite*(stream: Stream, n: SomeNumber) +proc sread*(stream: Stream, n: var SomeNumber) +func slen*(n: SomeNumber): int + +proc swrite*[T](stream: Stream, s: set[T]) +proc sread*[T](stream: Stream, s: var set[T]) +func slen*[T](s: set[T]): int + +proc swrite*[T: enum](stream: Stream, x: T) +proc sread*[T: enum](stream: Stream, x: var T) +func slen*[T: enum](x: T): int + +proc swrite*(stream: Stream, s: string) +proc sread*(stream: Stream, s: var string) +func slen*(s: string): int + +proc swrite*(stream: Stream, b: bool) +proc sread*(stream: Stream, b: var bool) +func slen*(b: bool): int + +proc swrite*(stream: Stream, url: Url) +proc sread*(stream: Stream, url: var Url) +func slen*(url: Url): int + +proc swrite*(stream: Stream, tup: tuple) +proc sread*(stream: Stream, tup: var tuple) +func slen*(tup: tuple): int + +proc swrite*[T](stream: Stream, s: seq[T]) +proc sread*[T](stream: Stream, s: var seq[T]) +func slen*(s: seq): int + +proc swrite*[U, V](stream: Stream, t: Table[U, V]) +proc sread*[U, V](stream: Stream, t: var Table[U, V]) +func slen*[U, V](t: Table[U, V]): int + +proc swrite*(stream: Stream, obj: object) +proc sread*(stream: Stream, obj: var object) +func slen*(obj: object): int + +proc swrite*(stream: Stream, obj: ref object) +proc sread*(stream: Stream, obj: var ref object) +func slen*(obj: ref object): int + +proc swrite*(stream: Stream, part: FormDataEntry) +proc sread*(stream: Stream, part: var FormDataEntry) +func slen*(part: FormDataEntry): int + +proc swrite*(stream: Stream, blob: Blob) +proc sread*(stream: Stream, blob: var Blob) +func slen*(blob: Blob): int + +proc swrite*[T](stream: Stream, o: Option[T]) +proc sread*[T](stream: Stream, o: var Option[T]) +func slen*[T](o: Option[T]): int + +proc swrite*[T, E](stream: Stream, o: Result[T, E]) +proc sread*[T, E](stream: Stream, o: var Result[T, E]) +func slen*[T, E](o: Result[T, E]): int + +proc swrite*(stream: Stream, regex: Regex) +proc sread*(stream: Stream, regex: var Regex) +func slen*(regex: Regex): int + +proc swrite*(stream: Stream, source: BufferSource) +proc sread*(stream: Stream, source: var BufferSource) +func slen*(source: BufferSource): int + +proc swrite*(stream: Stream, n: SomeNumber) = + stream.write(n) + +proc sread*(stream: Stream, n: var SomeNumber) = + if stream.readData(addr n, sizeof(n)) < sizeof(n): + raise newException(EOFError, "eof") + +func slen*(n: SomeNumber): int = + return sizeof(n) + +proc swrite*[T: enum](stream: Stream, x: T) = + static: + doAssert sizeof(int) >= sizeof(T) + stream.swrite(int(x)) + +proc sread*[T: enum](stream: Stream, x: var T) = + var i: int + stream.sread(i) + x = cast[T](i) + +func slen*[T: enum](x: T): int = + return sizeof(int) + +proc swrite*[T](stream: Stream, s: set[T]) = + stream.swrite(s.card) + for e in s: + stream.swrite(e) + +proc sread*[T](stream: Stream, s: var set[T]) = + var len: int + stream.sread(len) + for i in 0 ..< len: + var x: T + stream.sread(x) + s.incl(x) + +func slen*[T](s: set[T]): int = + result = slen(s.card) + for x in s: + result += slen(x) + +proc swrite*(stream: Stream, s: string) = + stream.swrite(s.len) + stream.write(s) + +proc sread*(stream: Stream, s: var string) = + var len: int + stream.sread(len) + if len > 0: + s = newString(len) + prepareMutation(s) + if stream.readData(addr s[0], len) < len: + raise newException(EOFError, "eof") + else: + s = "" + +func slen*(s: string): int = + slen(s.len) + s.len + +proc swrite*(stream: Stream, b: bool) = + if b: + stream.swrite(1u8) + else: + stream.swrite(0u8) + +proc sread*(stream: Stream, b: var bool) = + var n: uint8 + stream.sread(n) + if n == 1u8: + b = true + else: + assert n == 0u8 + b = false + +func slen*(b: bool): int = + return sizeof(uint8) + +proc swrite*(stream: Stream, url: URL) = + if url != nil: + stream.swrite(url.serialize()) + else: + stream.swrite("") + +proc sread*(stream: Stream, url: var URL) = + var s: string + stream.sread(s) + if s == "": + url = nil + else: + let x = newURL(s) + if x.isSome: + url = x.get + else: + url = nil + +func slen*(url: URL): int = + if url == nil: + return slen("") + return slen(url.serialize()) + +proc swrite*(stream: Stream, tup: tuple) = + for f in tup.fields: + stream.swrite(f) + +proc sread*(stream: Stream, tup: var tuple) = + for f in tup.fields: + stream.sread(f) + +func slen*(tup: tuple): int = + for f in tup.fields: + result += slen(f) + +proc swrite*[T](stream: Stream, s: seq[T]) = + stream.swrite(s.len) + var i = 0 + for m in s: + stream.swrite(m) + inc i + +proc sread*[T](stream: Stream, s: var seq[T]) = + var len: int + stream.sread(len) + s.setLen(len) + for i in 0..<len: + stream.sread(s[i]) + +func slen*(s: seq): int = + result = slen(s.len) + for x in s: + result += slen(x) + +proc swrite*[U, V](stream: Stream, t: Table[U, V]) = + stream.swrite(t.len) + for k, v in t: + stream.swrite(k) + stream.swrite(v) + +proc sread*[U, V](stream: Stream, t: var Table[U, V]) = + var len: int + stream.sread(len) + for i in 0..<len: + var k: U + stream.sread(k) + var v: V + stream.sread(v) + t[k] = v + +func slen*[U, V](t: Table[U, V]): int = + result = slen(t.len) + for k, v in t: + result += slen(k) + result += slen(v) + +proc swrite*(stream: Stream, obj: object) = + for f in obj.fields: + stream.swrite(f) + +proc sread*(stream: Stream, obj: var object) = + for f in obj.fields: + stream.sread(f) + +func slen*(obj: object): int = + for f in obj.fields: + result += slen(f) + +proc swrite*(stream: Stream, obj: ref object) = + stream.swrite(obj != nil) + if obj != nil: + stream.swrite(obj[]) + +proc sread*(stream: Stream, obj: var ref object) = + var n: bool + stream.sread(n) + if n: + new(obj) + stream.sread(obj[]) + +func slen*(obj: ref object): int = + result = slen(obj != nil) + if obj != nil: + result += slen(obj[]) + +proc swrite*(stream: Stream, part: FormDataEntry) = + stream.swrite(part.isstr) + stream.swrite(part.name) + stream.swrite(part.filename) + if part.isstr: + stream.swrite(part.svalue) + else: + stream.swrite(part.value) + +proc sread*(stream: Stream, part: var FormDataEntry) = + var isstr: bool + stream.sread(isstr) + if isstr: + part = FormDataEntry(isstr: true) + else: + part = FormDataEntry(isstr: false) + stream.sread(part.name) + stream.sread(part.filename) + if part.isstr: + stream.sread(part.svalue) + else: + stream.sread(part.value) + +func slen*(part: FormDataEntry): int = + result += slen(part.isstr) + result += slen(part.name) + result += slen(part.filename) + if part.isstr: + result += slen(part.svalue) + else: + result += slen(part.value) + +#TODO clean up this mess +proc swrite*(stream: Stream, blob: Blob) = + stream.swrite(blob.isfile) + if blob.isfile: + stream.swrite(WebFile(blob).path) + else: + stream.swrite(blob.ctype) + stream.swrite(blob.size) + stream.writeData(blob.buffer, int(blob.size)) + +proc sread*(stream: Stream, blob: var Blob) = + var isfile: bool + stream.sread(isfile) + if isfile: + var file = new WebFile + file.isfile = true + stream.sread(file.path) + blob = file + else: + new(blob) + stream.sread(blob.ctype) + stream.sread(blob.size) + blob.buffer = alloc(blob.size) + blob.deallocFun = dealloc + if blob.size > 0: + assert stream.readData(blob.buffer, int(blob.size)) == int(blob.size) + +func slen*(blob: Blob): int = + result += slen(blob.isfile) + if blob.isfile: + result = slen(WebFile(blob).path) + else: + result += slen(blob.ctype) + result += slen(blob.size) + result += int(blob.size) #TODO ?? + +proc swrite*[T](stream: Stream, o: Option[T]) = + stream.swrite(o.issome) + if o.isSome: + stream.swrite(o.get) + +proc sread*[T](stream: Stream, o: var Option[T]) = + var x: bool + stream.sread(x) + if x: + var m: T + stream.sread(m) + o = some(m) + else: + o = none(T) + +func slen*[T](o: Option[T]): int = + result = slen(o.isSome) + if o.isSome: + result += slen(o.get) + +proc swrite*[T, E](stream: Stream, o: Result[T, E]) = + stream.swrite(o.isOk) + if o.isOk: + when not (T is void): + stream.swrite(o.get) + else: + when not (E is void): + stream.swrite(o.error) + +proc sread*[T, E](stream: Stream, o: var Result[T, E]) = + var x: bool + stream.sread(x) + if x: + when not (T is void): + var m: T + stream.sread(m) + o.ok(m) + else: + o.ok() + else: + when not (E is void): + var e: E + stream.sread(e) + o.err(e) + else: + o.err() + +func slen*[T, E](o: Result[T, E]): int = + result = slen(o.isSome) + if o.isSome: + when not (T is void): + result += slen(o.get) + else: + when not (E is void): + result += slen(o.error) + +proc swrite*(stream: Stream, regex: Regex) = + stream.swrite(regex.plen) + stream.writeData(regex.bytecode, regex.plen) + stream.swrite(regex.buf) + +proc sread*(stream: Stream, regex: var Regex) = + assert regex.bytecode == nil + stream.sread(regex.plen) + regex.bytecode = cast[ptr uint8](alloc(regex.plen)) + regex.clone = true + let l = stream.readData(regex.bytecode, regex.plen) + stream.sread(regex.buf) + if l != regex.plen: + `=destroy`(regex) + +func slen*(regex: Regex): int = + result += slen(regex.plen) + result += regex.plen + result += slen(regex.buf) + +proc swrite*(stream: Stream, source: BufferSource) = + stream.swrite(source.t) + case source.t + of CLONE: stream.swrite(source.clonepid) + of LOAD_REQUEST: stream.swrite(source.request) + of LOAD_PIPE: stream.swrite(source.fd) + stream.swrite(source.location) + stream.swrite(source.contenttype) + stream.swrite(source.charset) + +proc sread*(stream: Stream, source: var BufferSource) = + var t: BufferSourceType + stream.sread(t) + case t + of CLONE: + source = BufferSource(t: CLONE) + stream.sread(source.clonepid) + of LOAD_REQUEST: + source = BufferSource(t: LOAD_REQUEST) + stream.sread(source.request) + of LOAD_PIPE: + source = BufferSource(t: LOAD_PIPE) + stream.sread(source.fd) + stream.sread(source.location) + stream.sread(source.contenttype) + stream.sread(source.charset) + +func slen*(source: BufferSource): int = + result += slen(source.t) + case source.t + of CLONE: result += slen(source.clonepid) + of LOAD_REQUEST: result += slen(source.request) + of LOAD_PIPE: result += slen(source.fd) + result += slen(source.location) + result += slen(source.contenttype) diff --git a/src/io/serversocket.nim b/src/io/serversocket.nim new file mode 100644 index 00000000..61b633a9 --- /dev/null +++ b/src/io/serversocket.nim @@ -0,0 +1,28 @@ +import nativesockets +import net +import os +when defined(posix): + import posix + +type ServerSocket* = object + sock*: Socket + path*: string + +var SocketDirectory* = "/tmp/cha" +const SocketPathPrefix = "cha_sock_" +proc getSocketPath*(pid: Pid): string = + SocketDirectory / SocketPathPrefix & $pid + +proc initServerSocket*(buffered = true, blocking = true): ServerSocket = + createDir(SocketDirectory) + result.sock = newSocket(Domain.AF_UNIX, SockType.SOCK_STREAM, Protocol.IPPROTO_IP, buffered) + if not blocking: + result.sock.getFd().setBlocking(false) + result.path = getSocketPath(getpid()) + discard unlink(cstring(result.path)) + bindUnix(result.sock, result.path) + listen(result.sock) + +proc close*(ssock: ServerSocket) = + close(ssock.sock) + discard unlink(cstring(ssock.path)) diff --git a/src/io/socketstream.nim b/src/io/socketstream.nim new file mode 100644 index 00000000..6b648003 --- /dev/null +++ b/src/io/socketstream.nim @@ -0,0 +1,147 @@ +import nativesockets +import net +import os +import streams + +when defined(posix): + import posix + +import io/posixstream +import io/serversocket + +type SocketStream* = ref object of Stream + source*: Socket + blk*: bool + isend: bool + +proc sockReadData(s: Stream, buffer: pointer, len: int): int = + assert len != 0 + let s = SocketStream(s) + let wasend = s.isend + if s.blk: + while result < len: + let n = s.source.recv(cast[pointer](cast[int](buffer) + result), len - result) + if n < 0: + if result == 0: + result = n + break + elif n == 0: + s.isend = true + break + result += n + else: + result = s.source.recv(buffer, len) + if result == 0: + if wasend: + raise newException(EOFError, "eof") + s.isend = true + if result < 0: + raisePosixIOError() + elif result == 0: + s.isend = true + +proc sockWriteData(s: Stream, buffer: pointer, len: int) = + var i = 0 + while i < len: + let n = SocketStream(s).source.send(cast[pointer](cast[int](buffer) + i), len - i) + if n < 0: + raise newException(IOError, $strerror(errno)) + i += n + +proc sockAtEnd(s: Stream): bool = + SocketStream(s).isend + +proc sockClose(s: Stream) = {.cast(tags: []).}: #...sigh + let s = SocketStream(s) + s.source.close() + +# See https://stackoverflow.com/a/4491203 +proc sendFileHandle*(s: SocketStream, fd: FileHandle) = + assert not s.source.hasDataBuffered + var hdr: Tmsghdr + var iov: IOVec + var space: csize_t + {.emit: [ + space, """ = CMSG_SPACE(sizeof(int));""", + ].} + var cmsgbuf = alloc(cast[int](space)) + var buf = char(0) + iov.iov_base = addr buf + iov.iov_len = csize_t(1) + zeroMem(addr hdr, sizeof(hdr)) + hdr.msg_iov = addr iov + hdr.msg_iovlen = 1 + hdr.msg_control = cmsgbuf + # ...sigh + {.emit: [ + hdr.msg_controllen, """ = CMSG_LEN(sizeof(int));""", + ].} + let cmsg = CMSG_FIRSTHDR(addr hdr) + # FileHandle is cint, so sizeof(FileHandle) in c is sizeof(int). + when sizeof(FileHandle) != sizeof(cint): + error("Or not...") + {.emit: [ + cmsg.cmsg_len, """ = CMSG_LEN(sizeof(int));""" + ].} + cmsg.cmsg_level = SOL_SOCKET + cmsg.cmsg_type = SCM_RIGHTS + cast[ptr FileHandle](CMSG_DATA(cmsg))[] = fd + let n = sendmsg(s.source.getFd(), addr hdr, 0) + dealloc(cmsgbuf) + assert n == int(iov.iov_len) #TODO remove this + +proc recvFileHandle*(s: SocketStream): FileHandle = + assert not s.source.hasDataBuffered + var iov: IOVec + var hdr: Tmsghdr + var buf: char + var cmsgbuf = alloc(CMSG_SPACE(csize_t(sizeof(FileHandle)))) + iov.iov_base = addr buf + iov.iov_len = 1 + zeroMem(addr hdr, sizeof(hdr)) + hdr.msg_iov = addr iov + hdr.msg_iovlen = 1 + hdr.msg_control = cmsgbuf + {.emit: [ + hdr.msg_controllen, """ = CMSG_SPACE(sizeof(int));""" + ].} + let n = recvmsg(s.source.getFd(), addr hdr, 0) + assert n != 0, "Unexpected EOF" #TODO remove this + assert n > 0, "Failed to receive message " & $osLastError() #TODO remove this + var cmsg = CMSG_FIRSTHDR(addr hdr) + result = cast[ptr FileHandle](CMSG_DATA(cmsg))[] + dealloc(cmsgbuf) + +func newSocketStream*(): SocketStream = + new(result) + result.readDataImpl = cast[proc (s: Stream, buffer: pointer, bufLen: int): int + {.nimcall, raises: [Defect, IOError, OSError], tags: [ReadIOEffect], gcsafe.} + ](sockReadData) # ... ??? + result.writeDataImpl = sockWriteData + result.atEndImpl = sockAtEnd + result.closeImpl = sockClose + +proc setBlocking*(ss: SocketStream, blocking: bool) = + ss.source.getFd().setBlocking(blocking) + +proc connectSocketStream*(path: string, buffered = true, blocking = true): SocketStream = + result = newSocketStream() + result.blk = blocking + let sock = newSocket(Domain.AF_UNIX, SockType.SOCK_STREAM, Protocol.IPPROTO_IP, buffered) + if not blocking: + sock.getFd().setBlocking(false) + connectUnix(sock, path) + result.source = sock + +proc connectSocketStream*(pid: Pid, buffered = true, blocking = true): SocketStream = + try: + connectSocketStream(getSocketPath(pid), buffered, blocking) + except OSError: + return nil + +proc acceptSocketStream*(ssock: ServerSocket, blocking = true): SocketStream = + result = newSocketStream() + result.blk = blocking + var sock: Socket + ssock.sock.accept(sock, inheritable = true) + result.source = sock diff --git a/src/io/tempfile.nim b/src/io/tempfile.nim deleted file mode 100644 index d99ea4dc..00000000 --- a/src/io/tempfile.nim +++ /dev/null @@ -1,18 +0,0 @@ -import os - -var tmpf_seq: int -proc getTempFile*(tmpdir: string, ext = ""): string = - if not dirExists(tmpdir): - createDir(tmpdir) - var tmpf = tmpdir / "chatmp" & $tmpf_seq - if ext != "": - tmpf &= "." - tmpf &= ext - while fileExists(tmpf): - inc tmpf_seq - tmpf = tmpdir / "chatmp" & $tmpf_seq - if ext != "": - tmpf &= "." - tmpf &= ext - inc tmpf_seq - return tmpf diff --git a/src/io/window.nim b/src/io/window.nim deleted file mode 100644 index 278fc3fb..00000000 --- a/src/io/window.nim +++ /dev/null @@ -1,54 +0,0 @@ -import terminal - -when defined(posix): - import termios - - -type - WindowAttributes* = object - width*: int - height*: int - ppc*: int # cell width - ppl*: int # cell height - cell_ratio*: float64 # ppl / ppc - width_px*: int - height_px*: int - -proc getWindowAttributes*(tty: File): WindowAttributes = - when defined(posix): - if tty.isatty(): - var win: IOctl_WinSize - if ioctl(cint(getOsFileHandle(tty)), TIOCGWINSZ, addr win) != -1: - var cols = win.ws_col - var rows = win.ws_row - if cols == 0: - cols = 80 - if rows == 0: - rows = 24 - # Filling the last row without raw mode breaks things. However, - # not supporting Windows means we can always have raw mode, so we can - # use all available columns. - result.width = int(cols) - result.height = int(rows) - result.ppc = int(win.ws_xpixel) div result.width - result.ppl = int(win.ws_ypixel) div result.height - # some terminal emulators (aka vte) don't set ws_xpixel or ws_ypixel. - # solution: use xterm. - if result.ppc == 0: - result.ppc = 9 - if result.ppl == 0: - result.ppl = 18 - result.width_px = result.width * result.ppc - result.height_px = result.height * result.ppl - result.cell_ratio = result.ppl / result.ppc - return - # for Windows. unused. - result.width = terminalWidth() - 1 - result.height = terminalHeight() - if result.height == 0: - result.height = 24 - result.ppc = 9 - result.ppl = 18 - result.cell_ratio = result.ppl / result.ppc - result.width_px = result.ppc * result.width - result.height_px = result.ppl * result.height |