1 ----------------------------------------------------------------------------
2 -- @author koniu <gkusnierz@gmail.com>
3 -- @copyright 2008 koniu
4 -- @release @AWESOME_VERSION@
5 ----------------------------------------------------------------------------
13 local tostring = tostring
14 local hooks
= require("awful.hooks")
18 local capi
= { screen
= screen
}
19 local bt
= require("beautiful")
23 local AWESOME_VERSION
= AWESOME_VERSION
25 --- Notification library
28 --- Naughty configuration - a table containing common/default popup settings.
29 -- You can override some of these for individual popups using args to notify().
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
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
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.
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
92 position
= "top_right",
107 -- DBUS Notification constants
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
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
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]
142 for s
= 1, screen
.count() do
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 in {x, y} dictionary
158 local function get_offset(screen
, position
, idx
, width
, height
)
159 local ws
= capi
.screen
[screen
].workarea
161 width
= width
or notifications
[screen
][position
][idx
].width
or config
.width
164 if position
:match("left") then
165 v
.x
= ws
.x
+ config
.padding
167 v
.x
= ws
.x
+ ws
.width
- (width
+ config
.padding
)
170 -- calculate existing popups' height
172 for i
= 1, idx
-1, 1 do
173 existing
= existing
+ notifications
[screen
][position
][i
].height
+ config
.spacing
177 if position
:match("top") then
178 v
.y
= ws
.y
+ config
.padding
+ existing
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
186 destroy(notifications
[screen
][position
][1])
187 v
= get_offset(screen
, position
, idx
, width
, height
)
193 --- Re-arrange notifications according to their position and index - internal
195 local function arrange(screen
)
196 for p
,pos
in pairs(notifications
[screen
]) do
197 for i
,notification
in pairs(notifications
[screen
][p
]) do
198 local offset
= get_offset(screen
, p
, i
, notification
.width
, notification
.height
)
199 notification
.box
:geometry({ x
= offset
.x
, y
= offset
.y
, width
= notification
.width
, height
= notification
.height
})
205 --- Destroy notification by index
206 -- @param notification Notification object to be destroyed
207 -- @return True if the popup was successfully destroyed, nil otherwise
208 function destroy(notification
)
209 if notification
and notification
.box
.screen
then
210 local scr
= notification
.box
.screen
211 table.remove(notifications
[notification
.box
.screen
][notification
.position
], notification
.idx
)
212 hooks
.timer
.unregister(notification
.die
)
213 notification
.box
.screen
= nil
219 --- Get notification by ID
220 -- @param id ID of the notification
221 -- @return notification object if it was found, nil otherwise
222 local function getById(id
)
223 -- iterate the notifications to get the notfications with the correct ID
224 for s
= 1, screen
.count() do
225 for p
,pos
in pairs(notifications
[s
]) do
226 for i
,notification
in pairs(notifications
[s
][p
]) do
227 if notification
.id
== id
then
235 --- Search for an icon in specified directories with a specified format
236 -- @param icon Name of the icon
237 -- @return full path of the icon, or nil of no icon was found
238 local function getIcon(name
)
239 for d
, dir
in pairs(config
.icon_dirs
) do
240 for f
, format in pairs(config
.icon_formats
) do
241 local icon
= dir
.. name
.. "." .. format
242 if awful
.util
.file_readable(icon
) then
249 --- Create notification. args is a dictionary of optional arguments. For more information and defaults see respective fields in config table.
250 -- @param text Text of the notification
251 -- @param timeout Time in seconds after which popup expires
252 -- @param title Title of the notification
253 -- @param position Corner of the workarea the popups will appear
254 -- @param icon Path to icon
255 -- @param icon_size Desired icon size in px
256 -- @param fg Foreground color
257 -- @param bg Background color
258 -- @param screen Target screen for the notification
259 -- @param ontop Target screen for the notification
260 -- @param run Function to run on left click
261 -- @param width The popup width
262 -- @field hover_timeout Delay in seconds after which hovered popup disappears.
263 -- @param replaces_id Replace the notification with the given ID
264 -- @usage naughty.notify({ title = 'Achtung!', text = 'You\'re idling', timeout = 0 })
265 -- @return The notification object
266 function notify(args
)
267 -- gather variables together
268 local timeout
= args
.timeout
or (args
.preset
and args
.preset
.timeout
) or config
.presets
.normal
.timeout
269 local icon
= args
.icon
or (args
.preset
and args
.preset
.icon
) or config
.icon
270 local icon_size
= args
.icon_size
or (args
.preset
and args
.preset
.icon_size
) or config
.icon_size
271 local text
= tostring(args
.text
) or ""
272 local screen
= args
.screen
or (args
.preset
and args
.preset
.screen
) or config
.presets
.normal
.screen
273 local ontop
= args
.ontop
or config
.ontop
274 local width
= args
.width
or (args
.preset
and args
.preset
.width
) or config
.presets
.normal
.width
275 local hover_timeout
= args
.hover_timeout
or (args
.preset
and args
.preset
.hover_timeout
) or config
.presets
.normal
.hover_timeout
278 local beautiful
= bt
.get()
279 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"
280 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'
281 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'
282 local border_color
= (args
.preset
and args
.preset
.border_color
) or config
.presets
.normal
.border_color
or beautiful
.bg_focus
or '#535d6c'
283 local notification
= {}
285 -- replace notification if needed
286 if args
.replaces_id
then
287 obj
= getById(args
.replaces_id
)
289 -- destroy this and ...
292 -- ... may use its ID
293 if args
.replaces_id
< counter
then
294 notification
.id
= args
.replaces_id
296 counter
= counter
+ 1
297 notification
.id
= counter
300 -- get a brand new ID
301 counter
= counter
+ 1
302 notification
.id
= counter
305 notification
.position
= args
.position
or (args
.preset
and args
.preset
.position
) or config
.presets
.normal
.position
306 notification
.idx
= #notifications
[screen
][notification
.position
] + 1
309 if args
.title
then title
= tostring(args
.title
) .. "\n" end
312 local die
= function () destroy(notification
) end
313 hooks
.timer
.register(timeout
, die
)
314 notification
.die
= die
316 local run
= function ()
318 args
.run(notification
)
324 local hover_destroy
= function ()
325 if hover_timeout
== 0 then die()
326 else hooks
.timer
.register(hover_timeout
, die
) end
330 local textbox
= widget({ type = "textbox", align
= "flex" })
331 textbox
:buttons({ button({ }, 1, run
),
332 button({ }, 3, die
) })
333 textbox
:margin({ right
= config
.margin
, left
= config
.margin
})
334 textbox
.text
= string.format('<span font_desc="%s"><b>%s</b>%s</span>', font
, title
, text
)
335 if hover_timeout
then textbox
.mouse_enter
= hover_destroy
end
340 -- try to guess icon if the provided one is non-existent/readable
341 if not awful
.util
.file_readable(icon
) then icon
= getIcon(icon
) end
343 -- if we have an icon, use it
345 iconbox
= widget({ type = "imagebox", align
= "left" })
346 iconbox
:buttons({ button({ }, 1, run
), button({ }, 3, die
) })
347 local img
= image(icon
)
349 img
= img
:crop_and_scale(0,0,img
.height
,img
.width
,icon_size
,icon_size
)
351 iconbox
.resize
= false
353 if hover_timeout
then iconbox
.mouse_enter
= hover_destroy
end
357 -- create container wibox
358 notification
.box
= wibox({ position
= "floating",
361 border_color
= args
.preset
and args
.preset
.border_color
or config
.presets
.normal
.border_color
,
362 border_width
= config
.border_width
})
364 -- position the wibox
365 local lines
= 1; for i
in string.gmatch(title
..text
, "\n") do lines
= lines
+ 1 end
366 local height
= args
.preset
and args
.preset
.height
or config
.presets
.normal
.height
367 if iconbox
and iconbox
.image
.height
> lines
* height
then
368 notification
.height
= iconbox
.image
.height
370 notification
.height
= lines
* height
end
371 notification
.width
= width
372 local offset
= get_offset(screen
, notification
.position
, notification
.idx
, notification
.width
, notification
.height
)
373 notification
.box
:geometry({ width
= notification
.width
,
374 height
= notification
.height
,
377 notification
.box
.ontop
= ontop
378 notification
.box
.screen
= screen
381 notification
.box
.widgets
= { iconbox
, textbox
}
383 -- insert the notification to the table
384 table.insert(notifications
[screen
][notification
.position
], notification
)
386 -- return the notification
390 -- DBUS/Notification support
392 if awful
.hooks
.dbus
then
393 awful
.hooks
.dbus
.register("org.freedesktop.Notifications", function (data
, appname
, replaces_id
, icon
, title
, text
, actions
, hints
, expire
)
395 if data
.member
== "Notify" then
409 for i
, obj
in pairs(config
.mapping
) do
410 local filter
, preset
, s
= obj
[1], obj
[2], 0
411 if (not filter
.urgency
or filter
.urgency
== hints
.urgency
) and
412 (not filter
.category
or filter
.category
== hints
.category
) and
413 (not filter
.appname
or filter
.appname
== appname
) then
414 for j
, el
in pairs(filter
) do s
= s
+ 1 end
421 if not args
.preset
.callback
or (type(args
.preset
.callback
) == "function" and
422 args
.preset
.callback(data
, appname
, replaces_id
, icon
, title
, text
, actions
, hints
, expire
)) then
426 if replaces_id
and replaces_id
~= "" and replaces_id
~= 0 then
427 args
.replaces_id
= replaces_id
429 if expire
and expire
> -1 then
430 args
.timeout
= expire
/ 1000
432 local id
= notify(args
).id
436 elseif data
.member
== "CloseNotification" then
437 local obj
= getById(arg1
)
441 elseif data
.member
== "GetServerInfo" or data
.member
== "GetServerInformation" then
442 -- name of notification app, name of vender, version
443 return "s", "naughty", "s", "awesome", "s", AWESOME_VERSION
:match("%d.%d")
447 awful
.hooks
.dbus
.register("org.freedesktop.DBus.Introspectable",
448 function (data
, text
)
449 if data
.member
== "Introspect" then
450 local xml
= [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
451 Introspection 1.0//EN"
452 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
454 <interface name="org.freedesktop.DBus.Introspectable">
455 <method name="Introspect">
456 <arg name="data" direction="out" type="s"/>
459 <interface name="org.freedesktop.Notifications">
460 <method name="CloseNotification">
461 <arg name="id" type="u" direction="in"/>
463 <method name="Notify">
464 <arg name="app_name" type="s" direction="in"/>
465 <arg name="id" type="u" direction="in"/>
466 <arg name="icon" type="s" direction="in"/>
467 <arg name="summary" type="s" direction="in"/>
468 <arg name="body" type="s" direction="in"/>
469 <arg name="actions" type="as" direction="in"/>
470 <arg name="hints" type="a{sv}" direction="in"/>
471 <arg name="timeout" type="i" direction="in"/>
472 <arg name="return_id" type="u" direction="out"/>
474 <method name="GetServerInformation">
475 <arg name="return_name" type="s" direction="out"/>
476 <arg name="return_vendor" type="s" direction="out"/>
477 <arg name="return_version" type="s" direction="out"/>
479 <method name="GetServerInfo">
480 <arg name="return_name" type="s" direction="out"/>
481 <arg name="return_vendor" type="s" direction="out"/>
482 <arg name="return_version" type="s" direction="out"/>
490 -- listen for dbus notification requests
491 dbus
.request_name("org.freedesktop.Notifications")
494 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80