EELisp Manual
Complete reference for the EELisp language, database, and agenda system.
Installation
EELisp is a Swift package. You need Swift 5.9+ (Xcode 15+).
git clone https://github.com/user/eelisp cd eelisp swift build
The built binary will be at .build/debug/eelisp.
Usage Modes
Interactive REPL
swift run eelisp
Launches a read-eval-print loop. Multi-line expressions are supported — the REPL waits for balanced parentheses before evaluating.
Execute a File
swift run eelisp script.el
Evaluate an Expression
swift run eelisp -e "(+ 1 2 3)"
;; => 6
Pipe Mode
swift run eelisp --pipe
No prompts, structured output for embedding in other programs (like EEditor). See Pipe Mode Protocol.
EEditor Integration
EELisp is designed to run embedded inside EEditor — a fast, native Markdown and code editor for Apple platforms (macOS 14+, iOS 17+, iPadOS 17+).
What is EEditor?
EEditor is a multi-platform editor that ships in three forms:
- SwiftUI App — Full graphical editor with syntax highlighting, live preview, PDF export, and an integrated EELisp REPL tab
- eeditor-term — Swift-based terminal UI with file tree, multi-tab editor, and REPL
- eeditor-nc — Minimal C/ncurses editor that communicates with EELisp via a persistent subprocess pipe
How It Works
EEditor owns a single Interpreter instance. All GUI panels — file tree, calendar sidebar, agenda view — query the interpreter via interpreter.eval(). This means the REPL and GUI are always in sync.
When you call (browse contacts) or (defform ...) in the REPL, EEditor renders them as rich, interactive UI components (scrollable tables, CRUD forms, calculator views).
Editor Builtins
When running inside EEditor, these builtins interact with the active text buffer:
(cursor-pos) ;; current cursor position (set-cursor-pos 42) ;; move cursor (selection) ;; get selected text (current-file) ;; path of active file (buffer-text) ;; entire buffer content (insert-at 10 "text") ;; insert at position (replace-range 5 10 "new") ;; replace range
nil or a message.Data Types
EELisp has 17 value types:
| Type | Example | Description |
|---|---|---|
number | 42, 3.14 | Double-precision float |
string | "hello" | Text with escape sequences |
bool | true, false | Boolean |
nil | nil | Null / empty |
symbol | foo, + | Identifier |
keyword | :name | Key identifier for dicts |
list | (1 2 3) | Ordered sequence |
dict | {:a 1 :b 2} | Ordered key-value map |
function | (fn (x) x) | Lambda / closure |
date | via (now) | Date/time value |
table | via (deftable) | Database table definition |
record | via (insert) | Single row with auto-ID |
result-set | via (query) | Multiple records |
item | via (add-item) | Agenda item |
category | via (defcategory) | Hierarchical category |
rule | via (defrule) | Auto-categorization rule |
view | via (defview) | Saved query |
Type Checking
(type 42) ;; => "number" (string? "hi") ;; => true (number? 3.14) ;; => true (list? '(1 2)) ;; => true (nil? nil) ;; => true (dict? {:a 1}) ;; => true (fn? +) ;; => true (item? x) ;; => true/false (table? x) ;; => true/false (record? x) ;; => true/false
Type Conversion
(->string 42) ;; => "42" (->number "3.14") ;; => 3.14 (->bool 0) ;; => false
Special Forms
Variables
(def x 42) ;; define (set! x 43) ;; mutate
Functions
(defn square (x) (* x x)) (def double (fn (x) (* 2 x))) ;; Rest parameters (defn sum (first . rest) (reduce + first rest))
Conditionals
(if (> x 0) "positive" "non-positive") (cond ((> x 0) "positive") ((= x 0) "zero") (true "negative"))
Bindings
(let ((a 1) (b 2)) (+ a b)) ;; parallel (let* ((a 1) (b (+ a 1))) b) ;; sequential
Sequencing & Loops
(do (println "a") (println "b") 42) ;; Tail-recursive loop (loop (n 5 acc 1) (if (= n 0) acc (recur (- n 1) (* acc n)))) ;; For-each (for-each (x '(1 2 3)) (println x))
Logic
(and true false) ;; => false (short-circuit) (or false 42) ;; => 42 (not true) ;; => false
Builtin Functions
Arithmetic
+ - * / mod abs min max floor ceil round pow
Comparison
= != < > <= >=
Strings
(str "hello" " world") ;; => "hello world" (str-len "hello") ;; => 5 (str-upper "hello") ;; => "HELLO" (str-lower "HELLO") ;; => "hello" (str-contains "hello" "ell") ;; => true (str-split "a,b,c" ",") ;; => ("a" "b" "c") (str-join ", " '("a" "b")) ;; => "a, b" (str-trim " hi ") ;; => "hi" (str-replace "hello" "l" "r") ;; => "herro" (str-starts-with "hello" "he") ;; => true (str-ends-with "hello" "lo") ;; => true (substr "hello" 1 3) ;; => "ell" (str-matches "abc123" "\\d+") ;; => true
Lists
(list 1 2 3) ;; => (1 2 3) (cons 0 '(1 2)) ;; => (0 1 2) (car '(1 2 3)) ;; => 1 (also: head) (cdr '(1 2 3)) ;; => (2 3) (also: tail) (nth 1 '(10 20 30)) ;; => 20 (length '(1 2 3)) ;; => 3 (append '(1) '(2 3)) ;; => (1 2 3) (reverse '(1 2 3)) ;; => (3 2 1) (map inc '(1 2 3)) ;; => (2 3 4) (filter even? '(1 2 3 4)) ;; => (2 4) (reduce + 0 '(1 2 3)) ;; => 6 (range 1 5) ;; => (1 2 3 4) (flatten '(1 (2 (3)))) ;; => (1 2 3) (sort-by id '(3 1 2)) ;; => (1 2 3) (zip '(1 2) '("a" "b")) ;; => ((1 "a") (2 "b")) (empty? '()) ;; => true
Dictionaries
(dict :name "Alice" :age 30) ;; => {:name "Alice" :age 30} (dict-get d :name) ;; => "Alice" (dict-set d :age 31) ;; => new dict with age=31 (dict-keys d) ;; => (:name :age) (dict-values d) ;; => ("Alice" 30) (dict-has d :name) ;; => true (dict-merge d1 d2) ;; merge two dicts
Date & Time
(now) ;; current date/time (today) ;; today as string "YYYY-MM-DD" (date-format (now) "yyyy-MM-dd") ;; format date (date-add (now) 7 :days) ;; add 7 days (date-diff d1 d2 :days) ;; difference in days
I/O
(print "hello") ;; print without newline (println "hello") ;; print with newline (read-line) ;; read a line from stdin
Meta
(eval '(+ 1 2)) ;; => 3 (parse "(+ 1 2)") ;; string => AST (apply + '(1 2 3)) ;; => 6
Prelude (Standard Library)
These functions are defined in EELisp itself and loaded on startup:
Functional
(id x) ;; identity (compose f g) ;; (compose f g) x => f(g(x)) (partial f . args) ;; partial application (complement pred) ;; negate a predicate
Numeric
(inc x) (dec x) (even? x) (odd? x) (zero? x) (pos? x) (neg? x)
List Shortcuts
(first xs) (second xs) (third xs) (last xs) (take n xs) (drop n xs) (some? pred xs) (every? pred xs) (count xs)
Macros
(pipe val f1 f2 ...) ;; threading macro (when test body...) ;; if without else (unless test body...) ;; if-not without else
Macros
(defmacro swap! (a b) `(let ((tmp ~a)) (set! ~a ~b) (set! ~b tmp))) ;; Quasiquote: ` Unquote: ~ Splice: ~@ (defmacro my-list (. items) `(list ~@items))
Tables & Records
Defining Tables
(deftable contacts (name:string email:string age:number active:bool)) ;; Field types: string, number, bool, date, memo, choice
CRUD Operations
;; Insert (insert contacts {:name "Alice" :email "alice@co.com" :age 30}) ;; Update by ID (update contacts 1 {:age 31}) ;; Soft delete (delete contacts 1) ;; Purge deleted records (pack contacts)
Utility
(tables) ;; list all tables (describe contacts) ;; show table schema (count-records contacts) ;; count rows (drop-table contacts) ;; delete table
Querying
(query contacts) (query contacts :where "age > ?" :params (list 25) :order "name" :desc true :limit 10)
The :where clause uses SQL syntax with ? placeholders. Parameters are passed via :params as a list.
Browse & Edit
Browse
(browse contacts)
Displays an interactive, scrollable table grid. In EEditor, this renders as a native TableView component.
Edit
(edit contacts) (edit contacts :where "age > ?" :params (list 25))
Opens a CRUD form to navigate, edit, save, and delete records one at a time.
Forms (defform)
Calculator Form
(defform compound-interest (principal:number rate:number years:number) :computed ((total (* principal (pow (+ 1 (/ rate 100)) years))) (gain (- total principal))))
Choice Fields
(defform ticket-form (title:string description:memo (priority:choice "High" "Medium" "Low") (status:choice "Open" "In Progress" "Done")))
Table-Backed Form
(deftable expenses (description:string amount:number (category:choice "Food" "Transport" "Other"))) (defform expense-entry (description:string amount:number (category:choice "Food" "Transport" "Other")) :source expenses)
With :source, the form reads from and saves to the database table.
Items
Adding Items
(add-item "Finish quarterly report") (add-item "Call dentist" :when "2026-03-01" :priority 2 :notes "check cavity" :category "personal/health")
Querying Items
(items) ;; all items (items :category "work") ;; by category (items :search "quarterly") ;; full-text search (items :priority 1) ;; by priority (items :when-before "2026-03-01") ;; before date
Managing Items
(item-get 1) ;; get by ID (item-edit 1) ;; interactive edit (item-set 1 :priority 1) ;; update property (item-done 1) ;; mark complete (item-count) ;; total count
Categories
;; Hierarchical (slash-separated) (defcategory work) (defcategory work/projects) (defcategory work/meetings) (defcategory personal/errands) ;; Exclusive categories (only one child per item) (defcategory priority :exclusive true :children (high medium low)) (defcategory status :exclusive true :children (active waiting done)) ;; Assign / unassign (assign 1 "work/projects") (assign 1 "priority/high") ;; auto-removes other priority (unassign 1 "work/projects") (categories) ;; list all
Auto-Categorization Rules
(defrule urgent-flag :when (str-contains text "URGENT") :assign "priority/high") (defrule meeting-detect :when (or (str-contains text "meeting") (str-contains text "call with")) :assign "work/meetings") ;; Rules with actions (e.g., extract dates via regex) (defrule date-extract :when (str-matches text "\\b(\\d{4}-\\d{2}-\\d{2})\\b") :action (item-set id :when (match 1)))
Applying Rules
(apply-rules) ;; all items (apply-rules 42) ;; single item (auto-categorize true) ;; auto-apply on new items (rules) ;; list all rules (drop-rule "urgent-flag") ;; remove a rule
Rule Context Variables
| Variable | Description |
|---|---|
text | Item text |
notes | Item notes |
id | Item ID |
categories | Current categories list |
created, modified | Timestamps |
(get :key) | Access any property |
(has-category "path") | Check category membership |
(match n) | Regex capture group |
Views
(defview work-board :source items :filter (has-category "work") :group-by category :sort-by when) (defview urgent :source items :filter (= priority "1") :sort-by when) (defview inbox :source items :filter (= (length categories) 0)) (defview overdue :source items :filter (overdue?) :sort-by when) (show work-board) ;; render a view (views) ;; list all views (drop-view "inbox") ;; remove a view
Smart Input (NLP)
The add function parses natural language to extract dates, priorities, and people:
(add "Meet Alice tomorrow for coffee !!") ;; => when: tomorrow, priority: 2, who: "Alice" (add "Call Bob next Monday about the project") ;; => when: next Monday, who: "Bob" (add "URGENT fix server crash") ;; => priority: 1 (add "email Sarah March 15 about renewal !!") ;; => when: 2026-03-15, priority: 2, who: "Sarah"
Recognized Patterns
| Category | Patterns |
|---|---|
| Dates | tomorrow, today, yesterday, next Monday, this weekend, in 3 days, in 2 weeks, end of week, end of month, ISO dates, month names |
| Priority | URGENT/ASAP → 1, !!! → 1, !! → 2, ! → 3, high priority → 2, low priority → 4 |
| People | @alice, with/for/from Name, call/email/meet/text Name |
Inspect Parsing
(smart-parse "email Sarah March 15 about renewal !!") ;; => {:text "email Sarah about renewal" ;; :when "2026-03-15" :priority 2 :who ("Sarah")}
Recurring Items & Templates
Recurring Items
(add-item "Team standup" :when "2026-02-24" :recur :weekly) (add-item "Pay rent" :when "2026-03-01" :recur :monthly) (add-item "Morning jog" :when "2026-02-22" :recur :daily) ;; Custom intervals (add-item "Quarterly review" :when "2026-04-01" :recur (every 3 :months)) (add-item "Biweekly report" :when "2026-02-28" :recur (every 2 :weeks))
When you mark a recurring item as done with (item-done id), a new item is automatically created with the date advanced by the recurrence interval.
Templates
(deftemplate weekly-review :text "Weekly review: reflect on goals" :category "work/admin" :priority 2 :notes "1. What went well?\n2. What to improve?") (from-template weekly-review :when "2026-03-07") (from-template weekly-review :when "2026-03-14" :priority 1) (templates) ;; list all templates (drop-template weekly-review) ;; remove a template
Multiple Agendas
;; Open separate agenda databases (open-agenda "~/agendas/work.db") (add-item "Deploy to staging") (open-agenda "~/agendas/personal.db") (add-item "Buy groceries") ;; Switch (use-agenda work) ;; List all (agendas) ;; => personal — ~/agendas/personal.db ;; work [active] — ~/agendas/work.db ;; Export / Import (export-agenda "work" :format :json :path "work-backup.json") (import-agenda "work-backup.json") ;; Close (close-agenda personal)
Calendar
(calendar) ;; current month (calendar 3) ;; March of current year (calendar 3 2026) ;; March 2026 ;; => {:year 2026 :month 3 :month-name "March" ;; :days-in-month 31 :first-weekday 5 ;; :today 12 :weeks [...]} ;; Items by date (items-on "2026-03-12") (items-between "2026-03-01" "2026-03-31") ;; Quick-add with today's date (add-item-today "Review pull requests" :priority 1)
File System
(read-file "document.txt") (write-file "output.txt" "content") (append-file "log.txt" "new line") (file-exists? "path.txt") (list-files) ;; current directory (list-files "subdir") ;; specific directory (current-dir)
Clipboard
(clipboard-get) ;; read clipboard (clipboard-set "text to copy") ;; write to clipboard
HTTP & JSON
;; HTTP requests (http-get "https://api.example.com/data") ;; => {:status 200 :body "..."} (http-post "https://api.example.com/data" "{\"key\": \"value\"}") ;; JSON (json-parse "{\"name\": \"Alice\", \"scores\": [95, 87]}") ;; => {:name "Alice" :scores (95 87)} (json-stringify {:name "Bob" :active true}) ;; => "{\"active\":true,\"name\":\"Bob\"}"
Editor Builtins
These work when EELisp is running inside EEditor:
(cursor-pos) ;; current cursor position (set-cursor-pos 42) ;; move cursor to position 42 (selection) ;; get selected text (current-file) ;; path of active file (buffer-text) ;; entire buffer content (insert-at 10 "text") ;; insert text at position (replace-range 5 10 "new") ;; replace characters 5-10
Pipe Mode Protocol
When running with --pipe, EELisp outputs structured data for embedding in other programs:
# Table results @@TABLE:contacts @@COLS:name\tstring\temail\tstring\tage\tnumber @@ROW:1\tAlice\talice@ex.com\t30 @@ROW:2\tBob\tbob@ex.com\t25 @@END \x04 # Scalar results 6 \x04 # Errors @@ERROR:undefined symbol: foo \x04
Each response is terminated with \x04 (EOT). This protocol is used by eeditor-nc (the C/ncurses version) to communicate with the EELisp interpreter as a subprocess.
All Builtins A–Z
| Function | Category | Description |
|---|---|---|
+ - * / | Arithmetic | Basic math operations |
mod | Arithmetic | Modulo |
abs min max | Arithmetic | Absolute value, min, max |
floor ceil round | Arithmetic | Rounding |
pow | Arithmetic | Exponentiation |
= != < > <= >= | Comparison | Comparisons |
not | Logic | Boolean negation |
str | String | Concatenate to string |
str-len | String | String length |
str-upper str-lower | String | Case conversion |
str-contains | String | Substring check |
str-split str-join | String | Split/join strings |
str-trim | String | Trim whitespace |
str-replace | String | Replace substring |
str-starts-with str-ends-with | String | Prefix/suffix check |
substr | String | Substring extraction |
str-matches | String | Regex match |
list | List | Create list |
cons | List | Prepend element |
car/head | List | First element |
cdr/tail | List | Rest of list |
nth | List | Element at index |
length | List | List length |
append | List | Concatenate lists |
reverse | List | Reverse list |
map filter reduce | List | Higher-order functions |
range | List | Number range |
flatten | List | Flatten nested lists |
sort-by | List | Sort with key function |
zip | List | Zip two lists |
empty? | List | Empty check |
dict | Dict | Create dictionary |
dict-get dict-set | Dict | Get/set value |
dict-keys dict-values | Dict | Keys/values |
dict-has | Dict | Key existence |
dict-merge | Dict | Merge dicts |
type | Type | Get type name |
string? number? bool? etc. | Type | Type predicates |
->string ->number ->bool | Type | Type conversion |
now today | Date | Current date/time |
date-format | Date | Format date |
date-add date-diff | Date | Date arithmetic |
calendar | Date | Month calendar data |
print println | I/O | Output text |
read-line | I/O | Read stdin line |
eval parse apply | Meta | Eval/parse/apply |
deftable | Database | Define typed table |
insert update delete | Database | CRUD |
query | Database | Query with filters |
browse edit | Database | Interactive views |
defform | Database | Calculator/CRUD form |
tables describe | Database | Schema info |
pack drop-table | Database | Maintenance |
count-records | Database | Row count |
add-item add | Agenda | Add items |
items | Agenda | Query items |
item-get item-set item-edit | Agenda | Item operations |
item-done item-count | Agenda | Complete/count |
defcategory | Agenda | Define category |
assign unassign | Agenda | Category assignment |
categories | Agenda | List categories |
defrule rules drop-rule | Agenda | Auto-categorization |
apply-rules auto-categorize | Agenda | Execute rules |
defview show views | Agenda | Saved queries |
items-on items-between | Agenda | Calendar queries |
add-item-today | Agenda | Quick-add for today |
smart-parse | Agenda | NLP parser |
deftemplate from-template | Agenda | Item templates |
templates drop-template | Agenda | Template management |
open-agenda use-agenda | Agenda | Multiple agendas |
agendas close-agenda | Agenda | Agenda management |
export-agenda import-agenda | Agenda | Import/export |
read-file write-file append-file | File | File I/O |
file-exists? list-files current-dir | File | File system |
clipboard-get clipboard-set | Clipboard | System clipboard |
http-get http-post | HTTP | HTTP requests |
json-parse json-stringify | HTTP | JSON encoding |
cursor-pos set-cursor-pos | Editor | Cursor position |
selection current-file buffer-text | Editor | Buffer access |
insert-at replace-range | Editor | Text editing |