about summary refs log blame commit diff stats
path: root/src/config/config.nim
blob: 79ef263b3f61a51fcb7fc0900104843bce22b91c (plain) (tree)
1
2
3
4
5
6
7
8
9



                  
 
                     

                       
                  
                   
               
                    
                          
               
                     
                    
                 
                  
                   
                
                    
                         
                
                      
                   
 
                       
 
    

                                           
 
                                
 

                            
 
                           



                            
                                   


                                 
                                  
                     
                           
                      


                         
                          

                      

                     
                                               
                      
                                   


                                  
                                   
                      
                            
                    


                    
                                                        
 
                      


                                        

                    
                                    

                       
                            
                                         

                         

                                                

                         
                                 
                                



                                            
                                      
 


                                         
                        

                                     
                                        

                                                        

                        
                                            

                                              


                                            
                                       



                                              


                                                         
 

                         
                               
                                     





                                       
                                 

                                     
                                   
                                 

                              


                        
                       
                                   
                    
                           
                 
                               
                         
                        




                            
                       






                            

                    





                                                                              

                                    



                                                                     











                                                           
                                                              




                       







                                                                





                                                                   

                                                               
 









                                                           


                                                   

                                                                            
                                                                      

                                                                     

                                   
                                       

                  
                      
                         
                               
                         
                       
                   
                         




                               
                     
                                 
                                                   

                                                                
     
   
 

                                                                        

                          
                              

                                            

                                            

                     
                                                   
                        
                                                     
                                      
                                                              


                                                            
                                                            




                                                                  


                                                                     
                              
                                          


                        
                                                                                
                                                                   
                    
 
                                      
                   
                 
              




                  
                
                  
                                
              
                                   
                 
                                   
                 


                                
                      
              


                      
         

                     



                                  
           
                  

                  

                

                 

              




                                                             
                   
              

                              
       
                                    

                                                    
                                            







                                                                              
                                                                                  
                                            

                                                             


                                                        










                                        

















                           
               
                             


                          

                          



















                                                                       
 
                                                                           
 

                                                                       
                                



                                                 
                                          


                                                             


                                                 
                          
                        


                                                           



                                                                               








                                                        


                                                             
                                                              



                                                                

                                                                      

                                                                 
                                                               
                                                                

                                                                
                                                                        
                                                                





















                                                                








                                                                          







                                                               



                                                                




                                                                  
            
       

                 



















                                                                             
                                                                       

                               


                                   

                                            


                                                                 
 
                                                                        

                                              
           


                             
           



                                                                   
                               


                                   







                                          
                                                                     





                                                                  

                                                            

           



                                                                 

                                                            

           
                                                                  
                                            
           
       
            
                             
           
 



                                                                  





                                                     
 




















                                                                              









                                                                  
                 














                                                             
                                 
                                        

                                   

                      
                                                            
           
                                                       
                        
                                                                          
 


                                                                               


                                     



                                                
                                                           
                                     

           


                                                                           
 
                                    
                       
                                                                       
                 
 
                                        
 
                                                    



                                              
               
                               
 
                                                          
                                    
 
                                                         
                                    
 
                                                        
                   
                          





                                                               

                                       
                             






                                  
                          
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 io/urlfilter
import js/error
import js/javascript
import js/propertyenumlist
import js/regex
import loader/headers
import loader/loader
import types/cell
import types/color
import types/cookie
import types/opt
import types/referer
import types/urimethodmap
import types/url
import utils/mimeguess
import utils/twtstr

import chagashi/charset

