1414import std/ [os, parsecfg, sequtils, streams, strformat, strutils, tables, terminal, uri]
1515when defined (posix):
1616 import std/ posix except SocketHandle
17- import types, prompts, util, auth_openai, modelname
17+ import types, prompts, util, auth_openai, modelname, minline
1818
1919type
2020 ProviderRec * = object
@@ -101,6 +101,10 @@ var activeSearchEngine*: string = "exa"
101101 # # used as-is, and a missing key is an error only if that engine needs one
102102 # # (brave does; exa and parallel are keyless by default).
103103
104+ var activeShortcuts* : Table [string , string ]
105+ # # [shortcuts] map loaded from the active config, exposed so minline can
106+ # # apply the user's bindings at startup and after config reloads.
107+
104108var bashPathOverride* : string
105109 # # Windows-only. `[settings]` `bash_path = "..."` overrides MSYS2
106110 # # detection in `streamexec.resolveBash` for users with bash at a
282286 # # can report `path:line:` for a bad section/key/value without re-reading.
283287
284288const
285- PermittedSections = [" settings" , " search" , " colors" , " provider" ]
289+ PermittedSections = [" settings" , " search" , " colors" , " provider" , " shortcuts " ]
286290 SettingsKeys = [" current" , " notify" , " streaming" , " sandbox" ,
287291 " sandbox_enabled" , " patient_retry" , " patient-retry" ,
288292 " sandbox_wall_warn" ,
@@ -307,6 +311,7 @@ proc permittedKey(section, key: string): bool =
307311 let base = if key.endsWith (" -light" ): key[0 ..< key.len - 6 ] else : key
308312 base in ColorKeys
309313 of " provider" : key in ProviderKeys
314+ of " shortcuts" : key in minline.ShortcutNames
310315 else : false
311316
312317proc validateConfig * (path: string ; entries: seq [RawEntry ]): string =
@@ -325,8 +330,10 @@ proc validateConfig*(path: string; entries: seq[RawEntry]): string =
325330 if ent.value.strip == " " :
326331 # Empty key is fine for auth=oauth providers (and is also a structural
327332 # gap loadProfile diagnoses). Empty current/url/models likewise.
333+ # Empty [shortcuts] values are explicit unbinds, so they are allowed.
328334 if not (ent.section == " provider" and ent.key == " key" ) and
329- not (ent.section == " settings" and ent.key == " current" ):
335+ not (ent.section == " settings" and ent.key == " current" ) and
336+ not (ent.section == " shortcuts" ):
330337 return & " { path} :{ ent.line} : empty value for '{ ent.key} ' in [{ ent.section} ]"
331338 case ent.section
332339 of " search" :
@@ -345,12 +352,18 @@ proc validateConfig*(path: string; entries: seq[RawEntry]): string =
345352 ent.value.strip.toLowerAscii notin ColorModes :
346353 return & " { path} :{ ent.line} : unknown tone '{ ent.value} ' " &
347354 " (expected one of: auto, dark, light)"
355+ of " shortcuts" :
356+ if ent.value.strip != " " :
357+ try :
358+ discard minline.parseShortcutSpec (ent.value)
359+ except ValueError :
360+ return & " { path} :{ ent.line} : invalid shortcut value '{ ent.value} ' for '{ ent.key} '"
348361 else : discard
349362 " "
350363
351- proc parseConfigFile * (path: string ): (string , seq [ProviderRec ], Table [string , string ], Table [string , string ], string ) =
364+ proc parseConfigFile * (path: string ): (string , seq [ProviderRec ], Table [string , string ], Table [string , string ], string , Table [ string , string ] ) =
352365 # # Streaming parse so that repeated [provider] sections accumulate as a list.
353- # # Returns `(current, providers, colors, searchKeys, searchEngine)`.
366+ # # Returns `(current, providers, colors, searchKeys, searchEngine, shortcuts )`.
354367 # # `searchKeys` maps engine name -> key for each `[search] exa-key` /
355368 # # `brave-key` set (empty table when none). A legacy bare `[search] key`
356369 # # is accepted and filed under the active engine for backward compat.
@@ -363,6 +376,7 @@ proc parseConfigFile*(path: string): (string, seq[ProviderRec], Table[string, st
363376 var searchEngine = " "
364377 var providers: seq [ProviderRec ]
365378 var colors: Table [string , string ]
379+ var shortcuts: Table [string , string ]
366380 var section = " "
367381 var prov: ProviderRec
368382 var inProvider = false
@@ -480,13 +494,15 @@ proc parseConfigFile*(path: string): (string, seq[ProviderRec], Table[string, st
480494 of " reasonings" : prov.reasonings = splitModels (v).mapIt (it.toLowerAscii)
481495 of " auth" : prov.auth = v.strip.toLowerAscii
482496 else : discard
497+ of " shortcuts" :
498+ shortcuts[e.key] = v
483499 else : discard
484500 of cfgError:
485501 die & " { path} : { e.msg} " , ExitConfig
486502 p.close
487503 let verr = validateConfig (path, entries)
488504 if verr != " " : die verr, ExitConfig
489- (current, providers, colors, searchKeys, searchEngine)
505+ (current, providers, colors, searchKeys, searchEngine, shortcuts )
490506
491507func quoteVal (s: string ): string =
492508 result = " \" "
@@ -532,6 +548,10 @@ proc writeConfigFile*(path: string, current: string,
532548 if colorModePref != cmAuto:
533549 let label = if colorModePref == cmDark: " dark" else : " light"
534550 buf.add " tone = " & quoteVal (label) & " \n "
551+ if activeShortcuts.len > 0 :
552+ buf.add " \n [shortcuts]\n "
553+ for cmd, spec in activeShortcuts:
554+ buf.add cmd & " = " & quoteVal (spec) & " \n "
535555 for pr in providers:
536556 buf.add " \n [provider]\n "
537557 buf.add " name = " & quoteVal (pr.name) & " \n "
@@ -565,14 +585,16 @@ proc resolveSearchKey*(engine: string; keys: Table[string, string]): string =
565585
566586proc loadStateOrEmpty * (path: string ): (string , seq [ProviderRec ], Table [string , string ]) =
567587 # # Returns `(current, providers, colors)` and updates `activeSearchKey` /
568- # # `activeSearchEngine` / `activeSearchKeys` as a side effect when the config
569- # # sets them. `colors` is the flat `[colors]` map for the caller to route
570- # # through the cascade. Missing file is benign.
588+ # # `activeSearchEngine` / `activeSearchKeys` / `activeShortcuts` as a side
589+ # # effect when the config sets them. `colors` is the flat `[colors]` map
590+ # # for the caller to route through the cascade. Missing file is benign.
571591 if not fileExists (path): return (" " , @ [], initTable [string , string ]())
572- let (current, providers, colors, searchKeys, searchEngine) = parseConfigFile (path)
592+ let (current, providers, colors, searchKeys, searchEngine, shortcuts ) = parseConfigFile (path)
573593 if searchEngine != " " : activeSearchEngine = searchEngine
574594 activeSearchKeys = searchKeys
575595 activeSearchKey = resolveSearchKey (activeSearchEngine, activeSearchKeys)
596+ activeShortcuts = shortcuts
597+ minline.configuredShortcuts = activeShortcuts
576598 (current, providers, colors)
577599
578600proc resolveFamily * (prov: ProviderRec , prof: Profile ): string =
@@ -657,10 +679,12 @@ proc loadProfile*(wanted: string): Profile =
657679 stderr.writeLine " "
658680 stderr.writeLine ConfigExample
659681 quit ExitConfig
660- let (current, providers, _, searchKeys, searchEngine) = parseConfigFile (path)
682+ let (current, providers, _, searchKeys, searchEngine, shortcuts ) = parseConfigFile (path)
661683 if searchEngine != " " : activeSearchEngine = searchEngine
662684 activeSearchKeys = searchKeys
663685 activeSearchKey = resolveSearchKey (activeSearchEngine, activeSearchKeys)
686+ activeShortcuts = shortcuts
687+ minline.configuredShortcuts = activeShortcuts
664688 if providers.len == 0 :
665689 die & " no [provider] section in { path} " , ExitConfig
666690 var pick = wanted
0 commit comments