.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | What's the point in an Emacs theme if the rest of Linux looks different?
7 |
8 |
9 |
10 | Apply your Emacs theme to the rest of Linux, using magic. Also works on Mac.
11 |
12 |
13 | ---
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Usage
21 |
22 | Just call `M-x` `theme-magic-from-emacs`. theme-magic will extract the colors from your Emacs theme and apply them to the rest of Linux with [Pywal](https://github.com/dylanaraps/pywal).
23 |
24 | If you want the Linux theme to update automatically whenever the Emacs theme is changed, enable the global minor mode `theme-magic-export-theme-mode`. For example:
25 |
26 | ```emacs-lisp
27 | (require 'theme-magic)
28 | (theme-magic-export-theme-mode)
29 | ```
30 |
31 | You can disable auto-updating by disabling the minor mode.
32 |
33 | ## Installation
34 |
35 | ### Dependencies
36 |
37 | First, you must install [Pywal](https://github.com/dylanaraps/pywal) as a dependency. Check if it's installed by calling `wal` in a shell. Make sure Python is installed too.
38 |
39 | ### Installing `theme-magic` from MELPA
40 |
41 | `theme-magic` is [available](http://melpa.org/#/theme-magic) on MELPA. Follow the [instructions](https://melpa.org/#/getting-started) to set up MELPA.
42 |
43 | Install `theme-magic` with `M-x package-install RET theme-magic RET`.
44 |
45 | ## Footnotes
46 |
47 | ### Restoring Your Theme
48 |
49 | [Pywal](https://github.com/dylanaraps/pywal) only applies your theme to the current session. See its documentation for details. To restore the last theme, call `wal -R` in the shell. To restore your theme automatically, add the following to your `.xprofile` (or whichever dotfile is loaded automatically once your desktop starts up):
50 |
51 | ```shell
52 | wal -R
53 | ```
54 |
55 | ### Setting Your Wallpaper
56 |
57 | Pywal was designed to generate a color scheme that matches your wallpaper. Because of some quirks in how Pywal works, you have to set the wallpaper before exporting a theme from Emacs, or it will not be saved. Call this command in a shell:
58 |
59 | ```shell
60 | wal -i "path/to/wallpaper.png"
61 | ```
62 |
63 | Pywal will set your wallpaper and save it in its cache. Now, you can apply your Emacs theme:
64 |
65 | ```emacs
66 | M-x theme-magic-from-emacs
67 | ```
68 |
69 | Now, when you call `wal -R`, both the wallpaper and the theme will be set.
70 |
71 | ### MacOS
72 |
73 | `theme-magic` [also works](https://github.com/jcaw/theme-magic/issues/11) on MacOS. iTerm2 should inherit from the exported Emacs theme. You will need to call `wal -R` to refresh when the terminal restarts.
74 |
--------------------------------------------------------------------------------
/RECIPE:
--------------------------------------------------------------------------------
1 | ;;; -*- mode: emacs-lisp -*-
2 |
3 | (theme-magic
4 | :fetcher github
5 | :repo "jcaw/theme-magic"
6 | ;; Must explicitly download the python scripts folder
7 | :files (:defaults "python"))
8 |
--------------------------------------------------------------------------------
/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcaw/theme-magic/844c4311bd26ebafd4b6a1d72ddcc65d87f074e3/media/logo.png
--------------------------------------------------------------------------------
/media/theming-linux-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcaw/theme-magic/844c4311bd26ebafd4b6a1d72ddcc65d87f074e3/media/theming-linux-demo.gif
--------------------------------------------------------------------------------
/python/wal_change_colors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys
4 | import re
5 | import os
6 | import shutil
7 | import json
8 | from subprocess import Popen, PIPE
9 |
10 |
11 | def expandpath(path):
12 | path = os.path.expanduser(path)
13 | path = os.path.expandvars(path)
14 | path = os.path.abspath(path)
15 | return path
16 |
17 |
18 | WAL_DIR = expandpath("~/.cache/wal")
19 | CONFIG_FILE_PATH = expandpath("~/.cache/wal/colors.json")
20 | WAL_FILE_PATH = expandpath("~/.cache/wal/wal")
21 |
22 |
23 | def create_wal_cache():
24 | """Create wal's .cache folder (if it doesn't exist)."""
25 | if not os.path.isdir(WAL_DIR):
26 | os.mkdir(WAL_DIR)
27 |
28 |
29 | def empty_config():
30 | """Construct an empty pywal config."""
31 | return {
32 | "colors": {},
33 | }
34 |
35 |
36 | def load_config():
37 | """Load the current pywal configuration."""
38 | if not config_exists():
39 | create_wal_cache()
40 | return empty_config()
41 | with open(CONFIG_FILE_PATH, "r") as f:
42 | return json.load(f)
43 |
44 |
45 | def save_config(config_dict):
46 | """Save the current pywal configuration."""
47 | with open(CONFIG_FILE_PATH, "w") as f:
48 | json.dump(config_dict, f)
49 |
50 |
51 | def rewrite_wal_file(new_wallpaper):
52 | """Adjust the `wal` file to hold the new wallpaper."""
53 | with open(WAL_FILE_PATH, "w") as f:
54 | # The `wal` file should just contain the wallpaper path - nothing else.
55 | f.write(new_wallpaper)
56 |
57 |
58 | def call_process(args):
59 | """Call an external process, with reasonable error handling."""
60 | process = Popen(args, stdout=PIPE, stderr=PIPE)
61 | stdout, stderr = process.communicate()
62 | # We want to print the output to track the underlying process.
63 | print(stdout.decode("utf-8"))
64 | return_code = process.returncode
65 | if return_code != 0:
66 | stderr_string = stderr.decode(encoding="utf-8")
67 | raise RuntimeError(
68 | "Subprocess {} failed with return code {}. Error message:"
69 | "\n{}".format(args, return_code, stderr_string))
70 |
71 |
72 | def refresh_wal():
73 | """Refresh the wal display (call `wal -R`)."""
74 | call_process(["wal", "-R"])
75 |
76 |
77 | def reload_theme():
78 | """Set the wal theme from the colors.json file.
79 |
80 | This is intended to ensure manual changes propogate out properly, i.e. that
81 | caches are rebuilt. Just reloading the last config (`wal -R`) might lead to
82 | out-of-date caches from the old config.
83 |
84 | """
85 | call_process(["wal", "--theme", CONFIG_FILE_PATH])
86 |
87 |
88 | def call_normally(image_path):
89 | """Call wal as normal - set the entire theme from an image."""
90 | call_process(["wal", "-i"])
91 |
92 |
93 | def copy_config(destination):
94 | """Copy the config file to another destination."""
95 | shutil.copy(CONFIG_FILE_PATH, expandpath(destination))
96 |
97 |
98 | def config_exists():
99 | """Does the config file exist?"""
100 | return os.path.isfile(CONFIG_FILE_PATH)
101 |
102 |
103 | def ensure_color_is_hex(color_string):
104 | # This regex should match a hex color.
105 | match = re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', color_string)
106 | return bool(match)
107 |
108 |
109 | def replace_color(index, color, config):
110 | """Replace one color in the `config` dict."""
111 | assert 0 <= index <= 15
112 | ensure_color_is_hex(color)
113 | color_name = "color{}".format(index)
114 | config["colors"][color_name] = str(color)
115 |
116 |
117 | def replace_colors(colors):
118 | """Replace the colors in the config dict with `colors`."""
119 | config = load_config()
120 | # Ensure colors dict exists.
121 | if "colors" not in config:
122 | config["colors"] = {}
123 | # Now replace each color.
124 | for i, color in enumerate(colors):
125 | replace_color(i, color, config)
126 | # Also replace the special colors.
127 | if "special" not in config:
128 | config["special"] = {}
129 | config["special"]["background"] = colors[0]
130 | config["special"]["foreground"] = colors[7]
131 | config["special"]["cursor"] = colors[7]
132 | save_config(config)
133 | reload_theme()
134 |
135 |
136 | if __name__ == "__main__":
137 | if len(sys.argv) != 17:
138 | raise ValueError("Please specify 16 colours as the command line "
139 | "arguments. No more, no less. You gave "
140 | "{}.".format(len(sys.argv) - 1))
141 | colors = sys.argv[1:17]
142 | replace_colors(colors)
143 |
--------------------------------------------------------------------------------
/theme-magic.el:
--------------------------------------------------------------------------------
1 | ;;; theme-magic.el --- Apply your Emacs theme to the rest of Linux -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2019
4 |
5 | ;; Author: GitHub user "jcaw" <40725916+jcaw@users.noreply.github.com>
6 | ;; URL: https://github.com/jcaw/theme-magic.el
7 | ;; Keywords: unix, faces, terminals, extensions
8 | ;; Version: 0.2.3
9 | ;; Package-Requires: ((emacs "25") (seq "1.8"))
10 |
11 | ;; This program is free software; you can redistribute it and/or modify
12 | ;; it under the terms of the GNU General Public License as published by
13 | ;; the Free Software Foundation, either version 3 of the License, or
14 | ;; (at your option) any later version.
15 |
16 | ;; This program is distributed in the hope that it will be useful,
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | ;; GNU General Public License for more details.
20 |
21 | ;; You should have received a copy of the GNU General Public License
22 | ;; along with this program. If not, see .
23 |
24 | ;;; Commentary:
25 |
26 | ;; What's the point in an Emacs theme if the rest of Linux looks different?
27 | ;;
28 | ;; Just call `theme-magic-from-emacs' and your Emacs theme will be applied
29 | ;; to your entire Linux session. Now all your colors match!
30 | ;;
31 | ;; `theme-magic' uses pywal to set its themes. Pywal must be installed
32 | ;; separately. When you log out, the theme will be reset to normal. To restore
33 | ;; your theme, call "wal -R" in a shell. To reload it whenever you log in, add
34 | ;; "pywal -R" to your .xprofile (or whatever file you use to initialise programs
35 | ;; when logging in graphically).
36 | ;;
37 | ;; See the documentation of Pywal for more information:
38 | ;; https://github.com/dylanaraps/pywal
39 | ;;
40 | ;; Please note that pywal version 1.0.0 or greater is required.
41 |
42 |
43 | ;;; Code:
44 |
45 |
46 | (require 'color)
47 | (require 'font-lock)
48 | (require 'ansi-color)
49 | (require 'seq)
50 | (require 'cl-lib)
51 |
52 |
53 | (defvar theme-magic--theming-functions
54 | '(
55 | load-theme
56 | ;; When these are enabled, changing the theme calls wal multiple times.
57 | ;; Might be fixable by running wal with an idle timer, but the updates would
58 | ;; be less synchronised. Note that without these, disabling a theme will not
59 | ;; trigger a wal update.
60 | ;;
61 | ;; enable-theme
62 | ;; disable-theme
63 | )
64 | "Functions that should trigger an update of the linux theme.
65 |
66 | \(Iff auto-updating is enabled.\)")
67 |
68 |
69 | (defvar theme-magic--scripts-directory
70 | (concat (file-name-directory
71 | (or
72 | ;; `load-file-name' should point to this file when loading.
73 | load-file-name
74 | ;; For debugging/development: if not loaded as a package, use the
75 | ;; buffer for this file instead.
76 | buffer-file-name))
77 | "python/")
78 | "Directory where the Python scripts for manipulating pywal should be.")
79 |
80 |
81 | (defvar theme-magic--pywal-python-script
82 | (concat theme-magic--scripts-directory "wal_change_colors.py")
83 | "Path to the Python script that sets the theme from 16 colours.")
84 |
85 |
86 | (defvar theme-magic--pywal-buffer-name "*pywal*"
87 | "Name to use for pywal's output buffer.")
88 |
89 |
90 | (defvar theme-magic--preferred-extracted-colors
91 | '(
92 | ;; Black
93 | ;; This is a special face - it should match the background.
94 | (0 . ((face-background 'default)))
95 | ;; Red
96 | ;; The red color should look like an error, because it is probably going
97 | ;; to be used to denote errors.
98 | (1 . (
99 | ;; The error face is best. The error face also tends to actually
100 | ;; be red.
101 | (face-foreground 'error)
102 | ;; Sometimes, errors are denoted by their background color.
103 | (face-background 'error)
104 | ;; The warning face hopefully also looks like an error. But, it is
105 | ;; less likely to be red.
106 | (face-foreground 'warning)
107 | ;; Likewise, sometimes warnings are denoted by their background.
108 | (face-background 'warning)))
109 | ;; Yellow
110 | ;; Try to give yellow a warning face, if available.
111 | (3 . ((face-foreground 'font-lock-warning-face)
112 | (face-foreground 'warning)))
113 | ;; Cyan
114 | ;; Cyan needs to be the secondary dominant face.
115 | (6 . ((face-foreground 'font-lock-function-name-face)
116 | (face-foreground 'font-lock-variable-name-face)))
117 | ;; White
118 | ;; Special color - it should match normal text.
119 | (7 . ((face-foreground 'default)))
120 | ;; Black-light
121 | ;; Special color - it is used for text that's faded, e.g. in code
122 | ;; comments (but note that while most themes make shadow a faded color,
123 | ;; the comment face can sometimes be vibrant).
124 | (8 . ((face-foreground 'shadow)
125 | (face-foreground 'font-lock-comment-face)))
126 | ;; The rest of the light faces should inherit from their regular
127 | ;; equivalents.
128 | )
129 | "How should we extract each color?
130 |
131 | This should be an alist of font numbers, mapped to a list of
132 | colors. Each color should be a form that can be evaluated. For
133 | example:
134 |
135 | '((1 . ((font-foreground 'preferred-face)
136 | (font-background 'backup-face))))")
137 |
138 |
139 | (defvar theme-magic--fallback-extracted-colors
140 | '(
141 | ;; These faces are ordered by preferred dominance. Colors at the top will be
142 | ;; placed in more dominant color slots.
143 | ;; ------------------------------------------------------------------------
144 |
145 | ;; These two faces are the two primary, dominant faces. Use them up first.
146 | (face-foreground 'font-lock-keyword-face)
147 | (face-foreground 'font-lock-function-name-face)
148 |
149 | ;; Some themes use a colorful comment face, such as `spacemacs-dark' and
150 | ;; `zenburn'. These colors consequently become very dominant. Use the
151 | ;; comment face, but only if it's colorful.
152 | (theme-magic--filter-unsaturated
153 | (face-foreground 'font-lock-comment-face))
154 | ;; Strings tend to be common (and long), so the string face becomes
155 | ;; dominant.
156 | (face-foreground 'font-lock-string-face)
157 | ;; Docstrings are common too (perhaps more common) but docstring colors tend
158 | ;; to be uglier than string colors. We therefore demote it, slightly.
159 | (theme-magic--filter-unsaturated
160 | (face-foreground 'font-lock-doc-face))
161 | ;; variables, constants and types are peppered throughout code. These colors
162 | ;; are less common, but are still defining colors of the color scheme.
163 | ;;
164 | ;; HACK: Some doom themes set the variable name to white
165 | ;; (e.g. `doom-vibrant'). Only accept colorful variable names.
166 | (theme-magic--filter-unsaturated
167 | (face-foreground 'font-lock-variable-name-face))
168 | (face-foreground 'font-lock-constant-face)
169 | ;; HACK: At least one doom theme sets the type face to be white too
170 | ;; (e.g. `doom-peacock').
171 | (theme-magic--filter-unsaturated
172 | (face-foreground 'font-lock-type-face))
173 |
174 | ;; Other faces of interest
175 | (face-foreground 'link)
176 | (face-foreground 'button)
177 | (face-foreground 'custom-variable-tag)
178 | (face-foreground 'success)
179 |
180 | ;; As a last resort, use the ansi colors themselves. These should only be
181 | ;; used if all the other colors have been used up.
182 | ;;
183 | ;; Don't use colors 0 or 7 (black and white).
184 | (theme-magic--get-ansi-color 4) ; Blue
185 | (theme-magic--get-ansi-color 6) ; Cyan
186 | (theme-magic--get-ansi-color 3) ; Yellow
187 | (theme-magic--get-ansi-color 5) ; Magenta
188 | (theme-magic--get-ansi-color 2) ; Green
189 | (theme-magic--get-ansi-color 1) ; Red
190 | )
191 | "Colors to fall back on if the preferred faces are invalid.
192 |
193 | Each color should be a form that can be evaluated. For example:
194 |
195 | '(face-foreground 'button)
196 |
197 | If a color cannot be filled by one of the preferred faces, this
198 | list will be scanned for the first valid color. That face will be
199 | used instead. This list is ordered best to worst.
200 |
201 | A valid color is defined as a color that hasn't been used
202 | already." )
203 |
204 |
205 | (defvar theme-magic--color-priority
206 | ;; Split over multiple lines for easy commenting and reordering.
207 | '(
208 | ;; Black (background)
209 | ;; Black and white _must_ be set correctly, so they're first.
210 | 0
211 | ;; White
212 | 7
213 | ;; Black-light
214 | 8
215 | ;; Blue - seems most popular
216 | 4
217 | ;; Cyan - also seems popular
218 | 6
219 | ;; Red
220 | ;;
221 | ;; Red is special because it's used for warnings. It's important that red
222 | ;; has a high chance of nabbing the error color, so we define it relatively
223 | ;; quickly.
224 | ;;
225 | ;; Note that this causes conflicts, e.g. in `monokai', where red is used for
226 | ;; errors and keywords. Nabbing red too early makes the output look
227 | ;; terrible.
228 | 1
229 | ;; Green - seems to be third most popular
230 | 2
231 | ;; Purple
232 | 5
233 | ;; Yellow
234 | 3
235 | )
236 | "The order in which to assign extracted colors to ANSI values.
237 |
238 | When extracting colors, the colors higher on this list get first
239 | pick. If a later color runs into a duplicate, it will have to use
240 | a fallback color.")
241 |
242 |
243 | (defvar theme-magic--same-color-threshold 0.1
244 | "Max difference between RGB values for two colors to be considered the same.
245 |
246 | Refers to RGB values on the 0.0 to 1.0 scale.
247 |
248 | When generating a set of colors, it's important that the same
249 | color is not duplicated. Each ANSI color should look different,
250 | if possible. Two very similar colors are generated. This is the
251 | threshold at which we say \"these colors are too visually
252 | similar, we should treat them as the same.\"
253 |
254 | There is some slack in this variable. At higher values, such as
255 | 0.1, colors that are visually distinct will be treated as the
256 | same. That's fine - it stops very similar colors from being
257 | generated.")
258 |
259 |
260 | (defvar theme-magic--saturated-color-threshold 0.1
261 | "Threshold at which a color counts as \"saturated\".
262 |
263 | This corresponds to the saturation component of the HSV color
264 | value (scale 0.0 to 1.0). If a color has a saturation value equal
265 | to or above this value, it counts as saturated, rather than
266 | greyscale.")
267 |
268 |
269 | (defun theme-magic--color-name-to-hex (color-name)
270 | "Convert a `COLOR-NAME' into a 6-digit hex value.
271 |
272 | E.g. \"Orange\" -> \"#FFA500\".
273 |
274 | Note that this conversion method IS LOSSY. If you supply a hex
275 | name as the color-name, it may spit out a slightly different hex
276 | value due to rounding errors."
277 | (if color-name
278 | ;; Upcase result to make it neat.
279 | (upcase
280 | ;; Have to convert to rgb first, *then* convert back to hex.
281 | (apply
282 | 'color-rgb-to-hex
283 | (append (color-name-to-rgb
284 | color-name)
285 | ;; We have to specify "2" as the fourth argument
286 | '(2))))
287 | nil))
288 |
289 |
290 | (defun theme-magic--color-difference (color1 color2)
291 | "Calculate the difference between two colors.
292 |
293 | For the purposes of this method, this is the max of all the
294 | differences in RGB values.
295 |
296 | The difference is returned on a scale of 0.0 to 1.0
297 |
298 | In more detail: the red, green and blue values of `COLOR1' and
299 | `COLOR2' are each compared. R to R, G to G, and B to B. The
300 | difference is the maximum of these differences."
301 | (let ((color1-rgb (color-name-to-rgb color1))
302 | (color2-rgb (color-name-to-rgb color2))
303 | (max-difference 0))
304 | (max (abs (- (nth 0 color1-rgb) (nth 0 color2-rgb)))
305 | (abs (- (nth 1 color1-rgb) (nth 1 color2-rgb)))
306 | (abs (- (nth 2 color1-rgb) (nth 2 color2-rgb))))))
307 |
308 |
309 | (defun theme-magic--measure-saturation (color)
310 | "How saturated is `COLOR' on a scale of 0.0 to 1.0?
311 |
312 | Uses the saturation component of HSV.
313 |
314 | If `COLOR' is nil, the saturation is treated as 0."
315 | (if color
316 | ;; Use HSV over HSL for more consistent results on light colors.
317 | (nth 1 (apply 'color-rgb-to-hsv
318 | (color-name-to-rgb color)))
319 | 0))
320 |
321 |
322 | (defun theme-magic--filter-unsaturated (color)
323 | "Return color iff `COLOR' is not close to greyscale.
324 |
325 | Otherwise, return nil.
326 |
327 | If color is saturated enough, it's ok. Otherwise, treat it as
328 | greyscale.
329 |
330 | In practical terms, this method eliminates colors that are shades
331 | of grey, rather than shades of a color."
332 | (if (> (theme-magic--measure-saturation color)
333 | theme-magic--saturated-color-threshold)
334 | color
335 | nil))
336 |
337 |
338 | ;; TODO: Rename to embody the fact it's comparing similarity, not equality.
339 | (defun theme-magic--colors-match (color1 color2)
340 | "Check if two colors look very similar.
341 |
342 | The R, G and B components of `COLOR1' and `COLOR2' are compared,
343 | and the biggest difference is measured. If this difference is
344 | below a certain threshold, it is assumed that the colors are
345 | similar enough that they count as a match.
346 |
347 | The threshold is defined in `theme-magic--same-color-threshold'.
348 |
349 | Returns t if they match, nil if not."
350 | ;; Failsafe - only compare if both colors are defined.
351 | (if (and color1 color2)
352 | (progn
353 | ;; The colors are only the same if the difference is within the acceptable
354 | ;; threshold.
355 | (<= (theme-magic--color-difference color1 color2)
356 | theme-magic--same-color-threshold))
357 | ;; If one of the colors is nil, they don't match. Even if both are nil, they
358 | ;; don't match.
359 | nil))
360 |
361 |
362 | (defun theme-magic--extract-background-color ()
363 | "Extract the background color from the default font."
364 | (theme-magic--color-name-to-hex
365 | (face-background 'default)))
366 |
367 |
368 | (defun theme-magic--extract-shadow-color ()
369 | "Extract the color of the shadow face, in hex."
370 | (theme-magic--color-name-to-hex
371 | (face-foreground 'shadow)))
372 |
373 |
374 | (defun theme-magic--extract-default-color ()
375 | "Extract the foreground color of the default face, in hex."
376 | (theme-magic--color-name-to-hex
377 | (face-foreground 'default)))
378 |
379 |
380 | (defun theme-magic--safe-eval (form)
381 | "Call `eval' on `FORM', ignoring any errors.
382 |
383 | This method ensures the program is not interrupted in the case of
384 | an error. If an error does occur, this method will catch it and
385 | return nil."
386 | (condition-case nil
387 | (eval form)
388 | (error nil)))
389 |
390 |
391 | (defun theme-magic--check-dependencies ()
392 | "Ensure dependencies are installed. Throw an error if not.
393 |
394 | Specifically, this checks that both Python and Pywal are
395 | installed - and accessible from the user's home dir."
396 | ;; If we're in a pyenv directory, we might accidentally run the virtual
397 | ;; version of Python instead of the user's root version. To fix this, we
398 | ;; temporarily change to the user's dir.
399 | (let ((default-directory "~/"))
400 | (unless (executable-find "python")
401 | (user-error (concat "Could not find 'python' executable. "
402 | "Is Python installed and on the path?")))
403 | (unless (executable-find "wal")
404 | (user-error (concat "Could not find 'wal' executable. "
405 | "Is Pywal installed and on the path?")))
406 | ;; TODO: Check wal is up-to-date enough to use, and the python implementation.
407 | ))
408 |
409 |
410 | (defun theme-magic--erase-pywal-buffer ()
411 | "Erase the contents of the pywal output buffer iff it exists."
412 | (when (get-buffer theme-magic--pywal-buffer-name)
413 | (with-current-buffer theme-magic--pywal-buffer-name
414 | (erase-buffer))))
415 |
416 |
417 | (defun theme-magic--call-pywal-process (colors)
418 | "Call the Python script that sets the theme with Pywal.
419 |
420 | `COLORS' should be the 16 hexadecimal colors to use as the theme.
421 |
422 | This just calls the python script from the home directory. It
423 | doesn't provide any wrapper feedback to the user."
424 | ;; Kill pywal buffer if it already exists
425 | (theme-magic--erase-pywal-buffer)
426 | (let (
427 | ;; If we're in a pyenv directory, we might accidentally run the virtual
428 | ;; version of Python instead of the user's root version. To fix this, we
429 | ;; temporarily change to the user's dir.
430 | (default-directory "~/")
431 | ;; The color modification script will work with python 2 or 3, so just
432 | ;; use the default Python.
433 | (python-executable "python")
434 | (theming-script theme-magic--pywal-python-script)
435 | )
436 | ;; We have to use apply here to expand the list of colors.
437 | (apply 'call-process
438 | (append
439 | ;; Append the first arguments to the colors list to create one long
440 | ;; list of arguments.
441 | (list
442 | python-executable
443 | ;; These are the positional arguments that `call-process' takes.
444 | nil theme-magic--pywal-buffer-name t
445 | theming-script)
446 | ;; Now we expand the list of colors
447 | colors))))
448 |
449 |
450 | (defun theme-magic--apply-colors-with-pywal (colors)
451 | "Change the linux theme to use the 16 `COLORS' (using pywal).
452 |
453 | `COLORS' should be a list of 16 hexadecimal terminal colors.
454 |
455 | Provides some wrapper feedback to the user, plus some error
456 | handling."
457 | (message "Applying colors:\n%s"
458 | ;; Number the colors to make it clearer for the user which color is
459 | ;; being applied where.
460 | (cl-mapcar #'cons
461 | (number-sequence 0 (length colors))
462 | colors))
463 | (if (zerop (theme-magic--call-pywal-process colors))
464 | (message "Successfully applied colors!")
465 | (user-error "There was an error applying the colors. See buffer \"*pywal*\" for details")))
466 |
467 |
468 | (defun theme-magic--get-ansi-color (ansi-index)
469 | "Get the ansi color at `ANSI-INDEX', as a hex string.
470 |
471 | Note that this refers to the *in-built, Emacs ANSI colors* - not
472 | the set of 16 generated by `theme-magic--16-colors-from-ansi'.
473 | Thus, it only works with *indexes 0-7* (inclusive)."
474 | (theme-magic--color-name-to-hex
475 | (aref ansi-color-names-vector ansi-index)))
476 |
477 |
478 | (defun theme-magic--16-colors-from-ansi ()
479 | "Construct a set of 16 terminal colors from the current ansi colors."
480 | (let* ((ansi-colors-vector
481 | ;; Duplicate the 8 basic ansi colors to get a 16-color palette.
482 | (vconcat ansi-color-names-vector
483 | ansi-color-names-vector)))
484 | ;; Ansi colors are inconsistent. The first of the 8 ansi colors may be the
485 | ;; background color, but it might also be the shadow color. We modify them
486 | ;; manually to ensure consistency.
487 | (aset ansi-colors-vector 0 (theme-magic--extract-background-color))
488 | (aset ansi-colors-vector 8 (theme-magic--extract-shadow-color))
489 | ;; Some themes mess up the foreground color (seen in `material-theme').
490 | ;; Foreground color is very important anyway, and should match Emacs even if
491 | ;; it deviates from the Ansi palette. Manually fix it.
492 | (aset ansi-colors-vector 7 (theme-magic--extract-default-color))
493 | ;; Finally, we ensure each color is hexadecimal. (We also want to output a
494 | ;; list - this will also serve that purpose.)
495 | (mapcar 'theme-magic--color-name-to-hex
496 | ansi-colors-vector)))
497 |
498 |
499 | (defun theme-magic--get-preferred-colors (ansi-index)
500 | "Get the best colors to use for a particular `ANSI-INDEX'.
501 |
502 | Colors are evaluated at runtime within this method. Each color
503 | should be a form that can be evaluated wth `eval'. If an error
504 | occurs while evaluating the form, that color will be skipped.
505 |
506 | Preferred colors are stored in
507 | `theme-magic--preferred-extracted-colors'. This is an alist
508 | mapping ANSI color indexes to a list of color forms, ranked best
509 | to worst. See `theme-magic--preferred-extracted-colors' for more
510 | details."
511 | (mapcar (lambda (color-form)
512 | (theme-magic--color-name-to-hex
513 | (theme-magic--safe-eval color-form)))
514 | (alist-get ansi-index theme-magic--preferred-extracted-colors)))
515 |
516 |
517 | (defun theme-magic--color-taken (color existing-colors)
518 | "Check if a particular `COLOR' has already been taken in `EXISTING-COLORS'.
519 |
520 | This method checks color similarity. If `COLOR' is too similar to
521 | another color that's already been assigned, we count it as taken.
522 | This ensures each ANSI color generated is fairly different from
523 | every other color.
524 |
525 | There are two main reasons to supress similar color assignments:
526 |
527 | 1. Terminal colors are primarily used to highlight and
528 | segregate information. It's important to ensure the colors
529 | stay visually distinct, so the user can clearly tell each
530 | color apart at a glance.
531 |
532 | 2. Some themes use many subtle variations of one color (e.g.
533 | `doom-one' uses many shades of deep purple). When processed,
534 | the color palette can end up being mainly different variants
535 | of that color. Back to our example: `doom-one' is not a
536 | purple theme, but without correcting for this tendency,
537 | the theme produced by `theme-magic' will look very purple.
538 |
539 | Suppressing similar colors prevents many similar colors from
540 | accruing in the result, which makes it harder for this kind
541 | of color shift to happen.
542 |
543 | Note that these results were determined via trial and error. In
544 | practice, banning similar colors simply produces better looking
545 | results, in general."
546 | (catch 'color-taken
547 | (mapc (lambda (existing-color)
548 | ;; `existing-color' will be a cons cell, because it comes from an
549 | ;; alist. Take the `cdr' - this is the color string.
550 | (when (theme-magic--colors-match (cdr existing-color) color)
551 | (throw 'color-taken t)))
552 | existing-colors)
553 | nil))
554 |
555 |
556 | (defun theme-magic--extract-color (ansi-index existing-colors)
557 | "Extract a preferred color from the current theme for `ANSI-INDEX'.
558 |
559 | `EXISTING-COLORS' should contain the colors that have already
560 | been assigned. It should be an alist mapping ANSI indexes to
561 | their assigned hexadecimal colors, e.g:
562 |
563 | '((0 . \"#FFFFFF\")
564 | (1 . \"#FF0000\"))
565 |
566 | Returns the best valid color, given `EXISTING-COLORS'.
567 |
568 | If none of the preferred colors are valid, returns nil."
569 | (let ((possible-colors (theme-magic--get-preferred-colors ansi-index)))
570 | ;; Check each color in turn to see if it's a new color. If it is, stop
571 | ;; immediately and return it.
572 | (catch 'new-color
573 | (mapc (lambda (possible-color)
574 | (when (and possible-color
575 | (not (theme-magic--color-taken possible-color existing-colors)))
576 | (throw 'new-color possible-color)))
577 | possible-colors)
578 | ;; If no color could be extracted, return nil for now.
579 | nil)))
580 |
581 |
582 | (defun theme-magic--extract-fallback-color (ansi-index existing-colors)
583 | "Extract a color for `ANSI-INDEX' from the set of fallback colors.
584 |
585 | `theme-magic--fallback-extracted-colors' is the list of fallback
586 | colors. See that variable for more information.
587 |
588 | This method returns the first fallback color that can be used,
589 | given `EXISTING-COLORS'. A color can be used if it is
590 | sufficiently different from all the existing colors.
591 |
592 | Returns nil if no valid color could be found."
593 | (catch 'new-color
594 | (mapc (lambda (possible-color-form)
595 | (let ((possible-color (theme-magic--color-name-to-hex
596 | (theme-magic--safe-eval possible-color-form))))
597 | ;; When the color exists and is not taken, we have a match.
598 | (when (and possible-color
599 | (not (theme-magic--color-taken possible-color existing-colors)))
600 | (throw 'new-color possible-color))))
601 | theme-magic--fallback-extracted-colors)
602 | nil))
603 |
604 |
605 | (defun theme-magic--force-extract-color (ansi-index)
606 | "Extract a color for `ANSI-INDEX', with no concern for the overall theme.
607 |
608 | This is a fallback method that should be used when no valid color
609 | could be found. It will provide the best possible color for a
610 | particular index, *even if* it clashes with another color."
611 | (theme-magic--color-name-to-hex
612 | (or (theme-magic--safe-eval (car (alist-get ansi-index theme-magic--preferred-extracted-colors)))
613 | ;; It's possible even the above will return nil, because the preferred
614 | ;; color form fails to evaluate. As a final fallback, just use the ANSI
615 | ;; color.
616 | ;;
617 | ;; TODO: The ansi colors should have already been in the fallback colors.
618 | ;; Is it worth duplicating them here?
619 | (theme-magic--get-ansi-color ansi-index)
620 | ;; Final failsafe - should never get here, but just in case, a neutral
621 | ;; color.
622 | "#888888")))
623 |
624 |
625 | (defun theme-magic--auto-extract-16-colors ()
626 | "Automatically extract a set of 16 ANSI colors from the current theme.
627 |
628 | The way this method works is it takes each ANSI color slot and
629 | tries to extract a color from the current theme, assigning it to
630 | that slot. Most of these colors are extracted from the currently
631 | assigned fonts.
632 |
633 | For example, one of the more prominent \"colors\" for the current
634 | theme is embedded in the font used for keywords. We can extract
635 | it as so:
636 |
637 | (face-foreground 'font-lock-keyword-face) -> \"#4f97d7\"
638 |
639 | This color can then be assigned to one of the ANSI slots.
640 |
641 | Certain colors are preferred for certain slots. For example:
642 |
643 | 1. The ANSI color at index \"1\" is \"red\". Many terminal
644 | applications use this color to denote errors, so we attempt
645 | to extract ANSI color 1 from the theme's `error' face. If
646 | that doesn't work, we try the `warning' face. If that
647 | doesn't work, we fall back to the other colors.The point is
648 | to ensure `red' looks like an error.
649 |
650 | 2. The first ANSI color is \"black\" and denotes the background
651 | for most terminal applications. We want this color to match
652 | the background color of the current theme, so we prefer
653 | that.
654 |
655 | We repeat this process for each of the first 8 ANSI colors (plus
656 | color 8, the off-background face[1], so 9 total), until all
657 | colors have been assigned. Note that we cross-reference against
658 | slots that have already been assigned, to ensure each color is
659 | sufficiently different. No two ANSI colors should be the same, or
660 | too similar[2].
661 |
662 | After this is done, the last *7* colors are filled in. These are
663 | the \"light variant\" colors[1]. These are simply duplicated from
664 | their non-light counterparts (this is the same method used by
665 | vanilla Pywal). For example, \"red-light\" (color 9) becomes the
666 | same color as \"red\" (color 1). \"White-light\" (color 15)
667 | becomes the same as \"white\" (color 7).
668 |
669 | ---
670 |
671 | Footnotes:
672 |
673 | [1]: Ansi color 8 is special. It is \"black-bright\" - i.e,
674 | grey. In practice, this means it is used for faded text -
675 | it's the color used to denote unimportant information or
676 | to prevent text from standing out. The Emacs corollary is
677 | the `shadow' face.
678 |
679 | Many syntax highlighters denote code comments with this
680 | color.
681 |
682 | Note that this means we cannot have \"black-bright\"
683 | inherit from \"black\" - it has to be extracted
684 | separately.
685 |
686 | [2]: All ANSI colors should be somewhat different because their
687 | purpose is to denote different types of information. They
688 | need to be differentiable at a glance.
689 |
690 | HOWEVER, some themes may not actually have enough distinct
691 | colors to construct an entire set. In these cases, this
692 | method will use a fallback and duplicates may be produced.
693 | In practice, this is very rare."
694 | ;; Note that color extraction is worst-case speed complexity o(n*16), where
695 | ;; `n' is (roughly) the number of color options (preferred and fallback). This
696 | ;; scales faster than O(n) but it should still be negligible.
697 | ;;
698 | ;; If the number of colors were to grow above 16, this complexity would
699 | ;; increase. If that became an issue, it is possible to rewrite this algorithm
700 | ;; to reduce that complexity, by maintaining a record of unused colors and
701 | ;; pruning it as we progress. Right now, that's not worth it.
702 | (let (
703 | ;; `extracted-colors' is an alist mapping ANSI numbers to colors.
704 | (extracted-colors '())
705 | )
706 | ;; Go through the colors in the preferred order, and attempt to extract a
707 | ;; color for each.
708 | (mapc (lambda (ansi-index)
709 | (push (cons ansi-index
710 | (or (theme-magic--extract-color ansi-index extracted-colors)
711 | ;; Try and find an unused color in the fallback colors.
712 | (theme-magic--extract-fallback-color ansi-index extracted-colors)
713 | ;; If we couldn't find a unique color, fall back to
714 | ;; the best duplicate color.
715 | (theme-magic--force-extract-color ansi-index)))
716 | extracted-colors))
717 | theme-magic--color-priority)
718 |
719 | ;; We now have an alist of the first 9 ANSI indexes, mapped to colors. We
720 | ;; need to return a straight list of 16 colors. Extract the colors one by
721 | ;; one.
722 | (append (mapcar (lambda (index)
723 | (alist-get index extracted-colors))
724 | '(0 1 2 3 4 5 6 7 8))
725 | ;; For now, colors 9-15 (the "light" color variants) should just
726 | ;; mirror their non-light counterparts.
727 | (mapcar (lambda (index)
728 | ;; Subtract 8 to get the dark version of the light index.
729 | (alist-get (- index 8) extracted-colors))
730 | '(9 10 11 12 13 14 15)))))
731 |
732 |
733 | ;;;###autoload
734 | (defun theme-magic-from-emacs ()
735 | "Apply the current Emacs theme to the rest of Linux.
736 |
737 | This method uses Pywal to set the theme. Ensure you have Pywal
738 | installed and that its executable, `wal', is available.
739 |
740 | See Pywal's documentation for more information:
741 |
742 | https://github.com/dylanaraps/pywal
743 |
744 | Pywal is designed to be unobtrusive, so it only sets your theme
745 | for the current session. You have to explicitly tell Pywal to
746 | reload its theme on a fresh login, by calling \"wal -R\". To do
747 | this automatically, place the line \"wal -R\" in your
748 | \"~/.xprofile\" file (or whichever file starts programs on a
749 | graphical login).
750 |
751 | See `theme-magic--auto-extract-16-colors' to understand how this
752 | method chooses colors for the Linux theme."
753 | (interactive)
754 | ;; This will actually check dependencies twice, but that's fine - it's cheap
755 | ;; and we want to do it up front.
756 | (theme-magic--check-dependencies)
757 | (theme-magic--apply-colors-with-pywal
758 | (theme-magic--auto-extract-16-colors)))
759 |
760 |
761 | (defun theme-magic-from-emacs--wrapper (&rest _)
762 | "Wrapper for `theme-magic-from-emacs' to be used as advice.
763 |
764 | Using the normal, autoloaded and interactive method can cause
765 | strange problems with the advice system. It will also fail if
766 | arguments are passed to the advised function. This is a wrapper
767 | method that can be used safely."
768 | (theme-magic-from-emacs))
769 |
770 |
771 | ;;;###autoload
772 | (define-minor-mode theme-magic-export-theme-mode
773 | "Automatically export the Emacs theme to all Linux terminals, using Pywal.
774 |
775 | If this mode is active, the Linux theme will be updated
776 | automatically when you change the Emacs theme.
777 |
778 | Note that if an Emacs theme has already been set, it will not be
779 | exported when this mode is activated. You must explicitly export
780 | it, or change the theme again to trigger the auto-update.
781 |
782 | Under the hood, this mode calls `theme-magic-from-emacs' when you
783 | change the theme. See `theme-magic-from-emacs' for more
784 | information."
785 | :lighter " TME"
786 | :global t
787 | :after-hook (if theme-magic-export-theme-mode
788 | ;; Was disabled. We should now enable.
789 | (progn
790 | (theme-magic--enable-auto-update)
791 | ;; TODO: Maybe update the theme overtly now? It will slow down
792 | ;; startup of the mode (and consquently, Emacs) so might be
793 | ;; best to leave this to the user.
794 | )
795 | ;; Was enabled. We should now disable.
796 | (theme-magic--disable-auto-update)))
797 |
798 |
799 | (defun theme-magic--enable-auto-update ()
800 | "Enable automatic Linux theme updating.
801 |
802 | Note for end users: DO NOT use this method directly. Use the
803 | minor mode function, `theme-magic-export-theme-mode', instead.
804 |
805 | Once enabled, the Linux theme will be updated whenever the Emacs
806 | theme is changed.
807 |
808 | Note that if an Emacs theme has already been set, it will not be
809 | exported - you must do that manually or change the theme again."
810 | (mapc (lambda (func)
811 | (advice-add func :after 'theme-magic-from-emacs--wrapper))
812 | theme-magic--theming-functions))
813 |
814 |
815 | (defun theme-magic--disable-auto-update ()
816 | "Disable automatic Linux theme updating.
817 |
818 | Note for end users: DO NOT use this method directly. Use the
819 | minor mode function, `theme-magic-export-theme-mode', instead.
820 |
821 | Once disabled, the Linux theme will need to be updated manually
822 | with `theme-magic-from-emacs'."
823 | (mapc (lambda (func)
824 | (advice-remove func 'theme-magic-from-emacs--wrapper))
825 | theme-magic--theming-functions))
826 |
827 |
828 | (provide 'theme-magic)
829 | ;;; theme-magic.el ends here
830 |
--------------------------------------------------------------------------------