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:

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
These builtins require EEditor's callback system. In standalone CLI mode, they return nil or a message.

Data Types

EELisp has 17 value types:

TypeExampleDescription
number42, 3.14Double-precision float
string"hello"Text with escape sequences
booltrue, falseBoolean
nilnilNull / empty
symbolfoo, +Identifier
keyword:nameKey identifier for dicts
list(1 2 3)Ordered sequence
dict{:a 1 :b 2}Ordered key-value map
function(fn (x) x)Lambda / closure
datevia (now)Date/time value
tablevia (deftable)Database table definition
recordvia (insert)Single row with auto-ID
result-setvia (query)Multiple records
itemvia (add-item)Agenda item
categoryvia (defcategory)Hierarchical category
rulevia (defrule)Auto-categorization rule
viewvia (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

VariableDescription
textItem text
notesItem notes
idItem ID
categoriesCurrent categories list
created, modifiedTimestamps
(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

CategoryPatterns
Datestomorrow, today, yesterday, next Monday, this weekend, in 3 days, in 2 weeks, end of week, end of month, ISO dates, month names
PriorityURGENT/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)
File operations are scoped to a base path for security. Paths are relative to this base.

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

FunctionCategoryDescription
+ - * /ArithmeticBasic math operations
modArithmeticModulo
abs min maxArithmeticAbsolute value, min, max
floor ceil roundArithmeticRounding
powArithmeticExponentiation
= != < > <= >=ComparisonComparisons
notLogicBoolean negation
strStringConcatenate to string
str-lenStringString length
str-upper str-lowerStringCase conversion
str-containsStringSubstring check
str-split str-joinStringSplit/join strings
str-trimStringTrim whitespace
str-replaceStringReplace substring
str-starts-with str-ends-withStringPrefix/suffix check
substrStringSubstring extraction
str-matchesStringRegex match
listListCreate list
consListPrepend element
car/headListFirst element
cdr/tailListRest of list
nthListElement at index
lengthListList length
appendListConcatenate lists
reverseListReverse list
map filter reduceListHigher-order functions
rangeListNumber range
flattenListFlatten nested lists
sort-byListSort with key function
zipListZip two lists
empty?ListEmpty check
dictDictCreate dictionary
dict-get dict-setDictGet/set value
dict-keys dict-valuesDictKeys/values
dict-hasDictKey existence
dict-mergeDictMerge dicts
typeTypeGet type name
string? number? bool? etc.TypeType predicates
->string ->number ->boolTypeType conversion
now todayDateCurrent date/time
date-formatDateFormat date
date-add date-diffDateDate arithmetic
calendarDateMonth calendar data
print printlnI/OOutput text
read-lineI/ORead stdin line
eval parse applyMetaEval/parse/apply
deftableDatabaseDefine typed table
insert update deleteDatabaseCRUD
queryDatabaseQuery with filters
browse editDatabaseInteractive views
defformDatabaseCalculator/CRUD form
tables describeDatabaseSchema info
pack drop-tableDatabaseMaintenance
count-recordsDatabaseRow count
add-item addAgendaAdd items
itemsAgendaQuery items
item-get item-set item-editAgendaItem operations
item-done item-countAgendaComplete/count
defcategoryAgendaDefine category
assign unassignAgendaCategory assignment
categoriesAgendaList categories
defrule rules drop-ruleAgendaAuto-categorization
apply-rules auto-categorizeAgendaExecute rules
defview show viewsAgendaSaved queries
items-on items-betweenAgendaCalendar queries
add-item-todayAgendaQuick-add for today
smart-parseAgendaNLP parser
deftemplate from-templateAgendaItem templates
templates drop-templateAgendaTemplate management
open-agenda use-agendaAgendaMultiple agendas
agendas close-agendaAgendaAgenda management
export-agenda import-agendaAgendaImport/export
read-file write-file append-fileFileFile I/O
file-exists? list-files current-dirFileFile system
clipboard-get clipboard-setClipboardSystem clipboard
http-get http-postHTTPHTTP requests
json-parse json-stringifyHTTPJSON encoding
cursor-pos set-cursor-posEditorCursor position
selection current-file buffer-textEditorBuffer access
insert-at replace-rangeEditorText editing