1 ----------------------------------------------------------------------------
2 -- @author koniu <gkusnierz@gmail.com>
3 -- @copyright 2008 koniu
4 -- @release @AWESOME_VERSION@
5 ----------------------------------------------------------------------------
13 local capi
= { screen
= screen
,
20 local button
= require("awful.button")
21 local util
= require("awful.util")
22 local bt
= require("beautiful")
23 local layout
= require("awful.widget.layout")
25 --- Notification library
28 --- Naughty configuration - a table containing common popup settings.
30 -- @field padding Space between popups and edge of the workarea. Default: 4
31 -- @field spacing Spacing between popups. Default: 1
32 -- @field icon_dirs List of directories that will be checked by getIcon()
33 -- Default: { "/usr/share/pixmaps/", }
34 -- @field icon_formats List of formats that will be checked by getIcon()
35 -- Default: { "png", "gif" }
36 -- @field default_preset Preset to be used by default.
37 -- Default: config.presets.normal
43 config
.icon_dirs
= { "/usr/share/pixmaps/", }
44 config
.icon_formats
= { "png", "gif" }
47 --- Notification Presets - a table containing presets for different purposes
48 -- Preset is a table of any parameters available to notify()
49 -- You have to pass a reference of a preset in your notify() call to use the preset
50 -- At least the default preset named "normal" has to be defined
51 -- The presets "low", "normal" and "critical" are used for notifications over DBUS
52 -- @name config.presets
53 -- @field low The preset for notifications with low urgency level
54 -- @field normal The default preset for every notification without a preset that will also be used for normal urgency level
55 -- @field critical The preset for notifications with a critical urgency level
70 config
.default_preset
= config
.presets
.normal
72 -- DBUS Notification constants
79 --- DBUS notification to preset mapping
80 -- @name config.mapping
81 -- The first element is an object containing the filter
82 -- If the rules in the filter matches the associated preset will be applied
83 -- The rules object can contain: urgency, category, appname
84 -- The second element is the preset
87 {{urgency
= urgency
.low
}, config
.presets
.low
},
88 {{urgency
= urgency
.normal
}, config
.presets
.normal
},
89 {{urgency
= urgency
.critical
}, config
.presets
.critical
}
92 -- Counter for the notifications
93 -- Required for later access via DBUS
96 -- True if notifying is suspended
97 local suspended
= false
99 --- Index of notifications. See config table for valid 'position' values.
100 -- Each element is a table consisting of:
101 -- @field box Wibox object containing the popup
102 -- @field height Popup height
103 -- @field width Popup width
104 -- @field die Function to be executed on timeout
105 -- @field id Unique notification id based on a counter
106 -- @name notifications[screen][position]
109 notifications
= { suspended
= { } }
110 for s
= 1, capi
.screen
.count() do
119 --- Suspend notifications
124 --- Resume notifications
127 for i
, v
in pairs(notifications
.suspended
) do
129 if v
.timer
then v
.timer
:start() end
131 notifications
.suspended
= { }
134 -- Evaluate desired position of the notification by index - internal
135 -- @param idx Index of the notification
136 -- @param position top_right | top_left | bottom_right | bottom_left
137 -- @param height Popup height
138 -- @param width Popup width (optional)
139 -- @return Absolute position and index in { x = X, y = Y, idx = I } table
140 local function get_offset(screen
, position
, idx
, width
, height
)
141 local ws
= capi
.screen
[screen
].workarea
143 local idx
= idx
or #notifications
[screen
][position
] + 1
144 local width
= width
or notifications
[screen
][position
][idx
].width
147 if position
:match("left") then
148 v
.x
= ws
.x
+ config
.padding
150 v
.x
= ws
.x
+ ws
.width
- (width
+ config
.padding
)
153 -- calculate existing popups' height
155 for i
= 1, idx
-1, 1 do
156 existing
= existing
+ notifications
[screen
][position
][i
].height
+ config
.spacing
160 if position
:match("top") then
161 v
.y
= ws
.y
+ config
.padding
+ existing
163 v
.y
= ws
.y
+ ws
.height
- (config
.padding
+ height
+ existing
)
166 -- if positioned outside workarea, destroy oldest popup and recalculate
167 if v
.y
+ height
> ws
.y
+ ws
.height
or v
.y
< ws
.y
then
169 destroy(notifications
[screen
][position
][1])
170 v
= get_offset(screen
, position
, idx
, width
, height
)
172 if not v
.idx
then v
.idx
= idx
end
177 -- Re-arrange notifications according to their position and index - internal
179 local function arrange(screen
)
180 for p
,pos
in pairs(notifications
[screen
]) do
181 for i
,notification
in pairs(notifications
[screen
][p
]) do
182 local offset
= get_offset(screen
, p
, i
, notification
.width
, notification
.height
)
183 notification
.box
:geometry({ x
= offset
.x
, y
= offset
.y
})
184 notification
.idx
= offset
.idx
189 --- Destroy notification by notification object
190 -- @param notification Notification object to be destroyed
191 -- @return True if the popup was successfully destroyed, nil otherwise
192 function destroy(notification
)
193 if notification
and notification
.box
.screen
then
195 for k
, v
in pairs(notifications
.suspended
) do
196 if v
.box
== notification
.box
then
197 table.remove(notifications
.suspended
, k
)
202 local scr
= notification
.box
.screen
203 table.remove(notifications
[notification
.box
.screen
][notification
.position
], notification
.idx
)
204 if notification
.timer
then
205 notification
.timer
:stop()
207 notification
.box
.screen
= nil
213 -- Get notification by ID
214 -- @param id ID of the notification
215 -- @return notification object if it was found, nil otherwise
216 local function getById(id
)
217 -- iterate the notifications to get the notfications with the correct ID
218 for s
= 1, capi
.screen
.count() do
219 for p
,pos
in pairs(notifications
[s
]) do
220 for i
,notification
in pairs(notifications
[s
][p
]) do
221 if notification
.id
== id
then
229 -- Search for an icon in specified directories with a specified format
230 -- @param icon Name of the icon
231 -- @return full path of the icon, or nil of no icon was found
232 local function getIcon(name
)
233 for d
, dir
in pairs(config
.icon_dirs
) do
234 for f
, format in pairs(config
.icon_formats
) do
235 local icon
= dir
.. name
.. "." .. format
236 if util
.file_readable(icon
) then
243 --- Create notification. args is a dictionary of (optional) arguments.
244 -- @param text Text of the notification. Default: ''
245 -- @param title Title of the notification. Default: nil
246 -- @param timeout Time in seconds after which popup expires.
247 -- Set 0 for no timeout. Default: 5
248 -- @param hover_timeout Delay in seconds after which hovered popup disappears.
250 -- @param screen Target screen for the notification. Default: 1
251 -- @param position Corner of the workarea displaying the popups.
252 -- Values: "top_right" (default), "top_left", "bottom_left", "bottom_right".
253 -- @param ontop Boolean forcing popups to display on top. Default: true
254 -- @param height Popup height. Default: nil (auto)
255 -- @param width Popup width. Default: nil (auto)
256 -- @param font Notification font. Default: beautiful.font or awesome.font
257 -- @param icon Path to icon. Default: nil
258 -- @param icon_size Desired icon size in px. Default: nil
259 -- @param fg Foreground color. Default: beautiful.fg_focus or '#ffffff'
260 -- @param bg Background color. Default: beautiful.bg_focus or '#535d6c'
261 -- @param border_width Border width. Default: 1
262 -- @param border_color Border color.
263 -- Default: beautiful.border_focus or '#535d6c'
264 -- @param run Function to run on left click. Default: nil
265 -- @param preset Table with any of the above parameters. Note: Any parameters
266 -- specified directly in args will override ones defined in the preset.
267 -- @param replaces_id Replace the notification with the given ID
268 -- @param callback function that will be called with all arguments
269 -- the notification will only be displayed if the function returns true
270 -- note: this function is only relevant to notifications sent via dbus
271 -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
272 -- @return The notification object
273 function notify(args
)
274 -- gather variables together
275 local preset
= args
.preset
or config
.default_preset
or {}
276 local timeout
= args
.timeout
or preset
.timeout
or 5
277 local icon
= args
.icon
or preset
.icon
278 local icon_size
= args
.icon_size
or preset
.icon_size
279 local text
= args
.text
or preset
.text
or ""
280 local title
= args
.title
or preset
.title
281 local screen
= args
.screen
or preset
.screen
or 1
282 local ontop
= args
.ontop
or preset
.ontop
or true
283 local width
= args
.width
or preset
.width
284 local height
= args
.height
or preset
.height
285 local hover_timeout
= args
.hover_timeout
or preset
.hover_timeout
286 local opacity
= args
.opacity
or preset
.opacity
287 local margin
= args
.margin
or preset
.margin
or "5"
288 local border_width
= args
.border_width
or preset
.border_width
or "1"
289 local position
= args
.position
or preset
.position
or "top_right"
292 local beautiful
= bt
.get()
293 local font
= args
.font
or preset
.font
or beautiful
.font
or capi
.awesome
.font
294 local fg
= args
.fg
or preset
.fg
or beautiful
.fg_normal
or '#ffffff'
295 local bg
= args
.bg
or preset
.bg
or beautiful
.bg_normal
or '#535d6c'
296 local border_color
= args
.border_color
or preset
.border_color
or beautiful
.bg_focus
or '#535d6c'
297 local notification
= {}
299 -- replace notification if needed
300 if args
.replaces_id
then
301 local obj
= getById(args
.replaces_id
)
303 -- destroy this and ...
306 -- ... may use its ID
307 if args
.replaces_id
< counter
then
308 notification
.id
= args
.replaces_id
310 counter
= counter
+ 1
311 notification
.id
= counter
314 -- get a brand new ID
315 counter
= counter
+ 1
316 notification
.id
= counter
319 notification
.position
= position
321 if title
then title
= title
.. "\n" else title
= "" end
324 local die
= function () destroy(notification
) end
326 local timer_die
= capi
.timer
{ timeout
= timeout
}
327 timer_die
:connect_signal("timeout", die
)
328 if not suspended
then
331 notification
.timer
= timer_die
333 notification
.die
= die
335 local run
= function ()
337 args
.run(notification
)
343 local hover_destroy
= function ()
344 if hover_timeout
== 0 then
347 if notification
.timer
then notification
.timer
:stop() end
348 notification
.timer
= capi
.timer
{ timeout
= hover_timeout
}
349 notification
.timer
:connect_signal("timeout", die
)
350 notification
.timer
:start()
355 local textbox
= capi
.widget({ type = "textbox", align
= "flex" })
356 textbox
:buttons(util
.table.join(button({ }, 1, run
), button({ }, 3, die
)))
357 layout
.margins
[textbox
] = { right
= margin
, left
= margin
, bottom
= margin
, top
= margin
}
358 textbox
.valign
= "middle"
360 local function setText(pattern
, replacements
)
361 textbox
.text
= string.format('<span font_desc="%s"><b>%s</b>%s</span>', font
, title
, text
:gsub(pattern
, replacements
))
364 -- First try to set the text while only interpreting <br>.
365 -- (Setting a textbox' .text to an invalid pattern throws a lua error)
366 if not pcall(setText
, "<br.->", "\n") then
367 -- That failed, escape everything which might cause an error from pango
368 if not pcall(setText
, "[<>&]", { ['<'] = "<", ['>'] = ">", ['&'] = "&" }) then
369 textbox
.text
= "<i><Invalid markup, cannot display message></i>"
376 -- try to guess icon if the provided one is non-existent/readable
377 if type(icon
) == "string" and not util
.file_readable(icon
) then
381 -- if we have an icon, use it
383 iconbox
= capi
.widget({ type = "imagebox", align
= "left" })
384 layout
.margins
[iconbox
] = { right
= margin
, left
= margin
, bottom
= margin
, top
= margin
}
385 iconbox
:buttons(util
.table.join(button({ }, 1, run
), button({ }, 3, die
)))
387 if type(icon
) == "string" then
388 img
= capi
.oocairo
.image_surface_create_from_png(icon
)
393 img
= img
:crop_and_scale(0,0,img
.height
,img
.width
,icon_size
,icon_size
)
395 iconbox
.resize
= false
400 -- create container wibox
401 notification
.box
= capi
.wibox({ fg
= fg
,
403 border_color
= border_color
,
404 border_width
= border_width
,
405 type = "notification" })
407 if hover_timeout
then notification
.box
:connect_signal("mouse::enter", hover_destroy
) end
409 -- calculate the height
411 if iconbox
and iconbox
:extents().height
+ 2 * margin
> textbox
:extents().height
+ 2 * margin
then
412 height
= iconbox
:extents().height
+ 2 * margin
414 height
= textbox
:extents().height
+ 2 * margin
418 -- calculate the width
420 width
= textbox
:extents().width
+ (iconbox
and iconbox
:extents().width
+ 2 * margin
or 0) + 2 * margin
423 -- crop to workarea size if too big
424 local workarea
= capi
.screen
[screen
].workarea
425 if width
> workarea
.width
- 2 * (border_width
or 0) - 2 * (config
.padding
or 0) then
426 width
= workarea
.width
- 2 * (border_width
or 0) - 2 * (config
.padding
or 0)
428 if height
> workarea
.height
- 2 * (border_width
or 0) - 2 * (config
.padding
or 0) then
429 height
= workarea
.height
- 2 * (border_width
or 0) - 2 * (config
.padding
or 0)
432 -- set size in notification object
433 notification
.height
= height
+ 2 * (border_width
or 0)
434 notification
.width
= width
+ 2 * (border_width
or 0)
436 -- position the wibox
437 local offset
= get_offset(screen
, notification
.position
, nil, notification
.width
, notification
.height
)
438 notification
.box
.ontop
= ontop
439 notification
.box
:geometry({ width
= width
,
443 notification
.box
.opacity
= opacity
444 notification
.box
.screen
= screen
445 notification
.idx
= offset
.idx
449 notification
.box
.widgets
= { iconbox
, textbox
, ["layout"] = layout
.horizontal
.leftright
}
451 notification
.box
.widgets
= { textbox
, ["layout"] = layout
.horizontal
.leftright
}
454 -- insert the notification to the table
455 table.insert(notifications
[screen
][notification
.position
], notification
)
458 notification
.box
.visible
= false
459 table.insert(notifications
.suspended
, notification
)
462 -- return the notification
466 -- DBUS/Notification support
469 capi
.dbus
.connect_signal("org.freedesktop.Notifications", function (data
, appname
, replaces_id
, icon
, title
, text
, actions
, hints
, expire
)
470 args
= { preset
= { } }
471 if data
.member
== "Notify" then
485 for i
, obj
in pairs(config
.mapping
) do
486 local filter
, preset
, s
= obj
[1], obj
[2], 0
487 if (not filter
.urgency
or filter
.urgency
== hints
.urgency
) and
488 (not filter
.category
or filter
.category
== hints
.category
) and
489 (not filter
.appname
or filter
.appname
== appname
) then
490 for j
, el
in pairs(filter
) do s
= s
+ 1 end
497 if not args
.preset
.callback
or (type(args
.preset
.callback
) == "function" and
498 args
.preset
.callback(data
, appname
, replaces_id
, icon
, title
, text
, actions
, hints
, expire
)) then
501 elseif hints
.icon_data
then
502 -- icon_data is an array:
503 -- 1 -> width, 2 -> height, 3 -> rowstride, 4 -> has alpha
504 -- 5 -> bits per sample, 6 -> channels, 7 -> data
507 -- If has alpha (ARGB32)
508 if hints
.icon_data
[6] == 4 then
509 imgdata
= hints
.icon_data
[7]
510 -- If has not alpha (RGB24)
511 elseif hints
.icon_data
[6] == 3 then
513 for i
= 1, #hints
.icon_data
[7], 3 do
514 imgdata
= imgdata
.. hints
.icon_data
[7]:sub(i
, i
+ 2):reverse()
515 imgdata
= imgdata
.. string.format("%c", 255) -- alpha is 255
519 args
.icon
= capi
.oocairo
.image
.surface_create_from_data(imgdata
, "argb32", hints
.icon_data
[1], hints
.icon_data
[2], hints
.icon_data
[1] * 4)
522 if replaces_id
and replaces_id
~= "" and replaces_id
~= 0 then
523 args
.replaces_id
= replaces_id
525 if expire
and expire
> -1 then
526 args
.timeout
= expire
/ 1000
528 local id
= notify(args
).id
532 elseif data
.member
== "CloseNotification" then
533 local obj
= getById(appname
)
537 elseif data
.member
== "GetServerInfo" or data
.member
== "GetServerInformation" then
538 -- name of notification app, name of vender, version
539 return "s", "naughty", "s", "awesome", "s", capi
.awesome
.version
:match("%d.%d"), "s", "1.0"
540 elseif data
.member
== "GetCapabilities" then
541 -- We actually do display the body of the message, we support <b>, <i>
542 -- and <u> in the body and we handle static (non-animated) icons.
543 return "as", { "s", "body", "s", "body-markup", "s", "icon-static" }
547 capi
.dbus
.connect_signal("org.freedesktop.DBus.Introspectable",
548 function (data
, text
)
549 if data
.member
== "Introspect" then
550 local xml
= [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
551 Introspection 1.0//EN"
552 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
554 <interface name="org.freedesktop.DBus.Introspectable">
555 <method name="Introspect">
556 <arg name="data" direction="out" type="s"/>
559 <interface name="org.freedesktop.Notifications">
560 <method name="GetCapabilities">
561 <arg name="caps" type="as" direction="out"/>
563 <method name="CloseNotification">
564 <arg name="id" type="u" direction="in"/>
566 <method name="Notify">
567 <arg name="app_name" type="s" direction="in"/>
568 <arg name="id" type="u" direction="in"/>
569 <arg name="icon" type="s" direction="in"/>
570 <arg name="summary" type="s" direction="in"/>
571 <arg name="body" type="s" direction="in"/>
572 <arg name="actions" type="as" direction="in"/>
573 <arg name="hints" type="a{sv}" direction="in"/>
574 <arg name="timeout" type="i" direction="in"/>
575 <arg name="return_id" type="u" direction="out"/>
577 <method name="GetServerInformation">
578 <arg name="return_name" type="s" direction="out"/>
579 <arg name="return_vendor" type="s" direction="out"/>
580 <arg name="return_version" type="s" direction="out"/>
581 <arg name="return_spec_version" type="s" direction="out"/>
583 <method name="GetServerInfo">
584 <arg name="return_name" type="s" direction="out"/>
585 <arg name="return_vendor" type="s" direction="out"/>
586 <arg name="return_version" type="s" direction="out"/>
594 -- listen for dbus notification requests
595 capi
.dbus
.request_name("session", "org.freedesktop.Notifications")
598 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80