Fix awful.util.table.reverse
[awesome.git] / lib / naughty.lua.in
blob2c5bda895ea1c689a9ce607ec59f65a6dc27c311
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 capi = { screen = screen,
13 awesome = awesome,
14 dbus = dbus,
15 widget = widget,
16 wibox = wibox,
17 image = image,
18 timer = timer }
19 local button = require("awful.button")
20 local util = require("awful.util")
21 local bt = require("beautiful")
22 local layout = require("awful.widget.layout")
24 --- Notification library
25 module("naughty")
27 --- Naughty configuration - a table containing common popup settings.
28 -- @name config
29 -- @field padding Space between popups and edge of the workarea. Default: 4
30 -- @field spacing Spacing between popups. Default: 1
31 -- @field icon_dirs List of directories that will be checked by getIcon()
32 -- Default: { "/usr/share/pixmaps/", }
33 -- @field icon_formats List of formats that will be checked by getIcon()
34 -- Default: { "png", "gif" }
35 -- @field default_preset Preset to be used by default.
36 -- Default: config.presets.normal
37 -- @class table
39 config = {}
40 config.padding = 4
41 config.spacing = 1
42 config.icon_dirs = { "/usr/share/pixmaps/", }
43 config.icon_formats = { "png", "gif" }
46 --- Notification Presets - a table containing presets for different purposes
47 -- Preset is a table of any parameters available to notify()
48 -- You have to pass a reference of a preset in your notify() call to use the preset
49 -- At least the default preset named "normal" has to be defined
50 -- The presets "low", "normal" and "critical" are used for notifications over DBUS
51 -- @name config.presets
52 -- @field low The preset for notifications with low urgency level
53 -- @field normal The default preset for every notification without a preset that will also be used for normal urgency level
54 -- @field critical The preset for notifications with a critical urgency level
55 -- @class table
57 config.presets = {
58 normal = {},
59 low = {
60 timeout = 5
62 critical = {
63 bg = "#ff0000",
64 fg = "#ffffff",
65 timeout = 0,
69 config.default_preset = config.presets.normal
71 -- DBUS Notification constants
72 urgency = {
73 low = "\0",
74 normal = "\1",
75 critical = "\2"
78 --- DBUS notification to preset mapping
79 -- @name config.mapping
80 -- The first element is an object containing the filter
81 -- If the rules in the filter matches the associated preset will be applied
82 -- The rules object can contain: urgency, category, appname
83 -- The second element is the preset
85 config.mapping = {
86 {{urgency = urgency.low}, config.presets.low},
87 {{urgency = urgency.normal}, config.presets.normal},
88 {{urgency = urgency.critical}, config.presets.critical}
91 -- Counter for the notifications
92 -- Required for later access via DBUS
93 local counter = 1
95 --- Index of notifications. See config table for valid 'position' values.
96 -- Each element is a table consisting of:
97 -- @field box Wibox object containing the popup
98 -- @field height Popup height
99 -- @field width Popup width
100 -- @field die Function to be executed on timeout
101 -- @field id Unique notification id based on a counter
102 -- @name notifications[screen][position]
103 -- @class table
105 notifications = {}
106 for s = 1, capi.screen.count() do
107 notifications[s] = {
108 top_left = {},
109 top_right = {},
110 bottom_left = {},
111 bottom_right = {},
115 -- Evaluate desired position of the notification by index - internal
116 -- @param idx Index of the notification
117 -- @param position top_right | top_left | bottom_right | bottom_left
118 -- @param height Popup height
119 -- @param width Popup width (optional)
120 -- @return Absolute position and index in { x = X, y = Y, idx = I } table
121 local function get_offset(screen, position, idx, width, height)
122 local ws = capi.screen[screen].workarea
123 local v = {}
124 local idx = idx or #notifications[screen][position] + 1
125 local width = width or notifications[screen][position][idx].width
127 -- calculate x
128 if position:match("left") then
129 v.x = ws.x + config.padding
130 else
131 v.x = ws.x + ws.width - (width + config.padding)
134 -- calculate existing popups' height
135 local existing = 0
136 for i = 1, idx-1, 1 do
137 existing = existing + notifications[screen][position][i].height + config.spacing
140 -- calculate y
141 if position:match("top") then
142 v.y = ws.y + config.padding + existing
143 else
144 v.y = ws.y + ws.height - (config.padding + height + existing)
147 -- if positioned outside workarea, destroy oldest popup and recalculate
148 if v.y + height > ws.y + ws.height or v.y < ws.y then
149 idx = idx - 1
150 destroy(notifications[screen][position][1])
151 v = get_offset(screen, position, idx, width, height)
153 if not v.idx then v.idx = idx end
155 return v
158 -- Re-arrange notifications according to their position and index - internal
159 -- @return None
160 local function arrange(screen)
161 for p,pos in pairs(notifications[screen]) do
162 for i,notification in pairs(notifications[screen][p]) do
163 local offset = get_offset(screen, p, i, notification.width, notification.height)
164 notification.box:geometry({ x = offset.x, y = offset.y })
165 notification.idx = offset.idx
170 --- Destroy notification by index
171 -- @param notification Notification object to be destroyed
172 -- @return True if the popup was successfully destroyed, nil otherwise
173 function destroy(notification)
174 if notification and notification.box.screen then
175 local scr = notification.box.screen
176 table.remove(notifications[notification.box.screen][notification.position], notification.idx)
177 if notification.timer then
178 notification.timer:stop()
180 notification.box.screen = nil
181 arrange(scr)
182 return true
186 -- Get notification by ID
187 -- @param id ID of the notification
188 -- @return notification object if it was found, nil otherwise
189 local function getById(id)
190 -- iterate the notifications to get the notfications with the correct ID
191 for s = 1, capi.screen.count() do
192 for p,pos in pairs(notifications[s]) do
193 for i,notification in pairs(notifications[s][p]) do
194 if notification.id == id then
195 return notification
202 -- Search for an icon in specified directories with a specified format
203 -- @param icon Name of the icon
204 -- @return full path of the icon, or nil of no icon was found
205 local function getIcon(name)
206 for d, dir in pairs(config.icon_dirs) do
207 for f, format in pairs(config.icon_formats) do
208 local icon = dir .. name .. "." .. format
209 if util.file_readable(icon) then
210 return icon
216 --- Create notification. args is a dictionary of (optional) arguments.
217 -- @param text Text of the notification. Default: ''
218 -- @param title Title of the notification. Default: nil
219 -- @param timeout Time in seconds after which popup expires.
220 -- Set 0 for no timeout. Default: 5
221 -- @param hover_timeout Delay in seconds after which hovered popup disappears.
222 -- Default: nil
223 -- @param screen Target screen for the notification. Default: 1
224 -- @param position Corner of the workarea displaying the popups.
225 -- Values: "top_right" (default), "top_left", "bottom_left", "bottom_right".
226 -- @param ontop Boolean forcing popups to display on top. Default: true
227 -- @param height Popup height. Default: nil (auto)
228 -- @param width Popup width. Default: nil (auto)
229 -- @param font Notification font. Default: beautiful.font or awesome.font
230 -- @param icon Path to icon. Default: nil
231 -- @param icon_size Desired icon size in px. Default: nil
232 -- @param fg Foreground color. Default: beautiful.fg_focus or '#ffffff'
233 -- @param bg Background color. Default: beautiful.bg_focus or '#535d6c'
234 -- @param border_width Border width. Default: 1
235 -- @param border_color Border color.
236 -- Default: beautiful.border_focus or '#535d6c'
237 -- @param run Function to run on left click. Default: nil
238 -- @param preset Table with any of the above parameters. Note: Any parameters
239 -- specified directly in args will override ones defined in the preset.
240 -- @param replaces_id Replace the notification with the given ID
241 -- @param callback function that will be called with all arguments
242 -- the notification will only be displayed if the function returns true
243 -- note: this function is only relevant to notifications sent via dbus
244 -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
245 -- @return The notification object
246 function notify(args)
247 -- gather variables together
248 local preset = args.preset or config.default_preset or {}
249 local timeout = args.timeout or preset.timeout or 5
250 local icon = args.icon or preset.icon
251 local icon_size = args.icon_size or preset.icon_size
252 local text = args.text or preset.text or ""
253 local title = args.title or preset.title
254 local screen = args.screen or preset.screen or 1
255 local ontop = args.ontop or preset.ontop or true
256 local width = args.width or preset.width
257 local height = args.height or preset.height
258 local hover_timeout = args.hover_timeout or preset.hover_timeout
259 local opacity = args.opacity or preset.opacity
260 local margin = args.margin or preset.margin or "5"
261 local border_width = args.border_width or preset.border_width or "1"
262 local position = args.position or preset.position or "top_right"
264 -- beautiful
265 local beautiful = bt.get()
266 local font = args.font or preset.font or beautiful.font or capi.awesome.font
267 local fg = args.fg or preset.fg or beautiful.fg_normal or '#ffffff'
268 local bg = args.bg or preset.bg or beautiful.bg_normal or '#535d6c'
269 local border_color = args.border_color or preset.border_color or beautiful.bg_focus or '#535d6c'
270 local notification = {}
272 -- replace notification if needed
273 if args.replaces_id then
274 obj = getById(args.replaces_id)
275 if obj then
276 -- destroy this and ...
277 destroy(obj)
279 -- ... may use its ID
280 if args.replaces_id < counter then
281 notification.id = args.replaces_id
282 else
283 counter = counter + 1
284 notification.id = counter
286 else
287 -- get a brand new ID
288 counter = counter + 1
289 notification.id = counter
292 notification.position = position
294 if title then title = title .. "\n" else title = "" end
296 -- hook destroy
297 local die = function () destroy(notification) end
298 if timeout > 0 then
299 local timer_die = capi.timer { timeout = timeout }
300 timer_die:add_signal("timeout", die)
301 timer_die:start()
302 notification.timer = timer_die
304 notification.die = die
306 local run = function ()
307 if args.run then
308 args.run(notification)
309 else
310 die()
314 local hover_destroy = function ()
315 if hover_timeout == 0 then
316 die()
317 else
318 if notification.timer then notification.timer:stop() end
319 notification.timer = capi.timer { timeout = hover_timeout }
320 notification.timer:add_signal("timeout", die)
321 notification.timer:start()
325 -- create textbox
326 local textbox = capi.widget({ type = "textbox", align = "flex" })
327 textbox:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
328 layout.margins[textbox] = { right = margin, left = margin, bottom = margin, top = margin }
329 textbox.text = string.format('<span font_desc="%s"><b>%s</b>%s</span>', font, title, text:gsub("<br.->", "\n"))
330 textbox.valign = "middle"
332 -- create iconbox
333 local iconbox = nil
334 if icon then
335 -- try to guess icon if the provided one is non-existent/readable
336 if type(icon) == "string" and not util.file_readable(icon) then
337 icon = getIcon(icon)
340 -- if we have an icon, use it
341 if icon then
342 iconbox = capi.widget({ type = "imagebox", align = "left" })
343 layout.margins[iconbox] = { right = margin, left = margin, bottom = margin, top = margin }
344 iconbox:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
345 local img
346 if type(icon) == "string" then
347 img = capi.image(icon)
348 else
349 img = icon
351 if icon_size then
352 img = img:crop_and_scale(0,0,img.height,img.width,icon_size,icon_size)
354 iconbox.resize = false
355 iconbox.image = img
359 -- create container wibox
360 notification.box = capi.wibox({ fg = fg,
361 bg = bg,
362 border_color = border_color,
363 border_width = border_width })
365 if hover_timeout then notification.box:add_signal("mouse::enter", hover_destroy) end
367 -- calculate the height
368 if not height then
369 if iconbox and iconbox:extents().height + 2 * margin > textbox:extents().height + 2 * margin then
370 height = iconbox:extents().height + 2 * margin
371 else
372 height = textbox:extents().height + 2 * margin
376 -- calculate the width
377 if not width then
378 width = textbox:extents().width + (iconbox and iconbox:extents().width + 2 * margin or 0) + 2 * margin
381 -- crop to workarea size if too big
382 local workarea = capi.screen[screen].workarea
383 if width > workarea.width - 2 * (border_width or 0) - 2 * (config.padding or 0) then
384 width = workarea.width - 2 * (border_width or 0) - 2 * (config.padding or 0)
386 if height > workarea.height - 2 * (border_width or 0) - 2 * (config.padding or 0) then
387 height = workarea.height - 2 * (border_width or 0) - 2 * (config.padding or 0)
390 -- set size in notification object
391 notification.height = height + 2 * (border_width or 0)
392 notification.width = width + 2 * (border_width or 0)
394 -- position the wibox
395 local offset = get_offset(screen, notification.position, nil, notification.width, notification.height)
396 notification.box.ontop = ontop
397 notification.box:geometry({ width = width,
398 height = height,
399 x = offset.x,
400 y = offset.y })
401 notification.box.opacity = opacity
402 notification.box.screen = screen
403 notification.idx = offset.idx
405 -- populate widgets
406 notification.box.widgets = { iconbox, textbox, ["layout"] = layout.horizontal.leftright }
408 -- insert the notification to the table
409 table.insert(notifications[screen][notification.position], notification)
411 -- return the notification
412 return notification
415 -- DBUS/Notification support
416 -- Notify
417 if capi.dbus then
418 capi.dbus.add_signal("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire)
419 args = { preset = { } }
420 if data.member == "Notify" then
421 if text ~= "" then
422 args.text = text
423 if title ~= "" then
424 args.title = title
426 else
427 if title ~= "" then
428 args.text = title
429 else
430 return nil
433 local score = 0
434 for i, obj in pairs(config.mapping) do
435 local filter, preset, s = obj[1], obj[2], 0
436 if (not filter.urgency or filter.urgency == hints.urgency) and
437 (not filter.category or filter.category == hints.category) and
438 (not filter.appname or filter.appname == appname) then
439 for j, el in pairs(filter) do s = s + 1 end
440 if s > score then
441 score = s
442 args.preset = preset
446 if not args.preset.callback or (type(args.preset.callback) == "function" and
447 args.preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
448 if icon ~= "" then
449 args.icon = icon
450 elseif hints.icon_data then
451 -- icon_data is an array:
452 -- 1 -> width, 2 -> height, 3 -> rowstride, 4 -> has alpha
453 -- 5 -> bits per sample, 6 -> channels, 7 -> data
455 local imgdata
456 -- If has alpha (ARGB32)
457 if hints.icon_data[6] == 4 then
458 imgdata = hints.icon_data[7]
459 -- If has not alpha (RGB24)
460 elseif hints.icon_data[6] == 3 then
461 imgdata = ""
462 for i = 1, #hints.icon_data[7], 3 do
463 imgdata = imgdata .. hints.icon_data[7]:sub(i , i + 2):reverse()
464 imgdata = imgdata .. string.format("%c", 255) -- alpha is 255
467 if imgdata then
468 args.icon = capi.image.argb32(hints.icon_data[1], hints.icon_data[2], imgdata)
471 if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
472 args.replaces_id = replaces_id
474 if expire and expire > -1 then
475 args.timeout = expire / 1000
477 local id = notify(args).id
478 return "u", id
480 return "u", "0"
481 elseif data.member == "CloseNotification" then
482 local obj = getById(arg1)
483 if obj then
484 destroy(obj)
486 elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
487 -- name of notification app, name of vender, version
488 return "s", "naughty", "s", "awesome", "s", capi.awesome.version:match("%d.%d"), "s", "1.0"
490 end)
492 capi.dbus.add_signal("org.freedesktop.DBus.Introspectable",
493 function (data, text)
494 if data.member == "Introspect" then
495 local xml = [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
496 Introspection 1.0//EN"
497 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
498 <node>
499 <interface name="org.freedesktop.DBus.Introspectable">
500 <method name="Introspect">
501 <arg name="data" direction="out" type="s"/>
502 </method>
503 </interface>
504 <interface name="org.freedesktop.Notifications">
505 <method name="CloseNotification">
506 <arg name="id" type="u" direction="in"/>
507 </method>
508 <method name="Notify">
509 <arg name="app_name" type="s" direction="in"/>
510 <arg name="id" type="u" direction="in"/>
511 <arg name="icon" type="s" direction="in"/>
512 <arg name="summary" type="s" direction="in"/>
513 <arg name="body" type="s" direction="in"/>
514 <arg name="actions" type="as" direction="in"/>
515 <arg name="hints" type="a{sv}" direction="in"/>
516 <arg name="timeout" type="i" direction="in"/>
517 <arg name="return_id" type="u" direction="out"/>
518 </method>
519 <method name="GetServerInformation">
520 <arg name="return_name" type="s" direction="out"/>
521 <arg name="return_vendor" type="s" direction="out"/>
522 <arg name="return_version" type="s" direction="out"/>
523 <arg name="return_spec_version" type="s" direction="out"/>
524 </method>
525 <method name="GetServerInfo">
526 <arg name="return_name" type="s" direction="out"/>
527 <arg name="return_vendor" type="s" direction="out"/>
528 <arg name="return_version" type="s" direction="out"/>
529 </method>
530 </interface>
531 </node>]=]
532 return "s", xml
534 end)
536 -- listen for dbus notification requests
537 capi.dbus.request_name("session", "org.freedesktop.Notifications")
540 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80