1 ;;; rainbow-fart.el --- Checks the keywords of code to play suitable sounds -*- lexical-binding: t; -*-
3 ;; Authors: stardiviner <numbchild@gmail.com>
4 ;; Package-Requires: ((emacs "25.1") (flycheck "32-cvs"))
5 ;; Package-Version: 0.1
7 ;; homepage: https://repo.or.cz/emacs-rainbow-fart.git
9 ;; rainbow-fart is free software; you can redistribute it and/or modify it
10 ;; under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 3, or (at your option)
14 ;; rainbow-fart is distributed in the hope that it will be useful, but WITHOUT
15 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17 ;; License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
26 ;; (add-hook 'after-init-hook #'rainbow-fart-mode)
34 (defgroup rainbow-fart nil
35 "rainbow-fart-mode customize group."
36 :prefix
"rainbow-fart-"
39 (defcustom rainbow-fart-voices-directory
40 (concat (file-name-directory (or load-file-name buffer-file-name
)) "voices/")
41 "The directory of voices."
46 (defcustom rainbow-fart-keyword-interval
(* 60 5)
47 "The time interval in seconds of rainbow-fart play voice for keywords.
48 If it is nil, will play sound for every keywords."
53 (defcustom rainbow-fart-time-interval
(* 60 15)
54 "The time interval in seconds of rainbow-fart play voice for hours.
55 If it's nil, the hours remind will not started."
60 (defcustom rainbow-fart-recorder-template nil
61 "The command line template to record voice file.
63 %f will be replaced to the voice file name."
68 (defcustom rainbow-fart-ignore-modes nil
69 "A list of major modes which will enable rainbow-fart-mode."
74 (defvar rainbow-fart--playing nil
75 "The status of rainbow-fart playing.")
77 (defvar rainbow-fart--play-last-time nil
78 "The last time of rainbow-fart play.")
80 (defcustom rainbow-fart-voice-pack-alist
'((t .
"JustKowalski"))
81 "A list of model voice packs."
86 ;;; TODO Support multiple voice packs data structure.
87 (defvar rainbow-fart-manifest-alist nil
88 "An alist of model voice pack's manifest info.")
90 ;;; TODO Support multiple voice packs data structure.
91 (defcustom rainbow-fart-keyword-voices-alist
'()
92 "An alist of pairs of programming language keywords and voice filenames."
97 ;;; Parsing voice pack manifest.json
99 (defun rainbow-fart-voice-pack-find-json-files (voice-pack)
100 "Find voice package manifest.json and contributes.json two files."
101 (let ((voice-model-dir (expand-file-name voice-pack rainbow-fart-voices-directory
)))
102 (if (file-exists-p (expand-file-name "contributes.json" voice-model-dir
))
104 (expand-file-name "manifest.json" voice-model-dir
)
105 (expand-file-name "contributes.json" voice-model-dir
))
106 (list (expand-file-name "manifest.json" voice-model-dir
) nil
))))
108 (defun rainbow-fart-voice-pack-parse-manifest (two-json)
109 "Read in manifest.json file."
110 (let* ((manifest-json-file (car two-json
))
111 (contributes-json-file (cadr two-json
))
112 (manifest (json-read-file manifest-json-file
))
113 (name (alist-get 'name manifest
))
114 (display-name (alist-get 'display-name manifest
))
115 (version (alist-get 'version manifest
))
116 (author (alist-get 'author manifest
))
117 ;; (description (alist-get 'description manifest))
118 ;; (avatar (alist-get 'avatar manifest)) ; "avatar.jpg"
119 ;; (avatar-dark (alist-get 'avatar-dark manifest)) ; "avatar-dark.jpg"
120 (languages (alist-get 'languages manifest
)) ; vector ["python"]
121 ;; (locale (alist-get 'locale manifest)) ; "zh"
122 ;; (gender (alist-get 'gender manifest)) ; "female"
123 ;; `contributes' is a vector of keywords, voices and texts.
124 (contributes (or (alist-get 'contributes manifest
) ; "contributes" is in "manifest.json"
125 ;; "contributes" is in another file "contributes.json"
126 (alist-get 'contributes
(json-read-file contributes-json-file
)))))
127 (setq rainbow-fart-manifest-alist manifest
)
128 (message "Loading rainbow-fart voice pack: %s (%s) by %s." name version author
)
129 (when (vectorp contributes
)
130 ;; reset voices alist
131 (setq rainbow-fart-keyword-voices-alist nil
)
132 ;; NOTE `contributes' is a vector. Can't use `loop' to iterate.
133 ;; append to data structure
135 (lambda (definition-alist)
136 (let ((keywords (mapcar #'identity
(alist-get 'keywords definition-alist
)))
137 (voices (mapcar #'identity
(alist-get 'voices definition-alist
)))
138 (texts (mapcar #'identity
(alist-get 'texts definition-alist
))))
141 (if-let ((keyword (string-trim key-str
)))
142 (add-to-list 'rainbow-fart-keyword-voices-alist
(cons keyword voices
) 'append
)))
145 (message "The rainbow-fart voice pack model: {%s} loaded." display-name
)))
147 ;; initialize with default voice pack.
148 (rainbow-fart-voice-pack-parse-manifest
149 (rainbow-fart-voice-pack-find-json-files (alist-get 't rainbow-fart-voice-pack-alist
)))
153 (defun rainbow-fart--get-media-uri (keyword)
154 "Get media uri based on KEYWORD."
155 (when-let ((uris (cdr (assoc keyword rainbow-fart-keyword-voices-alist
))))
156 (let ((uri (nth (random (length uris
)) uris
))
157 (voice-model-directory
158 (expand-file-name (alist-get 't rainbow-fart-voice-pack-alist
) rainbow-fart-voices-directory
)))
159 (if (url-type (url-generic-parse-url uri
))
161 (let ((uri (expand-file-name uri voice-model-directory
)))
162 (when (file-exists-p uri
)
165 (defun rainbow-fart--play (keyword)
166 "A private function to play voice for matched KEYWORD."
167 (unless (or rainbow-fart--playing
168 (when rainbow-fart-keyword-interval
169 (not (if rainbow-fart--play-last-time
170 (> (- (float-time) rainbow-fart--play-last-time
)
171 rainbow-fart-keyword-interval
)
172 (setq rainbow-fart--play-last-time
(float-time))))))
173 (when-let ((uri (rainbow-fart--get-media-uri keyword
))
175 (executable-find "mpg123")
176 (executable-find "mplayer")
177 (executable-find "mpv"))))
178 (setq rainbow-fart--playing t
)
179 (make-process :name
"rainbow-fart"
180 :command
`(,command
,uri
)
181 :buffer
" *rainbow-fart*"
182 :sentinel
(lambda (_ __
)
183 (setq rainbow-fart--playing nil
)
184 (setq rainbow-fart--play-last-time
(float-time)))))))
187 (defun rainbow-fart-get-prefix (regexp &optional expression limit
)
188 (when (looking-back regexp limit
)
189 (or (match-string-no-properties (or expression
0)) "")))
191 (defun rainbow-fart--post-self-insert ()
192 "A hook function on `post-self-insert-hook' to play audio."
193 (when (and (derived-mode-p 'prog-mode
)
194 (not (memq major-mode rainbow-fart-ignore-modes
)))
195 (let* ((prefix (save-excursion
196 ;; support prefix like "if(", "if (", "=>" etc keywords following punctuation.
197 (or (rainbow-fart-get-prefix "\\(?1:\\_<[^\\ ].\\_>\\)\\ ?[[:punct:]]?" 1)
198 (progn (goto-char (- (point) 1)) (thing-at-point 'symbol
)))))
199 (face (get-text-property (- (point) 1) 'face
)))
200 (when (or (memq face
'(font-lock-keyword-face))
202 (rainbow-fart--play prefix
)))))
204 ;;; linter like `flycheck'
206 (defun rainbow-fart--linter-display-error (err)
207 "Play voice for `flycheck-error' ERR."
208 (let ((level (flycheck-error-level err
)))
209 (rainbow-fart--play level
)))
211 (defun rainbow-fart--linter-display-errors (errors)
212 "A function to report ERRORS used as replacement of linter like `flycheck' and `flymake'."
214 (mapc #'rainbow-fart--linter-display-error
216 (seq-mapcat #'flycheck-related-errors errors
)))))
220 (defun rainbow-fart--timing ()
221 "Play voice for current time quantum."
222 (let* ((time (format-time-string "%H:%M"))
223 (pair (split-string time
":"))
224 (hour (string-to-number (car pair
))))
226 ((and (>= hour
05) (<= hour
08)) ; 05:00 -- 08:00
228 ((and (>= hour
08) (<= hour
10)) ; 08:00 -- 10:00
230 ((and (>= hour
10) (<= hour
11)) ; 10:00 -- 11:00
232 ((and (>= hour
11) (<= hour
13)) ; 11:00 -- 13:00
234 ((and (>= hour
13) (<= hour
15)) ; 13:00 -- 15:00
236 ((and (>= hour
15) (<= hour
17)) ; 15:00 -- 17:00
238 ((and (>= hour
18) (<= hour
22)) ; 18:00 -- 21:00
240 ((or (>= hour
23) (<= hour
01)) ; 23:00 -- 01:00
243 (defun rainbow-fart--timing-remind ()
244 "Remind you in specific time quantum."
245 (when (and rainbow-fart--play-last-time
246 (> (- (float-time) rainbow-fart--play-last-time
) rainbow-fart-time-interval
))
247 (rainbow-fart--play (rainbow-fart--timing))
248 (setq rainbow-fart--play-last-time
(float-time))))
250 (defvar rainbow-fart--timer nil
)
253 (defun rainbow-fart-record-voice-for-keyword ()
254 "Record a voice file which stored under the voice model directory."
256 (unless rainbow-fart-recorder-template
257 (error "The variable rainbow-fart-recorder-template is undefined!"))
258 (let* ((keyword (read-string "what keyword do you want to recorded for: " (thing-at-point 'symbol
)))
259 (model-directory (expand-file-name (alist-get 't rainbow-fart-voice-pack-alist
) rainbow-fart-voices-directory
))
260 (voice-file-name (format "%s-%s.mp3" keyword
(float-time)))
261 (voice-file-path (expand-file-name voice-file-name model-directory
))
262 (record-cmd (replace-regexp-in-string "%f" voice-file-path rainbow-fart-recorder-template
)))
263 (shell-command record-cmd
))
264 ;; TODO write new audio file and keyword to contributions JSON file.
269 (define-minor-mode rainbow-fart-mode
270 "A global minor mode add an encourager when you programming.
271 Usage: (add-hook 'after-init-hook #'rainbow-fart-mode)"
276 (if rainbow-fart-mode
278 (add-hook 'post-self-insert-hook
#'rainbow-fart--post-self-insert t t
)
279 (advice-add (eval 'flycheck-display-errors-function
)
280 :before
'rainbow-fart--linter-display-errors
)
281 (when (and rainbow-fart-time-interval
282 ;; only when media audio file available.
283 (rainbow-fart--get-media-uri "noon"))
284 (setq rainbow-fart--timer
285 (run-with-timer 0 rainbow-fart-time-interval
'rainbow-fart--timing-remind
))))
286 (remove-hook 'post-self-insert-hook
#'rainbow-fart--post-self-insert t
)
287 (advice-remove (eval 'flycheck-display-errors-function
)
288 'rainbow-fart--linter-display-errors
)
289 (when (timerp rainbow-fart--timer
)
290 (cancel-timer rainbow-fart--timer
))
291 ;; reset rainbow-fart playing status after toggled `rainbow-fart-mode'.
292 (setq rainbow-fart--playing nil
)))
296 (provide 'rainbow-fart
)
298 ;;; rainbow-fart.el ends here