For several years now I've been using color in my Emacs buffers to mark whether it's a "work" project or a "personal" project. I think I first showed this in a blog post I wrote in 2017, when I had switched to spaceline's mode line. Since then I've extended that color to also work on the tab line, current line, and header line. I wanted to make it work with the cursor color but couldn't figure that out.

I saw a question on stack exchange today that made me realize I could extend this to have one color per project, similar to how rainbow-identifiers works.

Screenshot of emacs with a purple theme for every buffer in this project
Color theme per project

How does this work? The magic function is face-remap-add-relative, which lets you change a face for one buffer only. I call this from several hooks that run rarely:

(defun amitp/set-modeline-color ()
  "Set mode line color based on current buffer's project"
  (let ((color (project-color-background)))
    (face-remap-add-relative 'tab-line-tab-current :background color :foreground "white")
    (face-remap-add-relative 'mode-line-active :background color :foreground "white")
    (face-remap-add-relative 'line-number-current-line :background color :foreground "white")
    ))

(add-hook 'find-file-hook #'amitp/set-modeline-color)
(add-hook 'dired-mode-hook #'amitp/set-modeline-color)
(add-hook 'change-major-mode-hook #'amitp/set-modeline-color)
(add-hook 'temp-buffer-setup-hook #'amitp/set-modeline-color)

But how do I set the color? I hash the project root and then separate that hash out into hue, saturation, and lightness to construct an HSL color. Every file/buffer in that project gets the same color theme:

(require 'project)
(require 'color)

(defun project-color-background ()
  "Return a background color for the project containing this directory"
  (let* ((project (project-current))
         (dirhash (sxhash (if project (project-root project) default-directory)))
         (hue (/ (mod dirhash 1000) 1000.0))
         (saturation (+ 0.3 (* 0.1 (mod (/ dirhash 1000) 3))))
         (lightness (+ 0.4 (* 0.05 (mod (/ (/ dirhash 1000) 3) 4))))
         (rgb (color-hsl-to-rgb hue saturation lightness)))
    (color-rgb-to-hex (nth 0 rgb) (nth 1 rgb) (nth 2 rgb) 2)))

If there's no current project, I'll use the current directory. I tried to pick medium saturation and lightness for the colors. Most of them are pleasing but I need to tweak the greens and yellows to be a little bit darker, or I need to switch to using oklch colors.

Screenshot of emacs with a green color for a different project
Color theme for a different project

An additional tweak not shown here is that I still want to distinguish work and personal projects, so I use one color range for work projects and another color range for personal projects.

I've put the code up in a github gist.

Labels:

0 comments: