feat: add fullscreen TUI with illwill
- New screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help - AI configuration persisted in ~/.config/cljnim/config.json - Navigate with arrow keys, Enter, Escape - illwill added as dependency in cljnim.nimble
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# TUI Configuration Manager
|
||||
# Handles persistent config in ~/.config/cljnim/config.json
|
||||
|
||||
import std/[json, os, strutils]
|
||||
import ai_assist
|
||||
|
||||
type
|
||||
TuiConfig* = object
|
||||
provider*: string # "deepseek", "openai", "mimo", "custom"
|
||||
model*: string
|
||||
baseUrl*: string
|
||||
apiKey*: string
|
||||
timeoutMs*: int
|
||||
lastFile*: string # last used .clj file
|
||||
|
||||
const
|
||||
DefaultProviders = {
|
||||
"deepseek": ("https://api.deepseek.com", "deepseek-chat"),
|
||||
"openai": ("https://api.openai.com", "gpt-4o-mini"),
|
||||
"mimo": ("https://api.mi-mo.ai", "mimo-chat"),
|
||||
"custom": ("", "")
|
||||
}
|
||||
|
||||
proc configDir(): string =
|
||||
getHomeDir() / ".config" / "cljnim"
|
||||
|
||||
proc configPath(): string =
|
||||
configDir() / "config.json"
|
||||
|
||||
proc defaultConfig*(): TuiConfig =
|
||||
result.provider = "deepseek"
|
||||
result.model = "deepseek-chat"
|
||||
result.baseUrl = "https://api.deepseek.com"
|
||||
result.apiKey = ""
|
||||
result.timeoutMs = 15000
|
||||
result.lastFile = ""
|
||||
|
||||
proc loadConfig*(): TuiConfig =
|
||||
let path = configPath()
|
||||
if not fileExists(path):
|
||||
return defaultConfig()
|
||||
try:
|
||||
let jsonStr = readFile(path)
|
||||
let j = parseJson(jsonStr)
|
||||
result.provider = j{"provider"}.getStr("deepseek")
|
||||
result.model = j{"model"}.getStr("deepseek-chat")
|
||||
result.baseUrl = j{"baseUrl"}.getStr("https://api.deepseek.com")
|
||||
result.apiKey = j{"apiKey"}.getStr("")
|
||||
result.timeoutMs = j{"timeoutMs"}.getInt(15000)
|
||||
result.lastFile = j{"lastFile"}.getStr("")
|
||||
except:
|
||||
result = defaultConfig()
|
||||
|
||||
proc saveConfig*(cfg: TuiConfig) =
|
||||
let dir = configDir()
|
||||
if not dirExists(dir):
|
||||
createDir(dir)
|
||||
let j = %*{
|
||||
"provider": cfg.provider,
|
||||
"model": cfg.model,
|
||||
"baseUrl": cfg.baseUrl,
|
||||
"apiKey": cfg.apiKey,
|
||||
"timeoutMs": cfg.timeoutMs,
|
||||
"lastFile": cfg.lastFile
|
||||
}
|
||||
writeFile(configPath(), pretty(j))
|
||||
|
||||
proc toAiConfig*(cfg: TuiConfig): AiConfig =
|
||||
## Convert TUI config to AiConfig for ai_assist module
|
||||
result.apiKey = cfg.apiKey
|
||||
result.baseUrl = cfg.baseUrl
|
||||
result.model = cfg.model
|
||||
result.timeoutMs = cfg.timeoutMs
|
||||
if cfg.provider == "deepseek":
|
||||
result.provider = aiDeepSeek
|
||||
else:
|
||||
result.provider = aiOpenAiCompatible
|
||||
|
||||
proc fromAiConfig*(cfg: AiConfig): TuiConfig =
|
||||
## Convert AiConfig to TUI config
|
||||
result.apiKey = cfg.apiKey
|
||||
result.baseUrl = cfg.baseUrl
|
||||
result.model = cfg.model
|
||||
result.timeoutMs = cfg.timeoutMs
|
||||
if cfg.provider == aiDeepSeek:
|
||||
result.provider = "deepseek"
|
||||
else:
|
||||
result.provider = "openai"
|
||||
|
||||
proc isValid*(cfg: TuiConfig): bool =
|
||||
cfg.apiKey.len > 0 and cfg.baseUrl.len > 0 and cfg.model.len > 0
|
||||
|
||||
proc providerDefaults*(provider: string): tuple[baseUrl, model: string] =
|
||||
for i in 0..<DefaultProviders.len:
|
||||
if DefaultProviders[i][0] == provider:
|
||||
return DefaultProviders[i][1]
|
||||
return ("", "")
|
||||
|
||||
proc cycleProvider*(current: string, direction: int): string =
|
||||
let providers = @["deepseek", "openai", "mimo", "custom"]
|
||||
var idx = providers.find(current)
|
||||
if idx < 0: idx = 0
|
||||
idx = (idx + direction + providers.len) mod providers.len
|
||||
return providers[idx]
|
||||
|
||||
proc maskedKey*(cfg: TuiConfig): string =
|
||||
if cfg.apiKey.len <= 4:
|
||||
return ""
|
||||
return "*".repeat(cfg.apiKey.len - 4) & cfg.apiKey[^4..^1]
|
||||
Reference in New Issue
Block a user