Skip to content

nez/xmas

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

xmas

A minimal Emacs in Clojure.

Architecture

Everything is a buffer, every action is a state transition.

xmas.buf — Buffer data type

A buffer is an immutable map: {:name :text :point :mark :file :modified :mode :undo} — a string, a cursor position (integer offset), and metadata.

Three operations define the type:

  • make — Constructor. Name, optional text, optional file path.
  • edit — The single edit primitive. Splices repl into text[from..to], adjusts point, records undo, marks modified. Every mutation (insert, delete, kill) bottoms out here.
  • undo — Pops the last edit. Returns nil when empty.

set-point clamps a movement function's result to valid range — all cursor movement goes through it.

xmas.text — Text primitives

Pure functions on strings. No editor state, no buffers — just text and positions.

xmas.ed — Editor core

A single atom holds the entire editor state:

{:buf "*scratch*"          ;; name of active buffer
 :bufs {"*scratch*" {...}} ;; all buffers by name
 :kill []                  ;; kill ring (clipboard stack)
 :msg nil                  ;; echo area message
 :mini nil                 ;; minibuffer state (when prompting)
 :scroll 0, :rows R, :cols C, :last-key K}

cur and update-cur bridge editor state to the current buffer. edit and set-point delegate to buf/ through this bridge.

Commands are pure state -> state functions. forward-char, kill-line, yank, save-buffer — they all take editor state and return new state. No side effects except save-buffer (which spits to disk).

Minibuffer — Prompts (find-file, switch-buffer) reuse the buffer abstraction. mini-start creates a temporary buffer named " *mini*" and swaps it in as active. Normal editing commands just work inside it. mini-accept reads the input and calls the callback.

Keybindings — A nested map. Simple keys map to command functions. [:ctrl \x] maps to a sub-map for prefix sequences (C-x C-f, C-x C-s, etc.). handle-key dispatches, with chars falling through to self-insert.

xmas.term — Terminal I/O

Raw terminal control without JLine. Two halves:

  • Input: enter-raw-mode! via stty, then reads bytes from System.in. read-key decodes raw bytes into semantic keys — chars, [:ctrl \f], [:meta \b], :up, :backspace, etc. Multi-byte UTF-8 is decoded into codepoints. CSI escape sequences are parsed for arrow keys and special keys.

  • Output: Thin wrappers over ANSI escape codes — move, cls, clreol, sg (set graphics/colors). Everything writes to System.out directly.

xmas.view — Screen rendering

Converts editor state into terminal output:

  • cursor-pos — Maps a text offset to a [row, col] screen position relative to scroll.
  • render — Draws the full screen: text lines, a mode line (Emacs-style -- *scratch* (fundamental)), and the minibuffer/echo area. Returns the new scroll position.

xmas.log — Debug logging

Append-only file logger to ~/.xmas/debug.log. Every keypress byte and decoded key is logged.

nREPL — live hacking

xmas starts an nREPL server on port 7888. Connect from Emacs/CIDER:

M-x cider-connect RET localhost RET 7888

Or from the command line:

emacsclient -e '(cider-connect-clj (list :host "localhost" :port 7888))'

Once connected, the running editor is fully inspectable and modifiable:

;; inspect state
(keys @xmas.ed/editor)
(:text (xmas.ed/cur @xmas.ed/editor))

;; inject text into the live editor
(swap! xmas.ed/editor xmas.ed/edit 0 0 "hello from emacs!\n")

;; add a keybinding at runtime
(alter-var-root #'xmas.ed/bindings assoc [:ctrl \t] xmas.ed/next-line)

;; define a new command and bind it
(defn my-cmd [s] (xmas.ed/msg s "it works"))
(alter-var-root #'xmas.ed/bindings assoc [:meta \z] my-cmd)

;; open a file programmatically
(swap! xmas.ed/editor xmas.ed/find-file "/tmp/test.txt")

;; switch buffer
(swap! xmas.ed/editor xmas.ed/switch-buffer "*scratch*")

Every command is a pure state -> state function. swap! applies it atomically. Changes appear on the next redisplay cycle (any keypress in xmas).

Data flow

read-key -> handle-key(state, key) -> new state -> render(state) -> terminal
     ^                                                                  |
     '----------------------- command-loop ----------------------------'

The main loop is tight: read a key, dispatch to a command, get new state, render, repeat. The atom is only reset!'d at the loop boundary — commands themselves are pure.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages