Lightweight Writing & Coding Emacs Configuration
Table of Contents
- 1. Introduction
- 2. Early Initialization
- 3. Core Settings
- 4. Theme
- 5. Workspaces & Projects
- 6. Completion
- 7. Distraction-Free Writing
- 8. Spell Checking
- 9. Org-mode
- 10. Markdown Support
- 11. Tree-sitter
- 12. Eglot (LSP)
- 13. Light Programming Support
- 14. Word Count & Writing Utilities
- 15. File Management
- 16. End of Init
- 17. Key Bindings Reference Card
- 18. Tangling & Launch Instructions
1. Introduction
A lightweight Emacs configuration focused on writing with enough coding support for editing code embedded in documents. This is a separate, lighter alternative to the full modular configuration.
1.1. What This Config Provides
- Distraction-free writing with Olivetti, flyspell, and word count
- Org-mode with org-modern styling and full org-babel language support
- Markdown editing with GFM and TOC generation
- Workspace management via tab-bar (Doom-like M-1 to M-9 switching)
- Project management with built-in project.el
- Tree-sitter syntax highlighting for all major languages
- Eglot LSP for code intelligence
- Magit for Git integration
- 8 external packages, everything else built-in
1.2. How to Use
1.2.1. Tangle (extract configuration files)
Open this file in Emacs and run:
C-c C-v t
This creates three files:
~/.emacs-writing.d/early-init.el~/.emacs-writing.d/init.el~/.emacs-writing.d/themes/hasliberg-theme.el
1.2.2. Launch
emacs --init-directory ~/.emacs-writing.d/
Or create an alias:
alias ew='emacs --init-directory ~/.emacs-writing.d/'
On first launch, Emacs will automatically install the 8 external packages.
2. Early Initialization
The early-init.el file loads before the package system and GUI, making it the right place for performance and visual settings.
;;; early-init.el --- Writing Config Early Init -*- lexical-binding: t; -*- ;;; Commentary: ;; Early init for the lightweight writing & coding configuration. ;;; Code: ;; Native compilation settings (when (and (fboundp 'native-comp-available-p) (native-comp-available-p)) (setq native-comp-async-report-warnings-errors nil native-comp-deferred-compilation t)) ;; Startup performance: disable GC during init (setq gc-cons-threshold most-positive-fixnum gc-cons-percentage 0.6 vc-handled-backends '(Git)) ;; Prevent visual flash (defun fu/avoid-initial-flash-of-light () "Avoid flash of light when starting Emacs." (setq mode-line-format nil)) (fu/avoid-initial-flash-of-light) ;; Frame configuration (setq frame-resize-pixelwise t frame-inhibit-implied-resize t frame-title-format '("Emacs Writing") inhibit-compacting-font-caches t) ;; Disable unused UI elements early (menu-bar-mode 0) (scroll-bar-mode 0) (tool-bar-mode 0) (tooltip-mode 0) (provide 'early-init) ;;; early-init.el ends here
3. Core Settings
Package management, sensible defaults, and UTF-8 everywhere.
;;; init.el --- Writing Config Init -*- lexical-binding: t; -*- ;;; Commentary: ;; Lightweight writing & coding configuration. ;;; Code: (use-package emacs :ensure nil :bind (("M-o" . other-window) ("C-x C-r" . recentf-open-files) ("C-x k" . (lambda () "Kill current buffer." (interactive) (kill-buffer (buffer-name)))) ("RET" . newline-and-indent) ("C-z" . nil) ("C-x C-z" . nil)) :custom ;; Cursor and display (cursor-type '(bar . 3)) (inhibit-startup-message t) (initial-scratch-message nil) (initial-major-mode 'org-mode) (visible-bell nil) (ring-bell-function 'ignore) (use-dialog-box nil) (use-file-dialog nil) (use-short-answers t) ;; Editing behavior (delete-selection-mode 1) (tab-always-indent 'complete) (tab-width 4) (create-lockfiles nil) (make-backup-files nil) (backup-inhibited t) ;; Scrolling (scroll-margin 0) (scroll-step 1) (scroll-conservatively 101) (pixel-scroll-precision-mode t) (pixel-scroll-precision-use-momentum nil) ;; Window behavior (split-width-threshold 170) (split-height-threshold nil) (window-combination-resize t) (help-window-select t) (switch-to-buffer-obey-display-actions t) ;; File handling (delete-by-moving-to-trash t) (global-auto-revert-non-file-buffers t) (auto-revert-verbose nil) ;; History and recent files (history-length 300) (recentf-max-saved-items 300) (recentf-max-menu-items 15) (savehist-save-minibuffer-history t) (savehist-additional-variables '(kill-ring register-alist mark-ring global-mark-ring search-ring regexp-search-ring)) (save-place-file (expand-file-name "saveplace" user-emacs-directory)) (save-place-limit 600) ;; Display (display-line-numbers-width 6) (display-line-numbers-widen t) (truncate-lines t) (treesit-font-lock-level 4) ;; UTF-8 everywhere (locale-coding-system 'utf-8) (keyboard-coding-system 'utf-8) (savehist-coding-system 'utf-8) (file-name-coding-system 'utf-8) (buffer-file-coding-system 'utf-8) ;; Search (completion-ignore-case t) (completions-detailed t) (completions-format 'one-column) :init (add-to-list 'default-frame-alist '(alpha-background . 100)) (global-auto-revert-mode 1) (global-goto-address-mode 1) (indent-tabs-mode nil) (recentf-mode 1) (repeat-mode 1) (savehist-mode 1) (save-place-mode 1) (winner-mode) (file-name-shadow-mode 1) (prefer-coding-system 'utf-8) (put 'narrow-to-region 'disabled nil) (message (emacs-init-time)) :config (modify-coding-system-alist 'file "" 'utf-8) (setq custom-file (locate-user-emacs-file "custom-vars.el")) (load custom-file 'noerror 'nomessage) ;; Line numbers in prog-mode (setq display-line-numbers-type 'absolute) (add-hook 'prog-mode-hook #'display-line-numbers-mode) ;; Visual line mode for text modes (add-hook 'text-mode-hook #'visual-line-mode) ;; Display table for terminal (set-display-table-slot standard-display-table 'vertical-border ?\u2502) (set-display-table-slot standard-display-table 'truncation ?\u2192) ;; Reset GC after startup (add-hook 'emacs-startup-hook (lambda () (setq gc-cons-threshold (* 16 1024 1024) gc-cons-percentage 0.1))) ;; Load private config if exists (add-hook 'after-init-hook (lambda () (let ((private-file (expand-file-name "private.el" user-emacs-directory))) (when (file-exists-p private-file) (load private-file))))) ;; Package archives (use-package use-package :custom (package-archives '(("melpa" . "https://melpa.org/packages/") ("elpa" . "https://elpa.gnu.org/packages/") ("nongnu" . "https://elpa.nongnu.org/nongnu/"))) (use-package-always-ensure t) (use-package-enable-at-startup nil) (package-native-compile t) (warning-minimum-level 'error)))
4. Theme
The Hasliberg theme provides a serene, dark color scheme inspired by the Swiss alps. It supports both dark and light variants with LuvLCh-based color management.
4.1. Theme Loading
;; Load Hasliberg theme (let ((theme-file (expand-file-name "themes/hasliberg-theme.el" user-emacs-directory))) (if (file-exists-p theme-file) (progn (load-file theme-file) (load-theme 'hasliberg t))))
4.2. Hasliberg Theme Source
;;; hasliberg-theme.el --- Serene theme inspired by Swiss alps. -*- lexical-binding:t -*- ;; Copyright (C) 2024 Free Software Foundation, Inc. ;; Author: Ryota Sawada <rytswd@gmail.com> ;; Maintainer: Ryota Sawada <rytswd@gmail.com> ;; URL: https://github.com/rytswd/hasliberg-theme ;; Keywords: theme ;; Version: 0.1 ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; The theme is only meant to provide my own prefered colour setup. ;;; Acknowledgments: ;; A lot of configurations were inspired by ef-themes by Prot. ;;; Code: ;;;###theme-autoload (deftheme hasliberg "Theme inspired by Swiss alps" :background-mode 'dark :kind 'color-scheme) ;;;;---------------------------------------- ;;; Configuration ;;------------------------------------------ (defgroup hasliberg-theme nil "Options for hasliberg-theme." :group 'hasliberg-theme :prefix "hasliberg-theme-") (defconst hasliberg-theme-lch-type '(plist :options ((:luminance float) (:chroma float) (:hue float))) "A plist defining LuvLCh input.") (defun hasliberg-theme--validate-and-set-lch (symbol value) "Set SYMBOL to VALUE if it is a valid LCH colour. VALUE must be a plist containing :luminance, :chroma, and :hue with float values. Luminance should be between 0 and 100, chroma should be non-negative, and hue should be between 0 and 360." (let ((luminance (plist-get value :luminance)) (chroma (plist-get value :chroma)) (hue (plist-get value :hue)) (errors '())) (unless (and luminance chroma hue) (push "LCH value must include :luminance, :chroma, and :hue" errors)) (unless (and (floatp luminance) (floatp chroma) (floatp hue)) (push (format "LCH components must be float values: %S" value) errors)) (when (and luminance (or (< luminance 0) (> luminance 100))) (push (format "Luminance value %f must be between 0 and 100" luminance) errors)) (when (and chroma (or (< chroma 0) (> chroma 220))) (push (format "Chroma value %f must be non-negative" chroma) errors)) (when (and hue (or (< hue 0) (>= hue 360))) (push (format "Hue value %f must be between 0 and 360" hue) errors)) (if errors (error "Invalid LuvLCh value: %s" (string-join (reverse errors) "; ")) (set-default symbol value)))) (defcustom hasliberg-theme-dark-or-light 'dark "The theme variant, either `dark` or `light`." :type '(choice (const :tag "Dark" dark) (const :tag "Light" light)) :group 'hasliberg-theme :set (lambda (symbol value) (set-default symbol value) (hasliberg-theme--update)) :initialize 'custom-initialize-default) (defcustom hasliberg-theme-colour-background '(:luminance 17.877 :chroma 1.800 :hue 236.421) ;; #2A2C2E "The background colour, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-background-variant '(:luminance 6.265 :chroma 11.827 :hue 252.428) ;; #00142D "Another background colour with slight variation, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-neutral '(:luminance 95.074 :chroma 12.330 :hue 252.652) ;; #ECF1FF "The neutral / default font colour, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-primary '(:luminance 80.335 :chroma 40.438 :hue 241.234) ;; #A7CBF1 "The primary font colour, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-secondary '(:luminance 63.743 :chroma 48.347 :hue 251.617) ;; #809BCE "The secondary font colour, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-accent '(:luminance 77.610 :chroma 86.648 :hue 47.245) ;; #FBB151 "The accent font colour, used sparingly for call-to-action, etc., in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-accent-variant '(:luminance 67.236 :chroma 86.052 :hue 335.603) ;; #FB74C3 "Another accent font colour with slight variation, used sparingly for call-to-action, etc., in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-subtle '(:luminance 77.751 :chroma 14.617 :hue 235.776) ;; #B4C2CF "The subtle font colour to slightly mix up, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-subtle-variant '(:luminance 73.823 :chroma 11.749 :hue 180.830) ;; #A4BAB7 "Another subtle font colour with slight variation to mix up even more, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-info '(:luminance 71.365 :chroma 21.506 :hue 257.597) ;; #A8AEC7 "The info font colour, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defcustom hasliberg-theme-colour-warning '(:luminance 67.236 :chroma 86.052 :hue 335.603) ;; #FB74C3 "The warning font colour, in LuvLCh values." :type hasliberg-theme-lch-type :set 'hasliberg-theme--validate-and-set-lch :group 'hasliberg-theme) (defun hasliberg-theme-use-dark-standard-colour-palette () "Use dark standard colour palette for Hasliberg Theme setup." (interactive) (setopt hasliberg-theme-dark-or-light 'dark hasliberg-theme-colour-background '(:luminance 17.877 :chroma 1.800 :hue 236.421) hasliberg-theme-colour-background-variant '(:luminance 6.265 :chroma 11.827 :hue 252.428) hasliberg-theme-colour-neutral '(:luminance 95.074 :chroma 12.330 :hue 252.652) hasliberg-theme-colour-primary '(:luminance 80.335 :chroma 40.438 :hue 241.234) hasliberg-theme-colour-secondary '(:luminance 63.743 :chroma 48.347 :hue 251.617) hasliberg-theme-colour-accent '(:luminance 77.610 :chroma 86.648 :hue 47.245) hasliberg-theme-colour-accent-variant '(:luminance 67.236 :chroma 86.052 :hue 335.603) hasliberg-theme-colour-subtle '(:luminance 77.751 :chroma 14.617 :hue 235.776) hasliberg-theme-colour-subtle-variant '(:luminance 73.823 :chroma 11.749 :hue 180.830) hasliberg-theme-colour-info '(:luminance 62.814 :chroma 70.124 :hue 123.247) hasliberg-theme-colour-warning '(:luminance 67.236 :chroma 86.052 :hue 335.603)) (hasliberg-theme--update)) (defun hasliberg-theme-use-dark-orange-colour-palette () "Use dark orange colour palette for Hasliberg Theme setup." (interactive) (setopt hasliberg-theme-dark-or-light 'dark hasliberg-theme-colour-background '(:luminance 17.877 :chroma 1.800 :hue 236.421) hasliberg-theme-colour-background-variant '(:luminance 6.265 :chroma 11.827 :hue 252.428) hasliberg-theme-colour-neutral '(:luminance 95.074 :chroma 12.330 :hue 252.652) hasliberg-theme-colour-primary '(:luminance 80.335 :chroma 40.438 :hue 41.234) hasliberg-theme-colour-secondary '(:luminance 63.743 :chroma 48.347 :hue 51.617) hasliberg-theme-colour-accent '(:luminance 77.610 :chroma 86.648 :hue 47.245) hasliberg-theme-colour-accent-variant '(:luminance 67.236 :chroma 86.052 :hue 35.603) hasliberg-theme-colour-subtle '(:luminance 77.751 :chroma 14.617 :hue 35.776) hasliberg-theme-colour-subtle-variant '(:luminance 73.823 :chroma 11.749 :hue 80.830) hasliberg-theme-colour-info '(:luminance 62.814 :chroma 70.124 :hue 23.247) hasliberg-theme-colour-warning '(:luminance 67.236 :chroma 86.052 :hue 35.603)) (hasliberg-theme--update)) (defun hasliberg-theme-use-dark-monotonic-colour-palette () "Use dark monotonic colour palette for Hasliberg Theme setup." (interactive) (setopt hasliberg-theme-dark-or-light 'dark hasliberg-theme-colour-background '(:luminance 17.877 :chroma 1.800 :hue 236.421) hasliberg-theme-colour-background-variant '(:luminance 6.265 :chroma 11.827 :hue 252.428) hasliberg-theme-colour-neutral '(:luminance 95.074 :chroma 12.330 :hue 252.652) hasliberg-theme-colour-primary '(:luminance 93.380 :chroma 2.848 :hue 192.490) hasliberg-theme-colour-secondary '(:luminance 93.535 :chroma 3.844 :hue 169.497) hasliberg-theme-colour-accent '(:luminance 76.373 :chroma 32.215 :hue 238.249) hasliberg-theme-colour-accent-variant '(:luminance 76.373 :chroma 32.215 :hue 238.249) hasliberg-theme-colour-subtle '(:luminance 77.751 :chroma 14.617 :hue 35.776) hasliberg-theme-colour-subtle-variant '(:luminance 73.823 :chroma 11.749 :hue 80.830) hasliberg-theme-colour-info '(:luminance 67.245 :chroma 10.890 :hue 19.862) hasliberg-theme-colour-warning '(:luminance 67.236 :chroma 86.052 :hue 35.603)) (hasliberg-theme--update)) (defun hasliberg-theme-use-dark-nature-colour-palette () "Use dark nature colour palette for Hasliberg Theme setup." (interactive) (setopt hasliberg-theme-dark-or-light 'dark hasliberg-theme-colour-background '(:luminance 17.736 :chroma 13.978 :hue 146.889) hasliberg-theme-colour-background-variant '(:luminance 25.925 :chroma 49.830 :hue 153.821) hasliberg-theme-colour-neutral '(:luminance 95.146 :chroma 12.143 :hue 253.229) hasliberg-theme-colour-primary '(:luminance 91.651 :chroma 82.416 :hue 128.406) hasliberg-theme-colour-secondary '(:luminance 84.833 :chroma 52.641 :hue 148.810) hasliberg-theme-colour-accent '(:luminance 75.501 :chroma 82.984 :hue 110.610) hasliberg-theme-colour-accent-variant '(:luminance 56.134 :chroma 27.575 :hue 241.601) hasliberg-theme-colour-subtle '(:luminance 95.771 :chroma 17.004 :hue 91.625) hasliberg-theme-colour-subtle-variant '(:luminance 73.828 :chroma 11.880 :hue 80.278) hasliberg-theme-colour-info '(:luminance 36.998 :chroma 22.023 :hue 101.245) hasliberg-theme-colour-warning '(:luminance 80.484 :chroma 80.188 :hue 67.991)) (hasliberg-theme--update)) (defun hasliberg-theme-use-light-standard-colour-palette () "Use light standard colour palette for Hasliberg Theme setup." (interactive) (setopt hasliberg-theme-dark-or-light 'light hasliberg-theme-colour-background '(:luminance 95.074 :chroma 12.330 :hue 252.652) hasliberg-theme-colour-background-variant '(:luminance 90.425 :chroma 3.975 :hue 192.174) hasliberg-theme-colour-neutral '(:luminance 17.877 :chroma 1.800 :hue 236.421) hasliberg-theme-colour-primary '(:luminance 30.335 :chroma 40.438 :hue 241.234) hasliberg-theme-colour-secondary '(:luminance 13.743 :chroma 48.347 :hue 251.617) hasliberg-theme-colour-accent '(:luminance 52.814 :chroma 70.124 :hue 23.247) hasliberg-theme-colour-accent-variant '(:luminance 17.236 :chroma 86.052 :hue 335.603) hasliberg-theme-colour-subtle '(:luminance 27.751 :chroma 14.617 :hue 235.776) hasliberg-theme-colour-subtle-variant '(:luminance 23.823 :chroma 11.749 :hue 180.830) hasliberg-theme-colour-info '(:luminance 12.814 :chroma 70.124 :hue 123.247) hasliberg-theme-colour-warning '(:luminance 17.236 :chroma 86.052 :hue 335.603)) (hasliberg-theme--update)) (defun hasliberg-theme--update () "Based on the base colour input, update the shades, faces, and then reload." (hasliberg-theme--update-shades) (hasliberg-theme--update-all-faces) (load-theme 'hasliberg t)) (defvar hasliberg-theme--load-path nil "Variable to store the load path of Hasliberg Theme.") (unless hasliberg-theme--load-path (setq hasliberg-theme--load-path load-file-name)) (defun hasliberg-theme-reload () "Re-evaluate the file and reload the config." (interactive) (load-file hasliberg-theme--load-path) (load-theme 'hasliberg t)) (defun hasliberg-theme--lch-to-luv (lch) "Convert a colour from LCH to Luv. LCH is a plist with properties :luminance, :chroma, and :hue." (let* ((L (plist-get lch :luminance)) (C (plist-get lch :chroma)) (H-degree (plist-get lch :hue)) (H-radians (* pi (/ H-degree 180.0)))) (list :l L :u (* C (cos H-radians)) :v (* C (sin H-radians))))) (defun hasliberg-theme--luv-to-xyz (luv) "Convert a colour from Luv to XYZ. Luv is a plist with properties :l, :u and :v." (let* ((L (plist-get luv :l)) (u (plist-get luv :u)) (v (plist-get luv :v)) (ref-u 0.19783000664283) (ref-v 0.46831999493879) (up (/ (+ u (* 13 L ref-u)) (* 13 L))) (vp (/ (+ v (* 13 L ref-v)) (* 13 L))) (Y (if (> L 7.9996) (expt (/ (+ L 16) 116.0) 3) (/ L 903.3))) (X (if (zerop vp) 0 (/ (* 9 Y up) (* 4 vp)))) (Z (if (zerop vp) 0 (/ (* (- 12 (* 3 up) (* 20 vp)) Y) (* 4 vp))))) (list :x (* 100 X) :y (* 100 Y) :z (* 100 Z)))) (defun hasliberg-theme--xyz-to-rgb (xyz) "Convert a colour from XYZ to RGB. XYZ is a plist with properties :x, :y, and :z." (let* ((X (/ (plist-get xyz :x) 100.0)) (Y (/ (plist-get xyz :y) 100.0)) (Z (/ (plist-get xyz :z) 100.0)) (R-linear (+ (* X 3.2406) (* Y -1.5372) (* Z -0.4986))) (G-linear (+ (* X -0.9689) (* Y 1.8758) (* Z 0.0415))) (B-linear (+ (* X 0.0557) (* Y -0.2040) (* Z 1.0570))) (R (if (<= R-linear 0.0031308) (* 12.92 R-linear) (- (* 1.055 (expt R-linear (/ 1.0 2.4))) 0.055))) (G (if (<= G-linear 0.0031308) (* 12.92 G-linear) (- (* 1.055 (expt G-linear (/ 1.0 2.4))) 0.055))) (B (if (<= B-linear 0.0031308) (* 12.92 B-linear) (- (* 1.055 (expt B-linear (/ 1.0 2.4))) 0.055)))) (list :r (min (max R 0.0) 1.0) :g (min (max G 0.0) 1.0) :b (min (max B 0.0) 1.0)))) (defun hasliberg-theme--rgb-to-hex (rgb) "Convert a colour from RGB to Hex. RGB is a plist with properties :r, :g, and :b, where each value is in the range [0, 1]." (let* ((r (round (* (plist-get rgb :r) 255))) (g (round (* (plist-get rgb :g) 255))) (b (round (* (plist-get rgb :b) 255)))) (format "#%02X%02X%02X" r g b))) (defun hasliberg-theme--lch-to-rgb (lch) "Convert a colour from LCH to RGB in Hex. LCH is a plist with properties :luminance, :chroma, and :hue." (let* ((luv (hasliberg-theme--lch-to-luv lch)) (xyz (hasliberg-theme--luv-to-xyz luv)) (rgb (hasliberg-theme--xyz-to-rgb xyz))) (hasliberg-theme--rgb-to-hex rgb))) (defun hasliberg-theme--hex-to-rgb (hex) "Convert a colour from Hex to RGB. HEX can be a string in the form \"#RRGGBB\", \"RRGGBB\", \"#RGB\", or \"RGB\"." (let* ((normalized-hex (if (eq (aref hex 0) ?#) (substring hex 1) hex)) (expanded-hex (if (= (length normalized-hex) 3) (apply 'concat (mapcar (lambda (c) (make-string 2 c)) normalized-hex)) normalized-hex))) (when (not (= (length expanded-hex) 6)) (error "Invalid hex colour format, expected 3 or 6 characters")) (list :r (/ (string-to-number (substring expanded-hex 0 2) 16) 255.0) :g (/ (string-to-number (substring expanded-hex 2 4) 16) 255.0) :b (/ (string-to-number (substring expanded-hex 4 6) 16) 255.0)))) (defun hasliberg-theme--rgb-to-xyz (rgb) "Convert a colour from RGB to XYZ. RGB is a plist with properties :r, :g, and :b, where each value is in the range [0, 1]." (let* ((linearize (lambda (c) (if (<= c 0.04045) (/ c 12.92) (expt (/ (+ c 0.055) 1.055) 2.4)))) (R-linear (funcall linearize (plist-get rgb :r))) (G-linear (funcall linearize (plist-get rgb :g))) (B-linear (funcall linearize (plist-get rgb :b)))) (list :x (* 100 (+ (* R-linear 0.4124) (* G-linear 0.3576) (* B-linear 0.1805))) :y (* 100 (+ (* R-linear 0.2126) (* G-linear 0.7152) (* B-linear 0.0722))) :z (* 100 (+ (* R-linear 0.0193) (* G-linear 0.1192) (* B-linear 0.9505)))))) (defun hasliberg-theme--xyz-to-luv (xyz) "Convert a colour from XYZ to Luv. XYZ is a plist with properties :x, :y, and :z." (let* ((X (/ (plist-get xyz :x) 100.0)) (Y (/ (plist-get xyz :y) 100.0)) (Z (/ (plist-get xyz :z) 100.0)) (ref-X 0.95047) (ref-Y 1.00000) (ref-Z 1.08883) (ref-u (/ (* 4 ref-X) (+ ref-X (* 15 ref-Y) (* 3 ref-Z)))) (ref-v (/ (* 9 ref-Y) (+ ref-X (* 15 ref-Y) (* 3 ref-Z)))) (u (/ (* 4 X) (+ X (* 15 Y) (* 3 Z)))) (v (/ (* 9 Y) (+ X (* 15 Y) (* 3 Z)))) (L (if (> Y 0.008856) (- (* 116 (expt Y (/ 1.0 3))) 16) (* 903.3 Y))) (u-prime (* 13 L (- u ref-u))) (v-prime (* 13 L (- v ref-v)))) (list :l L :u u-prime :v v-prime))) (defun hasliberg-theme--luv-to-lch (luv) "Convert a colour from Luv to LCH. LUV is a plist with properties :l, :u, and :v." (let* ((L (plist-get luv :l)) (u (plist-get luv :u)) (v (plist-get luv :v)) (C (sqrt (+ (* u u) (* v v)))) (H (atan v u)) (H-degree (mod (/ (* H 180.0) pi) 360.0)) (format-3dp (lambda (num) (string-to-number (format "%.3f" num))))) (list :luminance (funcall format-3dp L) :chroma (funcall format-3dp C) :hue (funcall format-3dp H-degree)))) (defun hasliberg-theme--rgb-to-lch (hex) "Convert a colour from RGB Hex to LCH in Luv space. HEX is a string in the form \"#RRGGBB\"." (let* ((rgb (hasliberg-theme--hex-to-rgb hex)) (xyz (hasliberg-theme--rgb-to-xyz rgb)) (luv (hasliberg-theme--xyz-to-luv xyz))) (hasliberg-theme--luv-to-lch luv))) (defvar hasliberg-theme-shades nil "All shades for the Hasliberg theme colours based on LuvLCh input. The values here are not meant to be updated manually.") (defvar hasliberg-theme-shades-hash (make-hash-table :test 'equal) "Hash table of all Hasliberg theme shades for fast lookup. The values here are not meant to be updated manually.") (defun hasliberg-theme--generate-lch-shades (base-lch) "Generate a list of shades for a given LCH base colour. This takes in the dark / light theme variable into account, and changes the way it generates the shades. The higher values (e.g. 600, 700, so on) are meant to provide more contrast and appear brighter based on the background. In case of dark background, the higher values would result in brighter, more white colours. In case of light background, they would result in darker, more black colours." (let* ((l (plist-get base-lch :luminance)) (c (plist-get base-lch :chroma)) (h (plist-get base-lch :hue)) (shades '(50 100 200 300 400 500 600 700 800 900 950)) (dark-or-light hasliberg-theme-dark-or-light) (luminance-steps (mapcar (lambda (step) (if (eq dark-or-light 'dark) (+ l (* (- step 500) 0.1)) (- l (* (- step 500) 0.1)))) shades))) (mapcar (lambda (lum) (hasliberg-theme--lch-to-rgb (list :luminance lum :chroma c :hue h))) luminance-steps))) (defun hasliberg-theme--generate-all-shades () "Generate all shades for the colours defined with customization with the prefix of `hasliberg-theme-colour-'." (let* ((prefix "hasliberg-theme-colour-") (pflen (length prefix)) (customs '(hasliberg-theme-colour-background hasliberg-theme-colour-background-variant hasliberg-theme-colour-neutral hasliberg-theme-colour-primary hasliberg-theme-colour-secondary hasliberg-theme-colour-accent hasliberg-theme-colour-accent-variant hasliberg-theme-colour-subtle hasliberg-theme-colour-subtle-variant hasliberg-theme-colour-info hasliberg-theme-colour-warning)) (colours (cl-loop for c in customs collect (cons (intern (substring (symbol-name c) pflen)) (symbol-value c))))) (mapcar (lambda (colour) (let* ((name (car colour)) (base-lch (cdr colour)) (shades (hasliberg-theme--generate-lch-shades base-lch))) `(,name . ,(cl-pairlis '(50 100 200 300 400 500 600 700 800 900 950) shades)))) colours))) (defun hasliberg-theme--update-shades () "Update the shades and hash table based on the colour variables." (setq hasliberg-theme-shades (hasliberg-theme--generate-all-shades)) (clrhash hasliberg-theme-shades-hash) (cl-loop for (name . shades) in hasliberg-theme-shades do (cl-loop for (shade-name . shade-value) in shades do (puthash (format "%s-%s" name shade-name) shade-value hasliberg-theme-shades-hash)))) (defun hasliberg-theme-hex-for (key) "Retrieve the Hex colour using a hashed KEY." (gethash (symbol-name key) hasliberg-theme-shades-hash)) ;;;;---------------------------------------- ;;; Faces (Fonts) ;;------------------------------------------ (defface oblique-only '((t :inherit default :slant oblique)) "A custom face only to give oblique look") (defun hasliberg-theme--update-standard-faces () (custom-theme-set-faces 'hasliberg ;;;;---------------------------------------- ;;; Basic Faces ;;------------------------------------------ `(default ((t :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) `(bold ((t :weight bold))) `(italic ((t :inherit (default oblique-only)))) `(bold-italic ((t :inherit (bold oblique-only)))) `(link ((t :inherit (default oblique-only) :foreground ,(hasliberg-theme-hex-for 'neutral-400)))) `(highlight ((t :background ,(hasliberg-theme-hex-for 'background-variant-700)))) `(cursor ((t :background ,(hasliberg-theme-hex-for 'accent-700)))) `(region ((t :background ,(hasliberg-theme-hex-for 'background-700) :foreground ,(hasliberg-theme-hex-for 'subtle-600)))) `(secondary-selection ((t :background ,(hasliberg-theme-hex-for 'background-variant-600) :foreground ,(hasliberg-theme-hex-for 'subtle-600)))) `(whitespace-space ((t :foreground ,(hasliberg-theme-hex-for 'background-600)))) `(whitespace-tab ((t :foreground ,(hasliberg-theme-hex-for 'background-600)))) `(isearch ((t :background ,(hasliberg-theme-hex-for 'primary-300)))) `(success ((t :foreground ,(hasliberg-theme-hex-for 'info-700)))) `(warning ((t :foreground ,(hasliberg-theme-hex-for 'warning-500)))) `(minibuffer-prompt ((t :foreground ,(hasliberg-theme-hex-for 'primary-500) :weight semibold))) ;;;;---------------------------------------- ;;; Visual Elements ;;------------------------------------------ `(fringe ((t :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'accent-100)))) `(menu ((t :background ,(hasliberg-theme-hex-for 'background-300) :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) `(scroll-bar ((t :background ,(hasliberg-theme-hex-for 'background-300) :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) `(tool-bar ((t :background ,(hasliberg-theme-hex-for 'background-300) :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) `(vertical-border ((t :foreground ,(hasliberg-theme-hex-for 'background-300)))) `(header-line ((t :inherit oblique-only :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'neutral-300)))) `(tab-bar ((t :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) `(tab-bar-tab ((t :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) `(tab-bar-inactive ((t :foreground ,(hasliberg-theme-hex-for 'neutral-700)))) `(tab-bar-tab-group-current ((t :foreground ,(hasliberg-theme-hex-for 'neutral-500) :underline t))) `(tab-bar-tab-group-inactive ((t :foreground ,(hasliberg-theme-hex-for 'secondary-400)))) `(child-frame-border ((t :background ,(hasliberg-theme-hex-for 'primary-300)))) ;; Mode Line `(mode-line ((t :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'neutral-300)))) `(mode-line-active ((t :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'neutral-300) :overline ,(hasliberg-theme-hex-for 'neutral-500)))) `(mode-line-inactive ((t :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'neutral-200) :underline nil :overline nil))) ;;;;---------------------------------------- ;;; Line Numbers ;;------------------------------------------ `(line-number ((t :inherit default :height 0.85 :foreground ,(hasliberg-theme-hex-for 'neutral-50)))) `(line-number-current-line ((t :inherit line-number :foreground ,(hasliberg-theme-hex-for 'accent-500) :weight semibold))) `(line-number-major-tick ((t :inherit line-number :foreground ,(hasliberg-theme-hex-for 'neutral-600)))) `(line-number-minor-tick ((t :inherit line-number))) ;;;;---------------------------------------- ;;; Font lock ;;------------------------------------------ `(font-lock-string-face ((t :foreground ,(hasliberg-theme-hex-for 'accent-500) :weight regular))) `(font-lock-function-name-face ((t :foreground ,(hasliberg-theme-hex-for 'primary-400) :weight regular))) `(font-lock-variable-name-face ((t :foreground ,(hasliberg-theme-hex-for 'primary-400)))) `(font-lock-constant-face ((t :foreground ,(hasliberg-theme-hex-for 'primary-700) :weight semibold))) `(font-lock-type-face ((t :foreground ,(hasliberg-theme-hex-for 'primary-600) :weight regular))) `(font-lock-keyword-face ((t :foreground ,(hasliberg-theme-hex-for 'neutral-700) :weight semibold))) `(font-lock-builtin-face ((t :foreground ,(hasliberg-theme-hex-for 'neutral-400)))) `(font-lock-property-name-face ((t :foreground ,(hasliberg-theme-hex-for 'secondary-700)))) `(font-lock-negation-char-face ((t :inherit bold :foreground ,(hasliberg-theme-hex-for 'secondary-500)))) `(font-lock-preprocessor-face ((t :foreground ,(hasliberg-theme-hex-for 'neutral-300)))) `(font-lock-comment-face ((t :inherit (fixed-pitch oblique-only) :foreground ,(hasliberg-theme-hex-for 'neutral-200) :weight light))) `(font-lock-comment-delimiter-face ((t :inherit (fixed-pitch oblique-only) :foreground ,(hasliberg-theme-hex-for 'primary-200) :weight light))) `(font-lock-doc-face ((t :inherit (fixed-pitch oblique-only) :foreground ,(hasliberg-theme-hex-for 'info-700)))) `(font-lock-property-use-face ((t :foreground ,(hasliberg-theme-hex-for 'primary-600)))) `(font-lock-regexp-grouping-backslash ((t :inherit bold :foreground ,(hasliberg-theme-hex-for 'subtle-variant-500)))) `(font-lock-regexp-grouping-construct ((t :inherit bold :foreground ,(hasliberg-theme-hex-for 'subtle-variant-500)))) `(font-lock-warning-face ((t :foreground ,(hasliberg-theme-hex-for 'warning-500)))) ;;;;---------------------------------------- ;;; Org Mode ;;------------------------------------------ `(org-document-title ((t :foreground ,(hasliberg-theme-hex-for 'neutral-700) :height 3.0))) `(org-level-1 ((t :inherit default :foreground ,(hasliberg-theme-hex-for 'accent-500) :height 1.5))) `(org-level-2 ((t :inherit default :foreground ,(hasliberg-theme-hex-for 'accent-600) :height 1.25))) `(org-level-3 ((t :inherit default :foreground ,(hasliberg-theme-hex-for 'accent-700) :height 1.125))) `(org-level-4 ((t :inherit default :foreground ,(hasliberg-theme-hex-for 'accent-variant-800) :height 1.0625))) `(org-level-5 ((t :inherit default :foreground ,(hasliberg-theme-hex-for 'accent-variant-900) :height 1.0625))) `(org-level-6 ((t :inherit default :foreground ,(hasliberg-theme-hex-for 'accent-variant-900)))) `(org-special-keyword ((t :inherit fixed-pitch :weight thin :foreground ,(hasliberg-theme-hex-for 'neutral-200)))) `(org-code ((t :inherit fixed-pitch :foreground ,(hasliberg-theme-hex-for 'primary-500)))) `(org-verbatim ((t :inherit fixed-pitch :background ,(hasliberg-theme-hex-for 'background-400) :foreground ,(hasliberg-theme-hex-for 'primary-400)))) `(org-ellipsis ((t :inherit fixed-pitch :foreground ,(hasliberg-theme-hex-for 'accent-700) :underline nil :height 0.7))) `(org-hide ((t :height 1.2))) `(org-block ((t :inherit (default) :foreground ,(hasliberg-theme-hex-for 'subtle-variant-500)))) `(org-block-begin-line ((t :inherit (fixed-pitch oblique-only) :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'primary-400) :overline ,(hasliberg-theme-hex-for 'background-700) :height 0.80))) `(org-block-end-line ((t :inherit (fixed-pitch oblique-only) :background ,(hasliberg-theme-hex-for 'background-500) :foreground ,(hasliberg-theme-hex-for 'primary-400) :underline (:color ,(hasliberg-theme-hex-for 'background-700) :position 0) :height 0.75))) `(org-quote ((t :foreground ,(hasliberg-theme-hex-for 'accent-900) :slant oblique :height 1.1))) `(org-verse ((t :foreground ,(hasliberg-theme-hex-for 'neutral-600)))) `(org-table ((t :inherit fixed-pitch :background ,(hasliberg-theme-hex-for 'background-600) :foreground ,(hasliberg-theme-hex-for 'neutral-600)))) `(org-drawer ((t :inherit (fixed-pitch font-lock-comment-face)))) `(org-property-value ((t :inherit (fixed-pitch font-lock-comment-face)))) `(org-tag ((t :inherit fixed-pitch :height 0.7))) `(org-document-info-keyword ((t :inherit fixed-pitch :foreground ,(hasliberg-theme-hex-for 'neutral-300) :height 0.8))) `(org-meta-line ((t :inherit org-document-info-keyword))) `(org-checkbox ((t :inherit fixed-pitch :box nil))) `(org-headline-done ((t :foreground ,(hasliberg-theme-hex-for 'neutral-200)))) `(org-agenda-structure ((t :foreground ,(hasliberg-theme-hex-for 'primary-200)))) `(org-agenda-done ((t :foreground ,(hasliberg-theme-hex-for 'neutral-100)))) `(org-scheduled-today ((t :foreground ,(hasliberg-theme-hex-for 'neutral-500)))) )) (defun hasliberg-theme--update-echo-buffer () (dolist (buffer (list " *Minibuf-0*" " *Echo Area 0*" " *Minibuf-1*" " *Echo Area 1*")) (when (get-buffer buffer) (with-current-buffer buffer (face-remap-add-relative 'default 'font-lock-preprocessor-face))))) (defun hasliberg-theme--update-org-related-faces () (custom-theme-set-faces 'hasliberg `(org-modern-todo ((t :inherit fixed-pitch :background ,(hasliberg-theme-hex-for 'primary-600)))) `(org-modern-symbol ((t :inherit fixed-pitch))))) (defun hasliberg-theme--update-markdown-faces () (custom-theme-set-faces 'hasliberg `(markdown-header-face-1 ((t :inherit org-level-1))) `(markdown-header-face-2 ((t :inherit org-level-2))) `(markdown-header-face-3 ((t :inherit org-level-3))) `(markdown-header-face-4 ((t :inherit org-level-4))) `(markdown-header-face-5 ((t :inherit org-level-5))) `(markdown-header-delimiter-face ((t :inherit fixed-pitch :foreground ,(hasliberg-theme-hex-for 'accent-200) :height 1.1))) `(markdown-language-keyword-face ((t :inherit org-block))) `(markdown-table-face ((t :inherit org-table))) `(markdown-pre-face ((t :inherit org-block))))) (defun hasliberg-theme--update-language-specific-faces () (custom-theme-set-faces 'hasliberg `(sh-quoted-exec ((t :foreground ,(hasliberg-theme-hex-for 'accent-600)))) `(sh-heredoc ((t :foreground ,(hasliberg-theme-hex-for 'accent-700)))))) (defun hasliberg-theme--update-all-faces () "Update all faces." (hasliberg-theme--update-standard-faces) (hasliberg-theme--update-echo-buffer) (hasliberg-theme--update-org-related-faces) (hasliberg-theme--update-markdown-faces) (hasliberg-theme--update-language-specific-faces)) ;;;;---------------------------------------- ;;; Finalise ;;------------------------------------------ (hasliberg-theme--update-shades) (hasliberg-theme--update-all-faces) (when load-file-name (add-to-list 'custom-theme-load-path (file-name-as-directory (file-name-directory load-file-name)))) (provide-theme 'hasliberg)
5. Workspaces & Projects
5.1. Display Buffer Rules
Control where special buffers appear.
;; Display buffer rules for side windows (use-package window :ensure nil :custom (display-buffer-alist '(("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\|Messages\\|Occur\\|eldoc\\)\\*" (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 0)) ("\\*\\([Hh]elp\\)\\*" (display-buffer-in-side-window) (window-width . 75) (side . right) (slot . 0)) ("\\*\\(Flymake diagnostics\\|xref\\|Completions\\)" (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 1)) ("\\*\\(grep\\|find\\)\\*" (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 2)))))
5.2. Tab Bar (Workspaces)
Each tab is an independent workspace with its own window layout. Use M-1 through M-9 to switch instantly between workspaces.
;; Tab Bar Mode - workspace tabs (use-package tab-bar :ensure nil :config (tab-bar-mode 1) :custom (tab-bar-show 1) (tab-bar-close-button-show nil) (tab-bar-new-button-show nil) (tab-bar-tab-hints t) (tab-bar-tab-name-function 'tab-bar-tab-name-truncated) (tab-bar-tab-name-truncated-max 20) (tab-bar-new-tab-choice "*scratch*") (tab-bar-new-tab-to 'rightmost) (tab-bar-close-tab-select 'recent) (tab-bar-position 'top) (tab-bar-format '(tab-bar-format-tabs tab-bar-separator)) (tab-bar-history-mode 1) :bind (("C-x t n" . tab-bar-new-tab) ("C-x t c" . tab-bar-close-tab) ("C-x t o" . tab-bar-switch-to-next-tab) ("C-<tab>" . tab-bar-switch-to-next-tab) ("C-x t O" . tab-bar-switch-to-prev-tab) ("C-S-<tab>" . tab-bar-switch-to-prev-tab) ("C-x t r" . tab-bar-rename-tab) ("C-x t m" . tab-bar-move-tab) ;; Quick workspace switching (M-1 through M-9) ("M-1" . (lambda () (interactive) (tab-bar-select-tab 1))) ("M-2" . (lambda () (interactive) (tab-bar-select-tab 2))) ("M-3" . (lambda () (interactive) (tab-bar-select-tab 3))) ("M-4" . (lambda () (interactive) (tab-bar-select-tab 4))) ("M-5" . (lambda () (interactive) (tab-bar-select-tab 5))) ("M-6" . (lambda () (interactive) (tab-bar-select-tab 6))) ("M-7" . (lambda () (interactive) (tab-bar-select-tab 7))) ("M-8" . (lambda () (interactive) (tab-bar-select-tab 8))) ("M-9" . (lambda () (interactive) (tab-bar-select-tab 9))) ;; History ("C-x t <left>" . tab-bar-history-back) ("C-x t <right>" . tab-bar-history-forward))) ;; Custom tab naming with project context (defun fu/tab-bar-tab-name-with-project () "Generate tab name showing project and buffer." (let ((project-name (when (project-current) (file-name-nondirectory (directory-file-name (project-root (project-current)))))) (buffer-name (buffer-name))) (if project-name (format "%s: %s" project-name buffer-name) buffer-name))) (setq tab-bar-tab-name-function 'fu/tab-bar-tab-name-with-project) ;; Tab bar appearance matching current theme (defun fu/tab-bar-setup-appearance () "Dynamically customize tab-bar to match the current theme." (let* ((mode-line-bg (face-attribute 'mode-line :background nil 'default)) (mode-line-fg (face-attribute 'mode-line :foreground nil 'default)) (mode-line-inactive-bg (face-attribute 'mode-line-inactive :background nil 'default)) (mode-line-inactive-fg (face-attribute 'mode-line-inactive :foreground nil 'default)) (default-bg (face-attribute 'default :background nil 'default)) (default-fg (face-attribute 'default :foreground nil 'default)) (highlight-bg (or (and (face-attribute 'highlight :background nil 'default) (not (string= (face-attribute 'highlight :background nil 'default) "unspecified")) (face-attribute 'highlight :background nil 'default)) mode-line-bg)) (has-mode-line (and (stringp mode-line-bg) (not (string= mode-line-bg "unspecified"))))) (if has-mode-line (set-face-attribute 'tab-bar-tab nil :foreground mode-line-fg :background highlight-bg :weight 'bold :box `(:line-width 2 :color ,highlight-bg :style nil) :inherit 'unspecified) (set-face-attribute 'tab-bar-tab nil :inherit 'mode-line :weight 'bold :box nil)) (if (and (stringp mode-line-inactive-bg) (not (string= mode-line-inactive-bg "unspecified"))) (set-face-attribute 'tab-bar-tab-inactive nil :foreground mode-line-inactive-fg :background mode-line-inactive-bg :weight 'normal :box `(:line-width 2 :color ,mode-line-inactive-bg :style nil) :inherit 'unspecified) (set-face-attribute 'tab-bar-tab-inactive nil :inherit 'mode-line-inactive :weight 'normal :box nil)) (set-face-attribute 'tab-bar nil :background (if (and (stringp default-bg) (not (string= default-bg "unspecified"))) default-bg (face-attribute 'mode-line-inactive :background nil 'default)) :foreground default-fg :box nil :inherit 'unspecified))) ;; Refresh tab-bar appearance when theme changes (defun fu/tab-bar-refresh-appearance (&rest _) "Refresh tab-bar appearance after theme changes." (when (fboundp 'fu/tab-bar-setup-appearance) (fu/tab-bar-setup-appearance))) (advice-add 'load-theme :after #'fu/tab-bar-refresh-appearance) (advice-add 'enable-theme :after #'fu/tab-bar-refresh-appearance) (if (daemonp) (add-hook 'after-make-frame-functions (lambda (frame) (with-selected-frame frame (fu/tab-bar-setup-appearance)))) (fu/tab-bar-setup-appearance))
5.3. Project Management
Built-in project.el with custom root markers and project-aware tab creation.
;; Project Management (use-package project :ensure nil :custom (project-list-file (expand-file-name "projects" user-emacs-directory)) (project-switch-commands '((project-find-file "Find file" ?f) (project-find-regexp "Find regexp" ?g) (project-find-dir "Find directory" ?d) (project-dired "Dired" ?D) (project-vc-dir "VC-Dir" ?v) (project-shell "Shell" ?s) (project-eshell "Eshell" ?e) (magit-project-status "Magit" ?m) (project-compile "Compile" ?c) (project-switch-to-buffer "Switch buffer" ?b))) (project-remember-projects-under "~/Projects" t) :bind (("C-x p C" . project-recompile) ("C-x p m" . magit-project-status)) :config ;; Custom project root markers (defun fu/project-try-local (dir) "Try to find a project root in DIR by looking for marker files." (let ((markers '(".project" ".projectile" "package.json" "Cargo.toml" "setup.py" "requirements.txt" "pom.xml" "build.gradle"))) (cl-some (lambda (marker) (when-let* ((root (locate-dominating-file dir marker))) (cons 'transient root))) markers))) (add-hook 'project-find-functions #'fu/project-try-local) ;; Ignore common build directories (add-to-list 'project-vc-ignores "node_modules") (add-to-list 'project-vc-ignores "target") (add-to-list 'project-vc-ignores "build") (add-to-list 'project-vc-ignores "dist") (add-to-list 'project-vc-ignores ".venv") (add-to-list 'project-vc-ignores "__pycache__")) ;; Open project in a new workspace tab (defun fu/project-tab-bar-new () "Switch to a project in a new tab." (interactive) (let* ((project-dir (project-prompt-project-dir)) (project-name (file-name-nondirectory (directory-file-name project-dir)))) (tab-bar-new-tab) (let ((project-current-directory-override project-dir)) (project-switch-project project-dir)))) (global-set-key (kbd "C-x p t") #'fu/project-tab-bar-new)
5.4. Workspace Presets
;; Workspace presets (defun fu/tab-bar-workspace-writing () "Create a focused writing workspace." (interactive) (tab-bar-new-tab) (tab-bar-rename-tab "Writing") (delete-other-windows) (when (file-exists-p "~/org-roam") (dired "~/org-roam"))) (defun fu/tab-bar-workspace-coding () "Create a coding workspace with three-pane layout." (interactive) (tab-bar-new-tab) (tab-bar-rename-tab "Coding") (delete-other-windows) (when (file-exists-p "~/Projects") (dired "~/Projects")) (split-window-right) (other-window 1) (split-window-below)) (global-set-key (kbd "C-x t w w") #'fu/tab-bar-workspace-writing) (global-set-key (kbd "C-x t w c") #'fu/tab-bar-workspace-coding)
5.5. Session Persistence
;; Desktop save mode for session persistence (use-package desktop :ensure nil :custom (desktop-restore-frames t) (desktop-restore-in-current-display t) (desktop-restore-forces-onscreen nil) (desktop-dirname user-emacs-directory) (desktop-path (list user-emacs-directory)) :config (desktop-save-mode 1))
6. Completion
Built-in completion using icomplete-vertical-mode with flex matching. Zero external packages.
;;; Minibuffer Completion UI (use-package icomplete :ensure nil :init (icomplete-vertical-mode 1) :custom (icomplete-delay-completions-threshold 0) (icomplete-compute-delay 0) (icomplete-show-matches-on-no-input t) (icomplete-scroll t) (icomplete-prospects-height 10) :bind (:map icomplete-minibuffer-map ("RET" . icomplete-force-complete-and-exit) ("TAB" . icomplete-force-complete) ("C-n" . icomplete-forward-completions) ("C-p" . icomplete-backward-completions))) ;;; Completion Styles (use-package minibuffer :ensure nil :custom (completion-styles '(flex basic partial-completion emacs22)) (completion-category-defaults nil) (completion-category-overrides '((file (styles partial-completion)))) (completion-ignore-case t) (read-buffer-completion-ignore-case t) (read-file-name-completion-ignore-case t) (completions-detailed t) (completions-format 'one-column) (completions-max-height 20)) ;;; Recent Files (use-package recentf :ensure nil :init (recentf-mode 1) :custom (recentf-max-saved-items 100) :bind ("C-x C-r" . recentf-open-files)) ;;; Completion History (use-package savehist :ensure nil :init (savehist-mode 1)) ;;; Built-in search commands (global-set-key (kbd "M-g i") 'imenu) (global-set-key (kbd "C-c s l") 'occur) (global-set-key (kbd "C-c s g") 'grep)
7. Distraction-Free Writing
Olivetti centers text for a focused writing experience. Toggle with F9.
;; Distraction-Free Writing (use-package olivetti :bind ("<f9>" . olivetti-mode) :custom (olivetti-body-width 80) (olivetti-recall-visual-line-mode-entry-state t))
8. Spell Checking
Flyspell with automatic detection of aspell or hunspell.
;; Spell Checking (use-package flyspell :ensure nil :custom (ispell-program-name (cond ((executable-find "aspell") "aspell") ((executable-find "hunspell") "hunspell") (t "ispell"))) :bind (("<f5>" . flyspell-mode) ("<f6>" . flyspell-goto-next-error) ("<f7>" . ispell-word)) :hook ((text-mode . flyspell-mode) (org-mode . flyspell-mode) (prog-mode . flyspell-prog-mode)))
9. Org-mode
9.1. Core Configuration
;; Org-mode (use-package org :ensure nil :commands (org-capture org-agenda) :custom (org-catch-invisible-edits 'show) (org-edit-timestamp-down-means-later t) (org-src-window-setup 'current-window) (org-export-coding-system 'utf-8) (org-html-validation-link nil) (org-hide-emphasis-markers t) (org-pretty-entities nil) (org-startup-indented nil) (org-startup-with-inline-images t) (org-image-actual-width '(450)) (org-tags-column 80) :hook ((org-mode . olivetti-mode)) :config ;; Load org-tempo for easy template expansion (< + TAB) (require 'org-tempo) ;; Structure templates for all supported languages (add-to-list 'org-structure-template-alist '("sh" . "src shell")) (add-to-list 'org-structure-template-alist '("bash" . "src bash")) (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp")) (add-to-list 'org-structure-template-alist '("py" . "src python")) (add-to-list 'org-structure-template-alist '("js" . "src js")) (add-to-list 'org-structure-template-alist '("ts" . "src typescript")) (add-to-list 'org-structure-template-alist '("cc" . "src C")) (add-to-list 'org-structure-template-alist '("rb" . "src ruby")) (add-to-list 'org-structure-template-alist '("lua" . "src lua")) (add-to-list 'org-structure-template-alist '("sql" . "src sql")) (add-to-list 'org-structure-template-alist '("txt" . "src text")) ;; Babel languages (only load available ones) (org-babel-do-load-languages 'org-babel-load-languages (seq-filter (lambda (pair) (locate-library (concat "ob-" (symbol-name (car pair))))) '((shell . t) (emacs-lisp . t) (python . t) (js . t) (C . t) (lua . t) (ruby . t) (sql . t)))) ;; Fix electric-pair-mode for org-mode angle brackets (defun fu/org-electric-pair-inhibit (c) (if (char-equal c ?<) t (electric-pair-inhibit-predicate c))) (add-hook 'org-mode-hook (lambda () (setq-local electric-pair-inhibit-predicate #'fu/org-electric-pair-inhibit))))
9.2. Org Modern
Modern visual styling for Org-mode with beautiful typography.
;; Modern Org-mode styling (use-package org-modern :hook (org-mode . org-modern-mode) (org-agenda-finalize . org-modern-agenda) :custom (org-modern-star 'replace) (org-modern-hide-stars t) (org-modern-table t) (org-modern-table-vertical 1) (org-modern-table-horizontal 0.2) (org-modern-list '((43 . "➤") ; + (45 . "–") ; - (42 . "•"))) ; * (org-modern-block-name '(("src" "»" "«") ("example" "»–" "–«") ("quote" "❝" "❞") ("export" "⏩" "⏪"))) (org-modern-keyword t) (org-modern-checkbox nil) (org-modern-horizontal-rule t) (org-modern-timestamp t) (org-modern-statistics t) (org-modern-tag t) (org-modern-priority t) (org-modern-todo t) (org-modern-todo-faces '(("TODO" :background "#ff6c6b" :foreground "#282c34" :weight bold) ("DOING" :background "#51afef" :foreground "#282c34" :weight bold) ("REVIEW" :background "#ecbe7b" :foreground "#282c34" :weight bold) ("DONE" :background "#98be65" :foreground "#282c34" :weight bold) ("CANCEL" :background "#5B6268" :foreground "#282c34" :weight bold))))
10. Markdown Support
GitHub-Flavored Markdown editing with TOC generation and distraction-free writing.
;; Markdown (use-package markdown-mode :mode ("README\\.md\\'" . gfm-mode) :init (setq markdown-command "multimarkdown") :bind (:map markdown-mode-map ("C-c C-e" . markdown-do) ("C-c C-t" . markdown-toc-generate-or-refresh-toc)) :custom (markdown-header-scaling t) (markdown-enable-wiki-links t) (markdown-italic-underscore t) (markdown-fontify-code-blocks-natively t) (markdown-gfm-use-electric-backquote nil) :hook ((markdown-mode . olivetti-mode))) (use-package markdown-toc :after markdown-mode)
11. Tree-sitter
Automatic tree-sitter grammar installation and mode remapping for rich syntax highlighting.
;; Tree-sitter: automatic grammar install + mode remapping (use-package treesit-auto :custom (treesit-auto-install 'prompt) :config (treesit-auto-add-to-auto-mode-alist 'all) (global-treesit-auto-mode))
12. Eglot (LSP)
Lightweight Language Server Protocol support (built-in since Emacs 29). Install language servers separately (e.g., pyright, typescript-language-server, clangd, lua-language-server).
;; LSP - Eglot (built-in) (use-package eglot :ensure nil :custom (eglot-autoshutdown t) (eglot-events-buffer-size 0) (eglot-events-buffer-config '(:size 0 :format full)) (eglot-prefer-plaintext t) (jsonrpc-event-hook nil) (eglot-code-action-indications nil) (eglot-ignored-server-capabilities '(:workspaceFolders)) :init (fset #'jsonrpc--log-event #'ignore) :bind (:map eglot-mode-map ("C-c l a" . eglot-code-actions) ("C-c l r" . eglot-rename) ("C-c l f" . eglot-format)) :hook ((python-ts-mode . eglot-ensure) (js-ts-mode . eglot-ensure) (typescript-ts-mode . eglot-ensure) (c-ts-mode . eglot-ensure) (ruby-ts-mode . eglot-ensure) (lua-mode . eglot-ensure)))
13. Light Programming Support
13.1. Editing Utilities
;; Auto bracket pairing (use-package elec-pair :ensure nil :hook (after-init . electric-pair-mode)) ;; Matching parentheses (use-package paren :ensure nil :hook (after-init . show-paren-mode) :custom (show-paren-delay 0) (show-paren-style 'mixed) (show-paren-context-when-offscreen t))
13.2. Syntax Checking
;; Flymake syntax checking (use-package flymake :ensure nil :defer t :hook ((prog-mode . flymake-mode) (emacs-lisp-mode . flymake-mode)) :bind (:map flymake-mode-map ("M-8" . flymake-goto-next-error) ("M-7" . flymake-goto-prev-error) ("C-c ! l" . flymake-show-buffer-diagnostics)) :custom (flymake-show-diagnostics-at-end-of-line nil) (flymake-indicator-type 'margins) (flymake-margin-indicators-string `((error "!" compilation-error) (warning "?" compilation-warning) (note "i" compilation-info))))
13.3. Language Modes
;; Lua (no built-in tree-sitter mode) (use-package lua-mode :defer t) ;; YAML (use-package yaml-mode :defer t) ;; Configuration files (use-package conf-mode :ensure nil :mode ("\\.env\\..*\\'" "\\.env\\'") :init (add-to-list 'auto-mode-alist '("\\.env\\'" . conf-mode))) ;; Documentation (use-package eldoc :ensure nil :init (global-eldoc-mode))
13.4. Version Control
;; Git interface (use-package magit :bind (("C-x g" . magit-status) ("C-c g" . magit-file-dispatch)) :custom (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1) (magit-save-repository-buffers 'dontask) (magit-bury-buffer-function #'magit-mode-quit-window) (magit-diff-refine-hunk t) (magit-diff-refine-ignore-whitespace t)) ;; Merge conflict resolution (use-package smerge-mode :ensure nil :bind (:map smerge-mode-map ("C-c ^ u" . smerge-keep-upper) ("C-c ^ l" . smerge-keep-lower) ("C-c ^ n" . smerge-next) ("C-c ^ p" . smerge-previous)))
13.5. Diff and Merge
;; Diff (use-package diff-mode :ensure nil :defer t :config (setq diff-default-read-only t diff-advance-after-apply-hunk t diff-update-on-the-fly t diff-font-lock-syntax 'hunk-also)) ;; Ediff with horizontal split (use-package ediff :ensure nil :commands (ediff-buffers ediff-files) :init (setq ediff-split-window-function 'split-window-horizontally ediff-window-setup-function 'ediff-setup-windows-plain) :config (setq ediff-keep-variants nil ediff-merge-revisions-with-ancestor t ediff-show-clashes-only t))
14. Word Count & Writing Utilities
;; Word count for buffer (defun fu/word-count-buffer () "Display the word count for the current buffer." (interactive) (message "Words in buffer: %d" (count-words (point-min) (point-max)))) ;; Word count for region (defun fu/word-count-region () "Display the word count for the current region." (interactive) (if (use-region-p) (message "Words in region: %d" (count-words (region-beginning) (region-end))) (message "No active region"))) ;; Insert timestamp (defun fu/insert-timestamp () "Insert current timestamp." (interactive) (insert (format-time-string "%Y-%m-%d %H:%M:%S"))) ;; Insert date (defun fu/insert-date () "Insert current date." (interactive) (insert (format-time-string "%Y-%m-%d"))) ;; Combined writing mode toggle (defun fu/toggle-writing-mode () "Toggle writing mode: olivetti + flyspell + visual-line." (interactive) (if (bound-and-true-p olivetti-mode) (progn (olivetti-mode -1) (flyspell-mode -1) (visual-line-mode -1) (message "Writing mode OFF")) (olivetti-mode 1) (flyspell-mode 1) (visual-line-mode 1) (message "Writing mode ON"))) ;; Mode-line word count (updated on idle) (defvar fu/word-count-string "" "Word count string displayed in mode line.") (defun fu/update-word-count () "Update word count for mode line display." (when (derived-mode-p 'text-mode 'org-mode 'markdown-mode) (setq fu/word-count-string (format " W:%d" (count-words (point-min) (point-max)))))) (run-with-idle-timer 2 t #'fu/update-word-count) (add-to-list 'global-mode-string '(:eval fu/word-count-string) t) ;; Key bindings (global-set-key (kbd "C-c w c") #'fu/word-count-buffer) (global-set-key (kbd "C-c w s") #'fu/word-count-region) (global-set-key (kbd "C-c w t") #'fu/insert-timestamp) (global-set-key (kbd "C-c w d") #'fu/insert-date) (global-set-key (kbd "<f8>") #'display-line-numbers-mode) (global-set-key (kbd "<f10>") #'fu/toggle-writing-mode)
15. File Management
Minimal dired configuration with human-readable sizes.
;; Dired (use-package dired :ensure nil :commands (dired dired-jump) :bind (("C-x C-j" . dired-jump)) :custom (dired-listing-switches "-ahl --group-directories-first") (dired-auto-revert-buffer t) (dired-recursive-copies 'always) (dired-recursive-deletes 'always) (dired-kill-when-opening-new-dired-buffer t) :config (put 'dired-find-alternate-file 'disabled nil))
16. End of Init
(provide 'init) ;;; init.el ends here
17. Key Bindings Reference Card
17.1. Workspaces & Projects
| Key | Action |
|---|---|
M-1 to M-9 |
Jump to workspace by number |
C-TAB |
Next workspace |
C-S-TAB |
Previous workspace |
C-x p p |
Switch project |
C-x p f |
Find file in project |
C-x p b |
Project buffers |
C-x p t |
Open project in new tab |
C-x t n |
New workspace |
C-x t c |
Close workspace |
C-x t r |
Rename workspace |
C-x t w w |
Writing workspace preset |
C-x t w c |
Coding workspace preset |
17.2. Writing
| Key | Action |
|---|---|
F5 |
Toggle spell checking |
F6 |
Next spelling error |
F7 |
Correct word at point |
F8 |
Toggle line numbers |
F9 |
Toggle olivetti (distraction-free) |
F10 |
Toggle writing mode (combined) |
C-c w c |
Word count (buffer) |
C-c w s |
Word count (region) |
C-c w t |
Insert timestamp |
C-c w d |
Insert date |
C-x C-r |
Open recent file |
M-o |
Other window |
17.3. Coding
| Key | Action |
|---|---|
C-c l a |
Eglot code actions |
C-c l r |
Eglot rename symbol |
C-c l f |
Eglot format |
M-7 |
Previous flymake error |
M-8 |
Next flymake error |
C-x g |
Magit status |
17.4. Org-mode (Babel)
| Template | Language |
|---|---|
< sh |
Shell |
< el |
Emacs Lisp |
< py |
Python |
< js |
JavaScript |
< ts |
TypeScript |
< cc |
C |
< lua |
Lua |
< rb |
Ruby |
< sql |
SQL |
Type the template prefix then press TAB to expand.
18. Tangling & Launch Instructions
18.1. Tangle the Configuration
Open this file in Emacs and run C-c C-v t to extract all source blocks to their target files:
~/.emacs-writing.d/early-init.el- Early initialization~/.emacs-writing.d/init.el- Main configuration~/.emacs-writing.d/themes/hasliberg-theme.el- Color theme
18.2. Launch
# Launch with the writing configuration emacs --init-directory ~/.emacs-writing.d/ # Or create a shell alias alias ew='emacs --init-directory ~/.emacs-writing.d/'
18.3. First Launch
On first launch, Emacs will automatically download and install the 8 external packages. This requires an internet connection and may take a minute.
18.4. External Packages
| Package | Purpose |
|---|---|
| olivetti | Distraction-free centered writing |
| org-modern | Modern org-mode visual styling |
| markdown-mode | Markdown editing with GFM |
| markdown-toc | TOC generation for Markdown |
| treesit-auto | Auto tree-sitter grammar install |
| lua-mode | Lua language support |
| yaml-mode | YAML editing |
| magit | Git interface |
18.5. Language Servers (install separately)
For LSP features via Eglot, install the appropriate language server:
| Language | Server | Install |
|---|---|---|
| Python | pyright | npm i -g pyright |
| JavaScript | typescript-language-server | npm i -g typescript-language-server |
| TypeScript | typescript-language-server | npm i -g typescript typescript-language-server |
| C/C++ | clangd | brew install llvm or system package manager |
| Ruby | solargraph | gem install solargraph |
| Lua | lua-language-server | brew install lua-language-server |