Lots of random documentation fixes
[awesome.git] / lib / naughty.lua.in
blob27b424aba21306b45893a8259a17eaad5dfbafee
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 timer = timer,
17 awesome = awesome }
18 local button = require("awful.button")
19 local util = require("awful.util")
20 local bt = require("beautiful")
21 local wibox = require("wibox")
22 local surface = require("gears.surface")
23 local cairo = require("lgi").cairo
25 --- Notification library
26 local naughty = {}
28 --- Naughty configuration - a table containing common popup settings.
29 naughty.config = {}
30 --- Space between popups and edge of the workarea. Default: 4
31 naughty.config.padding = 4
32 --- Spacing between popups. Default: 1
33 naughty.config.spacing = 1
34 --- List of directories that will be checked by getIcon()
35 -- Default: { "/usr/share/pixmaps/", }
36 naughty.config.icon_dirs = { "/usr/share/pixmaps/", }
37 --- List of formats that will be checked by getIcon()
38 -- Default: { "png", "gif" }
39 naughty.config.icon_formats = { "png", "gif" }
40 --- Callback used to modify or reject notifications.
41 -- Default: nil
42 -- Example:
43 -- naughty.config.notify_callback = function(args)
44 -- args.text = 'prefix: ' .. args.text
45 -- return args
46 -- end
47 naughty.config.notify_callback = nil
50 --- Notification Presets - a table containing presets for different purposes
51 -- Preset is a table of any parameters available to notify(), overriding default
52 -- values (@see defaults)
53 -- You have to pass a reference of a preset in your notify() call to use the preset
54 -- The presets "low", "normal" and "critical" are used for notifications over DBUS
55 -- @field low The preset for notifications with low urgency level
56 -- @field normal The default preset for every notification without a preset that will also be used for normal urgency level
57 -- @field critical The preset for notifications with a critical urgency level
58 -- @class table
59 naughty.config.presets = {
60 normal = {},
61 low = {
62 timeout = 5
64 critical = {
65 bg = "#ff0000",
66 fg = "#ffffff",
67 timeout = 0,
71 --- Default values for the params to notify().
72 -- These can optionally be overridden by specifying a preset
73 -- @see naughty.config.presets
74 -- @see naughty.notify
75 naughty.config.defaults = {
76 timeout = 5,
77 text = "",
78 screen = 1,
79 ontop = true,
80 margin = "5",
81 border_width = "1",
82 position = "top_right"
85 -- DBUS Notification constants
86 local urgency = {
87 low = "\0",
88 normal = "\1",
89 critical = "\2"
92 --- DBUS notification to preset mapping
93 -- The first element is an object containing the filter
94 -- If the rules in the filter matches the associated preset will be applied
95 -- The rules object can contain: urgency, category, appname
96 -- The second element is the preset
98 naughty.config.mapping = {
99 {{urgency = urgency.low}, naughty.config.presets.low},
100 {{urgency = urgency.normal}, naughty.config.presets.normal},
101 {{urgency = urgency.critical}, naughty.config.presets.critical}
104 -- Counter for the notifications
105 -- Required for later access via DBUS
106 local counter = 1
108 -- True if notifying is suspended
109 local suspended = false
111 --- Index of notifications. See config table for valid 'position' values.
112 -- Each element is a table consisting of:
113 -- @field box Wibox object containing the popup
114 -- @field height Popup height
115 -- @field width Popup width
116 -- @field die Function to be executed on timeout
117 -- @field id Unique notification id based on a counter
118 -- @name notifications[screen][position]
119 -- @class table
121 local notifications = { suspended = { } }
122 for s = 1, capi.screen.count() do
123 notifications[s] = {
124 top_left = {},
125 top_right = {},
126 bottom_left = {},
127 bottom_right = {},
131 --- Suspend notifications
132 function naughty.suspend()
133 suspended = true
136 --- Resume notifications
137 function naughty.resume()
138 suspended = false
139 for i, v in pairs(notifications.suspended) do
140 v.box.visible = true
141 if v.timer then v.timer:start() end
143 notifications.suspended = { }
146 --- Toggle notification state
147 function naughty.toggle()
148 if suspended then
149 naughty.resume()
150 else
151 naughty.suspend()
155 -- Evaluate desired position of the notification by index - internal
156 -- @param idx Index of the notification
157 -- @param position top_right | top_left | bottom_right | bottom_left
158 -- @param height Popup height
159 -- @param width Popup width (optional)
160 -- @return Absolute position and index in { x = X, y = Y, idx = I } table
161 local function get_offset(screen, position, idx, width, height)
162 local ws = capi.screen[screen].workarea
163 local v = {}
164 local idx = idx or #notifications[screen][position] + 1
165 local width = width or notifications[screen][position][idx].width
167 -- calculate x
168 if position:match("left") then
169 v.x = ws.x + naughty.config.padding
170 else
171 v.x = ws.x + ws.width - (width + naughty.config.padding)
174 -- calculate existing popups' height
175 local existing = 0
176 for i = 1, idx-1, 1 do
177 existing = existing + notifications[screen][position][i].height + naughty.config.spacing
180 -- calculate y
181 if position:match("top") then
182 v.y = ws.y + naughty.config.padding + existing
183 else
184 v.y = ws.y + ws.height - (naughty.config.padding + height + existing)
187 -- if positioned outside workarea, destroy oldest popup and recalculate
188 if v.y + height > ws.y + ws.height or v.y < ws.y then
189 idx = idx - 1
190 naughty.destroy(notifications[screen][position][1])
191 v = get_offset(screen, position, idx, width, height)
193 if not v.idx then v.idx = idx end
195 return v
198 -- Re-arrange notifications according to their position and index - internal
199 -- @return None
200 local function arrange(screen)
201 for p,pos in pairs(notifications[screen]) do
202 for i,notification in pairs(notifications[screen][p]) do
203 local offset = get_offset(screen, p, i, notification.width, notification.height)
204 notification.box:geometry({ x = offset.x, y = offset.y })
205 notification.idx = offset.idx
210 --- Destroy notification by notification object
211 -- @param notification Notification object to be destroyed
212 -- @return True if the popup was successfully destroyed, nil otherwise
213 function naughty.destroy(notification)
214 if notification and notification.box.visible then
215 if suspended then
216 for k, v in pairs(notifications.suspended) do
217 if v.box == notification.box then
218 table.remove(notifications.suspended, k)
219 break
223 local scr = notification.screen
224 table.remove(notifications[scr][notification.position], notification.idx)
225 if notification.timer then
226 notification.timer:stop()
228 notification.box.visible = false
229 arrange(scr)
230 return true
234 -- Get notification by ID
235 -- @param id ID of the notification
236 -- @return notification object if it was found, nil otherwise
237 local function getById(id)
238 -- iterate the notifications to get the notfications with the correct ID
239 for s = 1, capi.screen.count() do
240 for p,pos in pairs(notifications[s]) do
241 for i,notification in pairs(notifications[s][p]) do
242 if notification.id == id then
243 return notification
250 --- Create notification. args is a dictionary of (optional) arguments.
251 -- @param text Text of the notification. Default: ''
252 -- @param title Title of the notification. Default: nil
253 -- @param timeout Time in seconds after which popup expires.
254 -- Set 0 for no timeout. Default: 5
255 -- @param hover_timeout Delay in seconds after which hovered popup disappears.
256 -- Default: nil
257 -- @param screen Target screen for the notification. Default: 1
258 -- @param position Corner of the workarea displaying the popups.
259 -- Values: "top_right" (default), "top_left", "bottom_left", "bottom_right".
260 -- @param ontop Boolean forcing popups to display on top. Default: true
261 -- @param height Popup height. Default: nil (auto)
262 -- @param width Popup width. Default: nil (auto)
263 -- @param font Notification font. Default: beautiful.font or awesome.font
264 -- @param icon Path to icon. Default: nil
265 -- @param icon_size Desired icon size in px. Default: nil
266 -- @param fg Foreground color. Default: beautiful.fg_focus or '#ffffff'
267 -- @param bg Background color. Default: beautiful.bg_focus or '#535d6c'
268 -- @param border_width Border width. Default: 1
269 -- @param border_color Border color.
270 -- Default: beautiful.border_focus or '#535d6c'
271 -- @param run Function to run on left click. Default: nil
272 -- @param preset Table with any of the above parameters. Note: Any parameters
273 -- specified directly in args will override ones defined in the preset.
274 -- @param replaces_id Replace the notification with the given ID
275 -- @param callback function that will be called with all arguments
276 -- the notification will only be displayed if the function returns true
277 -- note: this function is only relevant to notifications sent via dbus
278 -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
279 -- @return The notification object
280 function naughty.notify(args)
281 if naughty.config.notify_callback then
282 args = naughty.config.notify_callback(args)
283 if not args then return end
286 -- gather variables together
287 local preset = util.table.join(naughty.config.defaults or {},
288 args.preset or naughty.config.presets.normal or {})
289 local timeout = args.timeout or preset.timeout
290 local icon = args.icon or preset.icon
291 local icon_size = args.icon_size or preset.icon_size
292 local text = args.text or preset.text
293 local title = args.title or preset.title
294 local screen = args.screen or preset.screen
295 local ontop = args.ontop or preset.ontop
296 local width = args.width or preset.width
297 local height = args.height or preset.height
298 local hover_timeout = args.hover_timeout or preset.hover_timeout
299 local opacity = args.opacity or preset.opacity
300 local margin = args.margin or preset.margin
301 local border_width = args.border_width or preset.border_width
302 local position = args.position or preset.position
304 -- beautiful
305 local beautiful = bt.get()
306 local font = args.font or preset.font or beautiful.font or capi.awesome.font
307 local fg = args.fg or preset.fg or beautiful.fg_normal or '#ffffff'
308 local bg = args.bg or preset.bg or beautiful.bg_normal or '#535d6c'
309 local border_color = args.border_color or preset.border_color or beautiful.bg_focus or '#535d6c'
310 local notification = { screen = screen }
312 -- replace notification if needed
313 if args.replaces_id then
314 local obj = getById(args.replaces_id)
315 if obj then
316 -- destroy this and ...
317 naughty.destroy(obj)
319 -- ... may use its ID
320 if args.replaces_id <= counter then
321 notification.id = args.replaces_id
322 else
323 counter = counter + 1
324 notification.id = counter
326 else
327 -- get a brand new ID
328 counter = counter + 1
329 notification.id = counter
332 notification.position = position
334 if title then title = title .. "\n" else title = "" end
336 -- hook destroy
337 local die = function () naughty.destroy(notification) end
338 if timeout > 0 then
339 local timer_die = capi.timer { timeout = timeout }
340 timer_die:connect_signal("timeout", die)
341 if not suspended then
342 timer_die:start()
344 notification.timer = timer_die
346 notification.die = die
348 local run = function ()
349 if args.run then
350 args.run(notification)
351 else
352 die()
356 local hover_destroy = function ()
357 if hover_timeout == 0 then
358 die()
359 else
360 if notification.timer then notification.timer:stop() end
361 notification.timer = capi.timer { timeout = hover_timeout }
362 notification.timer:connect_signal("timeout", die)
363 notification.timer:start()
367 -- create textbox
368 local textbox = wibox.widget.textbox()
369 local marginbox = wibox.layout.margin()
370 marginbox:set_margins(margin)
371 marginbox:set_widget(textbox)
372 textbox:set_valign("middle")
373 textbox:set_font(font)
375 local function setMarkup(pattern, replacements)
376 textbox:set_markup(string.format('<b>%s</b>%s', title:gsub(pattern, replacements), text:gsub(pattern, replacements)))
378 local function setText()
379 textbox:set_text(string.format('%s %s', title, text))
382 -- First try to set the text while only interpreting <br>.
383 -- (Setting a textbox' .text to an invalid pattern throws a lua error)
384 if not pcall(setMarkup, "<br.->", "\n") then
385 -- That failed, escape everything which might cause an error from pango
386 if not pcall(setMarkup, "[<>&]", { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }) then
387 -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8
388 if not pcall(setText) then
389 textbox:set_markup("<i>&lt;Invalid markup or UTF8, cannot display message&gt;</i>")
394 -- create iconbox
395 local iconbox = nil
396 local iconmargin = nil
397 local icon_w, icon_h = 0, 0
398 if icon then
399 -- try to guess icon if the provided one is non-existent/readable
400 if type(icon) == "string" and not util.file_readable(icon) then
401 icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size)
404 -- if we have an icon, use it
405 if icon then
406 iconbox = wibox.widget.imagebox()
407 iconmargin = wibox.layout.margin(iconbox, margin, margin, margin, margin)
408 local img = surface.load(icon)
409 if icon_size then
410 local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size)
411 local cr = cairo.Context(scaled)
412 cr:scale(icon_size / img:get_height(), icon_size / img:get_width())
413 cr:set_source_surface(img, 0, 0)
414 cr:paint()
415 img = scaled
417 iconbox:set_resize(false)
418 iconbox:set_image(img)
419 icon_w = img:get_width()
420 icon_h = img:get_height()
424 -- create container wibox
425 notification.box = wibox({ fg = fg,
426 bg = bg,
427 border_color = border_color,
428 border_width = border_width,
429 type = "notification" })
431 if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end
433 -- calculate the height
434 if not height then
435 local w, h = textbox:fit(-1, -1)
436 if iconbox and icon_h + 2 * margin > h + 2 * margin then
437 height = icon_h + 2 * margin
438 else
439 height = h + 2 * margin
443 -- calculate the width
444 if not width then
445 local w, h = textbox:fit(-1, -1)
446 width = w + (iconbox and icon_w + 2 * margin or 0) + 2 * margin
449 -- crop to workarea size if too big
450 local workarea = capi.screen[screen].workarea
451 if width > workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
452 width = workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0)
454 if height > workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
455 height = workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0)
458 -- set size in notification object
459 notification.height = height + 2 * (border_width or 0)
460 notification.width = width + 2 * (border_width or 0)
462 -- position the wibox
463 local offset = get_offset(screen, notification.position, nil, notification.width, notification.height)
464 notification.box.ontop = ontop
465 notification.box:geometry({ width = width,
466 height = height,
467 x = offset.x,
468 y = offset.y })
469 notification.box.opacity = opacity
470 notification.box.visible = true
471 notification.idx = offset.idx
473 -- populate widgets
474 local layout = wibox.layout.fixed.horizontal()
475 if iconmargin then
476 layout:add(iconmargin)
478 layout:add(marginbox)
479 notification.box:set_widget(layout)
481 -- Setup the mouse events
482 layout:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
484 -- insert the notification to the table
485 table.insert(notifications[screen][notification.position], notification)
487 if suspended then
488 notification.box.visible = false
489 table.insert(notifications.suspended, notification)
492 -- return the notification
493 return notification
496 -- DBUS/Notification support
497 -- Notify
498 if capi.dbus then
499 capi.dbus.connect_signal("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire)
500 local args = { }
501 if data.member == "Notify" then
502 if text ~= "" then
503 args.text = text
504 if title ~= "" then
505 args.title = title
507 else
508 if title ~= "" then
509 args.text = title
510 else
511 return
514 if appname ~= "" then
515 args.appname = appname
517 for i, obj in pairs(naughty.config.mapping) do
518 local filter, preset, s = obj[1], obj[2], 0
519 if (not filter.urgency or filter.urgency == hints.urgency) and
520 (not filter.category or filter.category == hints.category) and
521 (not filter.appname or filter.appname == appname) then
522 args.preset = util.table.join(args.preset, preset)
525 local preset = args.preset or naughty.config.defaults
526 if not preset.callback or (type(preset.callback) == "function" and
527 preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
528 if icon ~= "" then
529 args.icon = icon
530 elseif hints.icon_data or hints.image_data then
531 if hints.icon_data == nil then hints.icon_data = hints.image_data end
532 -- icon_data is an array:
533 -- 1 -> width, 2 -> height, 3 -> rowstride, 4 -> has alpha
534 -- 5 -> bits per sample, 6 -> channels, 7 -> data
535 local width, height, rowstride = hints.icon_data[1], hints.icon_data[2], hints.icon_data[3]
537 local imgdata = ""
538 local format, bpp
539 -- If has alpha (ARGB32)
540 if hints.icon_data[6] == 4 then
541 format, bpp = cairo.Format.ARGB32, 4
542 -- If has not alpha (RGB24)
543 elseif hints.icon_data[6] == 3 then
544 format, bpp = cairo.Format.RGB24, 3
546 -- Figure out some stride magic (cairo dictates rowstride)
547 local stride = cairo.Format.stride_for_width(format, width)
548 local append = string.format("%c", 0):rep(stride - 4 * width)
549 local offset = 0
550 -- Now convert each row on its own
551 for y = 1, height do
552 for i = 1 + offset, width * bpp + offset, bpp do
553 imgdata = imgdata .. hints.icon_data[7]:sub(i, i + 2):reverse()
554 if bpp == 4 then
555 imgdata = imgdata .. hints.icon_data[7]:sub(i + 3, i + 3)
556 else
557 imgdata = imgdata .. string.format("%c", 255)
560 -- Handle rowstride, offset is stride for the input, append for output
561 offset = offset + rowstride
562 imgdata = imgdata .. append
564 args.icon = cairo.ImageSurface.create_for_data(imgdata, format,
565 width, height, stride)
567 if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
568 args.replaces_id = replaces_id
570 if expire and expire > -1 then
571 args.timeout = expire / 1000
573 local id = naughty.notify(args).id
574 return "u", id
576 return "u", "0"
577 elseif data.member == "CloseNotification" then
578 local obj = getById(appname)
579 if obj then
580 naughty.destroy(obj)
582 elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
583 -- name of notification app, name of vender, version
584 return "s", "naughty", "s", "awesome", "s", capi.awesome.version:match("%d.%d"), "s", "1.0"
585 elseif data.member == "GetCapabilities" then
586 -- We actually do display the body of the message, we support <b>, <i>
587 -- and <u> in the body and we handle static (non-animated) icons.
588 return "as", { "s", "body", "s", "body-markup", "s", "icon-static" }
590 end)
592 capi.dbus.connect_signal("org.freedesktop.DBus.Introspectable",
593 function (data, text)
594 if data.member == "Introspect" then
595 local xml = [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
596 Introspection 1.0//EN"
597 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
598 <node>
599 <interface name="org.freedesktop.DBus.Introspectable">
600 <method name="Introspect">
601 <arg name="data" direction="out" type="s"/>
602 </method>
603 </interface>
604 <interface name="org.freedesktop.Notifications">
605 <method name="GetCapabilities">
606 <arg name="caps" type="as" direction="out"/>
607 </method>
608 <method name="CloseNotification">
609 <arg name="id" type="u" direction="in"/>
610 </method>
611 <method name="Notify">
612 <arg name="app_name" type="s" direction="in"/>
613 <arg name="id" type="u" direction="in"/>
614 <arg name="icon" type="s" direction="in"/>
615 <arg name="summary" type="s" direction="in"/>
616 <arg name="body" type="s" direction="in"/>
617 <arg name="actions" type="as" direction="in"/>
618 <arg name="hints" type="a{sv}" direction="in"/>
619 <arg name="timeout" type="i" direction="in"/>
620 <arg name="return_id" type="u" direction="out"/>
621 </method>
622 <method name="GetServerInformation">
623 <arg name="return_name" type="s" direction="out"/>
624 <arg name="return_vendor" type="s" direction="out"/>
625 <arg name="return_version" type="s" direction="out"/>
626 <arg name="return_spec_version" type="s" direction="out"/>
627 </method>
628 <method name="GetServerInfo">
629 <arg name="return_name" type="s" direction="out"/>
630 <arg name="return_vendor" type="s" direction="out"/>
631 <arg name="return_version" type="s" direction="out"/>
632 </method>
633 </interface>
634 </node>]=]
635 return "s", xml
637 end)
639 -- listen for dbus notification requests
640 capi.dbus.request_name("session", "org.freedesktop.Notifications")
643 return naughty
645 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80