notifications.el 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. ;;; notifications.el --- Client interface to desktop notifications.
  2. ;; Copyright (C) 2010-2017 Free Software Foundation, Inc.
  3. ;; Author: Julien Danjou <julien@danjou.info>
  4. ;; Keywords: comm desktop notifications
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; This package provides an implementation of the Desktop Notifications
  18. ;; <http://developer.gnome.org/notification-spec/>.
  19. ;; In order to activate this package, you must add the following code
  20. ;; into your .emacs:
  21. ;;
  22. ;; (require 'notifications)
  23. ;; For proper usage, Emacs must be started in an environment with an
  24. ;; active D-Bus session bus.
  25. ;;; Code:
  26. (require 'dbus)
  27. (defconst notifications-specification-version "1.2"
  28. "The version of the Desktop Notifications Specification implemented.")
  29. (defconst notifications-application-name "Emacs"
  30. "Default application name.")
  31. (defconst notifications-application-icon
  32. (expand-file-name
  33. "images/icons/hicolor/scalable/apps/emacs.svg"
  34. data-directory)
  35. "Default application icon.")
  36. (defconst notifications-service "org.freedesktop.Notifications"
  37. "D-Bus notifications service name.")
  38. (defconst notifications-path "/org/freedesktop/Notifications"
  39. "D-Bus notifications service path.")
  40. (defconst notifications-interface "org.freedesktop.Notifications"
  41. "D-Bus notifications service interface.")
  42. (defconst notifications-notify-method "Notify"
  43. "D-Bus notifications notify method.")
  44. (defconst notifications-close-notification-method "CloseNotification"
  45. "D-Bus notifications close notification method.")
  46. (defconst notifications-get-capabilities-method "GetCapabilities"
  47. "D-Bus notifications get capabilities method.")
  48. (defconst notifications-get-server-information-method "GetServerInformation"
  49. "D-Bus notifications get server information method.")
  50. (defconst notifications-action-signal "ActionInvoked"
  51. "D-Bus notifications action signal.")
  52. (defconst notifications-closed-signal "NotificationClosed"
  53. "D-Bus notifications closed signal.")
  54. (defconst notifications-closed-reason
  55. '((1 expired)
  56. (2 dismissed)
  57. (3 close-notification)
  58. (4 undefined))
  59. "List of reasons why a notification has been closed.")
  60. (defvar notifications-on-action-map nil
  61. "Mapping between notification and action callback functions.")
  62. (defvar notifications-on-action-object nil
  63. "Object for registered on-action signal.")
  64. (defvar notifications-on-close-map nil
  65. "Mapping between notification and close callback functions.")
  66. (defvar notifications-on-close-object nil
  67. "Object for registered on-close signal.")
  68. (defun notifications-on-action-signal (id action)
  69. "Dispatch signals to callback functions from `notifications-on-action-map'."
  70. (let* ((bus (dbus-event-bus-name last-input-event))
  71. (unique-name (dbus-event-service-name last-input-event))
  72. (entry (assoc (list bus unique-name id) notifications-on-action-map)))
  73. (when entry
  74. (funcall (cadr entry) id action)
  75. (when (and (not (setq notifications-on-action-map
  76. (remove entry notifications-on-action-map)))
  77. notifications-on-action-object)
  78. (dbus-unregister-object notifications-on-action-object)
  79. (setq notifications-on-action-object nil)))))
  80. (defun notifications-on-closed-signal (id &optional reason)
  81. "Dispatch signals to callback functions from `notifications-on-closed-map'."
  82. ;; notification-daemon prior 0.4.0 does not send a reason. So we
  83. ;; make it optional, and assume `undefined' as default.
  84. (let* ((bus (dbus-event-bus-name last-input-event))
  85. (unique-name (dbus-event-service-name last-input-event))
  86. (entry (assoc (list bus unique-name id) notifications-on-close-map))
  87. (reason (or reason 4)))
  88. (when entry
  89. (funcall (cadr entry)
  90. id (cadr (assoc reason notifications-closed-reason)))
  91. (when (and (not (setq notifications-on-close-map
  92. (remove entry notifications-on-close-map)))
  93. notifications-on-close-object)
  94. (dbus-unregister-object notifications-on-close-object)
  95. (setq notifications-on-close-object nil)))))
  96. (defun notifications-notify (&rest params)
  97. "Send notification via D-Bus using the Freedesktop notification protocol.
  98. Various PARAMS can be set:
  99. :bus The D-Bus bus, if different from `:session'.
  100. :title The notification title.
  101. :body The notification body text.
  102. :app-name The name of the application sending the notification.
  103. Default to `notifications-application-name'.
  104. :replaces-id The notification ID that this notification replaces.
  105. :app-icon The notification icon.
  106. Default is `notifications-application-icon'.
  107. Set to nil if you do not want any icon displayed.
  108. :actions A list of actions in the form:
  109. (KEY TITLE KEY TITLE ...)
  110. where KEY and TITLE are both strings.
  111. The default action (usually invoked by clicking the
  112. notification) should have a key named \"default\".
  113. The title can be anything, though implementations are free
  114. not to display it.
  115. :timeout The timeout time in milliseconds since the display
  116. of the notification at which the notification should
  117. automatically close.
  118. If -1, the notification's expiration time is dependent
  119. on the notification server's settings, and may vary for
  120. the type of notification.
  121. If 0, the notification never expires.
  122. Default value is -1.
  123. :urgency The urgency level.
  124. Either `low', `normal' or `critical'.
  125. :action-items Whether the TITLE of the actions is interpreted as
  126. a named icon.
  127. :category The type of notification this is.
  128. :desktop-entry This specifies the name of the desktop filename representing
  129. the calling program.
  130. :image-data This is a raw data image format which describes the width,
  131. height, rowstride, has alpha, bits per sample, channels and
  132. image data respectively.
  133. :image-path This is represented either as a URI (file:// is the
  134. only URI schema supported right now) or a name
  135. in a freedesktop.org-compliant icon theme.
  136. :sound-file The path to a sound file to play when the notification pops up.
  137. :sound-name A themable named sound from the freedesktop.org sound naming
  138. specification to play when the notification pops up.
  139. Similar to icon-name,only for sounds. An example would
  140. be \"message-new-instant\".
  141. :suppress-sound Causes the server to suppress playing any sounds, if it has
  142. that ability.
  143. :resident When set the server will not automatically remove the
  144. notification when an action has been invoked.
  145. :transient When set the server will treat the notification as transient
  146. and by-pass the server's persistence capability, if it
  147. should exist.
  148. :x Specifies the X location on the screen that the notification
  149. should point to. The \"y\" hint must also be specified.
  150. :y Specifies the Y location on the screen that the notification
  151. should point to. The \"x\" hint must also be specified.
  152. :on-action Function to call when an action is invoked.
  153. The notification id and the key of the action are passed
  154. as arguments to the function.
  155. :on-close Function to call when the notification has been closed
  156. by timeout or by the user.
  157. The function receive the notification id and the closing
  158. reason as arguments:
  159. - `expired' if the notification has expired
  160. - `dismissed' if the notification was dismissed by the user
  161. - `close-notification' if the notification was closed
  162. by a call to CloseNotification
  163. - `undefined' if the notification server hasn't provided
  164. a reason
  165. Which parameters are accepted by the notification server can be
  166. checked via `notifications-get-capabilities'.
  167. This function returns a notification id, an integer, which can be
  168. used to manipulate the notification item with
  169. `notifications-close-notification' or the `:replaces-id' argument
  170. of another `notifications-notify' call."
  171. (with-demoted-errors
  172. (let ((bus (or (plist-get params :bus) :session))
  173. (title (plist-get params :title))
  174. (body (plist-get params :body))
  175. (app-name (plist-get params :app-name))
  176. (replaces-id (plist-get params :replaces-id))
  177. (app-icon (plist-get params :app-icon))
  178. (actions (plist-get params :actions))
  179. (timeout (plist-get params :timeout))
  180. ;; Hints
  181. (hints '())
  182. (urgency (plist-get params :urgency))
  183. (category (plist-get params :category))
  184. (desktop-entry (plist-get params :desktop-entry))
  185. (image-data (plist-get params :image-data))
  186. (image-path (plist-get params :image-path))
  187. (action-items (plist-get params :action-items))
  188. (sound-file (plist-get params :sound-file))
  189. (sound-name (plist-get params :sound-name))
  190. (suppress-sound (plist-get params :suppress-sound))
  191. (resident (plist-get params :resident))
  192. (transient (plist-get params :transient))
  193. (x (plist-get params :x))
  194. (y (plist-get params :y))
  195. id)
  196. ;; Build hints array
  197. (when urgency
  198. (add-to-list 'hints `(:dict-entry
  199. "urgency"
  200. (:variant :byte ,(pcase urgency
  201. (`low 0)
  202. (`critical 2)
  203. (_ 1)))) t))
  204. (when category
  205. (add-to-list 'hints `(:dict-entry
  206. "category"
  207. (:variant :string ,category)) t))
  208. (when desktop-entry
  209. (add-to-list 'hints `(:dict-entry
  210. "desktop-entry"
  211. (:variant :string ,desktop-entry)) t))
  212. (when image-data
  213. (add-to-list 'hints `(:dict-entry
  214. "image-data"
  215. (:variant :struct ,image-data)) t))
  216. (when image-path
  217. (add-to-list 'hints `(:dict-entry
  218. "image-path"
  219. (:variant :string ,image-path)) t))
  220. (when action-items
  221. (add-to-list 'hints `(:dict-entry
  222. "action-items"
  223. (:variant :boolean ,action-items)) t))
  224. (when sound-file
  225. (add-to-list 'hints `(:dict-entry
  226. "sound-file"
  227. (:variant :string ,sound-file)) t))
  228. (when sound-name
  229. (add-to-list 'hints `(:dict-entry
  230. "sound-name"
  231. (:variant :string ,sound-name)) t))
  232. (when suppress-sound
  233. (add-to-list 'hints `(:dict-entry
  234. "suppress-sound"
  235. (:variant :boolean ,suppress-sound)) t))
  236. (when resident
  237. (add-to-list 'hints `(:dict-entry
  238. "resident"
  239. (:variant :boolean ,resident)) t))
  240. (when transient
  241. (add-to-list 'hints `(:dict-entry
  242. "transient"
  243. (:variant :boolean ,transient)) t))
  244. (when x
  245. (add-to-list 'hints `(:dict-entry "x" (:variant :int32 ,x)) t))
  246. (when y
  247. (add-to-list 'hints `(:dict-entry "y" (:variant :int32 ,y)) t))
  248. ;; Call Notify method.
  249. (setq id
  250. (dbus-call-method bus
  251. notifications-service
  252. notifications-path
  253. notifications-interface
  254. notifications-notify-method
  255. :string (or app-name
  256. notifications-application-name)
  257. :uint32 (or replaces-id 0)
  258. :string (if app-icon
  259. (expand-file-name app-icon)
  260. ;; If app-icon is nil because user
  261. ;; requested it to be so, send the
  262. ;; empty string
  263. (if (plist-member params :app-icon)
  264. ""
  265. ;; Otherwise send the
  266. ;; default icon path
  267. notifications-application-icon))
  268. :string (or title "")
  269. :string (or body "")
  270. `(:array ,@actions)
  271. (or hints '(:array :signature "{sv}"))
  272. :int32 (or timeout -1)))
  273. ;; Register close/action callback function. We must also
  274. ;; remember the daemon's unique name, because the daemon could
  275. ;; have restarted.
  276. (let ((on-action (plist-get params :on-action))
  277. (on-close (plist-get params :on-close))
  278. (unique-name (dbus-get-name-owner bus notifications-service)))
  279. (when on-action
  280. (add-to-list 'notifications-on-action-map
  281. (list (list bus unique-name id) on-action))
  282. (unless notifications-on-action-object
  283. (setq notifications-on-action-object
  284. (dbus-register-signal
  285. bus
  286. nil
  287. notifications-path
  288. notifications-interface
  289. notifications-action-signal
  290. 'notifications-on-action-signal))))
  291. (when on-close
  292. (add-to-list 'notifications-on-close-map
  293. (list (list bus unique-name id) on-close))
  294. (unless notifications-on-close-object
  295. (setq notifications-on-close-object
  296. (dbus-register-signal
  297. bus
  298. nil
  299. notifications-path
  300. notifications-interface
  301. notifications-closed-signal
  302. 'notifications-on-closed-signal)))))
  303. ;; Return notification id
  304. id)))
  305. (defun notifications-close-notification (id &optional bus)
  306. "Close a notification with identifier ID.
  307. BUS can be a string denoting a D-Bus connection, the default is `:session'."
  308. (dbus-call-method (or bus :session)
  309. notifications-service
  310. notifications-path
  311. notifications-interface
  312. notifications-close-notification-method
  313. :uint32 id))
  314. (defvar dbus-debug) ; used in the macroexpansion of dbus-ignore-errors
  315. (defun notifications-get-capabilities (&optional bus)
  316. "Return the capabilities of the notification server, a list of symbols.
  317. BUS can be a string denoting a D-Bus connection, the default is `:session'.
  318. The following capabilities can be expected:
  319. :actions The server will provide the specified actions
  320. to the user.
  321. :action-icons Supports using icons instead of text for
  322. displaying actions.
  323. :body Supports body text.
  324. :body-hyperlinks The server supports hyperlinks in the notifications.
  325. :body-images The server supports images in the notifications.
  326. :body-markup Supports markup in the body text.
  327. :icon-multi The server will render an animation of all the
  328. frames in a given image array.
  329. :icon-static Supports display of exactly 1 frame of any
  330. given image array. This value is mutually exclusive
  331. with `:icon-multi'.
  332. :persistence The server supports persistence of notifications.
  333. :sound The server supports sounds on notifications.
  334. Further vendor-specific caps start with `:x-vendor', like `:x-gnome-foo-cap'."
  335. (dbus-ignore-errors
  336. (mapcar
  337. (lambda (x) (intern (concat ":" x)))
  338. (dbus-call-method (or bus :session)
  339. notifications-service
  340. notifications-path
  341. notifications-interface
  342. notifications-get-capabilities-method))))
  343. (defun notifications-get-server-information (&optional bus)
  344. "Return information on the notification server, a list of strings.
  345. BUS can be a string denoting a D-Bus connection, the default is `:session'.
  346. The returned list is (NAME VENDOR VERSION SPEC-VERSION).
  347. NAME The product name of the server.
  348. VENDOR The vendor name. For example, \"KDE\", \"GNOME\".
  349. VERSION The server's version number.
  350. SPEC-VERSION The specification version the server is compliant with.
  351. If SPEC_VERSION is missing, the server supports a specification
  352. prior to \"1.0\".
  353. See `notifications-specification-version' for the specification
  354. version this library is compliant with."
  355. (dbus-ignore-errors
  356. (dbus-call-method (or bus :session)
  357. notifications-service
  358. notifications-path
  359. notifications-interface
  360. notifications-get-server-information-method)))
  361. (provide 'notifications)