Drop focus:raise() in magnifier.arrange
[awesome.git] / lib / naughty.lua.in
blob16dc384c0b9d2b39589ac83ac9ee549e078abc9a
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 tostring = tostring
13 local pcall = pcall
14 local capi = { screen = screen,
15 awesome = awesome,
16 dbus = dbus,
17 timer = timer,
18 awesome = awesome }
19 local button = require("awful.button")
20 local util = require("awful.util")
21 local bt = require("beautiful")
22 local wibox = require("wibox")
23 local surface = require("gears.surface")
24 local cairo = require("lgi").cairo
26 local schar = string.char
27 local sbyte = string.byte
28 local tcat = table.concat
29 local tins = table.insert
31 --- Notification library
32 local naughty = {}
34 --- Naughty configuration - a table containing common popup settings.
35 naughty.config = {}
36 --- Space between popups and edge of the workarea. Default: 4
37 naughty.config.padding = 4
38 --- Spacing between popups. Default: 1
39 naughty.config.spacing = 1
40 --- List of directories that will be checked by getIcon()
41 -- Default: { "/usr/share/pixmaps/", }
42 naughty.config.icon_dirs = { "/usr/share/pixmaps/", }
43 --- List of formats that will be checked by getIcon()
44 -- Default: { "png", "gif" }
45 naughty.config.icon_formats = { "png", "gif" }
46 --- Callback used to modify or reject notifications.
47 -- Default: nil
48 -- Example:
49 -- naughty.config.notify_callback = function(args)
50 -- args.text = 'prefix: ' .. args.text
51 -- return args
52 -- end
53 naughty.config.notify_callback = nil
56 --- Notification Presets - a table containing presets for different purposes
57 -- Preset is a table of any parameters available to notify(), overriding default
58 -- values (@see defaults)
59 -- You have to pass a reference of a preset in your notify() call to use the preset
60 -- The presets "low", "normal" and "critical" are used for notifications over DBUS
61 -- @field low The preset for notifications with low urgency level
62 -- @field normal The default preset for every notification without a preset that will also be used for normal urgency level
63 -- @field critical The preset for notifications with a critical urgency level
64 naughty.config.presets = {
65 normal = {},
66 low = {
67 timeout = 5
69 critical = {
70 bg = "#ff0000",
71 fg = "#ffffff",
72 timeout = 0,
76 --- Default values for the params to notify().
77 -- These can optionally be overridden by specifying a preset
78 -- @see naughty.config.presets
79 -- @see naughty.notify
80 naughty.config.defaults = {
81 timeout = 5,
82 text = "",
83 screen = 1,
84 ontop = true,
85 margin = "5",
86 border_width = "1",
87 position = "top_right"
90 -- DBUS Notification constants
91 local urgency = {
92 low = "\0",
93 normal = "\1",
94 critical = "\2"
97 --- DBUS notification to preset mapping
98 -- The first element is an object containing the filter
99 -- If the rules in the filter matches the associated preset will be applied
100 -- The rules object can contain: urgency, category, appname
101 -- The second element is the preset
103 naughty.config.mapping = {
104 {{urgency = urgency.low}, naughty.config.presets.low},
105 {{urgency = urgency.normal}, naughty.config.presets.normal},
106 {{urgency = urgency.critical}, naughty.config.presets.critical}
109 -- Counter for the notifications
110 -- Required for later access via DBUS
111 local counter = 1
113 -- True if notifying is suspended
114 local suspended = false
116 --- Index of notifications per screen and position. See config table for valid
117 -- 'position' values. Each element is a table consisting of:
118 -- @field box Wibox object containing the popup
119 -- @field height Popup height
120 -- @field width Popup width
121 -- @field die Function to be executed on timeout
122 -- @field id Unique notification id based on a counter
123 naughty.notifications = { suspended = { } }
124 for s = 1, capi.screen.count() do
125 naughty.notifications[s] = {
126 top_left = {},
127 top_right = {},
128 bottom_left = {},
129 bottom_right = {},
133 --- Suspend notifications
134 function naughty.suspend()
135 suspended = true
138 --- Resume notifications
139 function naughty.resume()
140 suspended = false
141 for i, v in pairs(naughty.notifications.suspended) do
142 v.box.visible = true
143 if v.timer then v.timer:start() end
145 naughty.notifications.suspended = { }
148 --- Toggle notification state
149 function naughty.toggle()
150 if suspended then
151 naughty.resume()
152 else
153 naughty.suspend()
157 -- Evaluate desired position of the notification by index - internal
158 -- @param idx Index of the notification
159 -- @param position top_right | top_left | bottom_right | bottom_left
160 -- @param height Popup height
161 -- @param width Popup width (optional)
162 -- @return Absolute position and index in { x = X, y = Y, idx = I } table
163 local function get_offset(screen, position, idx, width, height)
164 local ws = capi.screen[screen].workarea
165 local v = {}
166 local idx = idx or #naughty.notifications[screen][position] + 1
167 local width = width or naughty.notifications[screen][position][idx].width
169 -- calculate x
170 if position:match("left") then
171 v.x = ws.x + naughty.config.padding
172 else
173 v.x = ws.x + ws.width - (width + naughty.config.padding)
176 -- calculate existing popups' height
177 local existing = 0
178 for i = 1, idx-1, 1 do
179 existing = existing + naughty.notifications[screen][position][i].height + naughty.config.spacing
182 -- calculate y
183 if position:match("top") then
184 v.y = ws.y + naughty.config.padding + existing
185 else
186 v.y = ws.y + ws.height - (naughty.config.padding + height + existing)
189 -- if positioned outside workarea, destroy oldest popup and recalculate
190 if v.y + height > ws.y + ws.height or v.y < ws.y then
191 idx = idx - 1
192 naughty.destroy(naughty.notifications[screen][position][1])
193 v = get_offset(screen, position, idx, width, height)
195 if not v.idx then v.idx = idx end
197 return v
200 -- Re-arrange notifications according to their position and index - internal
201 -- @return None
202 local function arrange(screen)
203 for p,pos in pairs(naughty.notifications[screen]) do
204 for i,notification in pairs(naughty.notifications[screen][p]) do
205 local offset = get_offset(screen, p, i, notification.width, notification.height)
206 notification.box:geometry({ x = offset.x, y = offset.y })
207 notification.idx = offset.idx
212 --- Destroy notification by notification object
213 -- @param notification Notification object to be destroyed
214 -- @return True if the popup was successfully destroyed, nil otherwise
215 function naughty.destroy(notification)
216 if notification and notification.box.visible then
217 if suspended then
218 for k, v in pairs(naughty.notifications.suspended) do
219 if v.box == notification.box then
220 table.remove(naughty.notifications.suspended, k)
221 break
225 local scr = notification.screen
226 table.remove(naughty.notifications[scr][notification.position], notification.idx)
227 if notification.timer then
228 notification.timer:stop()
230 notification.box.visible = false
231 arrange(scr)
232 return true
236 -- Get notification by ID
237 -- @param id ID of the notification
238 -- @return notification object if it was found, nil otherwise
239 local function getById(id)
240 -- iterate the notifications to get the notfications with the correct ID
241 for s = 1, capi.screen.count() do
242 for p,pos in pairs(naughty.notifications[s]) do
243 for i,notification in pairs(naughty.notifications[s][p]) do
244 if notification.id == id then
245 return notification
252 --- Create notification. args is a dictionary of (optional) arguments.
253 -- @param text Text of the notification. Default: ''
254 -- @param title Title of the notification. Default: nil
255 -- @param timeout Time in seconds after which popup expires.
256 -- Set 0 for no timeout. Default: 5
257 -- @param hover_timeout Delay in seconds after which hovered popup disappears.
258 -- Default: nil
259 -- @param screen Target screen for the notification. Default: 1
260 -- @param position Corner of the workarea displaying the popups.
261 -- Values: "top_right" (default), "top_left", "bottom_left", "bottom_right".
262 -- @param ontop Boolean forcing popups to display on top. Default: true
263 -- @param height Popup height. Default: nil (auto)
264 -- @param width Popup width. Default: nil (auto)
265 -- @param font Notification font. Default: beautiful.font or awesome.font
266 -- @param icon Path to icon. Default: nil
267 -- @param icon_size Desired icon size in px. Default: nil
268 -- @param fg Foreground color. Default: beautiful.fg_focus or '#ffffff'
269 -- @param bg Background color. Default: beautiful.bg_focus or '#535d6c'
270 -- @param border_width Border width. Default: 1
271 -- @param border_color Border color.
272 -- Default: beautiful.border_focus or '#535d6c'
273 -- @param run Function to run on left click. Default: nil
274 -- @param preset Table with any of the above parameters. Note: Any parameters
275 -- specified directly in args will override ones defined in the preset.
276 -- @param replaces_id Replace the notification with the given ID
277 -- @param callback function that will be called with all arguments
278 -- the notification will only be displayed if the function returns true
279 -- note: this function is only relevant to notifications sent via dbus
280 -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
281 -- @return The notification object
282 function naughty.notify(args)
283 if naughty.config.notify_callback then
284 args = naughty.config.notify_callback(args)
285 if not args then return end
288 -- gather variables together
289 local preset = util.table.join(naughty.config.defaults or {},
290 args.preset or naughty.config.presets.normal or {})
291 local timeout = args.timeout or preset.timeout
292 local icon = args.icon or preset.icon
293 local icon_size = args.icon_size or preset.icon_size
294 local text = args.text or preset.text
295 local title = args.title or preset.title
296 local screen = args.screen or preset.screen
297 local ontop = args.ontop or preset.ontop
298 local width = args.width or preset.width
299 local height = args.height or preset.height
300 local hover_timeout = args.hover_timeout or preset.hover_timeout
301 local opacity = args.opacity or preset.opacity
302 local margin = args.margin or preset.margin
303 local border_width = args.border_width or preset.border_width
304 local position = args.position or preset.position
305 local escape_pattern = "[<>&]"
306 local escape_subs = { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }
308 -- beautiful
309 local beautiful = bt.get()
310 local font = args.font or preset.font or beautiful.font or capi.awesome.font
311 local fg = args.fg or preset.fg or beautiful.fg_normal or '#ffffff'
312 local bg = args.bg or preset.bg or beautiful.bg_normal or '#535d6c'
313 local border_color = args.border_color or preset.border_color or beautiful.bg_focus or '#535d6c'
314 local notification = { screen = screen }
316 -- replace notification if needed
317 if args.replaces_id then
318 local obj = getById(args.replaces_id)
319 if obj then
320 -- destroy this and ...
321 naughty.destroy(obj)
323 -- ... may use its ID
324 if args.replaces_id <= counter then
325 notification.id = args.replaces_id
326 else
327 counter = counter + 1
328 notification.id = counter
330 else
331 -- get a brand new ID
332 counter = counter + 1
333 notification.id = counter
336 notification.position = position
338 if title then title = title .. "\n" else title = "" end
340 -- hook destroy
341 local die = function () naughty.destroy(notification) end
342 if timeout > 0 then
343 local timer_die = capi.timer { timeout = timeout }
344 timer_die:connect_signal("timeout", die)
345 if not suspended then
346 timer_die:start()
348 notification.timer = timer_die
350 notification.die = die
352 local run = function ()
353 if args.run then
354 args.run(notification)
355 else
356 die()
360 local hover_destroy = function ()
361 if hover_timeout == 0 then
362 die()
363 else
364 if notification.timer then notification.timer:stop() end
365 notification.timer = capi.timer { timeout = hover_timeout }
366 notification.timer:connect_signal("timeout", die)
367 notification.timer:start()
371 -- create textbox
372 local textbox = wibox.widget.textbox()
373 local marginbox = wibox.layout.margin()
374 marginbox:set_margins(margin)
375 marginbox:set_widget(textbox)
376 textbox:set_valign("middle")
377 textbox:set_font(font)
379 local function setMarkup(pattern, replacements)
380 textbox:set_markup(string.format('<b>%s</b>%s', title, text:gsub(pattern, replacements)))
382 local function setText()
383 textbox:set_text(string.format('%s %s', title, text))
386 -- Since the title cannot contain markup, it must be escaped first so that
387 -- it is not interpreted by Pango later.
388 title = title:gsub(escape_pattern, escape_subs)
389 -- Try to set the text while only interpreting <br>.
390 -- (Setting a textbox' .text to an invalid pattern throws a lua error)
391 if not pcall(setMarkup, "<br.->", "\n") then
392 -- That failed, escape everything which might cause an error from pango
393 if not pcall(setMarkup, escape_pattern, escape_subs) then
394 -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8
395 if not pcall(setText) then
396 textbox:set_markup("<i>&lt;Invalid markup or UTF8, cannot display message&gt;</i>")
401 -- create iconbox
402 local iconbox = nil
403 local iconmargin = nil
404 local icon_w, icon_h = 0, 0
405 if icon then
406 -- try to guess icon if the provided one is non-existent/readable
407 if type(icon) == "string" and not util.file_readable(icon) then
408 icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size) or icon
411 -- is the icon file readable?
412 local success, res = pcall(function() return surface.load(icon) end)
413 if success then
414 icon = res
415 else
416 io.stderr:write(string.format("naughty: Couldn't load image '%s': %s\n", tostring(icon), res))
417 icon = nil
420 -- if we have an icon, use it
421 if icon then
422 iconbox = wibox.widget.imagebox()
423 iconmargin = wibox.layout.margin(iconbox, margin, margin, margin, margin)
424 if icon_size then
425 local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size)
426 local cr = cairo.Context(scaled)
427 cr:scale(icon_size / icon:get_height(), icon_size / icon:get_width())
428 cr:set_source_surface(icon, 0, 0)
429 cr:paint()
430 icon = scaled
432 iconbox:set_resize(false)
433 iconbox:set_image(icon)
434 icon_w = icon:get_width()
435 icon_h = icon:get_height()
439 -- create container wibox
440 notification.box = wibox({ fg = fg,
441 bg = bg,
442 border_color = border_color,
443 border_width = border_width,
444 type = "notification" })
446 if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end
448 -- calculate the height
449 if not height then
450 local w, h = textbox:fit(-1, -1)
451 if iconbox and icon_h + 2 * margin > h + 2 * margin then
452 height = icon_h + 2 * margin
453 else
454 height = h + 2 * margin
458 -- calculate the width
459 if not width then
460 local w, h = textbox:fit(-1, -1)
461 width = w + (iconbox and icon_w + 2 * margin or 0) + 2 * margin
464 -- crop to workarea size if too big
465 local workarea = capi.screen[screen].workarea
466 if width > workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
467 width = workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0)
469 if height > workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
470 height = workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0)
473 -- set size in notification object
474 notification.height = height + 2 * (border_width or 0)
475 notification.width = width + 2 * (border_width or 0)
477 -- position the wibox
478 local offset = get_offset(screen, notification.position, nil, notification.width, notification.height)
479 notification.box.ontop = ontop
480 notification.box:geometry({ width = width,
481 height = height,
482 x = offset.x,
483 y = offset.y })
484 notification.box.opacity = opacity
485 notification.box.visible = true
486 notification.idx = offset.idx
488 -- populate widgets
489 local layout = wibox.layout.fixed.horizontal()
490 if iconmargin then
491 layout:add(iconmargin)
493 layout:add(marginbox)
494 notification.box:set_widget(layout)
496 -- Setup the mouse events
497 layout:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
499 -- insert the notification to the table
500 table.insert(naughty.notifications[screen][notification.position], notification)
502 if suspended then
503 notification.box.visible = false
504 table.insert(naughty.notifications.suspended, notification)
507 -- return the notification
508 return notification
511 -- DBUS/Notification support
512 -- Notify
513 if capi.dbus then
514 capi.dbus.connect_signal("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire)
515 local args = { }
516 if data.member == "Notify" then
517 if text ~= "" then
518 args.text = text
519 if title ~= "" then
520 args.title = title
522 else
523 if title ~= "" then
524 args.text = title
525 else
526 return
529 if appname ~= "" then
530 args.appname = appname
532 for i, obj in pairs(naughty.config.mapping) do
533 local filter, preset, s = obj[1], obj[2], 0
534 if (not filter.urgency or filter.urgency == hints.urgency) and
535 (not filter.category or filter.category == hints.category) and
536 (not filter.appname or filter.appname == appname) then
537 args.preset = util.table.join(args.preset, preset)
540 local preset = args.preset or naughty.config.defaults
541 if not preset.callback or (type(preset.callback) == "function" and
542 preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
543 if icon ~= "" then
544 args.icon = icon
545 elseif hints.icon_data or hints.image_data then
546 if hints.icon_data == nil then hints.icon_data = hints.image_data end
548 -- icon_data is an array:
549 -- 1 -> width
550 -- 2 -> height
551 -- 3 -> rowstride
552 -- 4 -> has alpha
553 -- 5 -> bits per sample
554 -- 6 -> channels
555 -- 7 -> data
556 local w, h, rowstride, _, _, channels, data = unpack(hints.icon_data)
558 -- Do the arguments look sane? (e.g. we have enough data)
559 local expected_length = rowstride * (h - 1) + w * channels
560 if w < 0 or h < 0 or rowstride < 0 or (channels ~= 3 and channels ~= 4) or
561 string.len(data) < expected_length then
562 w = 0
563 h = 0
566 local format = cairo.Format[channels == 4 and 'ARGB32' or 'RGB24']
568 -- Figure out some stride magic (cairo dictates rowstride)
569 local stride = cairo.Format.stride_for_width(format, w)
570 local append = schar(0):rep(stride - 4 * w)
571 local offset = 0
573 -- Now convert each row on its own
574 local rows = {}
576 for y = 1, h do
577 local this_row = {}
579 for i = 1 + offset, w * channels + offset, channels do
580 local R, G, B, A = sbyte(data, i, i + channels - 1)
581 tins(this_row, schar(B, G, R, A or 255))
584 -- Handle rowstride, offset is stride for the input, append for output
585 tins(this_row, append)
586 tins(rows, tcat(this_row))
588 offset = offset + rowstride
591 args.icon = cairo.ImageSurface.create_for_data(tcat(rows), format,
592 w, h, stride)
594 if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
595 args.replaces_id = replaces_id
597 if expire and expire > -1 then
598 args.timeout = expire / 1000
600 local id = naughty.notify(args).id
601 return "u", id
603 return "u", "0"
604 elseif data.member == "CloseNotification" then
605 local obj = getById(appname)
606 if obj then
607 naughty.destroy(obj)
609 elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
610 -- name of notification app, name of vender, version
611 return "s", "naughty", "s", "awesome", "s", capi.awesome.version:match("%d.%d"), "s", "1.0"
612 elseif data.member == "GetCapabilities" then
613 -- We actually do display the body of the message, we support <b>, <i>
614 -- and <u> in the body and we handle static (non-animated) icons.
615 return "as", { "s", "body", "s", "body-markup", "s", "icon-static" }
617 end)
619 capi.dbus.connect_signal("org.freedesktop.DBus.Introspectable",
620 function (data, text)
621 if data.member == "Introspect" then
622 local xml = [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
623 Introspection 1.0//EN"
624 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
625 <node>
626 <interface name="org.freedesktop.DBus.Introspectable">
627 <method name="Introspect">
628 <arg name="data" direction="out" type="s"/>
629 </method>
630 </interface>
631 <interface name="org.freedesktop.Notifications">
632 <method name="GetCapabilities">
633 <arg name="caps" type="as" direction="out"/>
634 </method>
635 <method name="CloseNotification">
636 <arg name="id" type="u" direction="in"/>
637 </method>
638 <method name="Notify">
639 <arg name="app_name" type="s" direction="in"/>
640 <arg name="id" type="u" direction="in"/>
641 <arg name="icon" type="s" direction="in"/>
642 <arg name="summary" type="s" direction="in"/>
643 <arg name="body" type="s" direction="in"/>
644 <arg name="actions" type="as" direction="in"/>
645 <arg name="hints" type="a{sv}" direction="in"/>
646 <arg name="timeout" type="i" direction="in"/>
647 <arg name="return_id" type="u" direction="out"/>
648 </method>
649 <method name="GetServerInformation">
650 <arg name="return_name" type="s" direction="out"/>
651 <arg name="return_vendor" type="s" direction="out"/>
652 <arg name="return_version" type="s" direction="out"/>
653 <arg name="return_spec_version" type="s" direction="out"/>
654 </method>
655 <method name="GetServerInfo">
656 <arg name="return_name" type="s" direction="out"/>
657 <arg name="return_vendor" type="s" direction="out"/>
658 <arg name="return_version" type="s" direction="out"/>
659 </method>
660 </interface>
661 </node>]=]
662 return "s", xml
664 end)
666 -- listen for dbus notification requests
667 capi.dbus.request_name("session", "org.freedesktop.Notifications")
670 return naughty
672 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80