import std/options
import std/os
import std/streams
import std/tables
import config/chapath
import config/mailcap
import config/mimetypes
import config/toml
import js/error
import js/fromjs
import js/javascript
import js/propertyenumlist
import js/regex
import loader/headers
import types/cell
import types/color
import types/cookie
import types/opt
import types/urimethodmap
import types/url
import utils/twtstr
import chagashi/charset
type
ColorMode* = enum
MONOCHROME, ANSI, EIGHT_BIT, TRUE_COLOR
FormatMode* = set[FormatFlags]
ChaPathResolved* = distinct string
ActionMap = object
t: Table[string, string]
SiteConfig* = object
url*: Opt[Regex]
host*: Opt[Regex]
rewrite_url*: (proc(s: URL): JSResult[URL])
cookie*: Opt[bool]
third_party_cookie*: seq[Regex]
share_cookie_jar*: Opt[string]
referer_from*: Opt[bool]
scripting*: Opt[bool]
document_charset*: seq[Charset]
images*: Opt[bool]
stylesheet*: Opt[string]
proxy*: Opt[URL]
OmniRule* = object
match*: Regex
substitute_url*: (proc(s: string): JSResult[string])
StartConfig = object
visual_home* {.jsgetset.}: string
startup_script* {.jsgetset.}: string
headless* {.jsgetset.}: bool
console_buffer* {.jsgetset.}: bool
CSSConfig = object
stylesheet* {.jsgetset.}: string
SearchConfig = object
wrap* {.jsgetset.}: bool
ignore_case* {.jsgetset.}: bool
EncodingConfig = object
display_charset* {.jsgetset.}: Opt[Charset]
document_charset* {.jsgetset.}: seq[Charset]
ExternalConfig = object
tmpdir* {.jsgetset.}: ChaPathResolved
editor* {.jsgetset.}: string
mailcap*: Mailcap
mime_types*: MimeTypes
cgi_dir* {.jsgetset.}: seq[ChaPathResolved]
urimethodmap*: URIMethodMap
download_dir* {.jsgetset.}: string
w3m_cgi_compat* {.jsgetset.}: bool
InputConfig = object
vi_numeric_prefix* {.jsgetset.}: bool
use_mouse* {.jsgetset.}: bool
NetworkConfig = object
max_redirect* {.jsgetset.}: int32
prepend_https* {.jsgetset.}: bool
prepend_scheme* {.jsgetset.}: string
proxy* {.jsgetset.}: Opt[string]
default_headers* {.jsgetset.}: Table[string, string]
DisplayConfig = object
color_mode* {.jsgetset.}: Opt[ColorMode]
format_mode* {.jsgetset.}: Opt[FormatMode]
no_format_mode* {.jsgetset.}: FormatMode
emulate_overline* {.jsgetset.}: bool
alt_screen* {.jsgetset.}: Opt[bool]
highlight_color* {.jsgetset.}: RGBAColor
highlight_marks* {.jsgetset.}: bool
double_width_ambiguous* {.jsgetset.}: bool
minimum_contrast* {.jsgetset.}: int32
force_clear* {.jsgetset.}: bool
set_title* {.jsgetset.}: bool
default_background_color* {.jsgetset.}: Opt[RGBColor]
default_foreground_color* {.jsgetset.}: Opt[RGBColor]
query_da1* {.jsgetset.}: bool
columns* {.jsgetset.}: int32
lines* {.jsgetset.}: int32
pixels_per_column* {.jsgetset.}: int32
pixels_per_line* {.jsgetset.}: int32
force_columns* {.jsgetset.}: bool
force_lines* {.jsgetset.}: bool
force_pixels_per_column* {.jsgetset.}: bool
force_pixels_per_line* {.jsgetset.}: bool
Config* = ref object
jsctx: JSContext
configdir {.jsget.}: string
`include` {.jsget.}: seq[ChaPathResolved]
start* {.jsget.}: StartConfig
search* {.jsget.}: SearchConfig
css* {.jsget.}: CSSConfig
encoding* {.jsget.}: EncodingConfig
external* {.jsget.}: ExternalConfig
network* {.jsget.}: NetworkConfig
input* {.jsget.}: InputConfig
display* {.jsget.}: DisplayConfig
#TODO getset
siteconf*: seq[SiteConfig]
omnirule*: seq[OmniRule]
page* {.jsget.}: ActionMap
line* {.jsget.}: ActionMap
ForkServerConfig* = object
tmpdir*: string
ambiguous_double*: bool
jsDestructor(ActionMap)
jsDestructor(StartConfig)
jsDestructor(CSSConfig)
jsDestructor(SearchConfig)
jsDestructor(EncodingConfig)
jsDestructor(ExternalConfig)
jsDestructor(NetworkConfig)
jsDestructor(DisplayConfig)
jsDestructor(Config)
converter toStr*(p: ChaPathResolved): string {.inline.} =
return string(p)
proc fromJSChaPathResolved(ctx: JSContext; val: JSValue):
JSResult[ChaPathResolved] =
return cast[JSResult[ChaPathResolved]](fromJS[string](ctx, val))
proc `[]=`(a: var ActionMap; b, c: string) =
a.t[b] = c
proc `[]`*(a: ActionMap; b: string): string =
a.t[b]
proc contains*(a: ActionMap; b: string): bool =
return b in a.t
proc getOrDefault(a: ActionMap; b: string): string =
return a.t.getOrDefault(b)
proc hasKeyOrPut(a: var ActionMap; b, c: string): bool =
return a.t.hasKeyOrPut(b, c)
func getRealKey(key: string): string =
var realk: string
var control = 0
var meta = 0
var skip = false
for c in key:
if c == '\\':
skip = true
elif skip:
realk &= c
skip = false
elif c == 'M' and meta == 0:
inc meta
elif c == 'C' and control == 0:
inc control
elif c == '-' and control == 1:
inc control
elif c == '-' and meta == 1:
inc meta
elif meta == 1:
realk &= 'M' & c
meta = 0
elif control == 1:
realk &= 'C' & c
control = 0
else:
if meta == 2:
realk &= '\e'
meta = 0
if control == 2:
realk &= getControlChar(c)
control = 0
else:
realk &= c
if control == 1:
realk &= 'C'
if meta == 1:
realk &= 'M'
if skip:
realk &= '\\'
return realk
proc getter(a: ptr ActionMap; s: string): Opt[string] {.jsgetprop.} =
a.t.withValue(s, p):
return opt(p[])
proc setter(a: ptr ActionMap; k, v: string) {.jssetprop.} =
let k = getRealKey(k)
if k == "":
return
a[][k] = v
var teststr = k
teststr.setLen(teststr.high)
for i in countdown(k.high, 0):
if teststr notin a[]:
a[][teststr] = "client.feedNext()"
teststr.setLen(i)
proc delete(a: ptr ActionMap; k: string): bool {.jsdelprop.} =
let k = getRealKey(k)
let ina = k in a[]
a[].t.del(k)
return ina
func names(ctx: JSContext, a: ptr ActionMap): JSPropertyEnumList
{.jspropnames.} =
let L = uint32(a[].t.len)
var list = newJSPropertyEnumList(ctx, L)
for key in a[].t.keys:
list.add(key)
return list
proc bindPagerKey(config: Config; key, action: string) {.jsfunc.} =
(addr config.page).setter(key, action)
proc bindLineKey(config: Config; key, action: string) {.jsfunc.} =
(addr config.line).setter(key, action)
proc hasprop(a: ptr ActionMap; s: string): bool {.jshasprop.} =
return s in a[]
func getProxy*(config: Config): URL =
if config.network.proxy.isSome:
let s = config.network.proxy.get
let x = parseURL(s)
if x.isSome:
return x.get
else:
raise newException(ValueError, "Invalid proxy URL: " & s)
return nil
func getDefaultHeaders*(config: Config): Headers =
return newHeaders(config.network.default_headers)
proc openFileExpand(dir, file: string): FileStream =
if file.len == 0:
return nil
if file[0] == '/':
return newFileStream(file)
else:
return newFileStream(dir / file)
proc readUserStylesheet(dir, file: string): string =
let x = ChaPath(file).unquote()
if x.isNone:
raise newException(ValueError, x.error)
let s = openFileExpand(dir, x.get)
if s != nil:
result = s.readAll()
s.close()
proc getForkServerConfig*(config: Config): ForkServerConfig =
return ForkServerConfig(
tmpdir: config.external.tmpdir,
ambiguous_double: config.display.double_width_ambiguous
)
type ConfigParser = object
config: Config
dir: string
warnings: seq[string]
proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPath; v: TomlValue;
k: string)
proc parseConfigValue[T](ctx: var ConfigParser; x: var seq[T]; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Charset; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var int32; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Opt[ColorMode];
v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Opt[FormatMode];
v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var RGBAColor; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var RGBColor; v: TomlValue;
k: string)
proc parseConfigValue[T](ctx: var ConfigParser; x: var Opt[T]; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var ActionMap; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var CSSConfig; v: TomlValue;
k: string)
proc parseConfigValue[U; V](ctx: var ConfigParser; x: var Table[U, V];
v: TomlValue; k: string)
proc parseConfigValue[T](ctx: var ConfigParser; x: var set[T]; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var TomlTable; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Regex; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var URL; v: TomlValue;
k: string)
proc parseConfigValue[T](ctx: var ConfigParser; x: var proc(x: T): JSResult[T];
v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var MimeTypes; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue;
k: string)
proc typeCheck(v: TomlValue, vt: ValueType, k: string) =
if v.vt != vt:
raise newException(ValueError, "invalid type for key " & k &
" (got " & $v.vt & ", expected " & $vt & ")")
proc typeCheck(v: TomlValue, vt: set[ValueType], k: string) =
if v.vt notin vt:
raise newException(ValueError, "invalid type for key " & k &
" (got " & $v.vt & ", expected " & $vt & ")")
proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
k: string) =
typeCheck(v, VALUE_TABLE, k)
for fk, fv in x.fieldPairs:
when typeof(fv) isnot JSContext:
let kebabk = snakeToKebabCase(fk)
if kebabk in v:
let kkk = if k != "":
k & "." & fk
else:
fk
ctx.parseConfigValue(fv, v[kebabk], kkk)
proc parseConfigValue[U, V](ctx: var ConfigParser; x: var Table[U, V];
v: TomlValue; k: string) =
typeCheck(v, VALUE_TABLE, k)
x.clear()
for kk, vv in v:
var y: V
let kkk = k & "[" & kk & "]"
ctx.parseConfigValue(y, vv, kkk)
x[kk] = y
proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue;
k: string) =
typeCheck(v, VALUE_BOOLEAN, k)
x = v.b
proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
x = v.s
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPath;
v: TomlValue; k: string) =
typeCheck(v, VALUE_STRING, k)
x = ChaPath(v.s)
proc parseConfigValue[T](ctx: var ConfigParser; x: var seq[T]; v: TomlValue;
k: string) =
typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k)
if v.vt != VALUE_ARRAY:
var y: T
ctx.parseConfigValue(y, v, k)
x = @[y]
else:
if not v.ad:
x.setLen(0)
for i in 0 ..< v.a.len:
var y: T
ctx.parseConfigValue(y, v.a[i], k & "[" & $i & "]")
x.add(y)
proc parseConfigValue(ctx: var ConfigParser; x: var TomlTable; v: TomlValue;
k: string) =
typeCheck(v, {VALUE_TABLE}, k)
x = v.t
proc parseConfigValue(ctx: var ConfigParser; x: var Charset; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
x = getCharset(v.s)
if x == CHARSET_UNKNOWN:
raise newException(ValueError, "unknown charset '" & v.s & "' for key " &
k)
proc parseConfigValue(ctx: var ConfigParser; x: var int32; v: TomlValue;
k: string) =
typeCheck(v, VALUE_INTEGER, k)
x = int32(v.i)
proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
k: string) =
typeCheck(v, VALUE_INTEGER, k)
x = v.i
proc parseConfigValue(ctx: var ConfigParser; x: var Opt[ColorMode];
v: TomlValue; k: string) =
typeCheck(v, VALUE_STRING, k)
case v.s
of "auto": x.err()
of "monochrome": x.ok(MONOCHROME)
of "ansi": x.ok(ANSI)
of "8bit", "eight-bit": x.ok(EIGHT_BIT)
of "24bit", "true-color": x.ok(TRUE_COLOR)
else:
raise newException(ValueError, "unknown color mode '" & v.s &
"' for key " & k)
proc parseConfigValue(ctx: var ConfigParser; x: var Opt[FormatMode];
v: TomlValue; k: string) =
typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k)
if v.vt == VALUE_STRING and v.s == "auto":
x.err()
else:
var y: FormatMode
ctx.parseConfigValue(y, v, k)
x.ok(y)
proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
k: string) =
typeCheck(v, VALUE_ARRAY, k)
for i in 0 ..< v.a.len:
let kk = k & "[" & $i & "]"
let vv = v.a[i]
typeCheck(vv, VALUE_STRING, kk)
case vv.s
of "bold": x.incl(FLAG_BOLD)
of "italic": x.incl(FLAG_ITALIC)
of "underline": x.incl(FLAG_UNDERLINE)
of "reverse": x.incl(FLAG_REVERSE)
of "strike": x.incl(FLAG_STRIKE)
of "overline": x.incl(FLAG_OVERLINE)
of "blink": x.incl(FLAG_BLINK)
else:
raise newException(ValueError, "unknown format mode '" & vv.s &
"' for key " & kk)
proc parseConfigValue(ctx: var ConfigParser; x: var RGBAColor; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
let c = parseRGBAColor(v.s)
if c.isNone:
raise newException(ValueError, "invalid color '" & v.s &
"' for key " & k)
x = c.get
proc parseConfigValue(ctx: var ConfigParser; x: var RGBColor; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
let c = parseLegacyColor(v.s)
if c.isNone:
raise newException(ValueError, "invalid color '" & v.s &
"' for key " & k)
x = c.get
proc parseConfigValue[T](ctx: var ConfigParser; x: var Opt[T]; v: TomlValue;
k: string) =
if v.vt == VALUE_STRING and v.s == "auto":
x.err()
else:
var y: T
ctx.parseConfigValue(y, v, k)
x.ok(y)
proc parseConfigValue(ctx: var ConfigParser; x: var ActionMap; v: TomlValue;
k: string) =
typeCheck(v, VALUE_TABLE, k)
for kk, vv in v:
typeCheck(vv, VALUE_STRING, k & "[" & kk & "]")
let rk = getRealKey(kk)
var buf: string
for i in 0 ..< rk.high:
buf &= rk[i]
discard x.hasKeyOrPut(buf, "client.feedNext()")
x[rk] = vv.s
proc parseConfigValue[T: enum](ctx: var ConfigParser; x: var T; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
let e = strictParseEnum[T](v.s)
if e.isNone:
raise newException(ValueError, "invalid value '" & v.s & "' for key " & k)
x = e.get
proc parseConfigValue[T](ctx: var ConfigParser; x: var set[T]; v: TomlValue;
k: string) =
typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k)
if v.vt == VALUE_STRING:
var xx: T
xx.parseConfigValue(v, k)
x = {xx}
else:
x = {}
for i in 0 ..< v.a.len:
let kk = k & "[" & $i & "]"
var xx: T
xx.parseConfigValue(v.a[i], kk)
x.incl(xx)
proc parseConfigValue(ctx: var ConfigParser; x: var CSSConfig; v: TomlValue;
k: string) =
typeCheck(v, VALUE_TABLE, k)
for kk, vv in v:
let kkk = if k != "":
k & "." & kk
else:
kk
case kk
of "include":
typeCheck(vv, {VALUE_STRING, VALUE_ARRAY}, kkk)
case vv.vt
of VALUE_STRING:
x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
of VALUE_ARRAY:
for child in vv.a:
x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
else: discard
of "inline":
typeCheck(vv, VALUE_STRING, kkk)
x.stylesheet &= vv.s
proc parseConfigValue(ctx: var ConfigParser; x: var Regex; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
let y = compileMatchRegex(v.s)
if y.isNone:
raise newException(ValueError, "invalid regex " & k & " : " & y.error)
x = y.get
proc parseConfigValue(ctx: var ConfigParser; x: var URL; v: TomlValue;
k: string) =
typeCheck(v, VALUE_STRING, k)
let y = parseURL(v.s)
if y.isNone:
raise newException(ValueError, "invalid URL " & k)
x = y.get
proc parseConfigValue[T](ctx: var ConfigParser; x: var proc(x: T): JSResult[T];
v: TomlValue; k: string) =
typeCheck(v, VALUE_STRING, k)
let fun = ctx.config.jsctx.eval(v.s, "<config>", JS_EVAL_TYPE_GLOBAL)
x = getJSFunction[T, T](ctx.config.jsctx, fun)
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
v: TomlValue; k: string) =
typeCheck(v, VALUE_STRING, k)
let y = ChaPath(v.s).unquote()
if y.isErr:
raise newException(ValueError, y.error)
x = ChaPathResolved(y.get)
proc parseConfigValue(ctx: var ConfigParser; x: var MimeTypes; v: TomlValue;
k: string) =
var paths: seq[ChaPathResolved]
ctx.parseConfigValue(paths, v, k)
x = default(MimeTypes)
for p in paths:
let f = openFileExpand(ctx.config.configdir, p)
if f != nil:
x.parseMimeTypes(f)
proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
k: string) =
var paths: seq[ChaPathResolved]
ctx.parseConfigValue(paths, v, k)
x = default(Mailcap)
for p in paths:
let f = openFileExpand(ctx.config.configdir, p)
if f != nil:
let res = parseMailcap(f)
if res.isSome:
x.add(res.get)
else:
ctx.warnings.add("Error reading mailcap: " & res.error)
template uq(s: string): string =
ChaPath(s).unquote.get
let defaultCmds = {
"gopher": "${%CHA_LIBEXEC_DIR}/gopher2html -u \\$MAILCAP_URL".uq,
"gemini": "${%CHA_LIBEXEC_DIR}/gmi2html".uq,
"markdown": "${%CHA_LIBEXEC_DIR}/md2html".uq,
"x-ansi":"${%CHA_LIBEXEC_DIR}/ansi2html".uq
}
for (subt, cmd) in defaultCmds:
x.add(MailcapEntry(
mt: "text",
subt: subt,
cmd: cmd,
flags: {HTMLOUTPUT}
))
const DefaultURIMethodMap = parseURIMethodMap(staticRead"res/urimethodmap")
proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue;
k: string) =
var paths: seq[ChaPathResolved]
ctx.parseConfigValue(paths, v, k)
x = default(URIMethodMap)
for p in paths:
let f = openFileExpand(ctx.config.configdir, p)
if f != nil:
x.parseURIMethodMap(f.readAll())
x.append(DefaultURIMethodMap)
type ParseConfigResult* = object
success*: bool
warnings*: seq[string]
errorMsg*: string
proc parseConfig(config: Config; dir: string; stream: Stream; name = "<input>";
laxnames = false): ParseConfigResult
proc parseConfig(config: Config; dir: string; t: TomlValue): ParseConfigResult =
var ctx = ConfigParser(config: config, dir: dir)
config.configdir = dir
try:
var myRes = ParseConfigResult(success: true)
ctx.parseConfigValue(config[], t, "")
#TODO: for omnirule/siteconf, check if substitution rules are specified?
while config.`include`.len > 0:
#TODO: warn about recursive includes
var includes = config.`include`
config.`include`.setLen(0)
for s in includes:
let res = config.parseConfig(dir, openFileExpand(dir, s))
if not res.success:
return res
myRes.warnings.add(res.warnings)
myRes.warnings.add(ctx.warnings)
return myRes
except ValueError as e:
return ParseConfigResult(
success: false,
warnings: ctx.warnings,
errorMsg: e.msg
)
proc parseConfig(config: Config; dir: string; stream: Stream; name = "<input>";
laxnames = false): ParseConfigResult =
let toml = parseToml(stream, dir / name, laxnames)
if toml.isOk:
return config.parseConfig(dir, toml.get)
else:
return ParseConfigResult(
success: false,
errorMsg: "Fatal error: failed to parse config\n" & toml.error & '\n'
)
proc parseConfig*(config: Config; dir, s: string; name = "<input>";
laxnames = false): ParseConfigResult =
return config.parseConfig(dir, newStringStream(s), name, laxnames)
const defaultConfig = staticRead"res/config.toml"
proc readConfig(config: Config; dir, name: string): ParseConfigResult =
let fs = if name.len > 0 and name[0] == '/':
newFileStream(name)
else:
newFileStream(dir / name)
if fs != nil:
return config.parseConfig(dir, fs)
return ParseConfigResult(success: true)
proc loadConfig*(config: Config; s: string) {.jsfunc.} =
let s = if s.len > 0 and s[0] == '/':
s
else:
getCurrentDir() / s
if not fileExists(s):
return
discard config.parseConfig(parentDir(s), newFileStream(s))
proc getNormalAction*(config: Config; s: string): string =
return config.page.getOrDefault(s)
proc getLinedAction*(config: Config; s: string): string =
return config.line.getOrDefault(s)
proc readConfig*(pathOverride: Option[string]; jsctx: JSContext): Config =
result = Config(jsctx: jsctx)
discard result.parseConfig("res", newStringStream(defaultConfig)) #TODO TODO TODO
if pathOverride.isNone:
when defined(debug):
discard result.readConfig(getCurrentDir() / "res", "config.toml")
discard result.readConfig(getConfigDir() / "chawan", "config.toml")
else:
discard result.readConfig(getCurrentDir(), pathOverride.get)
proc addConfigModule*(ctx: JSContext) =
ctx.registerType(ActionMap)
ctx.registerType(StartConfig)
ctx.registerType(CSSConfig)
ctx.registerType(SearchConfig)
ctx.registerType(EncodingConfig)
ctx.registerType(ExternalConfig)
ctx.registerType(NetworkConfig)
ctx.registerType(DisplayConfig)
ctx.registerType(Config)