summaryrefslogtreecommitdiff
path: root/modules/ui-theme.el
blob: d6eaf40470ea871beb764059a249dcad8d172ab8 (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
129
130
131
132
133
134
135
;;; 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)  ;; For 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 ""))
    (setq 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))

(global-set-key (kbd "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 then 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 (equal (cj/write-file-contents (cj/get-active-theme-name) theme-file) nil)
      (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