import std/options
import std/os
import std/streams
import std/strutils
import std/tables
import bindings/quickjs
import config/chapath
import config/mailcap
import config/mimetypes
import config/toml
import js/jserror
import js/fromjs
import js/javascript
import js/jscolor
import js/jstypes
import js/jspropenumlist
import js/jsregex
import js/tojs
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
cmMonochrome, cmANSI, cmEightBit, cmTrueColor
FormatMode* = set[FormatFlags]
ImageMode* = enum
imNone = "none", imSixel = "sixel", imKitty = "kitty"
ChaPathResolved* = distinct string
ActionMap = object
t: Table[string, string]
SiteConfig* = object
url*: Option[Regex]
host*: Option[Regex]
rewrite_url*: Option[JSValueFunction]
cookie*: Option[bool]
third_party_cookie*: seq[Regex]
share_cookie_jar*: Option[string]
referer_from*: Option[bool]
scripting*: Option[bool]
document_charset*: seq[Charset]
images*: Option[bool]
stylesheet*: Option[string]
proxy*: Option[URL]
default_headers*: TableRef[string, string]
insecure_ssl_no_verify*: Option[bool]
OmniRule* = object
match*: Regex
substitute_url*: Option[JSValueFunction]
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.}: Option[Charset]
document_charset* {.jsgetset.}: seq[Charset]
CommandConfig = object
jsObj*: JSValue
init*: seq[tuple[k, cmd: string]] # initial k/v map
map*: Table[string, JSValue] # qualified name -> function
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.}: URL
default_headers* {.jsgetset.}: Table[string, string]
DisplayConfig = object
color_mode* {.jsgetset.}: Option[ColorMode]
format_mode* {.jsgetset.}: Option[FormatMode]
no_format_mode* {.jsgetset.}: FormatMode
image_mode* {.jsgetset.}: Option[ImageMode]
alt_screen* {.jsgetset.}: Option[bool]
highlight_color* {.jsgetset.}: ARGBColor
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.}: Option[RGBColor]
default_foreground_color* {.jsgetset.}: Option[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
jsvfns*: seq[JSValueFunction]
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]
cmd*: CommandConfig
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): Option[string] {.jsgetprop.} =
a.t.withValue(s, p):
return some(p[])
return none(string)
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[]
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 Option[ColorMode];
v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Option[FormatMode];
v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var ARGBColor; v: TomlValue;
k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var RGBColor; 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[U; V](ctx: var ConfigParser; x: var TableRef[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(ctx: var ConfigParser; x: var JSValueFunction;
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 parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue;
k: string)
proc typeCheck(v: TomlValue; t: TomlValueType; k: string) =
if v.t != t:
raise newException(ValueError, "invalid type for key " & k &
" (got " & $v.t & ", expected " & $t & ")")
proc typeCheck(v: TomlValue; t: set[TomlValueType]; k: string) =
if v.t notin t:
raise newException(ValueError, "invalid type for key " & k &
" (got " & $v.t & ", expected " & $t & ")")
proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
k: string) =
typeCheck(v, tvtTable, k)
for fk, fv in x.fieldPairs:
when typeof(fv) isnot JSContext|seq[JSValueFunction]:
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, tvtTable, 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[U, V](ctx: var ConfigParser; x: var TableRef[U, V];
v: TomlValue; k: string) =
typeCheck(v, tvtTable, k)
x = TableRef[U, V]()
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, tvtBoolean, k)
x = v.b
proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
k: string) =
typeCheck(v, tvtString, k)
x = v.s
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPath;
v: TomlValue; k: string) =
typeCheck(v, tvtString, k)
x = ChaPath(v.s)
proc parseConfigValue[T](ctx: var ConfigParser; x: var seq[T]; v: TomlValue;
k: string) =
typeCheck(v, {tvtString, tvtArray}, k)
if v.t != tvtArray:
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, {tvtTable}, k)
x = v.tab
proc parseConfigValue(ctx: var ConfigParser; x: var Charset; v: TomlValue;
k: string) =
typeCheck(v, tvtString, 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, tvtInteger, k)
x = int32(v.i)
proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
k: string) =
typeCheck(v, tvtInteger, k)
x = v.i
proc parseConfigValue(ctx: var ConfigParser; x: var Option[ColorMode];
v: TomlValue; k: string) =
typeCheck(v, tvtString, k)
case v.s
of "auto": x = none(ColorMode)
of "monochrome": x = some(cmMonochrome)
of "ansi": x = some(cmANSI)
of "8bit", "eight-bit": x = some(cmEightBit)
of "24bit", "true-color": x = some(cmTrueColor)
else:
raise newException(ValueError, "unknown color mode '" & v.s &
"' for key " & k)
proc parseConfigValue(ctx: var ConfigParser; x: var Option[FormatMode];
v: TomlValue; k: string) =
typeCheck(v, {tvtString, tvtArray}, k)
if v.t == tvtString and v.s == "auto":
x = none(FormatMode)
else:
var y: FormatMode
ctx.parseConfigValue(y, v, k)
x = some(y)
proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
k: string) =
typeCheck(v, tvtArray, k)
for i in 0 ..< v.a.len:
let kk = k & "[" & $i & "]"
let vv = v.a[i]
typeCheck(vv, tvtString, kk)
case vv.s
of "bold": x.incl(ffBold)
of "italic": x.incl(ffItalic)
of "underline": x.incl(ffUnderline)
of "reverse": x.incl(ffReverse)
of "strike": x.incl(ffStrike)
of "overline": x.incl(ffOverline)
of "blink": x.incl(ffBlink)
else:
raise newException(ValueError, "unknown format mode '" & vv.s &
"' for key " & kk)
proc parseConfigValue(ctx: var ConfigParser; x: var ARGBColor; v: TomlValue;
k: string) =
typeCheck(v, tvtString, k)
let c = parseARGBColor(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, tvtString, 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 Option[T]; v: TomlValue;
k: string) =
if v.t == tvtString and v.s == "auto":
x = none(T)
else:
var y: T
ctx.parseConfigValue(y, v, k)
x = some(y)
proc parseConfigValue(ctx: var ConfigParser; x: var ActionMap; v: TomlValue;
k: string) =
typeCheck(v, tvtTable, k)
for kk, vv in v:
typeCheck(vv, tvtString, 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, tvtString, 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, {tvtString, tvtArray}, k)
if v.t == tvtString:
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, tvtTable, k)
for kk, vv in v:
let kkk = if k != "":
k & "." & kk
else:
kk
case kk
of "include":
typeCheck(vv, {tvtString, tvtArray}, kkk)
case vv.t
of tvtString:
x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
of tvtArray:
for child in vv.a:
x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
else: discard
of "inline":
typeCheck(vv, tvtString, kkk)
x.stylesheet &= vv.s
proc parseConfigValue(ctx: var ConfigParser; x: var Regex; v: TomlValue;
k: string) =
typeCheck(v, tvtString, 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, tvtString, k)
let y = parseURL(v.s)
if y.isNone:
raise newException(ValueError, "invalid URL " & k)
x = y.get
proc parseConfigValue(ctx: var ConfigParser; x: var JSValueFunction;
v: TomlValue; k: string) =
typeCheck(v, tvtString, k)
let fun = ctx.config.jsctx.eval(v.s, "<config>", JS_EVAL_TYPE_GLOBAL)
if JS_IsException(fun):
raise newException(ValueError, "exception in " & k & ": " &
ctx.config.jsctx.getExceptionMsg())
if not JS_IsFunction(ctx.config.jsctx, fun):
raise newException(ValueError, k & " is not a function")
x = JSValueFunction(fun: fun)
ctx.config.jsvfns.add(x) # so we can clean it up on exit
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
v: TomlValue; k: string) =
typeCheck(v, tvtString, k)
let y = ChaPath(v.s).unquote()
if y.isNone:
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)
const DefaultMailcap = block:
let ss = newStringStream(staticRead"res/mailcap")
parseMailcap(ss).get
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)
x.add(DefaultMailcap)
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)
func isCompatibleIdent(s: string): bool =
if s.len == 0 or s[0] notin AsciiAlpha + {'_', '$'}:
return false
for i in 1 ..< s.len:
if s[i] notin AsciiAlphaNumeric + {'_', '$'}:
return false
return true
proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue;
k: string) =
typeCheck(v, tvtTable, k)
for kk, vv in v:
let kkk = k & "." & kk
typeCheck(vv, {tvtTable, tvtString}, kkk)
if not kk.isCompatibleIdent():
raise newException(ValueError, "invalid command name: " & kkk)
if vv.t == tvtTable:
ctx.parseConfigValue(x, vv, kkk)
else: # tvtString
x.init.add((kkk, vv.s))
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.isSome:
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)
type ReadConfigResult = tuple
config: Config
res: ParseConfigResult
proc readConfig*(pathOverride: Option[string]; jsctx: JSContext):
ReadConfigResult =
let config = Config(jsctx: jsctx)
var res = config.parseConfig("res", newStringStream(defaultConfig))
if not res.success:
return (nil, res)
if pathOverride.isNone:
when defined(debug):
res = config.readConfig(getCurrentDir() / "res", "config.toml")
if not res.success:
return (nil, res)
res = config.readConfig(getConfigDir() / "chawan", "config.toml")
else:
res = config.readConfig(getCurrentDir(), pathOverride.get)
if not res.success:
return (nil, res)
return (config, res)
# called after parseConfig returns
proc initCommands*(config: Config): Err[string] =
let ctx = config.jsctx
let obj = JS_NewObject(ctx)
defer: JS_FreeValue(ctx, obj)
if JS_IsException(obj):
return err(ctx.getExceptionMsg())
for i in countdown(config.cmd.init.high, 0):
let (k, cmd) = config.cmd.init[i]
if k in config.cmd.map:
# already in map; skip
continue
var objIt = JS_DupValue(ctx, obj)
let name = k.afterLast('.')
if name.len < k.len:
for ss in k.substr(0, k.high - name.len - 1).split('.'):
var prop = JS_GetPropertyStr(ctx, objIt, cstring(ss))
if JS_IsUndefined(prop):
prop = JS_NewObject(ctx)
ctx.definePropertyE(objIt, ss, JS_DupValue(ctx, prop))
if JS_IsException(prop):
return err(ctx.getExceptionMsg())
JS_FreeValue(ctx, objIt)
objIt = prop
if cmd == "":
config.cmd.map[k] = JS_UNDEFINED
continue
let fun = ctx.eval(cmd, "<" & k & ">", JS_EVAL_TYPE_GLOBAL)
if JS_IsException(fun):
return err(ctx.getExceptionMsg())
if not JS_IsFunction(ctx, fun):
JS_FreeValue(ctx, fun)
return err(k & " is not a function")
ctx.definePropertyE(objIt, name, JS_DupValue(ctx, fun))
config.cmd.map[k] = fun
JS_FreeValue(ctx, objIt)
config.cmd.jsObj = JS_DupValue(ctx, obj)
config.cmd.init = @[]
ok()
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)