change codename
[awesome.git] / lib / naughty.lua.in
blobf9658cbbdae49f7466cee7615113d66fabfe58f8
1 ----------------------------------------------------------------------------
2 -- @author koniu <gkusnierz@gmail.com>
3 -- @copyright 2008 koniu
4 -- @release @AWESOME_VERSION@
5 ----------------------------------------------------------------------------
7 -- Package environment
8 local pairs = pairs
9 local table = table
10 local type = type
11 local string = string
12 local pcall = pcall
13 local capi = { screen = screen,
14 awesome = awesome,
15 dbus = dbus,
16 widget = widget,
17 wibox = wibox,
18 image = image,
19 timer = timer }
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
26 module("naughty")
28 --- Naughty configuration - a table containing common popup settings.
29 -- @name config
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
38 -- @class table
40 config = {}
41 config.padding = 4
42 config.spacing = 1
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
56 -- @class table
58 config.presets = {
59 normal = {},
60 low = {
61 timeout = 5
63 critical = {
64 bg = "#ff0000",
65 fg = "#ffffff",
66 timeout = 0,
70 config.default_preset = config.presets.normal
72 -- DBUS Notification constants
73 urgency = {
74 low = "\0",
75 normal = "\1",
76 critical = "\2"
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
86 config.mapping = {
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
94 local counter = 1
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]
107 -- @class table
109 notifications = { suspended = { } }
110 for s = 1, capi.screen.count() do
111 notifications[s] = {
112 top_left = {},
113 top_right = {},
114 bottom_left = {},
115 bottom_right = {},
119 --- Suspend notifications
120 function suspend()
121 suspended = true
124 --- Resume notifications
125 function resume()
126 suspended = false
127 for i, v in pairs(notifications.suspended) do
128 v.box.visible = true
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
142 local v = {}
143 local idx = idx or #notifications[screen][position] + 1
144 local width = width or notifications[screen][position][idx].width
146 -- calculate x
147 if position:match("left") then
148 v.x = ws.x + config.padding
149 else
150 v.x = ws.x + ws.width - (width + config.padding)
153 -- calculate existing popups' height
154 local existing = 0
155 for i = 1, idx-1, 1 do
156 existing = existing + notifications[screen][position][i].height + config.spacing
159 -- calculate y
160 if position:match("top") then
161 v.y = ws.y + config.padding + existing
162 else
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
168 idx = idx - 1
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
174 return v
177 -- Re-arrange notifications according to their position and index - internal
178 -- @return None
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
194 if suspended then
195 for k, v in pairs(notifications.suspended) do
196 if v.box == notification.box then
197 table.remove(notifications.suspended, k)
198 break
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
208 arrange(scr)
209 return true
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
222 return notification
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
237 return icon
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.
249 -- Default: nil
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"
291 -- beautiful
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)
302 if obj then
303 -- destroy this and ...
304 destroy(obj)
306 -- ... may use its ID
307 if args.replaces_id < counter then
308 notification.id = args.replaces_id
309 else
310 counter = counter + 1
311 notification.id = counter
313 else
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
323 -- hook destroy
324 local die = function () destroy(notification) end
325 if timeout > 0 then
326 local timer_die = capi.timer { timeout = timeout }
327 timer_die:add_signal("timeout", die)
328 if not suspended then
329 timer_die:start()
331 notification.timer = timer_die
333 notification.die = die
335 local run = function ()
336 if args.run then
337 args.run(notification)
338 else
339 die()
343 local hover_destroy = function ()
344 if hover_timeout == 0 then
345 die()
346 else
347 if notification.timer then notification.timer:stop() end
348 notification.timer = capi.timer { timeout = hover_timeout }
349 notification.timer:add_signal("timeout", die)
350 notification.timer:start()
354 -- create textbox
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, "[<>&]", { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }) then
369 textbox.text = "<i>&lt;Invalid markup, cannot display message&gt;</i>"
373 -- create iconbox
374 local iconbox = nil
375 if icon then
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
378 icon = getIcon(icon)
381 -- if we have an icon, use it
382 if icon then
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)))
386 local img
387 if type(icon) == "string" then
388 img = capi.image(icon)
389 else
390 img = icon
392 if icon_size then
393 img = img:crop_and_scale(0,0,img.height,img.width,icon_size,icon_size)
395 iconbox.resize = false
396 iconbox.image = img
400 -- create container wibox
401 notification.box = capi.wibox({ fg = fg,
402 bg = bg,
403 border_color = border_color,
404 border_width = border_width })
406 if hover_timeout then notification.box:add_signal("mouse::enter", hover_destroy) end
408 -- calculate the height
409 if not height then
410 if iconbox and iconbox:extents().height + 2 * margin > textbox:extents().height + 2 * margin then
411 height = iconbox:extents().height + 2 * margin
412 else
413 height = textbox:extents().height + 2 * margin
417 -- calculate the width
418 if not width then
419 width = textbox:extents().width + (iconbox and iconbox:extents().width + 2 * margin or 0) + 2 * margin
422 -- crop to workarea size if too big
423 local workarea = capi.screen[screen].workarea
424 if width > workarea.width - 2 * (border_width or 0) - 2 * (config.padding or 0) then
425 width = workarea.width - 2 * (border_width or 0) - 2 * (config.padding or 0)
427 if height > workarea.height - 2 * (border_width or 0) - 2 * (config.padding or 0) then
428 height = workarea.height - 2 * (border_width or 0) - 2 * (config.padding or 0)
431 -- set size in notification object
432 notification.height = height + 2 * (border_width or 0)
433 notification.width = width + 2 * (border_width or 0)
435 -- position the wibox
436 local offset = get_offset(screen, notification.position, nil, notification.width, notification.height)
437 notification.box.ontop = ontop
438 notification.box:geometry({ width = width,
439 height = height,
440 x = offset.x,
441 y = offset.y })
442 notification.box.opacity = opacity
443 notification.box.screen = screen
444 notification.idx = offset.idx
446 -- populate widgets
447 notification.box.widgets = { iconbox, textbox, ["layout"] = layout.horizontal.leftright }
449 -- insert the notification to the table
450 table.insert(notifications[screen][notification.position], notification)
452 if suspended then
453 notification.box.visible = false
454 table.insert(notifications.suspended, notification)
457 -- return the notification
458 return notification
461 -- DBUS/Notification support
462 -- Notify
463 if capi.dbus then
464 capi.dbus.add_signal("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire)
465 args = { preset = { } }
466 if data.member == "Notify" then
467 if text ~= "" then
468 args.text = text
469 if title ~= "" then
470 args.title = title
472 else
473 if title ~= "" then
474 args.text = title
475 else
476 return
479 local score = 0
480 for i, obj in pairs(config.mapping) do
481 local filter, preset, s = obj[1], obj[2], 0
482 if (not filter.urgency or filter.urgency == hints.urgency) and
483 (not filter.category or filter.category == hints.category) and
484 (not filter.appname or filter.appname == appname) then
485 for j, el in pairs(filter) do s = s + 1 end
486 if s > score then
487 score = s
488 args.preset = preset
492 if not args.preset.callback or (type(args.preset.callback) == "function" and
493 args.preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
494 if icon ~= "" then
495 args.icon = icon
496 elseif hints.icon_data or hints.image_data then
497 if hints.icon_data == nil then hints.icon_data = hints.image_data end
498 -- icon_data is an array:
499 -- 1 -> width, 2 -> height, 3 -> rowstride, 4 -> has alpha
500 -- 5 -> bits per sample, 6 -> channels, 7 -> data
502 local imgdata
503 -- If has alpha (ARGB32)
504 if hints.icon_data[6] == 4 then
505 imgdata = hints.icon_data[7]
506 -- If has not alpha (RGB24)
507 elseif hints.icon_data[6] == 3 then
508 imgdata = ""
509 for i = 1, #hints.icon_data[7], 3 do
510 imgdata = imgdata .. hints.icon_data[7]:sub(i , i + 2):reverse()
511 imgdata = imgdata .. string.format("%c", 255) -- alpha is 255
514 if imgdata then
515 args.icon = capi.image.argb32(hints.icon_data[1], hints.icon_data[2], imgdata)
518 if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
519 args.replaces_id = replaces_id
521 if expire and expire > -1 then
522 args.timeout = expire / 1000
524 local id = notify(args).id
525 return "u", id
527 return "u", "0"
528 elseif data.member == "CloseNotification" then
529 local obj = getById(appname)
530 if obj then
531 destroy(obj)
533 elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
534 -- name of notification app, name of vender, version
535 return "s", "naughty", "s", "awesome", "s", capi.awesome.version:match("%d.%d"), "s", "1.0"
536 elseif data.member == "GetCapabilities" then
537 -- We actually do display the body of the message, we support <b>, <i>
538 -- and <u> in the body and we handle static (non-animated) icons.
539 return "as", { "s", "body", "s", "body-markup", "s", "icon-static" }
541 end)
543 capi.dbus.add_signal("org.freedesktop.DBus.Introspectable",
544 function (data, text)
545 if data.member == "Introspect" then
546 local xml = [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
547 Introspection 1.0//EN"
548 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
549 <node>
550 <interface name="org.freedesktop.DBus.Introspectable">
551 <method name="Introspect">
552 <arg name="data" direction="out" type="s"/>
553 </method>
554 </interface>
555 <interface name="org.freedesktop.Notifications">
556 <method name="GetCapabilities">
557 <arg name="caps" type="as" direction="out"/>
558 </method>
559 <method name="CloseNotification">
560 <arg name="id" type="u" direction="in"/>
561 </method>
562 <method name="Notify">
563 <arg name="app_name" type="s" direction="in"/>
564 <arg name="id" type="u" direction="in"/>
565 <arg name="icon" type="s" direction="in"/>
566 <arg name="summary" type="s" direction="in"/>
567 <arg name="body" type="s" direction="in"/>
568 <arg name="actions" type="as" direction="in"/>
569 <arg name="hints" type="a{sv}" direction="in"/>
570 <arg name="timeout" type="i" direction="in"/>
571 <arg name="return_id" type="u" direction="out"/>
572 </method>
573 <method name="GetServerInformation">
574 <arg name="return_name" type="s" direction="out"/>
575 <arg name="return_vendor" type="s" direction="out"/>
576 <arg name="return_version" type="s" direction="out"/>
577 <arg name="return_spec_version" type="s" direction="out"/>
578 </method>
579 <method name="GetServerInfo">
580 <arg name="return_name" type="s" direction="out"/>
581 <arg name="return_vendor" type="s" direction="out"/>
582 <arg name="return_version" type="s" direction="out"/>
583 </method>
584 </interface>
585 </node>]=]
586 return "s", xml
588 end)
590 -- listen for dbus notification requests
591 capi.dbus.request_name("session", "org.freedesktop.Notifications")
594 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80