🌈 ~khtdr/

home / Tutorials / Creating a Custom Emacs Mode

Learn how to create an interactive Emacs mode that maintains state between sessions

Introduction

In this tutorial, we'll create a custom major mode for Emacs that demonstrates how to:

  • Associate a new file extension with your mode
  • Create an interactive buffer with buttons
  • Maintain state between sessions
  • Handle file serialization

The Example: foo-mode

We'll create a simple mode that:

  • Associates with .foo files
  • Displays a clickable button
  • Tracks how many times the button was clicked
  • Saves the count when the buffer is saved
  • Restores the count when the file is reopened

Implementation

Here's the complete implementation of our foo-mode:

;;; foo-mode.el --- A tiny stateful major mode  -*- lexical-binding: t -*-

(defvar foo-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") #'foo-increment-count)
    map)
  "Keymap for `foo-mode'.")

(defvar-local foo-count 0
  "Number of clicks recorded in the current buffer.")

(define-derived-mode foo-mode special-mode "Foo"
  "Major mode for .foo files with a clickable counter.
The file on disk holds just the count as a number.  The buffer
shows a rendered view of it, which is regenerated as needed."
  ;; When the mode starts, the buffer contains the raw file
  ;; contents.  Parse the count out of them before redrawing.
  (setq foo-count (string-to-number (buffer-string)))
  ;; Take over saving: we write the count, not the buffer text.
  (add-hook 'write-contents-functions #'foo-mode-save nil t)
  (foo-mode-refresh)
  (set-buffer-modified-p nil))

(defun foo-mode-refresh ()
  "Redraw the buffer from `foo-count'."
  (let ((inhibit-read-only t))
    (erase-buffer)
    (insert (format "Count: %d

" foo-count))
    (insert-button "Click me!"
                   'action (lambda (_button) (foo-increment-count))
                   'follow-link t)
    (goto-char (point-min))))

(defun foo-increment-count ()
  "Increment the counter and refresh the display."
  (interactive)
  (setq foo-count (1+ foo-count))
  (foo-mode-refresh)
  (set-buffer-modified-p t))

(defun foo-mode-save ()
  "Write `foo-count' to the visited file.
Returns non-nil so that Emacs does not also write the buffer text."
  (write-region (number-to-string foo-count) nil buffer-file-name)
  (set-visited-file-modtime)
  (set-buffer-modified-p nil)
  (message "Saved count %d to %s" foo-count buffer-file-name)
  t)

(add-to-list 'auto-mode-alist '("\\.foo\\'" . foo-mode))

(provide 'foo-mode)

Usage

  1. Save the code above to ~/.emacs.d/lisp/foo-mode.el
  2. Add to your Emacs init file:

    (add-to-list 'load-path "~/.emacs.d/lisp")
    (require 'foo-mode)
  3. Create a new file with the .foo extension
  4. The buffer will show a count and a clickable button
  5. Click the button or press C-c C-c to increment
  6. Save the file (C-x C-s) to persist the count
  7. Close and reopen to see the count restored

How It Works

Mode Definition

The mode is defined using define-derived-mode, which creates a new major mode derived from special-mode. Deriving from special-mode gives us a read-only buffer out of the box, which is what we want for a buffer that is rendered rather than typed into. It also gives us the usual conveniences like q to quit the window.

State Management

  • foo-count is a buffer-local variable that stores the click count
  • foo-mode-refresh erases the buffer and redraws it from foo-count. Because the buffer is read-only, we bind inhibit-read-only while doing so.
  • foo-increment-count bumps the counter, redraws, and marks the buffer modified so that C-x C-s knows there is something to save

Loading

When Emacs opens a .foo file, it reads the file into the buffer and then activates the mode. So at the top of the mode body, the buffer contains the raw file contents: just a number. We parse it with string-to-number (an empty buffer, as with a brand new file, gives 0), then redraw and mark the buffer unmodified so Emacs does not immediately think you have unsaved changes.

Saving

This is the interesting part. The buffer text is a rendered view, not the data, so we do not want Emacs to write it to disk. Instead we add foo-mode-save to write-contents-functions, buffer-locally. Emacs runs these functions when you save, and if one of them returns non-nil, Emacs considers the save handled and skips writing the buffer itself.

Inside foo-mode-save we write just the number with write-region, then call set-visited-file-modtime so Emacs knows the change on disk was ours (otherwise the next save would warn that the file changed on disk), and clear the modified flag.

File Association

The mode automatically activates for .foo files through the auto-mode-alist addition.

Interaction

The mode provides two ways to increment the counter:

  1. Clicking the button (using insert-button)
  2. Using the C-c C-c keybinding

Conclusion

This example demonstrates key concepts in Emacs mode development:

  • Creating a new major mode
  • Managing buffer-local state
  • Handling file persistence
  • Adding interactive elements
  • Setting up key bindings

You can use these patterns to create more complex modes for your specific needs.