1 ----------------------------------------------------------------------------
2 -- @author koniu <gkusnierz@gmail.com>
3 -- @copyright 2008 koniu
4 -- @release @AWESOME_VERSION@
5 ----------------------------------------------------------------------------
12 local tostring = tostring
14 local capi
= { screen
= screen
,
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
34 --- Naughty configuration - a table containing common popup settings.
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.
49 -- naughty.config.notify_callback = function(args)
50 -- args.text = 'prefix: ' .. args.text
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 -- @table naughty.config.presets
65 naughty
.config
.presets
= {
77 --- Default values for the params to notify().
78 -- These can optionally be overridden by specifying a preset
79 -- @see naughty.config.presets
80 -- @see naughty.notify
81 naughty
.config
.defaults
= {
88 position
= "top_right"
91 -- DBUS Notification constants
98 --- DBUS notification to preset mapping
99 -- The first element is an object containing the filter
100 -- If the rules in the filter matches the associated preset will be applied
101 -- The rules object can contain: urgency, category, appname
102 -- The second element is the preset
104 naughty
.config
.mapping
= {
105 {{urgency
= urgency
.low
}, naughty
.config
.presets
.low
},
106 {{urgency
= urgency
.normal
}, naughty
.config
.presets
.normal
},
107 {{urgency
= urgency
.critical
}, naughty
.config
.presets
.critical
}
110 -- Counter for the notifications
111 -- Required for later access via DBUS
114 -- True if notifying is suspended
115 local suspended
= false
117 --- Index of notifications per screen and position. See config table for valid
118 -- 'position' values. Each element is a table consisting of:
119 -- @field box Wibox object containing the popup
120 -- @field height Popup height
121 -- @field width Popup width
122 -- @field die Function to be executed on timeout
123 -- @field id Unique notification id based on a counter
124 -- @table naughty.notifications
125 naughty
.notifications
= { suspended
= { } }
126 for s
= 1, capi
.screen
.count() do
127 naughty
.notifications
[s
] = {
135 --- Suspend notifications
136 function naughty
.suspend()
140 --- Resume notifications
141 function naughty
.resume()
143 for i
, v
in pairs(naughty
.notifications
.suspended
) do
145 if v
.timer
then v
.timer
:start() end
147 naughty
.notifications
.suspended
= { }
150 --- Toggle notification state
151 function naughty
.toggle()
159 -- Evaluate desired position of the notification by index - internal
160 -- @param idx Index of the notification
161 -- @param position top_right | top_left | bottom_right | bottom_left
162 -- @param height Popup height
163 -- @param width Popup width (optional)
164 -- @return Absolute position and index in { x = X, y = Y, idx = I } table
165 local function get_offset(screen
, position
, idx
, width
, height
)
166 local ws
= capi
.screen
[screen
].workarea
168 local idx
= idx
or #naughty
.notifications
[screen
][position
] + 1
169 local width
= width
or naughty
.notifications
[screen
][position
][idx
].width
172 if position
:match("left") then
173 v
.x
= ws
.x
+ naughty
.config
.padding
175 v
.x
= ws
.x
+ ws
.width
- (width
+ naughty
.config
.padding
)
178 -- calculate existing popups' height
180 for i
= 1, idx
-1, 1 do
181 existing
= existing
+ naughty
.notifications
[screen
][position
][i
].height
+ naughty
.config
.spacing
185 if position
:match("top") then
186 v
.y
= ws
.y
+ naughty
.config
.padding
+ existing
188 v
.y
= ws
.y
+ ws
.height
- (naughty
.config
.padding
+ height
+ existing
)
191 -- if positioned outside workarea, destroy oldest popup and recalculate
192 if v
.y
+ height
> ws
.y
+ ws
.height
or v
.y
< ws
.y
then
194 naughty
.destroy(naughty
.notifications
[screen
][position
][1])
195 v
= get_offset(screen
, position
, idx
, width
, height
)
197 if not v
.idx
then v
.idx
= idx
end
202 -- Re-arrange notifications according to their position and index - internal
204 local function arrange(screen
)
205 for p
,pos
in pairs(naughty
.notifications
[screen
]) do
206 for i
,notification
in pairs(naughty
.notifications
[screen
][p
]) do
207 local offset
= get_offset(screen
, p
, i
, notification
.width
, notification
.height
)
208 notification
.box
:geometry({ x
= offset
.x
, y
= offset
.y
})
209 notification
.idx
= offset
.idx
214 --- Destroy notification by notification object
215 -- @param notification Notification object to be destroyed
216 -- @return True if the popup was successfully destroyed, nil otherwise
217 function naughty
.destroy(notification
)
218 if notification
and notification
.box
.visible
then
220 for k
, v
in pairs(naughty
.notifications
.suspended
) do
221 if v
.box
== notification
.box
then
222 table.remove(naughty
.notifications
.suspended
, k
)
227 local scr
= notification
.screen
228 table.remove(naughty
.notifications
[scr
][notification
.position
], notification
.idx
)
229 if notification
.timer
then
230 notification
.timer
:stop()
232 notification
.box
.visible
= false
238 -- Get notification by ID
239 -- @param id ID of the notification
240 -- @return notification object if it was found, nil otherwise
241 local function getById(id
)
242 -- iterate the notifications to get the notfications with the correct ID
243 for s
= 1, capi
.screen
.count() do
244 for p
,pos
in pairs(naughty
.notifications
[s
]) do
245 for i
,notification
in pairs(naughty
.notifications
[s
][p
]) do
246 if notification
.id
== id
then
254 --- Create notification. args is a dictionary of (optional) arguments.
255 -- @param text Text of the notification. Default: ''
256 -- @param title Title of the notification. Default: nil
257 -- @param timeout Time in seconds after which popup expires.
258 -- Set 0 for no timeout. Default: 5
259 -- @param hover_timeout Delay in seconds after which hovered popup disappears.
261 -- @param screen Target screen for the notification. Default: 1
262 -- @param position Corner of the workarea displaying the popups.
263 -- Values: "top_right" (default), "top_left", "bottom_left", "bottom_right".
264 -- @param ontop Boolean forcing popups to display on top. Default: true
265 -- @param height Popup height. Default: nil (auto)
266 -- @param width Popup width. Default: nil (auto)
267 -- @param font Notification font. Default: beautiful.font or awesome.font
268 -- @param icon Path to icon. Default: nil
269 -- @param icon_size Desired icon size in px. Default: nil
270 -- @param fg Foreground color. Default: beautiful.fg_focus or '#ffffff'
271 -- @param bg Background color. Default: beautiful.bg_focus or '#535d6c'
272 -- @param border_width Border width. Default: 1
273 -- @param border_color Border color.
274 -- Default: beautiful.border_focus or '#535d6c'
275 -- @param run Function to run on left click. Default: nil
276 -- @param preset Table with any of the above parameters. Note: Any parameters
277 -- specified directly in args will override ones defined in the preset.
278 -- @param replaces_id Replace the notification with the given ID
279 -- @param callback function that will be called with all arguments
280 -- the notification will only be displayed if the function returns true
281 -- note: this function is only relevant to notifications sent via dbus
282 -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
283 -- @return The notification object
284 function naughty
.notify(args
)
285 if naughty
.config
.notify_callback
then
286 args
= naughty
.config
.notify_callback(args
)
287 if not args
then return end
290 -- gather variables together
291 local preset
= util
.table.join(naughty
.config
.defaults
or {},
292 args
.preset
or naughty
.config
.presets
.normal
or {})
293 local timeout
= args
.timeout
or preset
.timeout
294 local icon
= args
.icon
or preset
.icon
295 local icon_size
= args
.icon_size
or preset
.icon_size
296 local text
= args
.text
or preset
.text
297 local title
= args
.title
or preset
.title
298 local screen
= args
.screen
or preset
.screen
299 local ontop
= args
.ontop
or preset
.ontop
300 local width
= args
.width
or preset
.width
301 local height
= args
.height
or preset
.height
302 local hover_timeout
= args
.hover_timeout
or preset
.hover_timeout
303 local opacity
= args
.opacity
or preset
.opacity
304 local margin
= args
.margin
or preset
.margin
305 local border_width
= args
.border_width
or preset
.border_width
306 local position
= args
.position
or preset
.position
307 local escape_pattern
= "[<>&]"
308 local escape_subs
= { ['<'] = "<", ['>'] = ">", ['&'] = "&" }
311 local beautiful
= bt
.get()
312 local font
= args
.font
or preset
.font
or beautiful
.font
or capi
.awesome
.font
313 local fg
= args
.fg
or preset
.fg
or beautiful
.fg_normal
or '#ffffff'
314 local bg
= args
.bg
or preset
.bg
or beautiful
.bg_normal
or '#535d6c'
315 local border_color
= args
.border_color
or preset
.border_color
or beautiful
.bg_focus
or '#535d6c'
316 local notification
= { screen
= screen
}
318 -- replace notification if needed
319 if args
.replaces_id
then
320 local obj
= getById(args
.replaces_id
)
322 -- destroy this and ...
325 -- ... may use its ID
326 if args
.replaces_id
<= counter
then
327 notification
.id
= args
.replaces_id
329 counter
= counter
+ 1
330 notification
.id
= counter
333 -- get a brand new ID
334 counter
= counter
+ 1
335 notification
.id
= counter
338 notification
.position
= position
340 if title
then title
= title
.. "\n" else title
= "" end
343 local die
= function () naughty
.destroy(notification
) end
345 local timer_die
= capi
.timer
{ timeout
= timeout
}
346 timer_die
:connect_signal("timeout", die
)
347 if not suspended
then
350 notification
.timer
= timer_die
352 notification
.die
= die
354 local run
= function ()
356 args
.run(notification
)
362 local hover_destroy
= function ()
363 if hover_timeout
== 0 then
366 if notification
.timer
then notification
.timer
:stop() end
367 notification
.timer
= capi
.timer
{ timeout
= hover_timeout
}
368 notification
.timer
:connect_signal("timeout", die
)
369 notification
.timer
:start()
374 local textbox
= wibox
.widget
.textbox()
375 local marginbox
= wibox
.layout
.margin()
376 marginbox
:set_margins(margin
)
377 marginbox
:set_widget(textbox
)
378 textbox
:set_valign("middle")
379 textbox
:set_font(font
)
381 local function setMarkup(pattern
, replacements
)
382 textbox
:set_markup(string.format('<b>%s</b>%s', title
, text
:gsub(pattern
, replacements
)))
384 local function setText()
385 textbox
:set_text(string.format('%s %s', title
, text
))
388 -- Since the title cannot contain markup, it must be escaped first so that
389 -- it is not interpreted by Pango later.
390 title
= title
:gsub(escape_pattern
, escape_subs
)
391 -- Try to set the text while only interpreting <br>.
392 -- (Setting a textbox' .text to an invalid pattern throws a lua error)
393 if not pcall(setMarkup
, "<br.->", "\n") then
394 -- That failed, escape everything which might cause an error from pango
395 if not pcall(setMarkup
, escape_pattern
, escape_subs
) then
396 -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8
397 if not pcall(setText
) then
398 textbox
:set_markup("<i><Invalid markup or UTF8, cannot display message></i>")
405 local iconmargin
= nil
406 local icon_w
, icon_h
= 0, 0
408 -- Is this really an URI instead of a path?
409 if type(icon
) == "string" and string.sub(icon
, 1, 7) == "file://" then
410 icon
= string.sub(icon
, 8)
412 -- try to guess icon if the provided one is non-existent/readable
413 if type(icon
) == "string" and not util
.file_readable(icon
) then
414 icon
= util
.geticonpath(icon
, naughty
.config
.icon_formats
, naughty
.config
.icon_dirs
, icon_size
) or icon
417 -- is the icon file readable?
418 local success
, res
= pcall(function() return surface
.load_uncached(icon
) end)
422 io
.stderr
:write(string.format("naughty: Couldn't load image '%s': %s\n", tostring(icon
), res
))
426 -- if we have an icon, use it
428 iconbox
= wibox
.widget
.imagebox()
429 iconmargin
= wibox
.layout
.margin(iconbox
, margin
, margin
, margin
, margin
)
431 local scaled
= cairo
.ImageSurface(cairo
.Format
.ARGB32
, icon_size
, icon_size
)
432 local cr
= cairo
.Context(scaled
)
433 cr
:scale(icon_size
/ icon
:get_height(), icon_size
/ icon
:get_width())
434 cr
:set_source_surface(icon
, 0, 0)
438 iconbox
:set_resize(false)
439 iconbox
:set_image(icon
)
440 icon_w
= icon
:get_width()
441 icon_h
= icon
:get_height()
445 -- create container wibox
446 notification
.box
= wibox({ fg
= fg
,
448 border_color
= border_color
,
449 border_width
= border_width
,
450 type = "notification" })
452 if hover_timeout
then notification
.box
:connect_signal("mouse::enter", hover_destroy
) end
454 -- calculate the height
456 local w
, h
= textbox
:fit(-1, -1)
457 if iconbox
and icon_h
+ 2 * margin
> h
+ 2 * margin
then
458 height
= icon_h
+ 2 * margin
460 height
= h
+ 2 * margin
464 -- calculate the width
466 local w
, h
= textbox
:fit(-1, -1)
467 width
= w
+ (iconbox
and icon_w
+ 2 * margin
or 0) + 2 * margin
470 -- crop to workarea size if too big
471 local workarea
= capi
.screen
[screen
].workarea
472 if width
> workarea
.width
- 2 * (border_width
or 0) - 2 * (naughty
.config
.padding
or 0) then
473 width
= workarea
.width
- 2 * (border_width
or 0) - 2 * (naughty
.config
.padding
or 0)
475 if height
> workarea
.height
- 2 * (border_width
or 0) - 2 * (naughty
.config
.padding
or 0) then
476 height
= workarea
.height
- 2 * (border_width
or 0) - 2 * (naughty
.config
.padding
or 0)
479 -- set size in notification object
480 notification
.height
= height
+ 2 * (border_width
or 0)
481 notification
.width
= width
+ 2 * (border_width
or 0)
483 -- position the wibox
484 local offset
= get_offset(screen
, notification
.position
, nil, notification
.width
, notification
.height
)
485 notification
.box
.ontop
= ontop
486 notification
.box
:geometry({ width
= width
,
490 notification
.box
.opacity
= opacity
491 notification
.box
.visible
= true
492 notification
.idx
= offset
.idx
495 local layout
= wibox
.layout
.fixed
.horizontal()
497 layout
:add(iconmargin
)
499 layout
:add(marginbox
)
500 notification
.box
:set_widget(layout
)
502 -- Setup the mouse events
503 layout
:buttons(util
.table.join(button({ }, 1, run
), button({ }, 3, die
)))
505 -- insert the notification to the table
506 table.insert(naughty
.notifications
[screen
][notification
.position
], notification
)
509 notification
.box
.visible
= false
510 table.insert(naughty
.notifications
.suspended
, notification
)
513 -- return the notification
517 -- DBUS/Notification support
520 capi
.dbus
.connect_signal("org.freedesktop.Notifications", function (data
, appname
, replaces_id
, icon
, title
, text
, actions
, hints
, expire
)
522 if data
.member
== "Notify" then
535 if appname
~= "" then
536 args
.appname
= appname
538 for i
, obj
in pairs(naughty
.config
.mapping
) do
539 local filter
, preset
, s
= obj
[1], obj
[2], 0
540 if (not filter
.urgency
or filter
.urgency
== hints
.urgency
) and
541 (not filter
.category
or filter
.category
== hints
.category
) and
542 (not filter
.appname
or filter
.appname
== appname
) then
543 args
.preset
= util
.table.join(args
.preset
, preset
)
546 local preset
= args
.preset
or naughty
.config
.defaults
547 if not preset
.callback
or (type(preset
.callback
) == "function" and
548 preset
.callback(data
, appname
, replaces_id
, icon
, title
, text
, actions
, hints
, expire
)) then
551 elseif hints
.icon_data
or hints
.image_data
then
552 if hints
.icon_data
== nil then hints
.icon_data
= hints
.image_data
end
554 -- icon_data is an array:
559 -- 5 -> bits per sample
562 local w
, h
, rowstride
, _
, _
, channels
, data
= unpack(hints
.icon_data
)
564 -- Do the arguments look sane? (e.g. we have enough data)
565 local expected_length
= rowstride
* (h
- 1) + w
* channels
566 if w
< 0 or h
< 0 or rowstride
< 0 or (channels
~= 3 and channels
~= 4) or
567 string.len(data
) < expected_length
then
572 local format = cairo
.Format
[channels
== 4 and 'ARGB32' or 'RGB24']
574 -- Figure out some stride magic (cairo dictates rowstride)
575 local stride
= cairo
.Format
.stride_for_width(format, w
)
576 local append
= schar(0):rep(stride
- 4 * w
)
579 -- Now convert each row on its own
585 for i
= 1 + offset
, w
* channels
+ offset
, channels
do
586 local R
, G
, B
, A
= sbyte(data
, i
, i
+ channels
- 1)
587 tins(this_row
, schar(B
, G
, R
, A
or 255))
590 -- Handle rowstride, offset is stride for the input, append for output
591 tins(this_row
, append
)
592 tins(rows
, tcat(this_row
))
594 offset
= offset
+ rowstride
597 args
.icon
= cairo
.ImageSurface
.create_for_data(tcat(rows
), format,
600 if replaces_id
and replaces_id
~= "" and replaces_id
~= 0 then
601 args
.replaces_id
= replaces_id
603 if expire
and expire
> -1 then
604 args
.timeout
= expire
/ 1000
606 local id
= naughty
.notify(args
).id
610 elseif data
.member
== "CloseNotification" then
611 local obj
= getById(appname
)
615 elseif data
.member
== "GetServerInfo" or data
.member
== "GetServerInformation" then
616 -- name of notification app, name of vender, version
617 return "s", "naughty", "s", "awesome", "s", capi
.awesome
.version
:match("%d.%d"), "s", "1.0"
618 elseif data
.member
== "GetCapabilities" then
619 -- We actually do display the body of the message, we support <b>, <i>
620 -- and <u> in the body and we handle static (non-animated) icons.
621 return "as", { "s", "body", "s", "body-markup", "s", "icon-static" }
625 capi
.dbus
.connect_signal("org.freedesktop.DBus.Introspectable",
626 function (data
, text
)
627 if data
.member
== "Introspect" then
628 local xml
= [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
629 Introspection 1.0//EN"
630 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
632 <interface name="org.freedesktop.DBus.Introspectable">
633 <method name="Introspect">
634 <arg name="data" direction="out" type="s"/>
637 <interface name="org.freedesktop.Notifications">
638 <method name="GetCapabilities">
639 <arg name="caps" type="as" direction="out"/>
641 <method name="CloseNotification">
642 <arg name="id" type="u" direction="in"/>
644 <method name="Notify">
645 <arg name="app_name" type="s" direction="in"/>
646 <arg name="id" type="u" direction="in"/>
647 <arg name="icon" type="s" direction="in"/>
648 <arg name="summary" type="s" direction="in"/>
649 <arg name="body" type="s" direction="in"/>
650 <arg name="actions" type="as" direction="in"/>
651 <arg name="hints" type="a{sv}" direction="in"/>
652 <arg name="timeout" type="i" direction="in"/>
653 <arg name="return_id" type="u" direction="out"/>
655 <method name="GetServerInformation">
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 <arg name="return_spec_version" type="s" direction="out"/>
661 <method name="GetServerInfo">
662 <arg name="return_name" type="s" direction="out"/>
663 <arg name="return_vendor" type="s" direction="out"/>
664 <arg name="return_version" type="s" direction="out"/>
672 -- listen for dbus notification requests
673 capi
.dbus
.request_name("session", "org.freedesktop.Notifications")
678 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80