type
  ColorMode* = enum
    MONOCHROME, ANSI, EIGHT_BIT, TRUE_COLOR

  FormatMode* = set[FormatFlags]

  ActionMap = object
    t: Table[string, string]

  StaticSiteConfig = object
    url: Opt[string]
    host: Opt[string]
    rewrite_url: Opt[string]
    cookie: Opt[bool]
    third_party_cookie: seq[string]
    share_cookie_jar: Opt[string]
    referer_from*: Opt[bool]
    scripting: Opt[bool]
    document_charset: seq[Charset]
    images: Opt[bool]
    stylesheet: Opt[string]
    proxy: Opt[string]

  StaticOmniRule = object
    match: string
    substitute_url: 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

  CSSConfig = object
    stylesheet* {.jsgetset.}: string

  SearchConfig = object
    wrap* {.jsgetset.}: bool
    default_flags* {.jsgetset.}: LREFlags

  EncodingConfig = object
    display_charset* {.jsgetset.}: Opt[Charset]
    document_charset* {.jsgetset.}: seq[Charset]

  ExternalConfig = object
    tmpdir* {.jsgetset.}: ChaPath
    editor* {.jsgetset.}: string
    mailcap* {.jsgetset.}: seq[ChaPath]
    mime_types* {.jsgetset.}: seq[ChaPath]
    cgi_dir* {.jsgetset.}: seq[ChaPath]
    urimethodmap* {.jsgetset.}: seq[ChaPath]
    w3m_cgi_compat* {.jsgetset.}: bool

  InputConfig = object
    vi_numeric_prefix* {.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

  Config* = ref ConfigObj
  ConfigObj* = object
    configdir {.jsget.}: string
    `include` {.jsget.}: seq[ChaPath]
    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[StaticSiteConfig]
    omnirule: seq[StaticOmniRule]
    page* {.jsget.}: ActionMap
    line* {.jsget.}: ActionMap

  BufferConfig* = object
    userstyle*: string
    referer_from*: bool
    referrerpolicy*: ReferrerPolicy
    scripting*: bool
    charsets*: seq[Charset]
    images*: bool
    loaderConfig*: LoaderConfig
    mimeTypes*: MimeTypes
    cgiDir*: seq[string]

  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)

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 = b in a.t
proc getOrDefault(a: ActionMap, b: string): string = a.t.getOrDefault(b)
proc hasKeyOrPut(a: var ActionMap, b, c: string): bool = a.t.hasKeyOrPut(b, c)

func getRealKey(key: string): string

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(Defect, "Invalid proxy URL: " & s)
  return nil

func getDefaultHeaders*(config: Config): Headers =
  return newHeaders(config.network.default_headers)

proc getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar,
    headers: Headers, referer_from, scripting: bool, charsets: seq[Charset],
    images: bool, userstyle: string, proxy: URL, mimeTypes: MimeTypes,
    urimethodmap: URIMethodMap, cgiDir: seq[string], tmpdir: string):
    BufferConfig =
  let filter = newURLFilter(
    scheme = some(location.scheme),
    allowschemes = @["data", "stream"],
    default = true
  )
  return BufferConfig(
    userstyle: userstyle,
    referer_from: referer_from,
    scripting: scripting,
    charsets: charsets,
    images: images,
    mimeTypes: mimeTypes,
    loaderConfig: LoaderConfig(
      defaultHeaders: headers,
      filter: filter,
      cookiejar: cookiejar,
      proxy: proxy,
      cgiDir: cgiDir,
      urimethodmap: urimethodmap,
      w3mCGICompat: config.external.w3m_cgi_compat,
      libexecPath: ChaPath("${%CHA_LIBEXEC_DIR}").unquote().get,
      tmpdir: tmpdir
    )
  )

proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] =
  for sc in config.siteconf:
    var conf = SiteConfig(
      cookie: sc.cookie,
      scripting: sc.scripting,
      share_cookie_jar: sc.share_cookie_jar,
      referer_from: sc.referer_from,
      document_charset: sc.document_charset,
      images: sc.images
    )
    if sc.url.isSome:
      conf.url = opt(compileMatchRegex(sc.url.get))
    elif sc.host.isSome:
      conf.host = opt(compileMatchRegex(sc.host.get))
    for rule in sc.third_party_cookie:
      conf.third_party_cookie.add(compileMatchRegex(rule).get)
    if sc.rewrite_url.isSome:
      let fun = jsctx.eval(sc.rewrite_url.get, "<siteconf>",
        JS_EVAL_TYPE_GLOBAL)
      conf.rewrite_url = getJSFunction[URL, URL](jsctx, fun)
    if sc.proxy.isSome:
      let x = parseURL(sc.proxy.get)
      if x.isNone:
        raise newException(Defect, "invalid URL: " & sc.proxy.get)
      conf.proxy = opt(x.get)
    result.add(conf)

proc getOmniRules*(config: Config, jsctx: JSContext): seq[OmniRule] =
  for rule in config.omnirule:
    let re = compileMatchRegex(rule.match)
    var conf = OmniRule(
      match: re.get
    )
    let fun = jsctx.eval(rule.substitute_url, "<siteconf>", JS_EVAL_TYPE_GLOBAL)
    conf.substitute_url = getJSFunction[string, string](jsctx, fun)
    result.add(conf)

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 openFileExpand(dir: string, file: ChaPath): FileStream =
  let file0 = file.unquote()
  if file0.isNone:
    raise newException(ValueError, file0.error)
  let file = file0.get
  if file.len == 0:
    return nil
  if file[0] == '/':
    return newFileStream(file)
  else:
    return newFileStream(dir / file)

proc readUserStylesheet(dir, file: string): string =
  let s = openFileExpand(dir, ChaPath(file))
  if s != nil:
    result = s.readAll()
    s.close()

