summaryrefslogtreecommitdiff
path: root/modules/ui-theme.el
blob: 1575257a00853e74da52b62b01aa9b045ae3f9a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
;;; ui-theme.el --- UI Theme Configuration and Persistence -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;
;;; Commentary:

;; This module provides a theme management system with persistence across
;; Emacs sessions.
;;
;; - Theme selection via interactive interface (M-L)
;; - Automatic persistence of theme preference to a configurable file
;; - Safe theme loading with fallback mechanism
;; - Support for custom themes in the "themes" subdirectory
;;
;; The persistence mechanism saves theme choices to a file that can be
;; synchronized across machines, ensuring consistent appearance across
;; multiple computers.

;;; Code:

(require 'user-constants)

(eval-when-compile (defvar org-dir))

;; ----------------------------------- Themes ----------------------------------
;; theme choices and settings

;; downloaded custom themes go in themes subdirectory
(setq custom-safe-themes t)  ;; trust all custom themes
(add-to-list 'custom-theme-load-path
             (concat user-emacs-directory "themes"))

;; ------------------------------- Switch Themes -------------------------------
;; loads themes in completing read, then persists via the functions below

(defun cj/switch-themes ()
  "Function to switch themes and save chosen theme name for persistence.
Unloads any other applied themes before applying the chosen theme."
  (interactive)
  (let ((chosentheme (completing-read "Load custom theme: "
                                      (mapcar #'symbol-name
                                              (custom-available-themes)))))
    (mapc #'disable-theme custom-enabled-themes)
    (load-theme (intern chosentheme) t))
  (cj/save-theme-to-file))

(keymap-global-set "M-L" #'cj/switch-themes)

;; ----------------------------- Theme Persistence -----------------------------
;; persistence utility functions used by switch themes.

(defvar theme-file (concat org-dir "emacs-theme.persist")
  "The location of the file to persist the theme name.
If you want your theme change to persist across instances, put this in a
directory that is sync'd across machines with this configuration.")

(defvar fallback-theme-name "modus-vivendi"
  "The name of the theme to fallback on.
This is used when there's no file, or the theme name doesn't match
any of the installed themes. This should be a built-in theme. If theme name is
`nil', there will be no theme.")

(defun cj/read-file-contents (filename)
  "Read FILENAME and return its content as a string.

If FILENAME isn't readable, return nil."
  (when (file-readable-p filename)
    (with-temp-buffer
      (insert-file-contents filename)
      (string-trim (buffer-string)))))

(defun cj/write-file-contents (content filename)
  "Write CONTENT to FILENAME.
If FILENAME isn't writeable, return nil. If successful, return t."
  (when (file-writable-p filename)
    (condition-case err
        (progn
          (with-temp-buffer
            (insert content)
            (write-file filename))
          t)
      (error
       (message "Error writing to %s: %s" filename (error-message-string err))
       nil))))

(defun cj/get-active-theme-name ()
  "Return the name of the active UI theme as a string.
Returns fallback-theme-name if no theme is active."
  (if custom-enabled-themes
      (symbol-name (car custom-enabled-themes))
    fallback-theme-name))

(defun cj/save-theme-to-file ()
  "Save the string representing the current theme to the theme-file."
  (if (not (cj/write-file-contents (cj/get-active-theme-name) theme-file))
      (message "Cannot save theme: %s is unwriteable" theme-file)
    (message "%s theme saved to %s" (cj/get-active-theme-name) theme-file)))

(defun cj/load-fallback-theme (msg)
  "Display MSG and load ui-theme fallback-theme-name.
Used to handle errors with loading persisted theme."
  (message "%s Loading fallback theme %s" msg fallback-theme-name)
  (load-theme (intern fallback-theme-name) t))

(defun cj/load-theme-from-file ()
  "Apply the theme name contained in theme-file as the active UI theme.
If the theme is nil, it disables all current themes. If an error occurs
loading the file name, the fallback-theme-name is applied and saved."
  (let ((theme-name (cj/read-file-contents theme-file)))
    ;; if theme-name is nil, unload all themes and load fallback theme
    (if (not theme-name)
        (progn
          (mapc #'disable-theme custom-enabled-themes)
          (cj/load-fallback-theme "Theme file not found or empty."))
      ;; Check if theme is 'nil' string
      (if (string= theme-name "nil")
          (mapc #'disable-theme custom-enabled-themes)
        ;; apply theme name or if error, load fallback theme
        (condition-case err
            (load-theme (intern theme-name) t)
          (error
           (cj/load-fallback-theme
            (format "Error loading theme %s: %s."
                    theme-name (error-message-string err)))))))))

(cj/load-theme-from-file)

(provide 'ui-theme)
;;; ui-theme.el ends here