Emacs config file

Table of Contents

This note is part of org-roam repository and drives the generation of this site.

Motivation

Pablo Stafforini's post inspired me to make this config file as a literate configuration using org mode. org-babel tangles this config file to ~/.config/emacs/init.el, available as part of dotfiles.

Initialization

To future proof configuration file, set lexical-binding as true.

;;; init.el -*- lexical-binding: t; -*-

;; This file is maintained as a literate file in
;; 20260510115218-emacs_config_file.org

files and folders

(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "better-defaults" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "beancount-mode" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "ox-gemini" user-emacs-directory))

better-defaults

There are multiple packages named as better-defaults. The one i use is from technomancy. Based on my configuration and preferences, i repeat or override some of the settings in that package.

(use-package better-defaults
  :ensure nil
  :demand t
  :config
  ;; Overrides: these run after the package's own settings.
  (ido-mode -1)                                   ; vertico replaces ido
  (setq visible-bell nil)
  (setq ring-bell-function #'ignore)
  (global-set-key (kbd "C-x C-b") #'consult-buffer))

backup settings

The backup folder location and few related settings are repeated with some extra settings compared to better-defaults package.

(setq backup-directory-alist `(("." . ,(concat user-emacs-directory
                                               "backups")))
      version-control t
      delete-old-versions t
      kept-new-versions 5
      backup-by-copying t
      create-lockfiles nil) ;; Prevents .# creation

custom

Better-defaults redirects the "Custom" stuff to a separate file. This is kept here purely as reminder.

;; (setq custom-file (expand-file-name "custom.el" user-emacs-directory))
;; (when (file-exists-p custom-file)
;;   (load custom-file))

package sources

;; define package-user-dir
(setq package-user-dir
      (expand-file-name (format "elpa-%s" (system-name)) user-emacs-directory))
;;Define the package archives
(setq package-archives '(("gnu"   . "https://elpa.gnu.org/packages/")
                         ("melpa" . "https://melpa.org/packages/")))

;; Set priorities to prefer GNU over MELPA
(setq package-archive-priorities '(("gnu" . 10)
                                   ("melpa" . 5)))

;; Initialize the package system
(package-initialize)

use-package

As use-package is built into emacs 29 onwards simplified the version. Originally used the version from ianyepan.github.io. Later made some changes based on official guide and kingcons and steckemacs.

(require 'use-package)
(setq use-package-always-ensure t)

use-feature

For packages that are built-into Emacs, this function from Pablo Stafforini is being used.

(defmacro use-feature (name &rest args)
  "Like `use-package' but for built-in features (sets :ensure nil).
NAME and ARGS as in `use-package'."
  (declare (indent defun))
  `(use-package ,name
     :ensure nil
     ,@args))

Settings

smooth scrolling

This setting for wayland(pgtk) version of emacs was suggested at batsov.com.

(pixel-scroll-precision-mode)

consistency

To change default font or size in Emacs, i referred to stackoverflow post.

(set-face-attribute 'default nil :font "Hack-13" )

spelling

Suddenly corfu started reporting a lot of errors. So added spellchecker name.

;; (advice-add 'ispell-completion-at-point :override #'ignore)
(with-eval-after-load 'ispell
  (setq ispell-program-name "hunspell")
  (setenv "DICPATH" "/usr/share/hunspell")
  (remove-hook 'completion-at-point-functions #'ispell-completion-at-point))

transparency

This settings are needed to ensure that emacs remain usable both in light and dark mode.

(setq window-divider-default-places t)
(set-face-background 'internal-border (face-background 'default))
;; For PGTK, use alpha-background instead of alpha
(add-to-list 'default-frame-alist '(alpha-background . 85))
(defun toggle-transparency ()
  "Toggle between transparent and opaque background."
  (interactive)
  (let ((current-alpha (frame-parameter nil 'alpha-background)))
    (set-frame-parameter
     nil 'alpha-background
     (if (< (or current-alpha 100) 100) 100 85))))

(global-set-key (kbd "C-c t") 'toggle-transparency)

scratch buffer

Turn off the startup screen, splash screen and puts straight into the scratch buffer to quickly type or paste in emacs instead of standard emacs startpage.

(setq inhibit-startup-screen t)
(setq inhibit-startup-message t)
(setq initial-scratch-message "")
(setq initial-major-mode 'fundamental-mode)

theme

Using auto-dark package to switch theme for emacs. Emacs theme changes based on the gtk settings. The modus-operandi is a light theme built into emacs.

(use-package nord-theme
  :ensure t
  :init
  (setq nord-region-highlight "frost")
  :config
  (load-theme 'nord t))

(use-package auto-dark
  :ensure t
  :after nord-theme
  :custom
  (auto-dark-themes '((nord) (modus-operandi)))
  :init (auto-dark-mode))

programming setup

Define few reasonable defaults suitable for programming modes. Trailing whitespace is removed and tabs are converted to spaces before saving, except in buffers that use `indent-tabs-mode'.

(setq-default indent-tabs-mode nil)

(defun my/untabify-buffer ()
  "Untabify the whole buffer, unless this buffer is meant to use tabs."
  (unless (or indent-tabs-mode buffer-read-only)
    (untabify (point-min) (point-max))))

(defun my-programming-setup ()
  "General settings for ALL programming modes"
  (column-number-mode 1)
  (setq-local display-line-numbers-type 'relative)
  (display-line-numbers-mode 1)
  (add-hook 'before-save-hook #'delete-trailing-whitespace nil t)
  (add-hook 'before-save-hook #'my/untabify-buffer nil t))

;; Apply this to every programming language automatically
(add-hook 'prog-mode-hook #'my-programming-setup)

custom functions

(defun my/now ()
  "Insert string for the current time formatted like '2:34 PM'."
  (interactive)
  (insert (format-time-string "%R")))

(defun my/today ()
  "Insert string for today's date nicely formatted YYYY-MM-DD."
  (interactive) 
  (insert (format-time-string "%F")))

(defun my/doasedit ()
  "Re-open current file with doas (elevated) privileges."
  (interactive)
  (if (buffer-file-name)
      (let ((cursor-pos (point))
            (truename (file-truename (buffer-file-name))))
        (find-alternate-file (concat "/doas::" truename))
        (goto-char cursor-pos))
    (message "This buffer is not visiting a file!")))

Active packages

helpful

(use-package helpful
  :ensure t
  :init
  (require 'bind-key)
  :bind
  (("C-h f" . helpful-callable)  ; Replace describe-function
   ("C-h v" . helpful-variable)  ; Replace describe-variable
   ("C-h k" . helpful-key)       ; Replace describe-key
   ("C-h x" . helpful-command))) ; Replace describe-command

tramp

(use-feature tramp
  :defer t
  :config
  (setq tramp-auto-save-directory (expand-file-name "tramp-autosave" user-emacs-directory)
        password-cache-expiry 3600))

outshine

Outshine brings Org Mode to the world outside of Org;;

(use-package outshine
  :ensure t
  :diminish outline-minor-mode
  :commands outshine-hook-function
  :hook (emacs-lisp-mode . outshine-mode)
  :init
  (setq outshine-imenu-show-headlines-p nil))

diminish

Diminished modes are minor modes with no modeline display

(use-package diminish
  :ensure t
  )

vertico

vertico, consult, marginalia, orderless, embark, corfu and cape together have replaced helm, org-rifle, helm-swoop, Company etc

(use-package vertico
  :ensure t
  :init
  (vertico-mode)
  ;; Settings for a minimal UI.
  (setq vertico-cycle t)
  (setq vertico-count 10)
  :config
  (vertico-mouse-mode 1))
(use-package consult
  :ensure t
  :bind (
         ("C-x b" . consult-buffer)
         ("C-s" . consult-line)
         ("C-c r" . consult-ripgrep)
         :map isearch-mode-map
         ("M-s s" . consult-line))
  )

(use-package marginalia
  :ensure t
  :init
  (marginalia-mode))

(use-package orderless
  :ensure t
  :custom
  (completion-styles '(orderless basic))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles . (partial-completion)))))
  :init
  ;; Orderless provides powerful fuzzy-matching
  (setq completion-ignore-case t)
  (setq completion-flex-enable t))

(use-package embark
  :ensure t
  :bind (("C-." . embark-act)
         ("C-S-y" . embark-consult-kill-ring))
  :init
  (setq prefix-help-command #'embark-prefix-help-command)
  )
(use-package embark-consult
  :ensure t
  :after (embark consult))

corfu

(use-package corfu
  :ensure t
  :if (display-graphic-p)
  :custom
  (corfu-auto t)                 ; Enable auto-completion
  (corfu-auto-prefix 3)          ; Wait for 3 characters
  (corfu-auto-delay 0.2)         ; Wait 0.2s
  :bind (:map corfu-map
              ("M-p" . corfu-popupinfo-scroll-down)
              ("M-n" . corfu-popupinfo-scroll-up)
              ("M-d" . corfu-popupinfo-toggle))
  :init
  (global-corfu-mode)
  :config
  ;; Enable the documentation popup
  (corfu-popupinfo-mode 1)
  ;; Visual tweaks for the documentation child-frame
  (setq corfu-popupinfo-parameters
      '((vertical-scroll-bars . nil)
        (horizontal-scroll-bars . nil)
        (inner-border-width . 1)
        (left-fringe . 8)
        (right-fringe . 8))))

cape

Cape provides in-buffer drop-down completion

(defun my-cape-keyword-setup ()
  "Add keyword completion to the current buffer."
  (add-to-list 'completion-at-point-functions #'cape-keyword t))

(use-package cape
  :ensure t
  :bind ("C-c p f" . cape-file)
  :init
  (add-to-list 'completion-at-point-functions #'cape-dabbrev)
  :hook (prog-mode . my-cape-keyword-setup)
  )

ediff

;; Define a function to quit ediff with a single 'q' press
;; Enhanced settings for ediff in Emacs
(use-feature ediff
  :config
  (setq ediff-split-window-function 'split-window-horizontally
        ediff-window-setup-function 'ediff-setup-windows-plain
        ediff-diff-options "-w")
  (defun my-ediff-quit-session ()
    (interactive)
    (ediff-quit 'no-confirm))
  (add-hook 'ediff-keymap-setup-hook
            (lambda ()
              (define-key ediff-mode-map (kbd "q") 'my-ediff-quit-session))))

magit

magit is one nice front end for git

(use-package magit
  :ensure t
  :bind (
         ("C-x g" .     magit-status)
         ("C-x M-g"     . magit-dispatch)
         ("C-c M-g"     . magit-file-dispatch)))

org mode

Org Mode is used to maintain this file and used to create this site using Org Roam. To extend the auto-tangle for additional org files, a defvar list is used. For single init file, string was used. Now member is used to match the filename.

(defvar my/auto-tangle-files
  '("20260510115218-emacs_config_file.org"
    "20240317205837-swaywm.org"
    "20260616190442-sway_configuration_file.org"
    "20260617160421-sway_theme_toggle_script.org"
    "20240317210436-btrbk.org"
    "20260926215135-source_for_css.org"
    "20261011150111-ox_gemini.org"
    "20260529152048-graph_js.org"
    "20260529114550-gen_graph.org"
    "20260531212337-neighbour-loader_js.org"
    "20260601134637-neighbour_utils_js.org"
    "20260601135932-mobile_menu_js.org"
    "20260601144841-search_js.org"))
(defun my-org-mode-setup ()
  (electric-indent-local-mode -1)
  (when (and buffer-file-name
             (member (file-name-nondirectory buffer-file-name)
                     my/auto-tangle-files))
    (add-hook 'after-save-hook #'org-babel-tangle nil t)))

(use-feature org
  :custom
  (org-return-follows-link t) ;;Allows links to be opened without C-c C-o in emacs
  (org-id-link-to-org-use-id t) ;; Adds properties to record org ID's to non-roam files too
  (org-agenda-files '("/data/myhome/prabu/org/agenda"
                      ))
  (org-log-done t)
  (org-todo-keywords '((sequence "TODO(t)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELED(c@)")))
  (org-tag-alist '((:startgroup . nil)
                   ("@kpm" . ?k) ("@home" . ?h)
                   ("@errands" . ?e)
                   (:endgroup . nil)
                   ("@computer" . ?c) ("@phone" . ?p) ("@reading" . ?r)))
  (org-structure-template-alist '(("el" . "src emacs-lisp")
                                  ("py" . "src python")
                                  ("js" . "src javascript")
                                  ("s" . "src")
                                  ("e" . "example")
                                  ("q" . "quote")))
  :bind (
         ("C-c l" . org-store-link)
         ("C-c a" . org-agenda)
         ("C-c c" . org-capture)
         )
  :config
  (keymap-set org-mode-map "C-c [" nil)
  (keymap-set org-mode-map "C-c ]" nil)
  ;; (setq time-stamp-active t)
  ;; (setq time-stamp-line-limit 10)               ; Plenty of room for line 4
  ;; (setq time-stamp-start "^#\\+date:[ \t]*")    ; Just find the header
  ;; (setq time-stamp-end "$")                     ; Replace everything until end of line
  ;; (setq time-stamp-format "[%Y-%m-%d %a %H:%M:%S]")
  :hook (org-mode . my-org-mode-setup))

org-tempo

(use-feature org-tempo
  :after org)

org-roam

This site is generated from Org Roam. I installed and configured org-roam as per the sources mentioned.

;; https://emacs.stackexchange.com/questions/77295/org-roam-case-insensitive-search
(defun my/case-insensitive-org-roam-node-read (orig-fn &rest args)
  "Make org-roam node search case-insensitive."
  (let ((completion-ignore-case t))
    (apply orig-fn args)))

(use-package org-roam
  :ensure t
  :init
  (setq org-roam-database-connector 'sqlite-builtin)
  :custom
  (org-roam-v2-ack t)
  (org-roam-completion-everywhere t)
  (org-roam-directory (file-truename "/data/myhome/prabu/org/Resources"))
  (org-roam-dailies-directory ".")
  (org-roam-dailies-capture-templates
   '(("d" "default" entry
      "* %^{Title}\n%?"
      :target (file+head "%<%Y-%m-%d>.journal.org"
                         "#+title: %<%Y-%m-%d>\n#+date: %<[%Y-%m-%d %a]>\n#+filetags: :journal:\n")
      :empty-lines 1)))
  :bind
  (("C-c n f" . org-roam-node-find)
   ("C-c n r" . org-roam-node-random)
   ("C-c n j" . org-roam-dailies-capture-today)
   ("C-c n g" . org-roam-dailies-goto-today)
   :map org-mode-map
   ;; org-mode-specific keybindings
   ("C-c n i" . org-roam-node-insert)
   ("C-c n o" . org-id-get-create)
   ("C-c n t" . org-roam-tag-add)
   ("C-c n a" . org-roam-alias-add)
   ("C-c n l" . org-roam-buffer-toggle)
   ("C-M-i" . completion-at-point))
  :config
  (require 'org-roam-dailies)
  (global-set-key (kbd "C-c n d") org-roam-dailies-map)
  ;; Dailies Keymap definitions
  (define-key org-roam-dailies-map (kbd "Y") #'org-roam-dailies-capture-yesterday)
  (define-key org-roam-dailies-map (kbd "T") #'org-roam-dailies-capture-tomorrow)
  ;; Notes Template
  (setq org-roam-capture-templates
        '(("d" "default" plain
           "%?"
           :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
                              "#+title: ${title}\n#+date: %<[%Y-%m-%d %a %H:%M:%S]>\n#+filetags: :draft:\n")
           :unnarrowed t)))
  ;; org-Roam Database and Sync
  (org-roam-db-autosync-mode)
  (setq org-id-extra-files (org-roam--list-files org-roam-directory))
  (setq org-roam-db-node-include-function
        (lambda () (= (org-outline-level) 0)))
  ;; Refresh the list automatically whenever node is changed or saved
  (add-hook 'org-roam-db-sync-hook
            (lambda ()
              (setq org-id-extra-files (org-roam--list-files org-roam-directory))))
  (advice-add 'org-roam-node-read :around #'my/case-insensitive-org-roam-node-read)
  (message "org-roam loaded"))

(use-package org-roam-ui
  :ensure t)

org-publish

The publishing of org-mode notes into gemini and html format is built upon org-publish, which is part of org-mode. A personal fork of ox-gemini package is used to export Org mode content to Gemtext format. The fork is available in my dotfiles referred earlier. The folder structure of gemini mirrors the html site's directory structure and the both are published simultaneously.

A single flat folder, filenames/tags distinguish journal from notes, no root-vs-docs split, published via org-publish like html.

(defvar my/org-gemini-directory (file-truename "/data/myhome/prabu/org/gemini")
    "Directory where exported Gemini files should be stored.")

(defvar my/org-html-directory (file-truename "/data/myhome/prabu/org/html_site")
  "Directory where exported html files should be stored.")

(defvar my/sitemap-exclude-files '("20260517153259-index.org" "20260513193407-about.org")
  "Files published normally, but omitted from auto-generated sitemaps.")

(defvar my/publish-site-script "publish_site"
  "This is a shell script to push changes to the server.")

(defvar my/site-scss-file (expand-file-name "css/simple.scss" org-roam-directory))

(defvar my/site-css-output (expand-file-name "style.css" my/org-html-directory))

(defun my/compile-site-css ()
  "Compile simple.scss to style.css via dart-sass."
  (call-process "sass" nil "*sass-compile*" nil my/site-scss-file my/site-css-output))

(defun my/org-html-preamble (_info)
  "Top nav bar, matching simple.scss's structure."
  (concat
   "<nav class=\"topnav\">\n"
   "  <div class=\"topnav-links\">\n"
   "    <a href=\"/index.html\" class=\"topnav-title\">Home</a>\n"
   "    <a href=\"/notes.html\">Notes</a>\n"
   "    <a href=\"/journal.html\">Journal</a>\n"
   "    <a href=\"/about.html\">About</a>\n"
   "  </div>\n"
   "</nav>\n"))

(defun my/gemini-nav-block ()
  "Site nav block, gemtext link-line format."
  (concat
   "=> notes.gmi\tNotes\n"
   "=> journal.gmi\tJournal\n"
   "=> about.gmi\tAbout\n"
   "\n"))

(defun my/org-gemini-add-nav (result)
  "Prepend the site nav block to RESULT, an exported gemini page."
  (concat (my/gemini-nav-block) result))

(defun my/sitemap-sorted-files (project-name)
  "Return (PROJECT . FILES) for PROJECT-NAME, newest first.
Journals sort by filename (dates), other projects by modification time.
Drops the project's own sitemap file, which `org-publish-get-base-files'
always includes, plus `my/sitemap-exclude-files'."
  (let* ((project (assoc project-name org-publish-project-alist))
         (excluded (cons (plist-get (cdr project) :sitemap-filename)
                         my/sitemap-exclude-files))
         (files (seq-remove
                 (lambda (f) (member (file-name-nondirectory f) excluded))
                 (org-publish-get-base-files project))))
    (cons project
          (sort files (if (string-prefix-p "journal" project-name)
                          #'string>
                        #'file-newer-than-file-p)))))

(defun my/html-sitemap-function-for (project-name)
  "Return an html-syntax sitemap function bound to PROJECT-NAME."
  (lambda (title _list)
    (pcase-let ((`(,project . ,files) (my/sitemap-sorted-files project-name)))
      (concat
       "#+title: " title "\n\n"
       (mapconcat
        (lambda (file)
          (format "- [[file:%s][%s]]\n"
                  (file-name-nondirectory file)
                  (org-publish-find-title file project)))
        files "")))))

(defun my/gemini-sitemap-function-for (project-name &optional subtitle)
  "Return a gemtext sitemap function bound to PROJECT-NAME.
Each label starts with an ISO date, so the page is also a valid gemsub feed.
SUBTITLE, if given, is emitted as a ## line right after the title."
  (lambda (title _list)
    (pcase-let ((`(,project . ,files) (my/sitemap-sorted-files project-name)))
      (concat
       "#+title: " title "\n\n"
       (when subtitle (concat "** " subtitle "\n\n"))
       (unless (string-prefix-p "journal" project-name)
         "The date listed here is the original published date. However the notes are sorted by last updated date.\n\n")
       (mapconcat
        (lambda (file)
          (let* ((date (format-time-string "%F" (org-publish-find-date file project)))
                 (found (org-publish-find-title file project))
                 (label (if (string= found date) (my/feed--first-heading file) found)))
            (format "- [[file:%s][%s]]\n"
                    (file-name-nondirectory file)
                    (if label (concat date " - " label) date))))
        files "")))))

(use-package htmlize
  :ensure t)

(use-feature ox-html
  :custom
  (org-html-validation-link nil)
  (org-html-htmlize-output-type 'css))

(use-package ox-gemini
  :ensure nil
  :after ox
  :config
  ;; Verbatim text and inline source use same backticks as inline
  ;; code.
  (setq org-ascii-verbatim-format "`%s`")
  (advice-add 'org-gemini-template :filter-return #'my/org-gemini-add-nav))

(use-feature ox-publish
  :after ox
  :config
  (setq org-export-with-sub-superscripts '{})
  (setq org-publish-project-alist
        `(("notes-html"
           :base-directory ,org-roam-directory
           :base-extension "org"
           :exclude "\\.journal\\.org\\'\\|\\.blog\\.org\\'"
           :publishing-directory ,my/org-html-directory
           :publishing-function org-html-publish-to-html
           :with-broken-links mark
           :with-toc 3
           :section-numbers nil
           :html-preamble my/org-html-preamble
           :html-head-include-default-style nil
           :html-head-extra "<link rel=\"stylesheet\" href=\"/style.css\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Notes\" href=\"/notes.xml\">"
           :auto-sitemap t
           :sitemap-filename "notes.html"
           :sitemap-title "Notes"
           :sitemap-function ,(my/html-sitemap-function-for "notes-html")
           :completion-function ,(lambda (_) (my/feed-write-atom "notes")))
          ("notes-gemini"
           :base-directory ,org-roam-directory
           :base-extension "org"
           :exclude "\\.journal\\.org\\'\\|\\.blog\\.org\\'"
           :publishing-directory ,my/org-gemini-directory
           :publishing-function org-gemini-publish-to-gemini
           :with-broken-links mark
           :auto-sitemap t
           :sitemap-filename "notes.gmi"
           :sitemap-title "Prabu's Notes"
           :sitemap-function ,(my/gemini-sitemap-function-for "notes-gemini"))
          ("journal-html"
           :base-directory ,org-roam-directory
           :base-extension "journal\\.org"
           :publishing-directory ,my/org-html-directory
           :publishing-function org-html-publish-to-html
           :with-broken-links mark
           :with-toc nil
           :section-numbers nil
           :html-preamble my/org-html-preamble
           :html-head-include-default-style nil
           :html-head-extra "<link rel=\"stylesheet\" href=\"/style.css\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Journal\" href=\"/journal.xml\">"
           :auto-sitemap t
           :sitemap-filename "journal.html"
           :sitemap-title "Journal"
           :sitemap-function ,(my/html-sitemap-function-for "journal-html")
           :completion-function ,(lambda (_) (my/feed-write-atom "journal")))
          ("journal-gemini"
           :base-directory ,org-roam-directory
           :base-extension "journal\\.org"
           :publishing-directory ,my/org-gemini-directory
           :publishing-function org-gemini-publish-to-gemini
           :with-broken-links mark
           :auto-sitemap t
           :sitemap-filename "journal.gmi"
           :sitemap-title "Prabu's Journal"
           ;; :sitemap-function ,(my/gemini-sitemap-function-for "journal-gemini" "Daily entries, newest first"))
           :sitemap-function ,(my/gemini-sitemap-function-for "journal-gemini"))
          ("all-sites"
           :components ("notes-html" "notes-gemini" "journal-html" "journal-gemini")))))


(defun my/trigger-syncthing-scan ()
  "Asynchronously run publish_site.sh to trigger a Syncthing rescan."
  (let ((script (executable-find my/publish-site-script)))
    (if script
        (start-process "publish-site-scan" "*publish-site-scan*" script)
      (message "my/trigger-syncthing-scan: %s not found on exec-path"
               my/publish-site-script))))

(defun my/publish-complete-html-site ()
  "Force-export every org-roam note, Journal to HTML site."
  (interactive)
  (require 'org-id)
  (org-id-update-id-locations (org-roam--list-files org-roam-directory))
  (org-publish "notes-html" t)
  (org-publish "journal-html" t)
  (my/compile-site-css)
  (my/trigger-syncthing-scan)
  (message "Published complete HTML + compiled CSS to %s" my/site-css-output))

(defun my/publish-complete-gemini-site ()
  "Force-export every org-roam note, Journal to gemini."
  (interactive)
  (require 'org-id)
  (require 'ox-gemini)
  (org-id-update-id-locations (org-roam--list-files org-roam-directory))
  (org-publish "notes-gemini" t)
  (org-publish "journal-gemini" t)
  (my/trigger-syncthing-scan)
  (message "Published complete gemini site"))

(defun my/publish-all-sites ()
  "Publish only changes to both the HTML site and Gemini."
  (interactive)
  (require 'org-id)
  (org-id-update-id-locations (org-roam--list-files org-roam-directory))
  (org-publish "all-sites")
  (my/compile-site-css)
  (my/trigger-syncthing-scan)
  (message "Publishing to both html and gemini completed."))

(defvar my/publish-log-buffer "*org-publish-log*")
(defvar my/publish-running-process nil
  "Tracks the background publish process, to avoid overlapping runs.")

(defun my/publish-all-sites-async ()
  "Run my/publish-all-sites in a background Emacs batch process,
so this Emacs session doesn't block while publishing."
  (interactive)
  (if (process-live-p my/publish-running-process)
      (message "A publish is already running -- see %s" my/publish-log-buffer)
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (setq my/publish-running-process
            (start-process "org-publish" my/publish-log-buffer
                            "emacs" "--batch" "-l" init-file
                            "--eval" "(my/publish-all-sites)"))
      (set-process-sentinel
       my/publish-running-process
       (lambda (_proc event)
         (cond
          ((string-match-p "finished" event)
           (message "Publish finished. See %s for details." my/publish-log-buffer))
          ((string-match-p "\\(exited abnormally\\|failed\\)" event)
           (message "Publish FAILED -- check %s" my/publish-log-buffer)))))
      (message "Publishing started in background..."))))

feeds

The following code reads dates from the #+date: property of each file. The number of feed entries are set in the variable "my/feed-limit". The feed timestamps follow the system time zone.

Submission to antenna, the gemini aggregator will remain manual:

amfora gemini://antenna.michaelnordmeyer.com/submit-feed-url?gemini://prabu.freeddns.org/notes.gmi
(use-feature xml)

(defvar my/site-host "prabu.freeddns.org"
  "Public host name; the same for https:// and gemini://.")
(defvar my/feed-limit 10 "Maximum number of entries in each feed.")
(defvar my/feed-author "Prabu Anand Kalivaradhan")
(defvar my/feed-titles
  '(("notes" . "Prabu's Notes")
    ("journal" . "Prabu's Journal"))
  "Feed name -> feed title.  Gemsub readers use it as the author name.")

(defun my/feed--first-heading (file)
  "Text of the first headline in FILE, links reduced to their description, or nil."
  (with-temp-buffer
    (insert-file-contents file)
    (delay-mode-hooks (org-mode))
    (when-let* ((h (org-element-map (org-element-parse-buffer 'headline)
                       'headline #'identity nil t)))
      (org-link-display-format (org-element-property :raw-value h)))))

(defun my/feed--content (file)
    "Body of FILE as HTML, exported by Org (no title, table of contents or page frame)."
    (with-temp-buffer
      (insert-file-contents file)
      (setq default-directory (file-name-directory file))
      (delay-mode-hooks (org-mode))
      (org-export-as 'html nil nil t
                     '(:with-toc nil :section-numbers nil :with-broken-links mark))))

(defun my/feed-entries (name)
  "Return the newest `my/feed-limit' entries of feed NAME as plists.
This is the single source for every feed format.  NAME is \"notes\"
or \"journal\"; the file list comes from the matching gemini project."
  (pcase-let* ((`(,project . ,files)
                (my/sitemap-sorted-files (concat name "-gemini")))
               (entries
                (mapcar
                 (lambda (f)
                   (let* ((created (org-publish-find-date f project))
                          (mtime (file-attribute-modification-time (file-attributes f))))
                     (list :file f
                           :slug (file-name-sans-extension (file-name-nondirectory f))
                           :created created
                           :updated (if (time-less-p mtime created) created mtime))))
                 files)))
    (setq entries (sort entries (lambda (a b) (time-less-p (plist-get b :created)
                                                           (plist-get a :created)))))
    (when (> (length entries) my/feed-limit)
      (setq entries (seq-take entries my/feed-limit)))
    (mapcar
     (lambda (e)
       (let* ((title (org-publish-find-title (plist-get e :file) project))
              (date (format-time-string "%F" (plist-get e :created))))
         (plist-put e :content (my/feed--content (plist-get e :file)))
         (plist-put e :title
                    (or (and (string= name "journal")
                             (my/feed--first-heading (plist-get e :file)))
                        (if (string= title date) (capitalize name) title)))))
     entries)))

(defun my/feed-write-atom (name)
  "Write the Atom feed for NAME (\"notes\" or \"journal\") to `my/org-html-directory'."
  (let* ((base (format "https://%s/" my/site-host))
         (entries (my/feed-entries name))
         (stamp (lambda (time) (format-time-string "%FT%T%:z" time)))
         (feed-updated (or (car (sort (mapcar (lambda (e) (plist-get e :updated)) entries)
                                      (lambda (a b) (time-less-p b a))))
                           (current-time)))
         (coding-system-for-write 'utf-8-unix))
    (with-temp-file (expand-file-name (concat name ".xml") my/org-html-directory)
      (insert "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
      (xml-print
       `((feed ((xmlns . "http://www.w3.org/2005/Atom"))
               (title nil ,(cdr (assoc name my/feed-titles)))
               (link ((href . ,(format "%s%s.html" base name))))
               (link ((rel . "self") (type . "application/atom+xml")
                      (href . ,(format "%s%s.xml" base name))))
               (id nil ,(format "%s%s.html" base name))
               (updated nil ,(funcall stamp feed-updated))
               (author nil (name nil ,my/feed-author))
               ,@(mapcar
                  (lambda (e)
                    (let ((url (format "%s%s.html" base (plist-get e :slug))))
                      `(entry nil
                              (title nil ,(plist-get e :title))
                              (link ((rel . "alternate") (type . "text/html") (href . ,url)))
                              (id nil ,url)
                              (published nil ,(funcall stamp (plist-get e :created)))
                              (updated nil ,(funcall stamp (plist-get e :updated)))
                              (content ((type . "html") (xml:base . ,url))
                                       ,(plist-get e :content)))))
                  entries)))))))

org-transclusion

(use-package org-transclusion
  :ensure t   ;;Auto-install the package from Melpa
  :hook (org-mode . org-transclusion-mode)
  )

org-noter

(use-package org-noter
  :ensure t
  )

markdown-mode

Installed markdown package in the OS for previewing the markdown files.

(use-package markdown-mode
  :ensure t
  )

flymake

This package is inbuilt within emacs

(use-feature flymake
  ;; https://www.gnu.org/software/emacs/manual/html_node/flymake/Finding-diagnostics.html
  :bind(
        :map flymake-mode-map
             ("M-n" . flymake-goto-next-error)
             ("M-p" . flymake-goto-prev-error)
             ("<f2>" . flymake-show-buffer-diagnostics)))

eglot

I have installed the packages py3-lsp-server, ruff-lsp and beancount-language-server through the package manager.

(use-feature eglot
  :defer t
  :hook ((html-mode . eglot-ensure)
         (mhtml-mode . eglot-ensure))
  :bind(
        :map eglot-mode-map
             ("C-c o" . eglot-code-action-organize-imports)
             ("C-c h" . eldoc))
  :config
  (add-to-list 'eglot-server-programs
               '((html-mode mhtml-mode) . ("superhtml" "lsp")))
  (add-hook 'eglot-managed-mode-hook
            (lambda ()
              ;; Show flymake diagnostics first.
              (setq eldoc-documentation-functions
                    (cons #'flymake-eldoc-function
                          (remove #'flymake-eldoc-function eldoc-documentation-functions)))
              ;; Show all eldoc feedback.
              (setq eldoc-documentation-strategy #'eldoc-documentation-compose)))
  )

beancount

This mode is useful for Beancount, plain text accounting system. To ensure that PET finds the venv, a link has been created as follows in the beancount folder:

prabu@homepc2 /d/m/p/beancount> ls -al /data/myhome/prabu/beancount/ .venv
lrwxrwxrwx    1 prabu    prabu           28 May 20 15:41 .venv -> /data/myhome/prabu/bean_venv/
;; From etc/emacsrc. Bind a key to reformat the entire file using
;; bean-format. Fixed error in the original function
(defun my/beancount-format-file ()
  "Reformat the whole buffer with bean-format"
  (interactive)
  (let ((line-no (line-number-at-pos)))
    (call-process-region (point-min) (point-max) "bean-format" t (current-buffer) nil "-")
    (goto-char (point-min))
    (forward-line (1- line-no))
    (recenter)
      ))

(use-package beancount
  :ensure nil
  :commands beancount-mode
  :mode ("\\.beancount\\'" . beancount-mode)
  :hook
  (beancount-mode . (lambda ()
                      (setq-local electric-indent-chars nil)
                      (setq-local beancount-outline-regexp "\\(\\*+\\)")
                      (outline-minor-mode)
                      (flymake-mode) ; Enable flymake-mode here
                      (flymake-bean-check-enable)))
  :bind (:map beancount-mode-map
              ("C-c C-n" . outline-next-visible-heading)
              ("C-c C-p" . outline-previous-visible-heading)
              ("C-c C-u" . outline-up-heading)
              ("C-c C-b" . outline-backward-same-level)
              ("C-c C-f" . outline-forward-same-level)
              ("C-c C-a" . outline-show-all)
              ("C-c F" . my/beancount-format-file)
              ([backtab] . beancount-outline-cycle)))

(use-package flymake-bean-check
  :ensure nil
  :after (flymake beancount))

python

Currently using inbuilt Python, flymake and eglot along with hide-mode-line, live-py-mode. Earlier i started with Elpy. Eglot connects to python-lsp and ruff.

References:

(use-feature python
  :config
  (setq python-indent-guess-indent-offset-verbose nil)   ;; Remove guess indent python message
  (add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))   ;; Remap python-mode to python-ts-mode
  (add-hook 'python-ts-mode-hook
            (lambda ()
              (setq python-indent-offset 4)  ;; Use 4 spaces for indentation (PEP 8)
              (setq-local eglot-workspace-configuration
                        '((:pylsp :plugins
                                  (:autopep8 (:enabled :json-false)
                                   :yapf (:enabled :json-false)
                                   :pycodestyle (:enabled :json-false)
                                   :mccabe (:enabled :json-false)
                                   :flake8 (:enabled :json-false)
                                   :pylint (:enabled :json-false)))))))) ;; do not use pylsp for linting

(use-package ruff-format
  :ensure t
  :hook (python-base-mode . ruff-format-on-save-mode))

(use-package pet
  :ensure t
  :config
  (add-hook 'python-base-mode-hook 'pet-mode)
  (add-hook 'beancount-mode-hook 'pet-mode)
  (add-hook 'python-base-mode-hook
            (lambda ()
              (when-let* ((python (pet-executable-find "python")))
                (setq-local python-shell-interpreter python
                            python-shell-virtualenv-root (pet-virtualenv-root)))
              (pet-eglot-setup)
              (eglot-ensure)))
  (add-hook 'beancount-mode-hook
            (lambda ()
              (setq-local beancount-accounts-files
                          (directory-files default-directory t "\\.beancount$"))
              (when-let* ((root (pet-virtualenv-root))
                          (venv-bin (expand-file-name "bin" root))
                          (_ (file-directory-p venv-bin)))
                (setq-local exec-path (append (list venv-bin) exec-path))))))

hide-mode-line

hide-mode-line package hides the modeline for inferior python processes. This is not a necessary package but it's helpful to make better use of the screen real-estate at our disposal.

(use-package hide-mode-line
  :ensure t
  :hook (inferior-python-mode . hide-mode-line-mode)
  )

live-py-mode

(use-package live-py-mode
  :ensure t
  )

shell scripts

(defun my-sh-mode-setup ()
  "Specific settings for shell scripts."
  ;; sh-mode-hook inherits prog-mode-hook!
  (setq-local sh-basic-offset 4)
  (setq-local indent-tabs-mode t)
  (setq-local tab-width 4)
  )
;; Use custom function if sh-mode is activated anywhere
(add-hook 'sh-mode-hook #'my-sh-mode-setup)
;; Ensure APKBUILD files open in sh-mode so the hook above triggers
(add-to-list 'auto-mode-alist '("APKBUILD\\'" . sh-mode))

rainbow-delimiters

The official website of rainbow-delimiters strongly recommends to use rainbow-delimiters-mode as a major mode hook.

(defvar my/nord-rainbow-colors
  '("#88c0d0"   ; 1 cyan
    "#e07882"   ; 2 soft red
    "#ebcb8b"   ; 3 yellow
    "#a3be8c"   ; 4 green
    "#b48ead"   ; 5 purple
    "#8fbcbb"   ; 6 teal
    "#d08770"   ; 7 orange
    "#81a1c1"   ; 8 blue
    "#a692cc")  ; 9 lavender
  "Custom Rainbow-delimiters with depth colors for nord")

(use-package rainbow-delimiters
  :ensure t
  :hook (prog-mode . rainbow-delimiters-mode)
  :config
  (defun my/nord-rainbow-faces (&rest _)
    "Give rainbow-delimiters distinct hues while nord is enabled."
    (when (memq 'nord custom-enabled-themes)
      (cl-loop for color in my/nord-rainbow-colors
               for depth from 1
               do (set-face-attribute
                   (intern (format "rainbow-delimiters-depth-%d-face" depth))
                   nil :foreground color))))
  ;; Re-apply whenever auto-dark (or you) enables a theme
  (add-hook 'enable-theme-functions #'my/nord-rainbow-faces))

elfeed

elfeed is a atom/rss feed reader.

(use-package elfeed
  :ensure t
  :config
  (setq elfeed-feeds
        '(
          ("https://nullprogram.com/feed/" test)
          ("https://planet.emacslife.com/atom.xml" emacs)
          ("https://prabu.freeddns.org/notes.xml" prabu notes)
          )))

Obsolete packages

I have added a drawer on the heading above with ":tangle no". Hence all the codeblocks below this heading will not be tangled to the init.el. I still keep them here in the org source.

c/c++ configuration

(defun my-c-common-setup ()
  "Standardized settings for C-family languages."
  ;; Standards: PEP 8 (Python) and modern C++ (LLVM/Google) prefer spaces.
  ;; Install clangd or clang-extra-tools package for LSP features with eglot
  (setq-local indent-tabs-mode nil)
  (setq-local c-basic-offset 4) ; 4 is the modern industry standard
  (when (executable-find "clangd")
    (eglot-ensure)))
(add-hook 'c-mode-common-hook #'my-c-common-setup)

arduino

Useful for editing source files for arduino

;; (use-package arduino-mode
;;   :ensure t
;;   :mode ("\\.ino$" . arduino-mode)
;;   )

platformio

Useful for editing source files for IOT devices like esp32

;; (use-package platformio-mode
;;   :ensure t
;;   :hook ((c-mode . platformio-conditionally-enable)
;;          (c++-mode . platformio-conditionally-enable))
;;   )

projectile

This package was needed for platformio-mode

;; (use-package projectile
;;   :ensure t
;;   :init
;;   (projectile-mode +1)
;;   :bind (:map projectile-mode-map
;;               ("s-p" . projectile-command-map)
;;               ("C-c p" . projectile-command-map)))

Date: 2026-05-10 Sun 11:52

Author: Prabu Anand Kalivaradhan

Created: 2026-10-12 Mon 16:23