# The overall configuration will be obtained through the virtual concatenation
# of several individual configuration files known as mailcap files.
proc getMailcap*(config: Config): tuple[mailcap: Mailcap, errs: seq[string]] =
  let configDir = getConfigDir() / "chawan" #TODO store this in config?
  const gopherPath0 = ChaPath("${%CHA_LIBEXEC_DIR}/gopher2html -u \\$MAILCAP_URL")
  let gopherPath = gopherPath0.unquote().get
  const geminiPath0 = ChaPath("${%CHA_LIBEXEC_DIR}/gmi2html")
  let geminiPath = geminiPath0.unquote().get
  const mdPath0 = ChaPath("${%CHA_LIBEXEC_DIR}/md2html")
  let mdPath = mdPath0.unquote().get
  var mailcap: Mailcap = @[]
  var errs: seq[string]
  var found = false
  for p in config.external.mailcap:
    let f = openFileExpand(configDir, p)
    if f != nil:
      let res = parseMailcap(f)
      if res.isSome:
        mailcap.add(res.get)
      else:
        errs.add(res.error)
      found = true
  mailcap.add(MailcapEntry(
      mt: "text",
      subt: "gopher",
      cmd: gopherPath,
      flags: {HTMLOUTPUT}
  ))
  mailcap.add(MailcapEntry(
    mt: "text",
    subt: "gemini",
    cmd: geminiPath,
    flags: {HTMLOUTPUT}
  ))
  mailcap.add(MailcapEntry(
    mt: "text",
    subt: "markdown",
    cmd: mdPath,
    flags: {HTMLOUTPUT}
  ))
  if not found:
    mailcap.add(MailcapEntry(
      mt: "*",
      subt: "*",
      cmd: "xdg-open '%s'"
    ))
    return (mailcap, errs)
  return (mailcap, errs)

# We try to source mime types declared in config.
# If none of these files can be found, fall back to DefaultGuess.
#TODO some error handling would be nice, to at least show a warning to
# the user. Not sure how this could be done, though.
proc getMimeTypes*(config: Config): MimeTypes =
  if config.external.mime_types.len == 0:
    return DefaultGuess
  var mimeTypes: MimeTypes
  let configDir = getConfigDir() / "chawan" #TODO store this in config?
  var found = false
  for p in config.external.mime_types:
    let f = openFileExpand(configDir, p)
    if f != nil:
      mimeTypes.parseMimeTypes(f)
      found = true
  if not found:
    return DefaultGuess
  return mimeTypes

const DefaultURIMethodMap = parseURIMethodMap(staticRead"res/urimethodmap")

proc getURIMethodMap*(config: Config): URIMethodMap =
  let configDir = getConfigDir() / "chawan" #TODO store this in config?
  var urimethodmap: URIMethodMap
  for p in config.external.urimethodmap:
    let f = openFileExpand(configDir, p)
    if f != nil:
      urimethodmap.parseURIMethodMap(f.readAll())
  urimethodmap.append(DefaultURIMethodMap)
  return urimethodmap

proc getForkServerConfig*(config: Config): ForkServerConfig =
  let tmpdir0 = config.external.tmpdir.unquote()
  if tmpdir0.isNone:
    raise newException(ValueError, tmpdir0.error)
  return ForkServerConfig(
    tmpdir: tmpdir0.get,
    ambiguous_double: config.display.double_width_ambiguous
  )

proc parseConfig(config: Config, dir: string, stream: Stream, name = "<input>",
  laxnames = false)
proc parseConfig*(config: Config, dir: string, s: string, name = "<input>",
  laxnames = false)

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
  config.parseConfig(parentDir(s), newFileStream(s))

