A lightweight, modular text editor built on antirez's kilo. Features vim-like modal editing, async HTTP for AI completions, Lua scripting, and clean separation between core (1.3K lines) and features (1.2K lines). Zero configuration needed - just compile and run.
- Minimalist: ~2.5K lines of C99 code organized into focused modules
- Modular: Core (1.3K lines) + features separated into dedicated modules
- Fast: Direct VT100 escape sequences, no curses overhead
- Robust: All critical bugs fixed (buffer overflows, NULL checks, signal safety)
- Safe: Binary file detection, proper error handling
- Syntax highlighting: C/C++ built-in, extensible via Lua
- Modal editing: Vim-like modes (NORMAL/INSERT/VISUAL) with h/j/k/l navigation
- Lua/LuaJIT: Full Lua environment for extensibility
- Project-local config:
.loki/init.luaoverrides~/.loki/init.lua - Interactive console: Toggle the collapsible Lua REPL with
Ctrl-L - Built-in helpers:
help,history,clear,clear-history,exit - Full standard library: io, os, math, string, table, etc.
- Non-blocking requests: Editor stays responsive during API calls
- AI-ready: OpenAI, Anthropic Claude, local LLM integration
- Multi-concurrent: Up to 10 simultaneous requests
- Callback-based: Clean async pattern with Lua callbacks
- libcurl-powered: Reliable, battle-tested HTTP client
- Auto-detection: Finds Homebrew Lua/LuaJIT and libcurl automatically
- Local override: Project-specific
.loki/config takes precedence - Zero-config: Works out of the box with sensible defaults
- Example configs: Complete AI integration examples included
Loki uses a modular architecture with a minimal core and feature modules:
src/
├── loki_core.c (1,336 lines) - Minimal editor core
│ ├── Terminal I/O and raw mode
│ ├── Buffer and row management
│ ├── Syntax highlighting infrastructure
│ ├── Screen rendering (VT100 sequences)
│ ├── File I/O operations
│ ├── Cursor movement primitives
│ └── Basic editing operations
│
├── loki_languages.c (494 lines) - Language support
│ ├── Built-in language definitions (C, Python, Lua, Markdown)
│ ├── Dynamic language registration via Lua
│ └── Syntax highlighting rules
│
├── loki_modal.c (407 lines) - Modal editing
│ ├── NORMAL mode (navigation, commands)
│ ├── INSERT mode (text insertion)
│ ├── VISUAL mode (text selection)
│ └── Vim-like keybindings (h/j/k/l, i/a/o/v)
│
├── loki_selection.c (156 lines) - Selection & clipboard
│ ├── Selection tracking and highlighting
│ ├── OSC 52 clipboard protocol (SSH-compatible)
│ └── Base64 encoding for terminal clipboard
│
├── loki_search.c (128 lines) - Search functionality
│ ├── Incremental search with live preview
│ ├── Forward/backward navigation
│ └── Match highlighting
│
├── loki_editor.c - Main editor loop & Lua integration
├── loki_lua.c - Lua C API bindings
└── main_editor.c - Entry point
Benefits:
- Maintainability: Each module has single, well-defined responsibility
- Testability: Features can be tested in isolation
- Extensibility: New features don't bloat the core
- Clarity: Core editor logic separate from feature implementations
brew install lua curl # or: brew install luajit curl
# Optional: brew install readline # enables enhanced CLI history/highlighting# Build the editor (build/loki-editor)
make editor
# Build the REPL (build/loki-repl)
make repl
# Build everything (library + both binaries)
make allThe Makefile is a thin wrapper over CMake and drops artifacts under build/. Use make lib if you only need libloki.
Prefer direct CMake invocations? Run cmake -S . -B build once, then cmake --build build --target <target>. Available targets match the Makefile aliases (libloki, loki-editor, loki-repl, show-config).
Requires: C99 compiler, POSIX system (Linux, macOS, BSD)
./build/loki-editor <filename>Opens the file in the interactive editor.
# Run AI completion on a file and save the result
./build/loki-editor --complete <filename>
# Run AI explanation on a file and print to stdout
./build/loki-editor --explain <filename>
# Show help
./build/loki-editor --helpRequirements for AI commands:
- Set
OPENAI_API_KEYenvironment variable - Configure
.loki/init.luaor~/.loki/init.luawith AI functions (see.loki.example/)
./build/loki-repl # Interactive shell
./build/loki-repl script.lua # Execute a Lua script and exit- Shares the same Lua bootstrap/config loader as the editor.
- Use
--trace-httpto mirrorKILO_DEBUGlogging for async HTTP calls. - Command history is persisted to
.loki/repl_historywhen available. - Type
help(or:help) to list built-in commands. - Uses GNU Readline/libedit when available (history, emacs keybindings, inline syntax colour); falls back to a minimal line editor otherwise.
- Lines are re-rendered with Lua-aware syntax highlighting as you execute them.
The REPL loads the same Lua modules as the editor and exposes a higher-level namespace:
ai.prompt "call the project bot" -- sends an async HTTP request via loki.async_http
-- Provide explicit options
ai.prompt("summarise README", {
model = "gpt-5-nano",
callback = "ai_response_handler", -- Lua function name
url = os.getenv("LOKI_AI_URL"),
})
-- Use the editor namespace helpers
editor.status.set("hello from the repl")
print(editor.buffer.line_count())Global Commands (work in all modes):
CTRL-S: Save fileCTRL-Q: Quit (with unsaved changes warning)CTRL-F: Find string in file (ESC to exit, arrows to navigate)CTRL-L: Toggle Lua REPL (collapsed by default, typehelpfor commands)
Modal Editing (Vim-like):
Loki starts in NORMAL mode by default. Press i to enter INSERT mode and start typing.
NORMAL Mode (navigation and commands):
h/j/k/l- Move cursor left/down/up/righti- Enter INSERT mode before cursora- Enter INSERT mode after cursoro- Insert new line below and enter INSERT modeO- Insert new line above and enter INSERT modev- Enter VISUAL mode (text selection)x- Delete character under cursor{/}- Jump to previous/next empty line (paragraph motion)- Arrow keys also work for navigation
INSERT Mode (text editing):
- Type normally to insert text
ESC- Return to NORMAL mode- Arrow keys move cursor
SHIFT+Arrow- Start/extend selectionCTRL-C- Copy selection to clipboard (OSC 52)
VISUAL Mode (text selection):
h/j/k/lor Arrow keys - Extend selectiony- Yank (copy) selection and return to NORMAL modeESC- Return to NORMAL mode
Disable modal editing (optional):
Add to .loki/init.lua:
-- Start in INSERT mode instead (traditional editor)
modal.disable()See docs/modal_editing.md for complete documentation.
Loki includes an embedded Lua interpreter for customization and automation.
Loki loads Lua configuration with local override support:
.loki/init.lua- Project-specific config (current directory)~/.loki/init.lua- Global config (home directory)
If a local .loki/init.lua exists, the global config is not loaded.
Quick start:
# Copy example configuration
cp -r .loki.example .loki
# Edit to customize
vim .loki/init.luaPress Ctrl-L to toggle the Lua REPL docked below the status bar. The panel stays hidden when idle, so the editor keeps its full height until you need it.
- Type any expression at the
>>prompt and pressEnterto evaluate it. - Results (or errors) stream into the log above the prompt;
clearwipes the log. - Use
Up/Downto browse command history,Ctrl-Uto clear the current line, andclear-historyto drop past entries. - Built-in commands:
help,history,clear,clear-history,exit. - Press
Esc,Ctrl-C,Ctrl-L, or typeexitto return to normal editing.
>> count_lines() -- Show line count
= 421
>> insert_timestamp() -- Insert current date/time
>> loki.status("Hello!") -- Set status message- Call
loki.repl.register(name, description[, example])inside.loki/init.luato surface project-specific helpers in the REPLhelpoutput. - See
docs/REPL_EXTENSION.mdfor practical recipes and integration patterns.
The loki global table provides these functions:
Synchronous Functions:
loki.status(msg)- Set status bar messageloki.get_lines()- Get total number of linesloki.get_line(row)- Get line content (0-indexed)loki.get_cursor()- Get cursor position (row, col)loki.insert_text(text)- Insert text at cursorloki.get_filename()- Get current filename
Async HTTP:
loki.async_http(url, method, body, headers, callback)- Non-blocking HTTP requests
The async HTTP function enables powerful integrations:
- AI completions (OpenAI, Claude, local models)
- Code formatting/linting services
- Real-time documentation lookup
- Git/GitHub API integration
- Any HTTP API without blocking the editor
Example usage:
function ai_complete()
local text = get_buffer_text()
loki.async_http(
"https://api.openai.com/v1/chat/completions",
"POST",
json_body,
{"Content-Type: application/json", "Authorization: Bearer ..."},
"ai_response_handler"
)
end
function ai_response_handler(response)
-- Called automatically when response arrives
loki.insert_text(parse_response(response))
endSee .loki.example/init.lua for complete examples including AI integration.
Define loki.highlight_row(row_index, line_text, render_text, syntax_type, default_applied)
inside .loki/init.lua (or ~/.loki/init.lua) to extend or replace syntax
highlighting from Lua. Return nil to keep the built-in rules, or return a
table of span descriptors to colour specific regions:
function loki.highlight_row(idx, text, render, syntax_type, default_applied)
return {
replace = true, -- optional: clear existing highlight first
spans = {
{ start = 1, length = 3, style = "keyword1" },
{ start = 10, stop = 20, style = "comment" },
},
}
endStyle strings map to constants exposed under loki.hl (match, string,
keyword1, etc.). To layer extra cues without discarding the defaults, omit
replace and simply return { spans = {...} }.
This is a fork with enhancements:
- Modular architecture - Minimal core (1.3K lines) + feature modules (1.2K lines)
- Modal editing - Vim-like NORMAL/INSERT/VISUAL modes with h/j/k/l navigation
- All critical bugs fixed - Buffer overflows, NULL checks, signal safety
- Lua/LuaJIT scripting - Via Homebrew, dynamically linked
- Async HTTP support - Non-blocking, libcurl-based
- AI integration examples - OpenAI, compatible APIs
- Project-local configuration -
.loki/override - Binary file protection - Detects and refuses to open binary files
- Improved error handling - Comprehensive error checking throughout
Architecture:
- Core: 1,336 lines (terminal I/O, buffers, rendering, syntax infrastructure)
- Features: 1,185 lines across 4 modules (languages, modal, selection, search)
- Total: ~2.5K lines of clean, modular C99 code
Dependencies (via Homebrew):
- Lua or LuaJIT
- libcurl
Binary size: ~72KB (dynamically linked)
Original kilo editor by Salvatore Sanfilippo (antirez). Lua integration and enhancements added 2025
Released under the BSD 2 clause license.