Adding Consistent Color to Emacs Mode Line and iTerm Tab for Various Projects

An Evening of Hacking to Provide Visual Clues for Varying Contexts

, I was pairing with a co-worker and they were sharing their iTerm2 and Visual Studio Code 📖 tab colors. They mentioned that there are plugins for setting the iTerm tab color. Which had me wondering how I might configure my iTerm2 and Emacs 📖 to have the same colors for the same project?

I approached this problem in three steps:

  • How do I adjust the mode-line color of Emacs?
  • How do I adjust the tab color of iTerm2?
  • How do I create a common canonical source for colors?

After an of hacking, I have a solution that works well for my use-case.

Adjusting the Mode-line Color of Emacs

For Emacs, I use the Modus Themes by Protesilaos Stavrou 📖. This informed my solution, as I wanted to use the named colors. I love the modus-theme-list-colours function; it was helpful to see the range that I was working with.

I also use projectile, which provides a useful function for determining the project’s root file (e.g. “Where’s the Git 📖 folder located?”)

Emacs configuration for auto-adjusting mode-line colors by project.

The following code is available in jf-project-theme-colors.el.

(require 'modus-themes)
(require 'projectile)
(defvar jf/project/theme-colors/table
  '(("~/git/dotemacs/" . bg-green-subtle)
    ("~/git/dotzshrc/" . bg-green-nuanced)
    ("~/git/takeonrules.source/" . bg-magenta-subtle)
    ("~/git/org/" . bg-sage)
    ("~/git/britishlibrary/" . bg-blue-intense)
    ("~/git/adventist-dl/" . bg-yellow-intense)
    ("~/git/utk-hyku/" . bg-red-intense)
    ("~/git/bulkrax/" . bg-sage))
  "The `car' of each list item should be of begin with \"~/\" and
 end with \"/\" (so as to conform to multiple machines and
 projectile's interface.")

(cl-defun jf/project/theme-colors/current (&key (default 'bg-blue-subtle))
  "Returns a HEX color (e.g. \"#CCDDEE\") for the given project.

The DEFAULT is a named color in the `modus-themes' palette."
  (let* ((project-dir (abbreviate-file-name (or (projectile-project-root) "~/")))
         (name (alist-get project-dir
                          jf/project/theme-colors/table
                          default nil #'string=)))
    (modus-themes-get-color-value name)))

(defun jf/project/theme-colors/apply-to-buffer ()
  "Apply the the project's colors to the buffer (e.g. 'mode-line-active)"
  (unless (active-minibuffer-window)
    (progn
      (face-remap-add-relative
       'mode-line-active
       `( :background ,(jf/project/theme-colors/current)
          :foreground ,(face-attribute 'default :foreground))))))

;; I need to ensure that I'm not doing this while Emacs is initializing.  If I
;; don't have the 'after-init-hook I experience significant drag/failure to
;; initialize.
(add-hook 'after-init-hook
          (lambda ()
            (add-hook 'buffer-list-update-hook
                      #'jf/project/theme-colors/apply-to-buffer)
            (add-hook 'projectile-after-switch-project-hook
                      #'jf/project/theme-colors/apply-to-buffer)))

I did this work in three parts:

  1. Change the mode-line-active color once.
  2. Change the mode-line-active color based on a named Modus Theme color.
  3. Change the mode-line-active color when I changed projects or buffers.

Adjusting the Tab Color of iTerm2

With Emacs resolved, I set about adjust the iTerm2 tabs.

The auto_iterm_tag_color_cwd shell function

This code is available in my configs/functions.zsh file.

# This function sets the tab color for iTerm based on the "term-color-get"
# results.
function auto_iterm_tag_color_cwd () {
    preline="\r\033[A"
    # Assumes format of `"#aabbcc"'
    hex=`term-color-get`

    first="${hex:0:1}"

    if [ "#" = "$first" ]; then
        hex="${hex:1:6}"
    fi

    hex_r="${hex:0:2}"
    hex_g="${hex:2:2}"
    hex_b="${hex:4:2}"

    rgb_r=`echo $((0x${hex_r}))`
    rgb_g=`echo $((0x${hex_g}))`
    rgb_b=`echo $((0x${hex_b}))`

    echo -e "\033]6;1;bg;red;brightness;$rgb_r\a"$preline
    echo -e "\033]6;1;bg;green;brightness;$rgb_g\a"$preline
    echo -e "\033]6;1;bg;blue;brightness;$rgb_b\a"$preline
}

auto_iterm_tag_color_cwd
autoload -U add-zsh-hook
add-zsh-hook chpwd auto_iterm_tag_color_cwd

In working on this, I brought the solution into two steps:

  1. First get the auto_iterm_tag_color_cwd to work with a hard-coded hex value.
  2. Create a term-color-get function that would echo a hex value.

Common Canonical Source for Colors

A Ruby shell script to re-use the named color property of Emacs

This code is available at my bin/term-color-get.

#!/usr/bin/env ruby -wU

# This command is responsible for returning a hex color code, prefixed with the # sign.  It will determine

# The following colors come from the modus tinted color palette.  The names are common across
# modus-vivendi and modus-operandi but the hex colors vary.
COLOR_LOOKUP_LIGHT = {
  "bg-red-intense" => "#ff8f88",
  "bg-green-intense" => "#8adf80",
  "bg-yellow-intense" => "#f3d000",
  "bg-blue-intense" => "#bfc9ff",
  "bg-magenta-intense" => "#dfa0f0",
  "bg-cyan-intense" => "#a4d5f9",
  "bg-red-subtle" => "#ffcfbf",
  "bg-green-subtle" => "#b3fabf",
  "bg-yellow-subtle" => "#fff576",
  "bg-blue-subtle" => "#ccdfff",
  "bg-magenta-subtle" => "#ffddff",
  "bg-cyan-subtle" => "#bfefff",
  "bg-red-nuanced" => "#ffe8f0",
  "bg-green-nuanced" => "#e0f5e0",
  "bg-yellow-nuanced" => "#f9ead0",
  "bg-blue-nuanced" => "#ebebff",
  "bg-magenta-nuanced" => "#f6e7ff",
  "bg-cyan-nuanced" => "#e1f3fc",
  "bg-ochre" => "#f0e0cc",
  "bg-lavender" => "#dfdbfa",
  "bg-sage" => "#c0e7d4"
}

COLOR_LOOKUP_DARK = {
  "bg-red-intense" => "#9d1f1f",
  "bg-green-intense" => "#2f822f",
  "bg-yellow-intense" => "#7a6100",
  "bg-blue-intense" => "#1640b0",
  "bg-magenta-intense" => "#7030af",
  "bg-cyan-intense" => "#2266ae",
  "bg-red-subtle" => "#620f2a",
  "bg-green-subtle" => "#00422a",
  "bg-yellow-subtle" => "#4a4000",
  "bg-blue-subtle" => "#242679",
  "bg-magenta-subtle" => "#552f5f",
  "bg-cyan-subtle" => "#004065",
  "bg-red-nuanced" => "#350f14",
  "bg-green-nuanced" => "#002718",
  "bg-yellow-nuanced" => "#2c1f00",
  "bg-blue-nuanced" => "#131c4d",
  "bg-magenta-nuanced" => "#2f133f",
  "bg-cyan-nuanced" => "#04253f",
  "bg-ochre" => "#442c2f",
  "bg-lavender" => "#38325c",
  "bg-sage" => "#0f3d30"
}

COLOR_REGEXP = %r{\(mode-line-bg-color-name \. ([^\)]+)\)}

# When I have a "light" MacOS setting use the light colors.
table = `defaults read -g AppleInterfaceStyle 2>/dev/null`.strip.size.zero? ? COLOR_LOOKUP_LIGHT : COLOR_LOOKUP_DARK

# Set the default, which maps to my present default setting in Emacs.
color = table.fetch("bg-blue-subtle")

project_theme_colors_filename = File.join(Dir.home, "/git/dotemacs/emacs.d/jf-project-theme-colors.el")
if !File.exist?(project_theme_colors_filename)
  puts color
  exit! 0
end

# Recursively find the most dominant '.dir-locals.el' file in the ancestor directories
slugs = Dir.pwd.split("/")
(0...slugs.size).each do |i|
  filename = File.join(*slugs[0...(slugs.size - i)], ".git")
  next unless File.exist?(filename)
  project_name = filename.sub(Dir.home, "~").sub(/\.git$/, "")

  content = File.read(project_theme_colors_filename)
  match = %r{\("#{project_name}" \. (bg-[^\)]+)\)}.match(content)
  next unless match

  color = table.fetch(match[1], color)
  break
end
puts color

This bit of glue was the easiest to write. I chose to hard code the named color hex representation; as that decoupled me from needing an instance of Emacs running. Yes, sometimes I don’t have Emacs running on my machine.

Citing My Sources

Along the way, I used the following sources for reference to help me build this out:

Conclusion

I built this up from reading a variety of different sources and then experimenting in an incremental fashion. And now I have a little bit of color in my life to help me visually note when my Emacs buffer and iTerm2 path are pointing to the same project.