-
Notifications
You must be signed in to change notification settings - Fork 1
/
eosd-mode.el
468 lines (397 loc) · 15.7 KB
/
eosd-mode.el
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
;;; eosd-mode.el --- UI code to display notifications received ; -*- lexical-binding: t; -*-
;;
;;; Commentary:
;;
;; Copyright (C) 2016 Lincoln Clarete <[email protected]>
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;
;;; Code:
(require 'magit-popup)
(require 'shr)
(require 'eosd-cache)
(require 'eosd-pixbuf)
(defface eosd-heading-face
'((((class color) (background dark)) :foreground "dim gray")
(t (:foreground "gray")))
"Face for section headings."
:group 'eosd-faces)
(defface eosd-title-face
'((((class color) (background dark)) :foreground "#5180b3")
(t (:foreground "blue")))
"Face for notification titles."
:group 'eosd-faces)
(defface eosd-datetime-face
'((((class color) (background dark)) :foreground "#5180b3")
(t (:foreground "blue")))
"Face for notification date-time."
:group 'eosd-faces)
(defface eosd-action-link-face
'((((class color) (background dark)) :foreground "cornsilk4")
(t (:foreground "yellow")))
"Face used for links to notification actions."
:group 'eosd-faces)
(defface eosd-delete-link-face
'((((class color) (background dark)) :foreground "IndianRed")
(t (:foreground "red")))
"Face used for links to delete notifications."
:group 'eosd-faces)
(defface eosd-text-mark-face
'((t (:height 1)))
"Face for section headings."
:group 'eosd-faces)
(defvar eosd-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "q") 'bury-buffer)
(define-key map (kbd "f") 'eosd-mode-popup-filter)
(define-key map (kbd "g") 'eosd-mode-create-or-update-buffer)
(define-key map (kbd "n") 'eosd-mode-next-notification)
(define-key map (kbd "p") 'eosd-mode-previous-notification)
(define-key map (kbd "i") 'eosd-mode-notification-body-toggle)
(define-key map (kbd "d") 'eosd-mode-delete-notification-under-cursor)
(define-key map (kbd "<tab>") 'shr-next-link)
(define-key map (kbd "<backtab>") 'shr-previous-link)
map)
"The keymap to use with `eosd-mode'.")
(defvar eosd-buffer-name "*notifications*"
"Name of notifications buffer.")
(defvar eosd-mode-text-mark "⚫"
"Text mark that prefixes each notification.")
(defgroup eosd-mode nil
"Emacs Desktop Notifications."
:group 'eosd-mode)
(defcustom eosd-mode-enable-icon nil
"EOSD will render notification icons if this value is not nil."
:group 'eosd-mode
:type 'boolean)
(defcustom eosd-mode-datetime-format nil
"Date-Time format for rendering dates.
If set to nil, it will show approximate time."
:group 'eosd-mode
:type 'string)
(defcustom eosd-mode-notification-mark "¶"
"Character used as a text mark for each notification.
The default font configuration makes this char invisible, but
it can be changed through the variable `eosd-text-mark-face'"
:group 'eosd-mode
:type 'string)
(defcustom eosd-mode-notification-indent 4
"How spaces should be used to indent a notification message."
:group 'eosd-mode
:type 'integer)
(defcustom eosd-mode-hook nil
"Hook run after entering eosd-mode."
:group 'eosd-mode
:type 'hook)
(defcustom eosd-mode-section-hook
'(eosd-mode-section-header
eosd-mode-section-notifications)
"Hooks for building the UI in eosd-mode."
:group 'eosd-mode
:type 'hook)
(defun eosd-mode-link (text face &optional key func arg)
"Insert actionable link with custom face.
TEXT will be inserted. FACE will be used to configure all the
visual settings for the link.
The action will be triggered when keyboard shortcut defined by
`KEY'. Action means the execution of `FUNC' passing `ARG' as
arguments."
(let ((map (make-sparse-keymap)))
(when (and key func)
(define-key map (kbd key)
#'(lambda (e) (interactive "p") (apply func arg))))
(insert (propertize text
'shr-url func
'face face
'keymap map
'help-echo text
'follow-link t))))
(defmacro eosd-mode-notification-find-boundaries (body)
"Capture notification under cursor and pass it to BODY.
Any function that needs to use this functionality can start with
something like this:
(eosd-mode-notification-find-boundaries
#'(lambda (notification-id begin end)
(message \"N:%d B:%s E:%s\" notification-id begin end)))
The notification ID is retrieved from a hidden text that is
inserted right before the text marker.
The visibility of the text marker is configurable whereas the id
visibility isn't."
`(save-excursion
(end-of-line)
(when (re-search-backward (eosd-mode-mark "^[0-9]+%s.*") nil t 1)
;; Retrieve notification id hidden in text before the text
;; mark. It will also be fed into the BODY function.
(beginning-of-line)
(mark-word)
(let ((notification-id (string-to-number
(buffer-substring (mark) (point)))))
(forward-line)
(let* ((begin (point))
(_ (forward-line))
(end-point (re-search-forward
(eosd-mode-mark "^[0-9]+%s") nil t 1))
(_ (beginning-of-line))
(end (if end-point (point) (point-max)))
(inhibit-read-only t))
(funcall ,body notification-id begin end))))))
(defun eosd-mode-delete-notification-under-cursor ()
"Delete notification under cursor."
(interactive)
(eosd-mode-notification-find-boundaries
#'(lambda (notification-id begin end)
(re-search-backward (eosd-mode-mark "^[0-9]+%s.*$") nil t 1)
(beginning-of-line)
(delete-region (point) end)
(eosd-cache-delete-notification notification-id)))
(eosd-mode-next-notification))
(defun eosd-mode-mark (fmt)
"Return `FMT' filled in with configured text mark.
To change the configured text mark, refer to the variable
`eosd-mode-text-mark'."
(format fmt eosd-mode-text-mark))
(defun eosd-mode-notification-body-toggle ()
"Toggle visibility of the current notification's body."
(interactive)
(eosd-mode-notification-find-boundaries
#'(lambda (notification-id begin end)
(if (get-text-property begin 'invisible)
(facemenu-remove-special begin end)
(facemenu-set-invisible begin end)))))
(defun eosd-mode-next-notification ()
"Go to the next notification."
(interactive)
(re-search-forward (eosd-mode-mark "^[0-9]+%s") nil t 1))
(defun eosd-mode-previous-notification ()
"Go to the previous notification."
(interactive)
(forward-line -1)
(re-search-backward (eosd-mode-mark "^[0-9]+%s") nil t 1)
(eosd-mode-next-notification))
(defun eosd-mode-notification-mark (mark)
"Insert invisible MARK."
(insert (propertize (eosd-mode-mark "%s") 'face 'eosd-text-mark-face)))
(defun eosd-mode-parse-image-data (data)
"Parse image from within DATA.")
(defun eosd-mode-parse-icon-data (data)
"Parse icon within DATA."
(cl-destructuring-bind (w h rs alpha bps ch imgdata) data
(let* ((al (if alpha 1 0))
(thedata0 (apply #'unibyte-string imgdata))
(thedata1 (eosd-pixbuf-to-png w h rs al bps thedata0 48 48))
(theimage (create-image thedata1 'png t)))
(insert-image theimage))))
(defun eosd-mode-render-app-icon (notification)
"Render application info inside of NOTIFICATION."
(let ((icon (cdr (assoc 'app-icon notification))))
(if (file-regular-p icon)
(insert-image (create-image icon))
(let ((hints (cdr (assoc 'hints notification))))
(dolist (h hints)
(pcase (car h)
(`"icon_data" (eosd-mode-parse-icon-data (caadr h)))
(`"image-data" (eosd-mode-parse-image-data (caadr h)))))))
(insert " ")))
(defun eosd-mode-render-actions (notification)
"Render NOTIFICATION actions."
(dolist (action (cdr (assoc 'actions notification)))
(eosd-mode-link
(downcase action) 'eosd-action-link-face "<RET>"
#'(lambda () (message "%s" notification)))
(insert " ⋅ "))
(eosd-mode-link
"delete" 'eosd-delete-link-face "<RET>"
'eosd-mode-delete-notification-under-cursor)
(insert ?\n))
(defun eosd-mode-render-body (notification)
"Render `body' field of NOTIFICATION.
The text will be rendered along with the actions a notification
may present to the user. Check `eosd-mode-render-actions' to see
which callbacks are associated with printable text.
The `shr' library will be used to render simple HTML in order to
support `body-hyperlinks'. Blank lines are trimmed from text
after rendered as HTML."
(insert ?\n)
(let ((start (point)))
(insert (cdr (assoc 'body notification)) ?\n)
(fill-region start (point))
(eosd-mode-render-actions notification)
(indent-region
start (point)
eosd-mode-notification-indent)))
(defun eosd-mode-find-second-format (s)
"Find good format for S."
(cond ((zerop s) "just now")
((<= s 19) (format "%ds" s))
((and (> s 19) (< s 40)) "half a minute")
((< s 59) "~1m")
(t "1m")))
(defun eosd-mode-distance-from-current-time (timestamp)
"How much time past since TIMESTAMP in text.
* 1m <= X: calls `eosd-mode-find-second-format'
* 1m > X > 45m: Xm
* 45m >= X > 90m: about 1h
* 90m >= X > 240h: Xh
* 240h >= X: a while ago"
(let* ((current (float-time))
(minutes (fround (/ (- current timestamp) 60.0)))
(seconds (fround (- current timestamp))))
(cond ((<= minutes 1)
(eosd-mode-find-second-format seconds))
((and (> minutes 1) (< minutes 45))
(format "%dm" minutes))
((and (>= minutes 45) (< minutes 90))
"about 1h")
((and (>= minutes 90) (< minutes 14401))
(format "%dh" (fround (/ minutes 60))))
(t "a while ago"))))
(defun eosd-mode-notification-id (notification)
"Insert id of NOTIFICATION as an invisible text."
(let* ((begin (point))
(id (cdr (assoc 'id notification))))
(insert (number-to-string id))
(facemenu-set-invisible begin (point))))
(defun eosd-mode-render-title (notification)
"Render the title of NOTIFICATION.
Customize `eosd-title-face' and `eosd-datetime-face' to change
the font configuration for the title and date-time respectively.
Edit `eosd-mode-datetime-format' for customizing the date-time
format."
(eosd-mode-link
(cdr (assoc 'summary notification))
'eosd-title-face)
(let* ((timestamp (cdr (assoc 'timestamp notification)))
(printable (if eosd-mode-datetime-format
(format-time-string eosd-mode-datetime-format timestamp)
(eosd-mode-distance-from-current-time timestamp))))
(eosd-mode-link (format " ⋅ %s" printable) 'eosd-datetime-face)))
(defun eosd-mode-render-notification (notification)
"Render a single NOTIFICATION item.
A NOTIFICATION is rendered into three different parts: An icon, a
title, and some content. The title is usually the only item that
is always present. The application may not send any icon
information for example.
The rendering of the icon can be enabled or disabled through the
variable `eosd-mode-enable-icon'."
(eosd-mode-notification-id notification)
(eosd-mode-notification-mark "begin")
(when eosd-mode-enable-icon
(eosd-mode-render-app-icon notification))
(eosd-mode-render-title notification)
(eosd-mode-render-body notification)
(insert ?\n))
(defun eosd-mode-render-notification-list (notifications)
"Render each item in the NOTIFICATIONS list."
(dolist (notification notifications)
(eosd-mode-render-notification notification))
(goto-char 0))
(defun eosd-mode-notification-insert-if-buffer (notification)
"Insert NOTIFICATION if *notifications* buffer exists."
(eosd-mode-with-buffer
#'(lambda (b)
(goto-char 0)
(forward-line 2)
(eosd-mode-render-notification notification))))
(defun eosd-mode-setup ()
"Prepare ground for the `EOSD' buffer."
(buffer-disable-undo)
(setq truncate-lines t)
(setq buffer-read-only t)
(setq-local line-move-visual t)
(setq show-trailing-whitespace nil)
(hack-dir-local-variables-non-file-buffer)
(make-local-variable 'text-property-default-nonsticky)
(push (cons 'keymap t) text-property-default-nonsticky)
;; disable yasnippet & linum-mode
(when (fboundp 'yas-minor-mode)
(yas-minor-mode -1))
(when (fboundp 'linum-mode)
(linum-mode -1)))
(defun eosd-mode-heading-string ()
"Generate the text for the header of the EOSD buffer."
(let ((l (length (eosd-cache-list))))
(format "%d Notification%s\n\n" l (if (eq l 1) "" "s"))))
(defun eosd-mode-section-header ()
"Insert the header of the EOSD buffer.
Customize the variable `eosd-heading-face' to change font
settings of the header."
(insert
(propertize
(eosd-mode-heading-string) 'face 'eosd-heading-face)))
(defun eosd-mode-section-notifications ()
"Insert notifications from the cache in the EOSD buffer."
(eosd-mode-render-notification-list (eosd-cache-list)))
(define-derived-mode eosd-mode special-mode "Desktop Notifications"
"Major mode for displaying Desktop Notifications."
:group 'eosd-mode
(eosd-mode-setup)
(run-hooks 'eosd-mode-section-hook))
(defmacro eosd-mode-with-buffer (body)
"BODY."
`(let ((buf (get-buffer eosd-buffer-name)))
(when buf
(with-current-buffer buf
(let ((inhibit-read-only t))
(funcall ,body buf)))
buf)))
(defun eosd-mode-create-buffer ()
"Create the *notifications* buffer."
(with-output-to-temp-buffer eosd-buffer-name
(switch-to-buffer eosd-buffer-name)
(setq font-lock-mode nil)
(use-local-map eosd-mode-map)
(let ((inhibit-read-only t))
(eosd-mode))))
(defun eosd-mode-create-or-update-buffer ()
"Update or Create special *notifications* buffer."
(interactive)
(if (not (eosd-mode-with-buffer
#'(lambda (b)
(erase-buffer)
(eosd-mode)
(switch-to-buffer b))))
(eosd-mode-create-buffer)))
(defun eosd-mode-filter-time (time)
"Only show messages at most TIME old."
(eosd-cache-filter-by-time time)
(eosd-mode-update-filters))
(defun eosd-mode-filter-app (appname)
"Filter notifications by app that generated them with APPNAME."
(interactive "MFilter by application name: ")
(eosd-cache-filter-by-app appname)
(eosd-mode-update-filters))
(defun eosd-mode-update-filters ()
"Update `eosd-notification-filter' and `eosd-mode' buffer."
(setq eosd-notification-filter `(,eosd-notification-filter-time
,eosd-notification-filter-app))
(eosd-mode-create-or-update-buffer))
(defmacro --eosd--timef (time)
"Interactive wrapper for eosd-mode-filter-time with TIME."
`(lambda () (interactive) (eosd-mode-filter-time ,time)))
;;;;###autoload (autoload 'eosd-mode-filter-popup "magit" nil t)
(magit-define-popup eosd-mode-popup-filter
"Show popup buffer featuring filtering commands."
'eosd-popup-filter
:actions `("Time"
(?1 "Last 1m" ,(--eosd--timef '1m))
(?5 "Last 5m" ,(--eosd--timef '5m))
(?h "Last hour" ,(--eosd--timef '1h))
(?d "Last 24h" ,(--eosd--timef '24h))
(?w "Last Week" ,(--eosd--timef '1w))
(?m "Last Month" ,(--eosd--timef '1M))
"Text"
(?a "Application" eosd-mode-filter-app)
(?m "Message" #'eosd-mode-filter-message)))
(provide 'eosd-mode)
;;; eosd-mode.el ends here