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
+56 -22
View File
@@ -1,9 +1,11 @@
# Clojure → Nim Emitter
import strutils, sequtils, sets
import strutils, sets
import types
import macros
var requiredImports* = initHashSet[string]()
var emitLibMode* = false
var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
proc setNsAliases*(aliases: seq[(string, string)]) =
@@ -151,12 +153,6 @@ proc runtimeName(op: string): string =
of "close!": "cljChanClose"
else: ""
proc emitArgs(args: seq[CljVal]): string =
var parts: seq[string] = @[]
for a in args:
parts.add(emitExpr(a, 0))
return parts.join(", ")
proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let sp = indentStr(indent)
let head = items[0]
@@ -250,10 +246,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
return form.items[1]
# list
if hName == "list":
var result: seq[CljVal] = @[]
var evaluated: seq[CljVal] = @[]
for i in 1..<form.items.len:
result.add(eval(form.items[i]))
return cljList(result)
evaluated.add(eval(form.items[i]))
return cljList(evaluated)
# cons
if hName == "cons" and form.items.len == 3:
let fst = eval(form.items[1])
@@ -346,7 +342,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
# if/when branches return their last expression properly.
bodyCode = bodyCode.replace("discard ", "result = ")
let procName = mangleName(name.symName)
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
of "defn-":
if items.len < 4:
@@ -433,7 +430,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if not (thenStripped.startsWith("echo ") or thenStripped.startsWith("discard ") or
thenStripped.startsWith("block:") or thenStripped.startsWith("if ") or
thenStripped.startsWith("try:") or thenStripped.startsWith("var ") or
thenStripped.startsWith("let ") or thenStripped.startsWith("proc ")):
thenStripped.startsWith("let ") or thenStripped.startsWith("proc ") or
thenStripped.contains("\n") or thenStripped.contains(" = ")):
thenFinal = indentStr(indent + 1) & "discard " & thenStripped
var ifBlock = sp & "if cljIsTruthy(" & condCode & "):\n" & thenFinal
if items.len == 4:
@@ -443,7 +441,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if not (elseStripped.startsWith("echo ") or elseStripped.startsWith("discard ") or
elseStripped.startsWith("block:") or elseStripped.startsWith("if ") or
elseStripped.startsWith("try:") or elseStripped.startsWith("var ") or
elseStripped.startsWith("let ") or elseStripped.startsWith("proc ")):
elseStripped.startsWith("let ") or elseStripped.startsWith("proc ") or
elseStripped.contains("\n") or elseStripped.contains(" = ")):
elseFinal = indentStr(indent + 1) & "discard " & elseStripped
ifBlock.add("\n" & sp & "else:\n" & elseFinal)
return ifBlock
@@ -540,22 +539,32 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "loop binding name must be a symbol")
loopParams.add((mangleName(lname.symName), emitExpr(lval, 0)))
li += 2
var loopVars: seq[string] = @[]
var lines: seq[string] = @[]
for (lpName, lpVal) in loopParams:
lines.add(sp & "var " & lpName & ": CljVal = " & lpVal)
loopVars.add(lpName)
loopStack.add(loopVars)
lines.add(sp & "while true:")
let body = items[2..^1]
for b in body:
lines.add(emitExpr(b, indent + 1))
lines.add(indentStr(indent + 1) & "break")
discard loopStack.pop()
return lines.join("\n")
of "recur":
if items.len < 2:
raise newException(EmitterError, "recur requires arguments")
# Simplified: emit as discard (real impl needs loop var assignment)
if loopStack.len == 0:
raise newException(EmitterError, "recur outside of loop")
let loopVars = loopStack[^1]
if items.len - 1 != loopVars.len:
raise newException(EmitterError, "recur requires " & $loopVars.len & " arguments, got " & $(items.len - 1))
var lines: seq[string] = @[]
for ri in 1..<items.len:
lines.add(sp & "discard " & emitExpr(items[ri], 0))
lines.add(sp & loopVars[ri-1] & " = " & emitExpr(items[ri], 0))
lines.add(sp & "continue")
return lines.join("\n")
of "do":
@@ -567,7 +576,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if items.len < 2:
raise newException(EmitterError, "try requires body")
var bodyForms: seq[CljVal] = @[]
var catchClauses: seq[(string, seq[CljVal])] = @[]
var catchClauses: seq[(string, string, seq[CljVal])] = @[]
var finallyBody: seq[CljVal] = @[]
var ti = 1
while ti < items.len:
@@ -576,10 +585,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if form.items[0].symName == "catch":
if form.items.len < 3:
raise newException(EmitterError, "catch requires exception type, name, and body")
let exType = form.items[1]
var exTypeStr = "CatchableError"
if exType.kind == ckSymbol:
exTypeStr = exType.symName
let exName = form.items[2]
if exName.kind != ckSymbol:
raise newException(EmitterError, "catch name must be a symbol")
catchClauses.add((mangleName(exName.symName), form.items[3..^1]))
catchClauses.add((exTypeStr, mangleName(exName.symName), form.items[3..^1]))
elif form.items[0].symName == "finally":
finallyBody = form.items[1..^1]
else:
@@ -591,9 +604,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
lines.add(sp & "try:")
for b in bodyForms:
lines.add(emitExpr(b, indent + 1))
for (exVar, exBody) in catchClauses:
lines.add(sp & "except CatchableError:")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljString(getCurrentExceptionMsg())")
for (exType, exVar, exBody) in catchClauses:
lines.add(sp & "except " & exType & ":")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljMap(@[cljKeyword(\"message\")], @[cljString(getCurrentExceptionMsg())])")
lines.add(indentStr(indent + 1) & "discard cljAssoc(" & exVar & ", cljKeyword(\"type\"), cljString(\"" & exType & "\"))")
for eb in exBody:
lines.add(emitExpr(eb, indent + 1))
if finallyBody.len > 0:
@@ -889,7 +903,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
for i in 1..<items.len:
argParts.add(emitExpr(items[i], 0))
# Known Nim module interop patterns
let fullFn = module & "." & funcChain
case module
of "math":
requiredImports.incl("math")
@@ -1029,6 +1042,19 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
keyParts.add(emitExpr(v.mapKeys[i], 0))
valParts.add(emitExpr(v.mapVals[i], 0))
return sp & "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckFn:
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = discard cljNil())"
of ckAtom:
return sp & "cljAtom(" & emitExpr(v.atomVal, 0) & ")"
of ckTransient:
return sp & "cljTransient(cljNil())"
of ckAgent:
return sp & "cljAgent(" & emitExpr(v.agentVal, 0) & ")"
proc emitBlock(items: seq[CljVal], indent: int): string =
if items.len == 0:
@@ -1038,7 +1064,7 @@ proc emitBlock(items: seq[CljVal], indent: int): string =
lines.add(emitExpr(item, indent))
return lines.join("\n")
proc emitProgram*(forms: seq[CljVal]): string =
proc emitProgramInternal(forms: seq[CljVal]): string =
requiredImports = initHashSet[string]()
var headerLines: seq[string] = @[
"# Generated by Clojure/Nim",
@@ -1080,3 +1106,11 @@ proc emitProgram*(forms: seq[CljVal]): string =
lines.add(form)
return lines.join("\n") & "\n"
proc emitProgram*(forms: seq[CljVal]): string =
emitLibMode = false
emitProgramInternal(forms)
proc emitProgramLib*(forms: seq[CljVal]): string =
emitLibMode = true
emitProgramInternal(forms)