naughty: add vertical margin
[awesome.git] / lib / naughty.lua.in
bloba8d48e3e9c5b90d29a11c4e1a72f63eb15b32d54
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 wibox = wibox
11 local image = image
12 local type = type
13 local tostring = tostring
14 local hooks = require("awful.hooks")
15 local string = string
16 local widget = widget
17 local button = require("awful.button")
18 local util = require("awful.util")
19 local capi = { screen = screen, awesome = awesome }
20 local bt = require("beautiful")
21 local screen = screen
22 local awful = awful
23 local dbus = dbus
25 --- Notification library
26 module("naughty")
28 --- Naughty configuration - a table containing common/default popup settings.
29 -- You can override some of these for individual popups using args to notify().
30 -- @name config
31 -- @field screen Screen on which the popups will appear number. Default: 1
32 -- @field position Corner of the workarea the popups will appear.
33 -- Valid values: 'top_right', 'top_left', 'bottom_right', 'bottom_left'.
34 -- Default: 'top_right'
35 -- @field padding Space between popups and edge of the workarea. Default: 4
36 -- @field width Width of a popup. Default: 300
37 -- @field spacing Spacing between popups. Default: 1
38 -- @field ontop Boolean forcing popups to display on top. Default: true
39 -- @field margin Space between popup edge and content. Default: 10
40 -- @field icon_dirs List of directories that will be checked by getIcon()
41 -- Default: { "/usr/share/pixmaps/", }
42 -- @field icon_formats List of formats that will be checked by getIcon()
43 -- Default: { "png", "gif" }
44 -- @field border_width Border width. Default: 1
45 -- @class table
47 config = {}
48 config.padding = 4
49 config.spacing = 1
50 config.margin = 10
51 config.ontop = true
52 config.icon_dirs = { "/usr/share/pixmaps/", }
53 config.icon_formats = { "png", "gif" }
55 config.border_width = 1
57 --- Notification Presets - a table containing presets for different purposes
58 -- You have to pass a reference of a preset in your notify() call to use the preset
59 -- At least the default preset named "normal" has to be defined
60 -- The presets "low", "normal" and "critical" are used for notifications over DBUS
61 -- @name config.presets
62 -- @field low The preset for notifications with low urgency level
63 -- @field normal The default preset for every notification without a preset that will also be used for normal urgency level
64 -- @field critical The preset for notifications with a critical urgency level
65 -- @class table
67 --- Default preset for notifications
68 -- @name config.presets.normal
69 -- @field timeout Number of seconds after which popups disappear.
70 -- Set to 0 for no timeout. Default: 5
71 -- @field hover_timeout Delay in seconds after which hovered popup disappears.
72 -- Default: nil
73 -- @field border_color Border color.
74 -- Default: beautiful.border_focus or '#535d6c'
75 -- @field fg Foreground color. Default: beautiful.fg_focus or '#ffffff'
76 -- @field bg Background color. Default: beautiful.bg_focus or '#535d6c'
77 -- @field font Popup font. Default: beautiful.font or "Verdana 8"
78 -- @field height Height of a single line of text. Default: 16
79 -- @field icon Popup icon. Default: nil
80 -- @field icon_size Size of the icon in pixels. Default: nil
81 -- @field callback function that will be called with all arguments
82 -- the notification will only be displayed if the function returns true
83 -- note: this function is only relevant to notifications sent via dbus
85 config.presets = {
86 low = {
87 timeout = 5
89 normal = {
90 timeout = 8,
91 hover_timeout = nil,
92 position = "top_right",
93 screen = 1,
94 width = nil,
95 height = nil,
96 icon = nil,
97 icon_size = nil
99 critical = {
100 bg = "#ff0000",
101 fg = "#ffffff",
102 timeout = 0,
103 height = 25
107 -- DBUS Notification constants
108 urgency = {
109 low = "\0",
110 normal = "\1",
111 critical = "\2"
114 --- DBUS notification to preset mapping
115 -- @name config.mapping
116 -- The first element is an object containing the filter
117 -- If the rules in the filter matches the associated preset will be applied
118 -- The rules object can contain: urgency, category, appname
119 -- The second element is the preset
121 config.mapping = {
122 {{urgency = urgency.low}, config.presets.low},
123 {{urgency = urgency.normal}, config.presets.normal},
124 {{urgency = urgency.critical}, config.presets.critical}
127 -- Counter for the notifications
128 -- Required for later access via DBUS
129 local counter = 1
131 --- Index of notifications. See config table for valid 'position' values.
132 -- Each element is a table consisting of:
133 -- @field box Wibox object containing the popup
134 -- @field height Popup height
135 -- @field width Popup width
136 -- @field die Function to be executed on timeout
137 -- @field id Unique notification id based on a counter
138 -- @name notifications[position]
139 -- @class table
141 notifications = {}
142 for s = 1, screen.count() do
143 notifications[s] = {
144 top_left = {},
145 top_right = {},
146 bottom_left = {},
147 bottom_right = {},
151 -- Evaluate desired position of the notification by index - internal
152 -- @param idx Index of the notification
153 -- @param position top_right | top_left | bottom_right | bottom_left
154 -- @param height Popup height
155 -- @param width Popup width (optional)
156 -- @return Absolute position and index in { x = X, y = Y, idx = I } table
157 local function get_offset(screen, position, idx, width, height)
158 local ws = capi.screen[screen].workarea
159 local v = {}
160 width = width or notifications[screen][position][idx].width or config.width
161 local idx = idx or #notifications[screen][position] + 1
163 -- calculate x
164 if position:match("left") then
165 v.x = ws.x + config.padding
166 else
167 v.x = ws.x + ws.width - (width + config.padding)
170 -- calculate existing popups' height
171 local existing = 0
172 for i = 1, idx-1, 1 do
173 existing = existing + notifications[screen][position][i].height + config.spacing
176 -- calculate y
177 if position:match("top") then
178 v.y = ws.y + config.padding + existing
179 else
180 v.y = ws.y + ws.height - (config.padding + height + existing)
183 -- if positioned outside workarea, destroy oldest popup and recalculate
184 if v.y + height > ws.y + ws.height or v.y < ws.y then
185 idx = idx - 1
186 destroy(notifications[screen][position][1])
187 v = get_offset(screen, position, idx, width, height)
189 if not v.idx then v.idx = idx end
191 return v
194 -- Re-arrange notifications according to their position and index - internal
195 -- @return None
196 local function arrange(screen)
197 for p,pos in pairs(notifications[screen]) do
198 for i,notification in pairs(notifications[screen][p]) do
199 local offset = get_offset(screen, p, i, notification.width, notification.height)
200 notification.box:geometry({ x = offset.x, y = offset.y, width = notification.width, height = notification.height })
201 notification.idx = offset.idx
206 --- Destroy notification by index
207 -- @param notification Notification object to be destroyed
208 -- @return True if the popup was successfully destroyed, nil otherwise
209 function destroy(notification)
210 if notification and notification.box.screen then
211 local scr = notification.box.screen
212 table.remove(notifications[notification.box.screen][notification.position], notification.idx)
213 hooks.timer.unregister(notification.die)
214 notification.box.screen = nil
215 arrange(scr)
216 return true
220 -- Get notification by ID
221 -- @param id ID of the notification
222 -- @return notification object if it was found, nil otherwise
223 local function getById(id)
224 -- iterate the notifications to get the notfications with the correct ID
225 for s = 1, screen.count() do
226 for p,pos in pairs(notifications[s]) do
227 for i,notification in pairs(notifications[s][p]) do
228 if notification.id == id then
229 return notification
236 -- Search for an icon in specified directories with a specified format
237 -- @param icon Name of the icon
238 -- @return full path of the icon, or nil of no icon was found
239 local function getIcon(name)
240 for d, dir in pairs(config.icon_dirs) do
241 for f, format in pairs(config.icon_formats) do
242 local icon = dir .. name .. "." .. format
243 if awful.util.file_readable(icon) then
244 return icon
250 --- Create notification. args is a dictionary of optional arguments. For more information and defaults see respective fields in config table.
251 -- @param text Text of the notification
252 -- @param timeout Time in seconds after which popup expires
253 -- @param title Title of the notification
254 -- @param position Corner of the workarea the popups will appear
255 -- @param icon Path to icon
256 -- @param icon_size Desired icon size in px
257 -- @param fg Foreground color
258 -- @param bg Background color
259 -- @param screen Target screen for the notification
260 -- @param ontop Target screen for the notification
261 -- @param run Function to run on left click
262 -- @param width The popup width
263 -- @field hover_timeout Delay in seconds after which hovered popup disappears.
264 -- @param replaces_id Replace the notification with the given ID
265 -- @usage naughty.notify({ title = 'Achtung!', text = 'You\'re idling', timeout = 0 })
266 -- @return The notification object
267 function notify(args)
268 -- gather variables together
269 local timeout = args.timeout or (args.preset and args.preset.timeout) or config.presets.normal.timeout
270 local icon = args.icon or (args.preset and args.preset.icon) or config.icon
271 local icon_size = args.icon_size or (args.preset and args.preset.icon_size) or config.icon_size
272 local text = tostring(args.text) or ""
273 local screen = args.screen or (args.preset and args.preset.screen) or config.presets.normal.screen
274 local ontop = args.ontop or config.ontop
275 local width = args.width or (args.preset and args.preset.width) or config.presets.normal.width
276 local height = args.preset and args.preset.height or config.presets.normal.height
277 local hover_timeout = args.hover_timeout or (args.preset and args.preset.hover_timeout) or config.presets.normal.hover_timeout
278 local opacity = args.opacity or (args.preset and args.preset.opacity) or config.presets.normal.opacity
280 -- beautiful
281 local beautiful = bt.get()
282 local font = args.font or config.font or (args.preset and args.preset.font) or config.presets.normal.font or beautiful.font or "Verdana 8"
283 local fg = args.fg or config.fg or (args.preset and args.preset.fg) or config.presets.normal.fg or beautiful.fg_normal or '#ffffff'
284 local bg = args.bg or config.bg or (args.preset and args.preset.bg) or config.presets.normal.bg or beautiful.bg_normal or '#535d6c'
285 local border_color = (args.preset and args.preset.border_color) or config.presets.normal.border_color or beautiful.bg_focus or '#535d6c'
286 local notification = {}
288 -- replace notification if needed
289 if args.replaces_id then
290 obj = getById(args.replaces_id)
291 if obj then
292 -- destroy this and ...
293 destroy(obj)
295 -- ... may use its ID
296 if args.replaces_id < counter then
297 notification.id = args.replaces_id
298 else
299 counter = counter + 1
300 notification.id = counter
302 else
303 -- get a brand new ID
304 counter = counter + 1
305 notification.id = counter
308 notification.position = args.position or (args.preset and args.preset.position) or config.presets.normal.position
310 local title = ""
311 if args.title then title = tostring(args.title) .. "\n" end
313 -- hook destroy
314 local die = function () destroy(notification) end
315 hooks.timer.register(timeout, die)
316 notification.die = die
318 local run = function ()
319 if args.run then
320 args.run(notification)
321 else
322 die()
326 local hover_destroy = function ()
327 if hover_timeout == 0 then die()
328 else hooks.timer.register(hover_timeout, die) end
331 -- create textbox
332 local textbox = widget({ type = "textbox", align = "flex" })
333 textbox:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
334 textbox:margin({ right = config.margin, left = config.margin, bottom = 2 * config.margin })
335 textbox.text = string.format('<span font_desc="%s"><b>%s</b>%s</span>', font, title, text)
336 if hover_timeout then textbox.mouse_enter = hover_destroy end
338 -- create iconbox
339 local iconbox = nil
340 if icon then
341 -- try to guess icon if the provided one is non-existent/readable
342 if type(icon) == "string" and not awful.util.file_readable(icon) then
343 icon = getIcon(icon)
346 -- if we have an icon, use it
347 if icon then
348 iconbox = widget({ type = "imagebox", align = "left" })
349 iconbox:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
350 local img
351 if type(icon) == "string" then
352 img = image(icon)
353 else
354 img = icon
356 if icon_size then
357 img = img:crop_and_scale(0,0,img.height,img.width,icon_size,icon_size)
359 iconbox.resize = false
360 iconbox.image = img
361 iconbox.valign = "center"
362 if hover_timeout then iconbox.mouse_enter = hover_destroy end
366 -- create container wibox
367 notification.box = wibox({ position = "floating",
368 fg = fg,
369 bg = bg,
370 border_color = args.preset and args.preset.border_color or config.presets.normal.border_color,
371 border_width = config.border_width })
373 -- position the wibox
374 if height then
375 if iconbox and iconbox.image.height > height then
376 notification.height = iconbox.image.height
377 else
378 notification.height = height
380 else
381 if iconbox and iconbox:extents().height > textbox:extents().height then
382 notification.height = iconbox:extents().height
383 else
384 notification.height = textbox:extents().height
387 if width then
388 notification.width = width
389 else
390 notification.width = textbox:extents().width + (iconbox and iconbox:extents().width or 0) + (2 * (config.border_width or 0))
392 if notification.width > capi.screen[screen].workarea.width - 2 * (config.border_width or 0) then
393 notification.width = capi.screen[screen].workarea.width - 2 * (config.border_width or 0)
395 if notification.height > capi.screen[screen].workarea.height - 2 * (config.border_width or 0) - 2 * (config.padding or 0) then
396 notification.height = capi.screen[screen].workarea.height - 2 * (config.border_width or 0) - 2 * (config.padding or 0)
399 local offset = get_offset(screen, notification.position, nil, notification.width, notification.height)
400 notification.box:geometry({ width = notification.width,
401 height = notification.height,
402 x = offset.x,
403 y = offset.y })
404 notification.box.ontop = ontop
405 notification.box.opacity = opacity
406 notification.box.screen = screen
407 notification.idx = offset.idx
409 -- populate widgets
410 notification.box.widgets = { iconbox, textbox }
412 -- insert the notification to the table
413 table.insert(notifications[screen][notification.position], notification)
415 -- return the notification
416 return notification
419 -- DBUS/Notification support
420 -- Notify
421 if awful.hooks.dbus then
422 awful.hooks.dbus.register("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire)
423 args = { preset = { } }
424 if data.member == "Notify" then
425 if text ~= "" then
426 args.text = text
427 if title ~= "" then
428 args.title = title
430 else
431 if title ~= "" then
432 args.text = title
433 else
434 return nil
437 local score = 0
438 for i, obj in pairs(config.mapping) do
439 local filter, preset, s = obj[1], obj[2], 0
440 if (not filter.urgency or filter.urgency == hints.urgency) and
441 (not filter.category or filter.category == hints.category) and
442 (not filter.appname or filter.appname == appname) then
443 for j, el in pairs(filter) do s = s + 1 end
444 if s > score then
445 score = s
446 args.preset = preset
450 if not args.preset.callback or (type(args.preset.callback) == "function" and
451 args.preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
452 if icon ~= "" then
453 args.icon = icon
454 elseif hints.icon_data then
455 -- icon_data is an array:
456 -- 1 -> width, 2 -> height, 3 -> rowstride, 4 -> has alpha
457 -- 5 -> bits per sample, 6 -> channels, 7 -> data
459 local imgdata
460 -- If has alpha (ARGB32)
461 if hints.icon_data[6] == 4 then
462 imgdata = hints.icon_data[7]
463 -- If has not alpha (RGB24)
464 elseif hints.icon_data[6] == 3 then
465 imgdata = ""
466 for i = 1, #hints.icon_data[7], 3 do
467 imgdata = imgdata .. hints.icon_data[7]:sub(i , i + 2):reverse()
468 imgdata = imgdata .. string.format("%c", 255) -- alpha is 255
471 if imgdata then
472 args.icon = image.argb32(hints.icon_data[1], hints.icon_data[2], imgdata)
475 if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
476 args.replaces_id = replaces_id
478 if expire and expire > -1 then
479 args.timeout = expire / 1000
481 local id = notify(args).id
482 return "u", id
484 return "u", "0"
485 elseif data.member == "CloseNotification" then
486 local obj = getById(arg1)
487 if obj then
488 destroy(obj)
490 elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
491 -- name of notification app, name of vender, version
492 return "s", "naughty", "s", "awesome", "s", capi.awesome.version:match("%d.%d")
494 end)
496 awful.hooks.dbus.register("org.freedesktop.DBus.Introspectable",
497 function (data, text)
498 if data.member == "Introspect" then
499 local xml = [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
500 Introspection 1.0//EN"
501 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
502 <node>
503 <interface name="org.freedesktop.DBus.Introspectable">
504 <method name="Introspect">
505 <arg name="data" direction="out" type="s"/>
506 </method>
507 </interface>
508 <interface name="org.freedesktop.Notifications">
509 <method name="CloseNotification">
510 <arg name="id" type="u" direction="in"/>
511 </method>
512 <method name="Notify">
513 <arg name="app_name" type="s" direction="in"/>
514 <arg name="id" type="u" direction="in"/>
515 <arg name="icon" type="s" direction="in"/>
516 <arg name="summary" type="s" direction="in"/>
517 <arg name="body" type="s" direction="in"/>
518 <arg name="actions" type="as" direction="in"/>
519 <arg name="hints" type="a{sv}" direction="in"/>
520 <arg name="timeout" type="i" direction="in"/>
521 <arg name="return_id" type="u" direction="out"/>
522 </method>
523 <method name="GetServerInformation">
524 <arg name="return_name" type="s" direction="out"/>
525 <arg name="return_vendor" type="s" direction="out"/>
526 <arg name="return_version" type="s" direction="out"/>
527 </method>
528 <method name="GetServerInfo">
529 <arg name="return_name" type="s" direction="out"/>
530 <arg name="return_vendor" type="s" direction="out"/>
531 <arg name="return_version" type="s" direction="out"/>
532 </method>
533 </interface>
534 </node>]=]
535 return "s", xml
537 end)
539 -- listen for dbus notification requests
540 dbus.request_name("session", "org.freedesktop.Notifications")
543 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80