, 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:
- Change the
mode-line-active
color once. - Change the
mode-line-active
color based on a named Modus Theme color. - 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:
- First get the
auto_iterm_tag_color_cwd
to work with a hard-coded hex value. - 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:
- elisp - Is there an Emacs hook that runs after every buffer is created? - Stack Overflow
- macos - Different colour for new tabs in iTerm2 - Ask Different
- Modus Themes | Protesilaos Stavrou
- script-commands/hex-to-rgb.sh at master · raycast/script-commands
- Standard Hooks (GNU Emacs Lisp Reference Manual)
- tysonwolker/iterm-tab-colors: Tabs automatically change colors depending on the folder you are in.
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.