notifications.el 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. ;;; notifications.el --- Client interface to desktop notifications.
  2. ;; Copyright (C) 2010-2012 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. (eval-when-compile
  27. (require 'cl))
  28. (require 'dbus)
  29. (defconst notifications-specification-version "1.2"
  30. "The version of the Desktop Notifications Specification implemented.")
  31. (defconst notifications-application-name "Emacs"
  32. "Default application name.")
  33. (defconst notifications-application-icon
  34. (expand-file-name
  35. "images/icons/hicolor/scalable/apps/emacs.svg"
  36. data-directory)
  37. "Default application icon.")
  38. (defconst notifications-service "org.freedesktop.Notifications"
  39. "D-Bus notifications service name.")
  40. (defconst notifications-path "/org/freedesktop/Notifications"
  41. "D-Bus notifications service path.")
  42. (defconst notifications-interface "org.freedesktop.Notifications"
  43. "D-Bus notifications service interface.")
  44. (defconst notifications-notify-method "Notify"
  45. "D-Bus notifications notify method.")
  46. (defconst notifications-close-notification-method "CloseNotification"
  47. "D-Bus notifications close notification method.")
  48. (defconst notifications-get-capabilities-method "GetCapabilities"
  49. "D-Bus notifications get capabilities 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* ((unique-name (dbus-event-service-name last-input-event))
  71. (entry (assoc (cons unique-name id) notifications-on-action-map)))
  72. (when entry
  73. (funcall (cadr entry) id action)
  74. (when (and (not (setq notifications-on-action-map
  75. (remove entry notifications-on-action-map)))
  76. notifications-on-action-object)
  77. (dbus-unregister-object notifications-on-action-object)
  78. (setq notifications-on-action-object nil)))))
  79. (defun notifications-on-closed-signal (id &optional reason)
  80. "Dispatch signals to callback functions from `notifications-on-closed-map'."
  81. ;; notification-daemon prior 0.4.0 does not send a reason. So we
  82. ;; make it optional, and assume `undefined' as default.
  83. (let* ((unique-name (dbus-event-service-name last-input-event))
  84. (entry (assoc (cons unique-name id) notifications-on-close-map))
  85. (reason (or reason 4)))
  86. (when entry
  87. (funcall (cadr entry)
  88. id (cadr (assoc reason notifications-closed-reason)))
  89. (when (and (not (setq notifications-on-close-map
  90. (remove entry notifications-on-close-map)))
  91. notifications-on-close-object)
  92. (dbus-unregister-object notifications-on-close-object)
  93. (setq notifications-on-close-object nil)))))
  94. (defun notifications-notify (&rest params)
  95. "Send notification via D-Bus using the Freedesktop notification protocol.
  96. Various PARAMS can be set:
  97. :title The notification title.
  98. :body The notification body text.
  99. :app-name The name of the application sending the notification.
  100. Default to `notifications-application-name'.
  101. :replaces-id The notification ID that this notification replaces.
  102. :app-icon The notification icon.
  103. Default is `notifications-application-icon'.
  104. Set to nil if you do not want any icon displayed.
  105. :actions A list of actions in the form:
  106. (KEY TITLE KEY TITLE ...)
  107. where KEY and TITLE are both strings.
  108. The default action (usually invoked by clicking the
  109. notification) should have a key named \"default\".
  110. The title can be anything, though implementations are free
  111. not to display it.
  112. :timeout The timeout time in milliseconds since the display
  113. of the notification at which the notification should
  114. automatically close.
  115. If -1, the notification's expiration time is dependent
  116. on the notification server's settings, and may vary for
  117. the type of notification.
  118. If 0, the notification never expires.
  119. Default value is -1.
  120. :urgency The urgency level.
  121. Either `low', `normal' or `critical'.
  122. :action-items Whether the TITLE of the actions is interpreted as
  123. a named icon.
  124. :category The type of notification this is.
  125. :desktop-entry This specifies the name of the desktop filename representing
  126. the calling program.
  127. :image-data This is a raw data image format which describes the width,
  128. height, rowstride, has alpha, bits per sample, channels and
  129. image data respectively.
  130. :image-path This is represented either as a URI (file:// is the
  131. only URI schema supported right now) or a name
  132. in a freedesktop.org-compliant icon theme.
  133. :sound-file The path to a sound file to play when the notification pops up.
  134. :sound-name A themable named sound from the freedesktop.org sound naming
  135. specification to play when the notification pops up.
  136. Similar to icon-name,only for sounds. An example would
  137. be \"message-new-instant\".
  138. :suppress-sound Causes the server to suppress playing any sounds, if it has
  139. that ability.
  140. :resident When set the server will not automatically remove the
  141. notification when an action has been invoked.
  142. :transient When set the server will treat the notification as transient
  143. and by-pass the server's persistence capability, if it
  144. should exist.
  145. :x Specifies the X location on the screen that the notification
  146. should point to. The \"y\" hint must also be specified.
  147. :y Specifies the Y location on the screen that the notification
  148. should point to. The \"x\" hint must also be specified.
  149. :on-action Function to call when an action is invoked.
  150. The notification id and the key of the action are passed
  151. as arguments to the function.
  152. :on-close Function to call when the notification has been closed
  153. by timeout or by the user.
  154. The function receive the notification id and the closing
  155. reason as arguments:
  156. - `expired' if the notification has expired
  157. - `dismissed' if the notification was dismissed by the user
  158. - `close-notification' if the notification was closed
  159. by a call to CloseNotification
  160. - `undefined' if the notification server hasn't provided
  161. a reason
  162. Which parameters are accepted by the notification server can be
  163. checked via `notifications-get-capabilities'.
  164. This function returns a notification id, an integer, which can be
  165. used to manipulate the notification item with
  166. `notifications-close-notification' or the `:replaces-id' argument
  167. of another `notifications-notify' call."
  168. (let ((title (plist-get params :title))
  169. (body (plist-get params :body))
  170. (app-name (plist-get params :app-name))
  171. (replaces-id (plist-get params :replaces-id))
  172. (app-icon (plist-get params :app-icon))
  173. (actions (plist-get params :actions))
  174. (timeout (plist-get params :timeout))
  175. ;; Hints
  176. (hints '())
  177. (urgency (plist-get params :urgency))
  178. (category (plist-get params :category))
  179. (desktop-entry (plist-get params :desktop-entry))
  180. (image-data (plist-get params :image-data))
  181. (image-path (plist-get params :image-path))
  182. (action-items (plist-get params :action-items))
  183. (sound-file (plist-get params :sound-file))
  184. (sound-name (plist-get params :sound-name))
  185. (suppress-sound (plist-get params :suppress-sound))
  186. (resident (plist-get params :resident))
  187. (transient (plist-get params :transient))
  188. (x (plist-get params :x))
  189. (y (plist-get params :y))
  190. id)
  191. ;; Build hints array
  192. (when urgency
  193. (add-to-list 'hints `(:dict-entry
  194. "urgency"
  195. (:variant :byte ,(case urgency
  196. (low 0)
  197. (critical 2)
  198. (t 1)))) t))
  199. (when category
  200. (add-to-list 'hints `(:dict-entry
  201. "category"
  202. (:variant :string ,category)) t))
  203. (when desktop-entry
  204. (add-to-list 'hints `(:dict-entry
  205. "desktop-entry"
  206. (:variant :string ,desktop-entry)) t))
  207. (when image-data
  208. (add-to-list 'hints `(:dict-entry
  209. "image-data"
  210. (:variant :struct ,image-data)) t))
  211. (when image-path
  212. (add-to-list 'hints `(:dict-entry
  213. "image-path"
  214. (:variant :string ,image-path)) t))
  215. (when action-items
  216. (add-to-list 'hints `(:dict-entry
  217. "action-items"
  218. (:variant :boolean ,action-items)) t))
  219. (when sound-file
  220. (add-to-list 'hints `(:dict-entry
  221. "sound-file"
  222. (:variant :string ,sound-file)) t))
  223. (when sound-name
  224. (add-to-list 'hints `(:dict-entry
  225. "sound-name"
  226. (:variant :string ,sound-name)) t))
  227. (when suppress-sound
  228. (add-to-list 'hints `(:dict-entry
  229. "suppress-sound"
  230. (:variant :boolean ,suppress-sound)) t))
  231. (when resident
  232. (add-to-list 'hints `(:dict-entry
  233. "resident"
  234. (:variant :boolean ,resident)) t))
  235. (when transient
  236. (add-to-list 'hints `(:dict-entry
  237. "transient"
  238. (:variant :boolean ,transient)) t))
  239. (when x
  240. (add-to-list 'hints `(:dict-entry "x" (:variant :int32 ,x)) t))
  241. (when y
  242. (add-to-list 'hints `(:dict-entry "y" (:variant :int32 ,y)) t))
  243. ;; Call Notify method
  244. (setq id
  245. (dbus-call-method :session
  246. notifications-service
  247. notifications-path
  248. notifications-interface
  249. notifications-notify-method
  250. :string (or app-name
  251. notifications-application-name)
  252. :uint32 (or replaces-id 0)
  253. :string (if app-icon
  254. (expand-file-name app-icon)
  255. ;; If app-icon is nil because user
  256. ;; requested it to be so, send the
  257. ;; empty string
  258. (if (plist-member params :app-icon)
  259. ""
  260. ;; Otherwise send the default icon path
  261. notifications-application-icon))
  262. :string (or title "")
  263. :string (or body "")
  264. `(:array ,@actions)
  265. (or hints '(:array :signature "{sv}"))
  266. :int32 (or timeout -1)))
  267. ;; Register close/action callback function. We must also remember
  268. ;; the daemon's unique name, because the daemon could have
  269. ;; restarted.
  270. (let ((on-action (plist-get params :on-action))
  271. (on-close (plist-get params :on-close))
  272. (unique-name (dbus-get-name-owner :session notifications-service)))
  273. (when on-action
  274. (add-to-list 'notifications-on-action-map
  275. (list (cons unique-name id) on-action))
  276. (unless notifications-on-action-object
  277. (setq notifications-on-action-object
  278. (dbus-register-signal
  279. :session
  280. nil
  281. notifications-path
  282. notifications-interface
  283. notifications-action-signal
  284. 'notifications-on-action-signal))))
  285. (when on-close
  286. (add-to-list 'notifications-on-close-map
  287. (list (cons unique-name id) on-close))
  288. (unless notifications-on-close-object
  289. (setq notifications-on-close-object
  290. (dbus-register-signal
  291. :session
  292. nil
  293. notifications-path
  294. notifications-interface
  295. notifications-closed-signal
  296. 'notifications-on-closed-signal)))))
  297. ;; Return notification id
  298. id))
  299. (defun notifications-close-notification (id)
  300. "Close a notification with identifier ID."
  301. (dbus-call-method :session
  302. notifications-service
  303. notifications-path
  304. notifications-interface
  305. notifications-close-notification-method
  306. :int32 id))
  307. (defvar dbus-debug) ; used in the macroexpansion of dbus-ignore-errors
  308. (defun notifications-get-capabilities ()
  309. "Return the capabilities of the notification server, a list of strings.
  310. The following capabilities can be expected:
  311. :actions The server will provide the specified actions
  312. to the user.
  313. :action-icons Supports using icons instead of text for
  314. displaying actions.
  315. :body Supports body text.
  316. :body-hyperlinks The server supports hyperlinks in the notifications.
  317. :body-images The server supports images in the notifications.
  318. :body-markup Supports markup in the body text.
  319. :icon-multi The server will render an animation of all the
  320. frames in a given image array.
  321. :icon-static Supports display of exactly 1 frame of any
  322. given image array. This value is mutually exclusive
  323. with `:icon-multi'.
  324. :persistence The server supports persistence of notifications.
  325. :sound The server supports sounds on notifications.
  326. Further vendor-specific caps start with `:x-vendor', like `:x-gnome-foo-cap'."
  327. (dbus-ignore-errors
  328. (mapcar
  329. (lambda (x) (intern (concat ":" x)))
  330. (dbus-call-method :session
  331. notifications-service
  332. notifications-path
  333. notifications-interface
  334. notifications-get-capabilities-method))))
  335. (provide 'notifications)