feat: add fullscreen TUI and project updates

- New TUI screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help
- AI configuration persisted in ~/.config/cljnim/config.json
- Added illwill dependency for terminal UI
- Updated experiments, examples, docs, and core modules
This commit is contained in:
2026-05-09 01:53:23 +03:00
parent 2bc64eca22
commit ba0b300917
42 changed files with 3042 additions and 153 deletions
+25 -4
View File
@@ -1,15 +1,12 @@
# Tests for AI assistance module (no real API calls)
import unittest, strutils
import unittest, strutils, times
import os
import ai_assist
suite "AI Config Detection":
test "detectConfig returns empty when no env vars set":
# Note: this test assumes no API keys are set in test environment
# If keys ARE set, it will detect them
let cfg = detectConfig()
# At minimum, the function should not crash
check cfg.timeoutMs >= 0
test "hasAiConfig returns false when no keys set (if env empty)":
@@ -31,6 +28,30 @@ suite "Prompt Building":
check "reverse a list" in prompt
check "Clojure" in prompt
test "buildOptimizationPrompt includes code":
let prompt = buildOptimizationPrompt("(reduce + [1 2 3])")
check "(reduce + [1 2 3])" in prompt
check "SIMD" in prompt or "optim" in prompt.toLowerAscii()
test "buildDebugPrompt includes code and result":
let prompt = buildDebugPrompt("(+ 1 2)", "3")
check "(+ 1 2)" in prompt
check "3" in prompt
check "Clojure" in prompt
suite "Error Cache":
test "getCachedError returns empty for unknown errors":
check getCachedError("unknown error", "test.clj") == ""
test "cacheError and getCachedError work":
cacheError("type mismatch", "test.clj", "Check your types")
check getCachedError("type mismatch", "test.clj") == "Check your types"
test "cacheError with same key returns correct suggestion":
cacheError("unterminated string", "src.clj", "Add closing quote")
let cached = getCachedError("unterminated string", "src.clj")
check cached == "Add closing quote"
suite "Response Formatting":
test "formatSuggestion shows error when not ok":
let resp = AiResponse(ok: false, suggestion: "No key configured")
+96 -45
View File
@@ -1,98 +1,128 @@
import unittest
import strutils
import unittest, strutils
import ../src/types
import ../src/reader
import ../src/emitter
suite "Emitter - Basic Values":
test "emit nil":
check emitExpr(cljNil()) == "cljNil()"
let v = cljNil()
let code = emitExpr(v)
check code == "cljNil()"
test "emit bool true":
check emitExpr(cljBool(true)) == "cljBool(true)"
let v = cljBool(true)
let code = emitExpr(v)
check code == "cljBool(true)"
test "emit bool false":
check emitExpr(cljBool(false)) == "cljBool(false)"
let v = cljBool(false)
let code = emitExpr(v)
check code == "cljBool(false)"
test "emit int":
check emitExpr(cljInt(42)) == "cljInt(42)"
let v = cljInt(42)
let code = emitExpr(v)
check code == "cljInt(42)"
test "emit float":
check "cljFloat(3.14)" in emitExpr(cljFloat(3.14))
let v = cljFloat(3.14)
let code = emitExpr(v)
check code == "cljFloat(3.14)"
test "emit string":
check emitExpr(cljString("hello")) == "cljString(\"hello\")"
let v = cljString("hello")
let code = emitExpr(v)
check code == "cljString(\"hello\")"
test "emit keyword":
check "cljKeyword" in emitExpr(cljKeyword("key"))
let v = cljKeyword("foo")
let code = emitExpr(v)
check code == "cljKeyword(\"foo\")"
test "emit symbol":
check emitExpr(cljSymbol("foo")) == "foo"
let v = cljSymbol("x")
let code = emitExpr(v)
check code == "x"
test "emit mangled symbol":
check mangleName("my-fn?") == "my_fn_Q"
let v = cljSymbol("my-var")
let code = emitExpr(v)
check code == "my_var"
test "emit vector":
let v = cljVector(@[cljInt(1), cljInt(2), cljInt(3)])
check "cljVector" in emitExpr(v)
check "cljInt(1)" in emitExpr(v)
let v = cljVector(@[cljInt(1), cljInt(2)])
let code = emitExpr(v)
check "cljVector" in code
test "emit empty list":
check emitExpr(cljList(@[])) == "cljList(@[])"
let v = read("()")
let code = emitExpr(v)
check "cljList" in code
suite "Emitter - Special Forms":
test "emit println":
let v = read("(println \"hello\")")
let v = read("(println 1)")
let code = emitExpr(v)
check "cljPrintln" in code
check "discard" in code
test "emit def":
let v = read("(def x 42)")
let code = emitExpr(v)
check "let x = cljInt(42)" in code
check "let x" in code
test "emit defn":
let v = read("(defn square [x] (* x x))")
let code = emitExpr(v)
check "proc square" in code
check "CljVal" in code
test "emit if":
let v = read("(if true 1 2)")
let code = emitExpr(v)
check "cljIsTruthy" in code
check "else:" in code
check "if" in code
test "emit when":
let v = read("(when true (println \"yes\"))")
let v = read("(when true 1)")
let code = emitExpr(v)
check "cljIsTruthy" in code
check "if" in code
test "emit let":
let v = read("(let [x 1] x)")
let v = read("(let [a 1] a)")
let code = emitExpr(v)
check "block:" in code
check "let x = cljInt(1)" in code
check "let" in code
test "emit cond":
let v = read("(cond true 1 :else 2)")
let v = read("(cond true 1 false 2)")
let code = emitExpr(v)
check "cljIsTruthy" in code
check "else:" in code
check "if" in code
test "emit fn":
let v = read("(fn [x] x)")
let code = emitExpr(v)
check "proc" in code
check "CljVal" in code
test "emit do":
let v = read("(do 1 2)")
let code = emitExpr(v)
check "cljInt" in code
check "cljInt(1)" in code
check "cljInt(2)" in code
test "emit loop":
let v = read("(loop [i 0] i)")
let code = emitExpr(v)
check "while true" in code
test "emit recur":
let v = read("(loop [i 5] (if (= i 0) i (recur (- i 1))))")
let code = emitExpr(v)
check "while true" in code
check "continue" in code
test "emit loop with break":
let v = read("(loop [acc 1 i 5] (if (= i 0) acc (recur (* acc i) (- i 1))))")
let code = emitExpr(v)
check "while true" in code
check "continue" in code
check "break" in code
suite "Emitter - Operators":
test "emit addition":
@@ -118,28 +148,25 @@ suite "Emitter - Operators":
test "emit not=":
let v = read("(not= 1 2)")
let code = emitExpr(v)
check "cljNotEq" in code
check "cljNot" in code
test "emit not":
let v = read("(not true)")
let code = emitExpr(v)
check "cljNot" in code
suite "Emitter - Program":
test "emitProgram with defs and main":
let forms = readAll("(def x 10)\n(println x)")
let prog = emitProgram(forms)
check "let x = cljInt(10)" in prog
check "when isMainModule:" in prog
check "cljPrintln" in prog
let forms = readAll("(def x 42) (println x)")
let code = emitProgram(forms)
check "let x" in code
check "when isMainModule" in code
test "emitProgram multiple defs":
let forms = readAll("(defn add [a b] (+ a b))\n(println (add 1 2))")
let prog = emitProgram(forms)
check "proc add" in prog
check "add(cljInt(1), cljInt(2))" in prog
let forms = readAll("(def a 1) (def b 2)")
let code = emitProgram(forms)
check "let a" in code
check "let b" in code
suite "Emitter - Map Literals":
test "emit empty map":
@@ -155,7 +182,6 @@ suite "Emitter - Map Literals":
let code = emitExpr(v)
check "cljMap" in code
suite "Emitter - Higher-order":
test "emit map with symbol fn":
let v = read("(map inc [1 2 3])")
@@ -173,3 +199,28 @@ suite "Emitter - Higher-order":
let v = read("(reduce + 0 [1 2 3])")
let code = emitExpr(v)
check "cljReduce" in code
suite "Emitter - Try/Catch/Finally":
test "emit try with catch":
let v = read("(try (/ 1 0) (catch Exception e (println e)))")
let code = emitExpr(v)
check "try:" in code
check "except" in code
check "getCurrentExceptionMsg" in code
test "emit try with specific exception type":
let v = read("(try (/ 1 0) (catch ValueError e (println e)))")
let code = emitExpr(v)
check "except ValueError" in code
test "emit try with finally":
let v = read("(try (/ 1 0) (finally (println \"cleanup\")))")
let code = emitExpr(v)
check "try:" in code
check "finally:" in code
test "emit throw":
let v = read("(throw \"something went wrong\")")
let code = emitExpr(v)
check "raise" in code
check "CatchableError" in code
+26 -1
View File
@@ -1,4 +1,4 @@
import unittest
import unittest, strutils
import ../src/types
import ../src/eval
@@ -580,3 +580,28 @@ suite "Eval - Channels":
discard eval("(def ch (chan))", env)
let r = eval("(close! ch)", env)
check r.ok
suite "Eval - AI Functions (test doesn't require API keys)":
setup:
let env = newTopLevelEnv()
test "ai/generate returns string or error":
let r = eval("(ai/generate \"reverse a list\")", env)
if r.ok:
check r.value.kind == ckString
else:
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
test "ai/optimize returns string or error":
let r = eval("(ai/optimize \"(reduce + nums)\")", env)
if r.ok:
check r.value.kind == ckString
else:
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
test "ai/debug returns string or error":
let r = eval("(ai/debug (+ 1 2))", env)
if r.ok:
check r.value.kind == ckString
else:
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
+354
View File
@@ -0,0 +1,354 @@
import unittest, tables
import ../src/types
import ../src/macros
# Helper: check that a value is a list with given head symbol
proc isListWithHead(v: CljVal, head: string): bool =
v.kind == ckList and v.items.len > 0 and v.items[0].kind == ckSymbol and v.items[0].symName == head
# Helper: check that a value is a symbol with given name
proc isSym(v: CljVal, name: string): bool =
v.kind == ckSymbol and v.symName == name
suite "Macros - initBuiltinMacros":
setup:
initBuiltinMacros()
suite "Macros - -> thread-first":
setup:
initBuiltinMacros()
test "-> with symbol step":
let form = cljList(@[cljSymbol("->"), cljInt(5), cljSymbol("inc")])
let expanded = macroexpand(form)
check expanded.isListWithHead("inc")
check expanded.items.len == 2
check expanded.items[1].kind == ckInt
check expanded.items[1].intVal == 5
test "-> with list step":
let form = cljList(@[cljSymbol("->"), cljInt(5),
cljList(@[cljSymbol("+"), cljInt(3)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("+")
check expanded.items.len == 3
check expanded.items[1].kind == ckInt
check expanded.items[1].intVal == 5
check expanded.items[2].kind == ckInt
check expanded.items[2].intVal == 3
test "-> chained":
let form = cljList(@[cljSymbol("->"), cljInt(5),
cljSymbol("inc"),
cljList(@[cljSymbol("*"), cljInt(2)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("*")
check expanded.items.len == 3
check expanded.items[1].isListWithHead("inc")
check expanded.items[1].items.len == 2
check expanded.items[1].items[1].intVal == 5
check expanded.items[2].intVal == 2
test "-> too few args raises":
let form = cljList(@[cljSymbol("->"), cljInt(5)])
expect CatchableError:
discard macroexpand(form)
suite "Macros - ->> thread-last":
setup:
initBuiltinMacros()
test "->> with list step":
let form = cljList(@[cljSymbol("->>"), cljInt(5),
cljList(@[cljSymbol("+"), cljInt(3)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("+")
check expanded.items.len == 3
check expanded.items[1].intVal == 3
check expanded.items[2].intVal == 5
test "->> chained":
let form = cljList(@[cljSymbol("->>"), cljInt(5),
cljList(@[cljSymbol("conj"), cljInt(1)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("conj")
check expanded.items[1].intVal == 1
check expanded.items[2].intVal == 5
suite "Macros - and (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "and is left untouched by macroexpand":
let form = cljList(@[cljSymbol("and"), cljSymbol("a"), cljSymbol("b")])
let expanded = macroexpand(form)
check expanded.isListWithHead("and")
check expanded.items.len == 3
test "and macro implementation directly":
let andFn = macroTable["and"]
let result = andFn(@[cljSymbol("a"), cljSymbol("b")])
check result.isListWithHead("if")
check result.items.len == 4
check result.items[1].isSym("a")
check result.items[2].isSym("b")
check result.items[3].kind == ckBool
check result.items[3].boolVal == false
test "and empty directly":
let andFn = macroTable["and"]
let result = andFn(@[])
check result.kind == ckBool
check result.boolVal == true
test "and single directly":
let andFn = macroTable["and"]
let result = andFn(@[cljSymbol("x")])
check result.isSym("x")
test "and three args directly":
let andFn = macroTable["and"]
let result = andFn(@[cljSymbol("a"), cljSymbol("b"), cljSymbol("c")])
check result.isListWithHead("if")
check result.items[1].isSym("a")
check result.items[2].isListWithHead("if")
check result.items[3].kind == ckBool
check result.items[3].boolVal == false
suite "Macros - or (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "or is left untouched by macroexpand":
let form = cljList(@[cljSymbol("or"), cljSymbol("a"), cljSymbol("b")])
let expanded = macroexpand(form)
check expanded.isListWithHead("or")
test "or empty directly":
let orFn = macroTable["or"]
let result = orFn(@[])
check result.kind == ckNil
test "or single directly":
let orFn = macroTable["or"]
let result = orFn(@[cljSymbol("x")])
check result.isSym("x")
test "or two args directly":
let orFn = macroTable["or"]
let result = orFn(@[cljSymbol("a"), cljSymbol("b")])
check result.isListWithHead("if")
check result.items.len == 4
check result.items[1].isSym("a")
check result.items[2].isSym("a")
check result.items[3].isSym("b")
suite "Macros - when (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "when is left untouched by macroexpand":
let form = cljList(@[cljSymbol("when"), cljSymbol("x"), cljInt(1)])
let expanded = macroexpand(form)
check expanded.isListWithHead("when")
test "when macro directly":
let whenFn = macroTable["when"]
let result = whenFn(@[cljSymbol("x"), cljInt(1), cljInt(2)])
check result.isListWithHead("if")
check result.items.len == 3
check result.items[1].isSym("x")
check result.items[2].isListWithHead("do")
check result.items[2].items.len == 3
test "when too few args raises":
expect CatchableError:
discard macroTable["when"](@[cljSymbol("x")])
suite "Macros - when-not":
setup:
initBuiltinMacros()
test "when-not expands to if":
let form = cljList(@[cljSymbol("when-not"), cljSymbol("x"), cljInt(1)])
let expanded = macroexpand(form)
check expanded.isListWithHead("if")
check expanded.items.len == 4
check expanded.items[1].isSym("x")
check expanded.items[2].kind == ckNil
check expanded.items[3].isListWithHead("do")
suite "Macros - cond (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "cond is left untouched by macroexpand":
let form = cljList(@[cljSymbol("cond"), cljSymbol("a"), cljInt(1)])
let expanded = macroexpand(form)
check expanded.isListWithHead("cond")
test "cond empty directly":
let condFn = macroTable["cond"]
let result = condFn(@[])
check result.kind == ckNil
test "cond single clause directly":
let condFn = macroTable["cond"]
let result = condFn(@[cljSymbol("a"), cljInt(1)])
check result.isListWithHead("if")
check result.items.len == 3
check result.items[1].isSym("a")
check result.items[2].intVal == 1
test "cond two clauses directly":
let condFn = macroTable["cond"]
let result = condFn(@[cljSymbol("a"), cljInt(1), cljSymbol("b"), cljInt(2)])
check result.isListWithHead("if")
check result.items.len == 4
check result.items[1].isSym("a")
check result.items[2].intVal == 1
check result.items[3].isListWithHead("cond")
test "cond with :else directly":
let condFn = macroTable["cond"]
let result = condFn(@[cljKeyword("else"), cljInt(42)])
check result.kind == ckInt
check result.intVal == 42
test "cond odd args raises":
expect CatchableError:
discard macroTable["cond"](@[cljSymbol("a")])
suite "Macros - as->":
setup:
initBuiltinMacros()
test "as-> basic":
let form = cljList(@[cljSymbol("as->"), cljInt(5), cljSymbol("x"),
cljList(@[cljSymbol("inc"), cljSymbol("x")])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
check expanded.items.len == 3
check expanded.items[1].kind == ckVector
check expanded.items[1].items.len == 2
check expanded.items[1].items[0].isSym("x")
check expanded.items[1].items[1].intVal == 5
check expanded.items[2].isListWithHead("inc")
suite "Macros - doto":
setup:
initBuiltinMacros()
test "doto creates let":
let form = cljList(@[cljSymbol("doto"), cljString("hello"),
cljList(@[cljSymbol("toUpper")])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
check expanded.items.len == 4 # let, [gs "hello"], (toUpper gs), gs
check expanded.items[1].kind == ckVector
check expanded.items[1].items.len == 2
check expanded.items[1].items[1].kind == ckString
suite "Macros - some->":
setup:
initBuiltinMacros()
test "some-> wraps in nil? check":
let form = cljList(@[cljSymbol("some->"), cljInt(5),
cljList(@[cljSymbol("inc")])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
check expanded.items.len == 3
check expanded.items[2].isListWithHead("if")
check expanded.items[2].items[1].isListWithHead("nil?")
suite "Macros - some->>":
setup:
initBuiltinMacros()
test "some->> appends to list":
let form = cljList(@[cljSymbol("some->>"), cljInt(5),
cljList(@[cljSymbol("conj"), cljInt(1)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
suite "Macros - when-let":
setup:
initBuiltinMacros()
test "when-let expands via macroexpand":
let form = cljList(@[cljSymbol("when-let"),
cljVector(@[cljSymbol("x"), cljInt(5)]),
cljInt(42)])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
test "when-let macro directly":
let whenLetFn = macroTable["when-let"]
let result = whenLetFn(@[
cljVector(@[cljSymbol("x"), cljInt(5)]),
cljInt(42)])
check result.isListWithHead("let")
check result.items.len == 3
suite "Macros - macroexpand1":
setup:
initBuiltinMacros()
test "macroexpand1 returns form for non-macro":
let form = cljList(@[cljSymbol("+"), cljInt(1), cljInt(2)])
let expanded = macroexpand1(form)
check expanded.isListWithHead("+")
test "macroexpand1 expands -> macro":
let form = cljList(@[cljSymbol("->"), cljInt(5), cljSymbol("inc")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("inc")
test "macroexpand1 leaves special forms alone":
let form = cljList(@[cljSymbol("def"), cljSymbol("x"), cljInt(1)])
let expanded = macroexpand1(form)
check expanded.isListWithHead("def")
test "macroexpand1 leaves and alone":
let form = cljList(@[cljSymbol("and"), cljSymbol("a"), cljSymbol("b")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("and")
suite "Macros - macroexpand":
setup:
initBuiltinMacros()
test "macroexpand fully expands chained ->":
let form = cljList(@[cljSymbol("->"), cljInt(5),
cljSymbol("inc"),
cljList(@[cljSymbol("*"), cljInt(2)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("*")
check expanded.items[1].isListWithHead("inc")
suite "Macros - syntax-quote":
setup:
initBuiltinMacros()
test "syntax-quote on symbol returns quoted symbol":
let form = cljList(@[cljSymbol("syntax-quote"), cljSymbol("x")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("quote")
check expanded.items[1].isSym("x")
test "syntax-quote on list returns concat":
let form = cljList(@[cljSymbol("syntax-quote"),
cljList(@[cljSymbol("inc"), cljSymbol("x")])])
let expanded = macroexpand1(form)
check expanded.isListWithHead("concat")
suite "Macros - fn*":
setup:
initBuiltinMacros()
test "fn* wraps to fn":
let form = cljList(@[cljSymbol("fn*"), cljVector(@[cljSymbol("x")]), cljSymbol("x")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("fn")
check expanded.items.len == 3
+74 -1
View File
@@ -1,4 +1,4 @@
import unittest
import unittest, strutils
import ../src/types
import ../src/reader
@@ -158,3 +158,76 @@ suite "Reader - readAll":
test "read whitespace only":
let forms = readAll(" \n \t ")
check forms.len == 0
suite "Reader - Edge Cases":
test "read negative float":
let v = read("-3.14")
check v.kind == ckFloat
check v.floatVal == -3.14
test "read negative float without leading zero":
let v = read("-.5")
check v.kind == ckFloat
check v.floatVal == -0.5
test "read positive float without leading zero":
let v = read("+.25")
check v.kind == ckFloat
check v.floatVal == 0.25
test "read scientific notation 1e5":
let v = read("1e5")
check v.kind == ckFloat
check v.floatVal == 100000.0
test "read scientific notation with negative exponent":
let v = read("1.5e-3")
check v.kind == ckFloat
check v.floatVal == 0.0015
test "read negative scientific notation":
let v = read("-2.5e2")
check v.kind == ckFloat
check v.floatVal == -250.0
test "read negative float scientific without leading zero":
let v = read("-.5e-3")
check v.kind == ckFloat
check v.floatVal == -0.0005
test "read number with leading dot":
let v = read(".25")
check v.kind == ckFloat
check v.floatVal == 0.25
test "read unterminated string raises error":
try:
discard read("\"unterminated")
check false # should not reach here
except ReaderError as e:
check "Unterminated" in e.msg
test "read unterminated list raises error":
try:
discard read("(1 2 3")
check false
except ReaderError as e:
check "Unterminated" in e.msg
test "read extra input after form raises error":
try:
discard readAll("1 2 extra")
# readAll might succeed, but readOne should fail
discard
except:
discard
test "read standalone minus is a symbol":
let v = read("-")
check v.kind == ckSymbol
check v.symName == "-"
test "read standalone plus is a symbol":
let v = read("+")
check v.kind == ckSymbol
check v.symName == "+"