proc parseConfigValue(x: var object, v: TomlValue, k: string)
proc parseConfigValue(x: var bool, v: TomlValue, k: string)
proc parseConfigValue(x: var string, v: TomlValue, k: string)
proc parseConfigValue(x: var ChaPath, v: TomlValue, k: string)
proc parseConfigValue[T](x: var seq[T], v: TomlValue, k: string)
proc parseConfigValue(x: var Charset, v: TomlValue, k: string)
proc parseConfigValue(x: var int32, v: TomlValue, k: string)
proc parseConfigValue(x: var int64, v: TomlValue, k: string)
proc parseConfigValue(x: var Opt[ColorMode], v: TomlValue, k: string)
proc parseConfigValue(x: var Opt[FormatMode], v: TomlValue, k: string)
proc parseConfigValue(x: var FormatMode, v: TomlValue, k: string)
proc parseConfigValue(x: var RGBAColor, v: TomlValue, k: string)
proc parseConfigValue(x: var RGBColor, v: TomlValue, k: string)
proc parseConfigValue[T](x: var Opt[T], v: TomlValue, k: string)
proc parseConfigValue(x: var ActionMap, v: TomlValue, k: string)
proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string)
proc parseConfigValue[U, V](x: var Table[U, V], v: TomlValue, k: string)
proc parseConfigValue[T](x: var set[T], 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(x: var object, v: TomlValue, k: string) =
  typeCheck(v, VALUE_TABLE, k)
  for fk, fv in x.fieldPairs:
    let kebabk = snakeToKebabCase(fk)
    if kebabk in v:
      let kkk = if k != "":
        k & "." & fk
      else:
        fk
      parseConfigValue(fv, v[kebabk], kkk)

proc parseConfigValue[U, V](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 & "]"
    parseConfigValue(y, vv, kkk)
    x[kk] = y

proc parseConfigValue(x: var bool, v: TomlValue, k: string) =
  typeCheck(v, VALUE_BOOLEAN, k)
  x = v.b

proc parseConfigValue(x: var string, v: TomlValue, k: string) =
  typeCheck(v, VALUE_STRING, k)
  x = v.s

proc parseConfigValue(x: var ChaPath, v: TomlValue, k: string) =
  typeCheck(v, VALUE_STRING, k)
  x = ChaPath(v.s)

proc parseConfigValue[T](x: var seq[T], v: TomlValue, k: string) =
  typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k)
  if v.vt != VALUE_ARRAY:
    var y: T
    parseConfigValue(y, v, k)
    x = @[y]
  else:
    if not v.ad:
      x.setLen(0)
    for i in 0 ..< v.a.len:
      var y: T
      parseConfigValue(y, v.a[i], k & "[" & $i & "]")
      x.add(y)

proc parseConfigValue(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(x: var int32, v: TomlValue, k: string) =
  typeCheck(v, VALUE_INTEGER, k)
  x = int32(v.i)

proc parseConfigValue(x: var int64, v: TomlValue, k: string) =
  typeCheck(v, VALUE_INTEGER, k)
  x = v.i

proc parseConfigValue(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(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
    parseConfigValue(y, v, k)
    x.ok(y)

proc parseConfigValue(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(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(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](x: var Opt[T], v: TomlValue, k: string) =
  if v.vt == VALUE_STRING and v.s == "auto":
    x.err()
  else:
    var y: T
    parseConfigValue(y, v, k)
    x.ok(y)

proc parseConfigValue(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](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](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)

var gdir {.compileTime.}: string
proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string) =
  typeCheck(v, VALUE_TABLE, k)
  let dir = gdir
  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(dir, vv.s)
      of VALUE_ARRAY:
        for child in vv.a:
          x.stylesheet &= readUserStylesheet(dir, vv.s)
      else: discard
    of "inline":
      typeCheck(vv, VALUE_STRING, kkk)
      x.stylesheet &= vv.s

proc parseConfig(config: Config, dir: string, t: TomlValue) =
  gdir = dir
  parseConfigValue(config[], t, "")
  while config.`include`.len > 0:
    #TODO: warn about recursive includes
    var includes = config.`include`
    config.`include`.setLen(0)
    for s in includes:
      when nimvm:
        config.parseConfig(dir, staticRead(dir / string(s)))
      else:
        config.parseConfig(dir, openFileExpand(dir, s))
  config.configdir = dir
  #TODO: for omnirule/siteconf, check if substitution rules are specified?

proc parseConfig(config: Config, dir: string, stream: Stream, name = "<input>",
    laxnames = false) =
  let toml = parseToml(stream, dir / name, laxnames)
  if toml.isOk:
    config.parseConfig(dir, toml.get)
  else:
    when nimvm:
      echo "Fatal error: Failed to parse config"
      echo toml.error
    else:
      stderr.write("Fatal error: Failed to parse config\n")
      stderr.write(toml.error & '\n')
    quit(1)

proc parseConfig*(config: Config, dir: string, s: string, name = "<input>",
    laxnames = false) =
  config.parseConfig(dir, newStringStream(s), name, laxnames)

proc staticReadConfig(): ConfigObj =
  var config = Config()
  config.parseConfig("res", staticRead"res/config.toml", "config.toml")
  return config[]

const defaultConfig = staticReadConfig()

proc readConfig(config: Config, dir, name: string) =
  let fs = if name.len > 0 and name[0] == '/':
    newFileStream(name)
  else:
    newFileStream(dir / name)
  if fs != nil:
    config.parseConfig(dir, fs)

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]): Config =
  result = Config()
  result[] = defaultConfig
  if pathOverride.isNone:
    when defined(debug):
      result.readConfig(getCurrentDir() / "res", "config.toml")
    result.readConfig(getConfigDir() / "chawan", "config.toml")
  else:
    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)