summaryrefslogtreecommitdiff
path: root/lib/naughty
diff options
context:
space:
mode:
authorache <ache@ache.one>2017-03-13 23:17:19 +0100
committerache <ache@ache.one>2017-03-13 23:17:19 +0100
commit22d656903563f75678f3634964731ccf93355dfd (patch)
treee3cb6279d95c9764093072d5e946566ea6533799 /lib/naughty
Init commit
Diffstat (limited to 'lib/naughty')
-rw-r--r--lib/naughty/core.lua688
-rw-r--r--lib/naughty/dbus.lua261
-rw-r--r--lib/naughty/init.lua14
3 files changed, 963 insertions, 0 deletions
diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua
new file mode 100644
index 0000000..764afe2
--- /dev/null
+++ b/lib/naughty/core.lua
@@ -0,0 +1,688 @@
+----------------------------------------------------------------------------
+--- Notification library
+--
+-- @author koniu &lt;gkusnierz@gmail.com&gt;
+-- @copyright 2008 koniu
+-- @module naughty
+----------------------------------------------------------------------------
+
+-- Package environment
+local pairs = pairs
+local table = table
+local type = type
+local string = string
+local pcall = pcall
+local capi = { screen = screen,
+ awesome = awesome }
+local timer = require("gears.timer")
+local button = require("awful.button")
+local screen = require("awful.screen")
+local util = require("awful.util")
+local bt = require("beautiful")
+local wibox = require("wibox")
+local surface = require("gears.surface")
+local cairo = require("lgi").cairo
+local dpi = require("beautiful").xresources.apply_dpi
+
+local function get_screen(s)
+ return s and capi.screen[s]
+end
+
+local naughty = {}
+
+--[[--
+Naughty configuration - a table containing common popup settings.
+
+@table naughty.config
+@tfield[opt=apply_dpi(4)] int padding Space between popups and edge of the
+ workarea.
+@tfield[opt=apply_dpi(1)] int spacing Spacing between popups.
+@tfield[opt={"/usr/share/pixmaps/"}] table icon_dirs List of directories
+ that will be checked by `getIcon()`.
+@tfield[opt={ "png", "gif" }] table icon_formats List of formats that will be
+ checked by `getIcon()`.
+@tfield[opt] function notify_callback Callback used to modify or reject
+notifications, e.g.
+ naughty.config.notify_callback = function(args)
+ args.text = 'prefix: ' .. args.text
+ return args
+ end
+
+@tfield table presets Notification presets. See `config.presets`.
+
+@tfield table defaults Default values for the params to `notify()`. These can
+ optionally be overridden by specifying a preset. See `config.defaults`.
+
+--]]
+--
+naughty.config = {
+ padding = dpi(4),
+ spacing = dpi(1),
+ icon_dirs = { "/usr/share/pixmaps/", },
+ icon_formats = { "png", "gif" },
+ notify_callback = nil,
+}
+
+--- Notification presets for `naughty.notify`.
+-- This holds presets for different purposes. A preset is a table of any
+-- parameters for `notify()`, overriding the default values
+-- (`naughty.config.defaults`).
+--
+-- You have to pass a reference of a preset in your `notify()` as the `preset`
+-- argument.
+--
+-- The presets `"low"`, `"normal"` and `"critical"` are used for notifications
+-- over DBUS.
+--
+-- @table config.presets
+-- @tfield table low The preset for notifications with low urgency level.
+-- @tfield[opt=5] int low.timeout
+-- @tfield[opt=empty] table normal The default preset for every notification without a
+-- preset that will also be used for normal urgency level.
+-- @tfield table critical The preset for notifications with a critical urgency
+-- level.
+-- @tfield[opt="#ff0000"] string critical.bg
+-- @tfield[opt="#ffffff"] string critical.fg
+-- @tfield[opt=0] string critical.timeout
+naughty.config.presets = {
+ low = {
+ timeout = 5
+ },
+ normal = {},
+ critical = {
+ bg = "#ff0000",
+ fg = "#ffffff",
+ timeout = 0,
+ }
+}
+
+--- Defaults for `naughty.notify`.
+--
+-- @table config.defaults
+-- @tfield[opt=5] int timeout
+-- @tfield[opt=""] string text
+-- @tfield[opt] int screen Defaults to `awful.screen.focused`.
+-- @tfield[opt=true] boolean ontop
+-- @tfield[opt=apply_dpi(5)] int margin
+-- @tfield[opt=apply_dpi(1)] int border_width
+-- @tfield[opt="top_right"] string position
+naughty.config.defaults = {
+ timeout = 5,
+ text = "",
+ screen = nil,
+ ontop = true,
+ margin = dpi(5),
+ border_width = dpi(1),
+ position = "top_right"
+}
+
+naughty.notificationClosedReason = {
+ silent = -1,
+ expired = 1,
+ dismissedByUser = 2,
+ dismissedByCommand = 3,
+ undefined = 4
+}
+
+-- Counter for the notifications
+-- Required for later access via DBUS
+local counter = 1
+
+-- True if notifying is suspended
+local suspended = false
+
+--- Index of notifications per screen and position.
+-- See config table for valid 'position' values.
+-- Each element is a table consisting of:
+--
+-- @field box Wibox object containing the popup
+-- @field height Popup height
+-- @field width Popup width
+-- @field die Function to be executed on timeout
+-- @field id Unique notification id based on a counter
+-- @table notifications
+naughty.notifications = { suspended = { } }
+screen.connect_for_each_screen(function(s)
+ naughty.notifications[s] = {
+ top_left = {},
+ top_middle = {},
+ top_right = {},
+ bottom_left = {},
+ bottom_middle = {},
+ bottom_right = {},
+ }
+end)
+
+capi.screen.connect_signal("removed", function(scr)
+ -- Destroy all notifications on this screen
+ for _, list in pairs(naughty.notifications[scr]) do
+ while #list > 0 do
+ naughty.destroy(list[1])
+ end
+ end
+ naughty.notifications[scr] = nil
+end)
+
+--- Notification state
+function naughty.is_suspended()
+ return suspended
+end
+
+--- Suspend notifications
+function naughty.suspend()
+ suspended = true
+end
+
+--- Resume notifications
+function naughty.resume()
+ suspended = false
+ for _, v in pairs(naughty.notifications.suspended) do
+ v.box.visible = true
+ if v.timer then v.timer:start() end
+ end
+ naughty.notifications.suspended = { }
+end
+
+--- Toggle notification state
+function naughty.toggle()
+ if suspended then
+ naughty.resume()
+ else
+ naughty.suspend()
+ end
+end
+
+--- Evaluate desired position of the notification by index - internal
+--
+-- @param s Screen to use
+-- @param position top_right | top_left | bottom_right | bottom_left
+-- | top_middle | bottom_middle
+-- @param idx Index of the notification
+-- @param[opt] width Popup width.
+-- @param height Popup height
+-- @return Absolute position and index in { x = X, y = Y, idx = I } table
+local function get_offset(s, position, idx, width, height)
+ s = get_screen(s)
+ local ws = s.workarea
+ local v = {}
+ idx = idx or #naughty.notifications[s][position] + 1
+ width = width or naughty.notifications[s][position][idx].width
+
+ -- calculate x
+ if position:match("left") then
+ v.x = ws.x + naughty.config.padding
+ elseif position:match("middle") then
+ v.x = (ws.width / 2) - (width / 2)
+ else
+ v.x = ws.x + ws.width - (width + naughty.config.padding)
+ end
+
+ -- calculate existing popups' height
+ local existing = 0
+ for i = 1, idx-1, 1 do
+ existing = existing + naughty.notifications[s][position][i].height + naughty.config.spacing
+ end
+
+ -- calculate y
+ if position:match("top") then
+ v.y = ws.y + naughty.config.padding + existing
+ else
+ v.y = ws.y + ws.height - (naughty.config.padding + height + existing)
+ end
+
+ -- Find old notification to replace in case there is not enough room.
+ -- This tries to skip permanent notifications (without a timeout),
+ -- e.g. critical ones.
+ local find_old_to_replace = function()
+ for i = 1, idx-1 do
+ local n = naughty.notifications[s][position][i]
+ if n.timeout > 0 then
+ return n
+ end
+ end
+ -- Fallback to first one.
+ return naughty.notifications[s][position][1]
+ end
+
+ -- if positioned outside workarea, destroy oldest popup and recalculate
+ if v.y + height > ws.y + ws.height or v.y < ws.y then
+ naughty.destroy(find_old_to_replace())
+ idx = idx - 1
+ v = get_offset(s, position, idx, width, height)
+ end
+ if not v.idx then v.idx = idx end
+
+ return v
+end
+
+--- Re-arrange notifications according to their position and index - internal
+--
+-- @return None
+local function arrange(s)
+ for p in pairs(naughty.notifications[s]) do
+ for i,notification in pairs(naughty.notifications[s][p]) do
+ local offset = get_offset(s, p, i, notification.width, notification.height)
+ notification.box:geometry({ x = offset.x, y = offset.y })
+ notification.idx = offset.idx
+ end
+ end
+end
+
+--- Destroy notification by notification object
+--
+-- @param notification Notification object to be destroyed
+-- @param reason One of the reasons from notificationClosedReason
+-- @return True if the popup was successfully destroyed, nil otherwise
+function naughty.destroy(notification, reason)
+ if notification and notification.box.visible then
+ if suspended then
+ for k, v in pairs(naughty.notifications.suspended) do
+ if v.box == notification.box then
+ table.remove(naughty.notifications.suspended, k)
+ break
+ end
+ end
+ end
+ local scr = notification.screen
+ table.remove(naughty.notifications[scr][notification.position], notification.idx)
+ if notification.timer then
+ notification.timer:stop()
+ end
+ notification.box.visible = false
+ arrange(scr)
+ if notification.destroy_cb and reason ~= naughty.notificationClosedReason.silent then
+ notification.destroy_cb(reason or naughty.notificationClosedReason.undefined)
+ end
+ return true
+ end
+end
+
+--- Get notification by ID
+--
+-- @param id ID of the notification
+-- @return notification object if it was found, nil otherwise
+function naughty.getById(id)
+ -- iterate the notifications to get the notfications with the correct ID
+ for s in pairs(naughty.notifications) do
+ for p in pairs(naughty.notifications[s]) do
+ for _, notification in pairs(naughty.notifications[s][p]) do
+ if notification.id == id then
+ return notification
+ end
+ end
+ end
+ end
+end
+
+--- Install expiration timer for notification object.
+-- @tparam notification notification Notification object.
+-- @tparam number timeout Time in seconds to be set as expiration timeout.
+local function set_timeout(notification, timeout)
+ local die = function (reason)
+ naughty.destroy(notification, reason)
+ end
+ if timeout > 0 then
+ local timer_die = timer { timeout = timeout }
+ timer_die:connect_signal("timeout", function() die(naughty.notificationClosedReason.expired) end)
+ if not suspended then
+ timer_die:start()
+ end
+ notification.timer = timer_die
+ end
+ notification.die = die
+end
+
+--- Set new notification timeout.
+-- @tparam notification notification Notification object, which timer is to be reset.
+-- @tparam number new_timeout Time in seconds after which notification disappears.
+-- @return None.
+function naughty.reset_timeout(notification, new_timeout)
+ if notification.timer then notification.timer:stop() end
+
+ local timeout = new_timeout or notification.timeout
+ set_timeout(notification, timeout)
+ notification.timeout = timeout
+
+ notification.timer:start()
+end
+
+--- Escape and set title and text for notification object.
+-- @tparam notification notification Notification object.
+-- @tparam string title Title of notification.
+-- @tparam string text Main text of notification.
+-- @return None.
+local function set_text(notification, title, text)
+ local escape_pattern = "[<>&]"
+ local escape_subs = { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }
+
+ local textbox = notification.textbox
+
+ local function setMarkup(pattern, replacements)
+ return textbox:set_markup_silently(string.format('<b>%s</b>%s', title, text:gsub(pattern, replacements)))
+ end
+ local function setText()
+ textbox:set_text(string.format('%s %s', title, text))
+ end
+
+ -- Since the title cannot contain markup, it must be escaped first so that
+ -- it is not interpreted by Pango later.
+ title = title:gsub(escape_pattern, escape_subs)
+ -- Try to set the text while only interpreting <br>.
+ if not setMarkup("<br.->", "\n") then
+ -- That failed, escape everything which might cause an error from pango
+ if not setMarkup(escape_pattern, escape_subs) then
+ -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8
+ if not pcall(setText) then
+ textbox:set_markup("<i>&lt;Invalid markup or UTF8, cannot display message&gt;</i>")
+ end
+ end
+ end
+end
+
+--- Replace title and text of an existing notification.
+-- @tparam notification notification Notification object, which contents are to be replaced.
+-- @tparam string new_title New title of notification. If not specified, old title remains unchanged.
+-- @tparam string new_text New text of notification. If not specified, old text remains unchanged.
+-- @return None.
+function naughty.replace_text(notification, new_title, new_text)
+ local title = new_title
+
+ if title then title = title .. "\n" else title = "" end
+
+ set_text(notification, title, new_text)
+end
+
+--- Create a notification.
+--
+-- @tab args The argument table containing any of the arguments below.
+-- @string[opt=""] args.text Text of the notification.
+-- @string[opt] args.title Title of the notification.
+-- @int[opt=5] args.timeout Time in seconds after which popup expires.
+-- Set 0 for no timeout.
+-- @int[opt] args.hover_timeout Delay in seconds after which hovered popup disappears.
+-- @tparam[opt=focused] integer|screen args.screen Target screen for the notification.
+-- @string[opt="top_right"] args.position Corner of the workarea displaying the popups.
+-- Values: `"top_right"`, `"top_left"`, `"bottom_left"`,
+-- `"bottom_right"`, `"top_middle"`, `"bottom_middle"`.
+-- @bool[opt=true] args.ontop Boolean forcing popups to display on top.
+-- @int[opt=auto] args.height Popup height.
+-- @int[opt=auto] args.width Popup width.
+-- @string[opt=beautiful.font or awesome.font] args.font Notification font.
+-- @string[opt] args.icon Path to icon.
+-- @int[opt] args.icon_size Desired icon size in px.
+-- @string[opt=`beautiful.fg_focus` or `'#ffffff'`] args.fg Foreground color.
+-- @string[opt=`beautiful.bg_focus` or `'#535d6c'`] args.bg Background color.
+-- @int[opt=1] args.border_width Border width.
+-- @string[opt=`beautiful.border_focus` or `'#535d6c'`] args.border_color Border color.
+-- @tparam[opt] func args.run Function to run on left click. The notification
+-- object will be passed to it as an argument.
+-- You need to call e.g.
+-- `notification.die(naughty.notificationClosedReason.dismissedByUser)` from
+-- there to dismiss the notification yourself.
+-- @tparam[opt] func args.destroy Function to run when notification is destroyed.
+-- @tparam[opt] table args.preset Table with any of the above parameters.
+-- Note: Any parameters specified directly in args will override ones defined
+-- in the preset.
+-- @tparam[opt] int args.replaces_id Replace the notification with the given ID.
+-- @tparam[opt] func args.callback Function that will be called with all arguments.
+-- The notification will only be displayed if the function returns true.
+-- Note: this function is only relevant to notifications sent via dbus.
+-- @tparam[opt] table args.actions Mapping that maps a string to a callback when this
+-- action is selected.
+-- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
+-- @treturn ?table The notification object, or nil in case a notification was
+-- not displayed.
+function naughty.notify(args)
+ if naughty.config.notify_callback then
+ args = naughty.config.notify_callback(args)
+ if not args then return end
+ end
+
+ -- gather variables together
+ local preset = util.table.join(naughty.config.defaults or {},
+ args.preset or naughty.config.presets.normal or {})
+ local timeout = args.timeout or preset.timeout
+ local icon = args.icon or preset.icon
+ local icon_size = args.icon_size or preset.icon_size
+ local text = args.text or preset.text
+ local title = args.title or preset.title
+ local s = get_screen(args.screen or preset.screen or screen.focused())
+ if not s then
+ local err = "naughty.notify: there is no screen available to display the following notification:"
+ err = string.format("%s title='%s' text='%s'", err, tostring(title or ""), tostring(text or ""))
+ require("gears.debug").print_warning(err)
+ return
+ end
+ local ontop = args.ontop or preset.ontop
+ local width = args.width or preset.width
+ local height = args.height or preset.height
+ local hover_timeout = args.hover_timeout or preset.hover_timeout
+ local opacity = args.opacity or preset.opacity
+ local margin = args.margin or preset.margin
+ local border_width = args.border_width or preset.border_width
+ local position = args.position or preset.position
+ local actions = args.actions
+ local destroy_cb = args.destroy
+
+ -- beautiful
+ local beautiful = bt.get()
+ local font = args.font or preset.font or beautiful.font or capi.awesome.font
+ local fg = args.fg or preset.fg or beautiful.fg_normal or '#ffffff'
+ local bg = args.bg or preset.bg or beautiful.bg_normal or '#535d6c'
+ local border_color = args.border_color or preset.border_color or beautiful.bg_focus or '#535d6c'
+ local notification = { screen = s, destroy_cb = destroy_cb, timeout = timeout }
+
+ -- replace notification if needed
+ if args.replaces_id then
+ local obj = naughty.getById(args.replaces_id)
+ if obj then
+ -- destroy this and ...
+ naughty.destroy(obj, naughty.notificationClosedReason.silent)
+ end
+ -- ... may use its ID
+ if args.replaces_id <= counter then
+ notification.id = args.replaces_id
+ else
+ counter = counter + 1
+ notification.id = counter
+ end
+ else
+ -- get a brand new ID
+ counter = counter + 1
+ notification.id = counter
+ end
+
+ notification.position = position
+
+ if title then title = title .. "\n" else title = "" end
+
+ -- hook destroy
+ set_timeout(notification, timeout)
+ local die = notification.die
+
+ local run = function ()
+ if args.run then
+ args.run(notification)
+ else
+ die(naughty.notificationClosedReason.dismissedByUser)
+ end
+ end
+
+ local hover_destroy = function ()
+ if hover_timeout == 0 then
+ die(naughty.notificationClosedReason.expired)
+ else
+ if notification.timer then notification.timer:stop() end
+ notification.timer = timer { timeout = hover_timeout }
+ notification.timer:connect_signal("timeout", function() die(naughty.notificationClosedReason.expired) end)
+ notification.timer:start()
+ end
+ end
+
+ -- create textbox
+ local textbox = wibox.widget.textbox()
+ local marginbox = wibox.container.margin()
+ marginbox:set_margins(margin)
+ marginbox:set_widget(textbox)
+ textbox:set_valign("middle")
+ textbox:set_font(font)
+
+ notification.textbox = textbox
+
+ set_text(notification, title, text)
+
+ local actionslayout = wibox.layout.fixed.vertical()
+ local actions_max_width = 0
+ local actions_total_height = 0
+ if actions then
+ for action, callback in pairs(actions) do
+ local actiontextbox = wibox.widget.textbox()
+ local actionmarginbox = wibox.container.margin()
+ actionmarginbox:set_margins(margin)
+ actionmarginbox:set_widget(actiontextbox)
+ actiontextbox:set_valign("middle")
+ actiontextbox:set_font(font)
+ actiontextbox:set_markup(string.format('☛ <u>%s</u>', action))
+ -- calculate the height and width
+ local w, h = actiontextbox:get_preferred_size(s)
+ local action_height = h + 2 * margin
+ local action_width = w + 2 * margin
+
+ actionmarginbox:buttons(util.table.join(
+ button({ }, 1, callback),
+ button({ }, 3, callback)
+ ))
+ actionslayout:add(actionmarginbox)
+
+ actions_total_height = actions_total_height + action_height
+ if actions_max_width < action_width then
+ actions_max_width = action_width
+ end
+ end
+ end
+
+ -- create iconbox
+ local iconbox = nil
+ local iconmargin = nil
+ local icon_w, icon_h = 0, 0
+ if icon then
+ -- Is this really an URI instead of a path?
+ if type(icon) == "string" and string.sub(icon, 1, 7) == "file://" then
+ icon = string.sub(icon, 8)
+ end
+ -- try to guess icon if the provided one is non-existent/readable
+ if type(icon) == "string" and not util.file_readable(icon) then
+ icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size) or icon
+ end
+
+ -- is the icon file readable?
+ icon = surface.load_uncached(icon)
+
+ -- if we have an icon, use it
+ if icon then
+ iconbox = wibox.widget.imagebox()
+ iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin)
+ if icon_size then
+ local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size)
+ local cr = cairo.Context(scaled)
+ cr:scale(icon_size / icon:get_height(), icon_size / icon:get_width())
+ cr:set_source_surface(icon, 0, 0)
+ cr:paint()
+ icon = scaled
+ end
+ iconbox:set_resize(false)
+ iconbox:set_image(icon)
+ icon_w = icon:get_width()
+ icon_h = icon:get_height()
+ end
+ end
+
+ -- create container wibox
+ notification.box = wibox({ fg = fg,
+ bg = bg,
+ border_color = border_color,
+ border_width = border_width,
+ type = "notification" })
+
+ if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end
+
+ -- calculate the width
+ if not width then
+ local w, _ = textbox:get_preferred_size(s)
+ width = w + (iconbox and icon_w + 2 * margin or 0) + 2 * margin
+ end
+
+ if width < actions_max_width then
+ width = actions_max_width
+ end
+
+ -- calculate the height
+ if not height then
+ local w = width - (iconbox and icon_w + 2 * margin or 0) - 2 * margin
+ local h = textbox:get_height_for_width(w, s)
+ if iconbox and icon_h + 2 * margin > h + 2 * margin then
+ height = icon_h + 2 * margin
+ else
+ height = h + 2 * margin
+ end
+ end
+
+ height = height + actions_total_height
+
+ -- crop to workarea size if too big
+ local workarea = s.workarea
+ if width > workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
+ width = workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0)
+ end
+ if height > workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
+ height = workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0)
+ end
+
+ -- set size in notification object
+ notification.height = height + 2 * (border_width or 0)
+ notification.width = width + 2 * (border_width or 0)
+
+ -- position the wibox
+ local offset = get_offset(s, notification.position, nil, notification.width, notification.height)
+ notification.box.ontop = ontop
+ notification.box:geometry({ width = width,
+ height = height,
+ x = offset.x,
+ y = offset.y })
+ notification.box.opacity = opacity
+ notification.box.visible = true
+ notification.idx = offset.idx
+
+ -- populate widgets
+ local layout = wibox.layout.fixed.horizontal()
+ if iconmargin then
+ layout:add(iconmargin)
+ end
+ layout:add(marginbox)
+
+ local completelayout = wibox.layout.fixed.vertical()
+ completelayout:add(layout)
+ completelayout:add(actionslayout)
+ notification.box:set_widget(completelayout)
+
+ -- Setup the mouse events
+ layout:buttons(util.table.join(button({ }, 1, run),
+ button({ }, 3, function()
+ die(naughty.notificationClosedReason.dismissedByUser)
+ end)))
+
+ -- insert the notification to the table
+ table.insert(naughty.notifications[s][notification.position], notification)
+
+ if suspended then
+ notification.box.visible = false
+ table.insert(naughty.notifications.suspended, notification)
+ end
+
+ -- return the notification
+ return notification
+end
+
+return naughty
+
+-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua
new file mode 100644
index 0000000..c49dc7e
--- /dev/null
+++ b/lib/naughty/dbus.lua
@@ -0,0 +1,261 @@
+---------------------------------------------------------------------------
+-- DBUS/Notification support
+-- Notify
+--
+-- @author koniu &lt;gkusnierz@gmail.com&gt;
+-- @copyright 2008 koniu
+-- @module naughty.dbus
+---------------------------------------------------------------------------
+
+assert(dbus)
+
+-- Package environment
+local pairs = pairs
+local type = type
+local string = string
+local capi = { awesome = awesome,
+ dbus = dbus }
+local util = require("awful.util")
+local cairo = require("lgi").cairo
+
+local schar = string.char
+local sbyte = string.byte
+local tcat = table.concat
+local tins = table.insert
+local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
+local naughty = require("naughty.core")
+
+--- Notification library, dbus bindings
+local dbus = { config = {} }
+
+-- DBUS Notification constants
+local urgency = {
+ low = "\0",
+ normal = "\1",
+ critical = "\2"
+}
+
+--- DBUS notification to preset mapping.
+-- The first element is an object containing the filter.
+-- If the rules in the filter match, the associated preset will be applied.
+-- The rules object can contain the following keys: urgency, category, appname.
+-- The second element is the preset.
+-- @tfield table 1 low urgency
+-- @tfield table 2 normal urgency
+-- @tfield table 3 critical urgency
+-- @table config.mapping
+dbus.config.mapping = {
+ {{urgency = urgency.low}, naughty.config.presets.low},
+ {{urgency = urgency.normal}, naughty.config.presets.normal},
+ {{urgency = urgency.critical}, naughty.config.presets.critical}
+}
+
+local function sendActionInvoked(notificationId, action)
+ if capi.dbus then
+ capi.dbus.emit_signal("session", "/org/freedesktop/Notifications",
+ "org.freedesktop.Notifications", "ActionInvoked",
+ "u", notificationId,
+ "s", action)
+ end
+end
+
+local function sendNotificationClosed(notificationId, reason)
+ if capi.dbus then
+ capi.dbus.emit_signal("session", "/org/freedesktop/Notifications",
+ "org.freedesktop.Notifications", "NotificationClosed",
+ "u", notificationId,
+ "u", reason)
+ end
+end
+
+local function convert_icon(w, h, rowstride, channels, data)
+ -- Do the arguments look sane? (e.g. we have enough data)
+ local expected_length = rowstride * (h - 1) + w * channels
+ if w < 0 or h < 0 or rowstride < 0 or (channels ~= 3 and channels ~= 4) or
+ string.len(data) < expected_length then
+ w = 0
+ h = 0
+ end
+
+ local format = cairo.Format[channels == 4 and 'ARGB32' or 'RGB24']
+
+ -- Figure out some stride magic (cairo dictates rowstride)
+ local stride = cairo.Format.stride_for_width(format, w)
+ local append = schar(0):rep(stride - 4 * w)
+ local offset = 0
+
+ -- Now convert each row on its own
+ local rows = {}
+
+ for _ = 1, h do
+ local this_row = {}
+
+ for i = 1 + offset, w * channels + offset, channels do
+ local R, G, B, A = sbyte(data, i, i + channels - 1)
+ tins(this_row, schar(B, G, R, A or 255))
+ end
+
+ -- Handle rowstride, offset is stride for the input, append for output
+ tins(this_row, append)
+ tins(rows, tcat(this_row))
+
+ offset = offset + rowstride
+ end
+
+ return cairo.ImageSurface.create_for_data(tcat(rows), format, w, h, stride)
+end
+
+capi.dbus.connect_signal("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire)
+ local args = { }
+ if data.member == "Notify" then
+ if text ~= "" then
+ args.text = text
+ if title ~= "" then
+ args.title = title
+ end
+ else
+ if title ~= "" then
+ args.text = title
+ else
+ return
+ end
+ end
+ if appname ~= "" then
+ args.appname = appname
+ end
+ for _, obj in pairs(dbus.config.mapping) do
+ local filter, preset = obj[1], obj[2]
+ if (not filter.urgency or filter.urgency == hints.urgency) and
+ (not filter.category or filter.category == hints.category) and
+ (not filter.appname or filter.appname == appname) then
+ args.preset = util.table.join(args.preset, preset)
+ end
+ end
+ local preset = args.preset or naughty.config.defaults
+ local notification
+ if actions then
+ args.actions = {}
+
+ for i = 1,#actions,2 do
+ local action_id = actions[i]
+ local action_text = actions[i + 1]
+
+ if action_id == "default" then
+ args.run = function()
+ sendActionInvoked(notification.id, "default")
+ naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser)
+ end
+ elseif action_id ~= nil and action_text ~= nil then
+ args.actions[action_text] = function()
+ sendActionInvoked(notification.id, action_id)
+ naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser)
+ end
+ end
+ end
+ end
+ args.destroy = function(reason)
+ sendNotificationClosed(notification.id, reason)
+ end
+ if not preset.callback or (type(preset.callback) == "function" and
+ preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
+ if icon ~= "" then
+ args.icon = icon
+ elseif hints.icon_data or hints.image_data then
+ if hints.icon_data == nil then hints.icon_data = hints.image_data end
+
+ -- icon_data is an array:
+ -- 1 -> width
+ -- 2 -> height
+ -- 3 -> rowstride
+ -- 4 -> has alpha
+ -- 5 -> bits per sample
+ -- 6 -> channels
+ -- 7 -> data
+ local w, h, rowstride, _, _, channels, icon_data = unpack(hints.icon_data)
+ args.icon = convert_icon(w, h, rowstride, channels, icon_data)
+ end
+ if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
+ args.replaces_id = replaces_id
+ end
+ if expire and expire > -1 then
+ args.timeout = expire / 1000
+ end
+ notification = naughty.notify(args)
+ return "u", notification.id
+ end
+ return "u", "0"
+ elseif data.member == "CloseNotification" then
+ local obj = naughty.getById(appname)
+ if obj then
+ naughty.destroy(obj, naughty.notificationClosedReason.dismissedByCommand)
+ end
+ elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
+ -- name of notification app, name of vender, version, specification version
+ return "s", "naughty", "s", "awesome", "s", capi.awesome.version, "s", "1.0"
+ elseif data.member == "GetCapabilities" then
+ -- We actually do display the body of the message, we support <b>, <i>
+ -- and <u> in the body and we handle static (non-animated) icons.
+ return "as", { "s", "body", "s", "body-markup", "s", "icon-static", "s", "actions" }
+ end
+end)
+
+capi.dbus.connect_signal("org.freedesktop.DBus.Introspectable", function (data)
+ if data.member == "Introspect" then
+ local xml = [=[<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object
+ Introspection 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+ <node>
+ <interface name="org.freedesktop.DBus.Introspectable">
+ <method name="Introspect">
+ <arg name="data" direction="out" type="s"/>
+ </method>
+ </interface>
+ <interface name="org.freedesktop.Notifications">
+ <method name="GetCapabilities">
+ <arg name="caps" type="as" direction="out"/>
+ </method>
+ <method name="CloseNotification">
+ <arg name="id" type="u" direction="in"/>
+ </method>
+ <method name="Notify">
+ <arg name="app_name" type="s" direction="in"/>
+ <arg name="id" type="u" direction="in"/>
+ <arg name="icon" type="s" direction="in"/>
+ <arg name="summary" type="s" direction="in"/>
+ <arg name="body" type="s" direction="in"/>
+ <arg name="actions" type="as" direction="in"/>
+ <arg name="hints" type="a{sv}" direction="in"/>
+ <arg name="timeout" type="i" direction="in"/>
+ <arg name="return_id" type="u" direction="out"/>
+ </method>
+ <method name="GetServerInformation">
+ <arg name="return_name" type="s" direction="out"/>
+ <arg name="return_vendor" type="s" direction="out"/>
+ <arg name="return_version" type="s" direction="out"/>
+ <arg name="return_spec_version" type="s" direction="out"/>
+ </method>
+ <method name="GetServerInfo">
+ <arg name="return_name" type="s" direction="out"/>
+ <arg name="return_vendor" type="s" direction="out"/>
+ <arg name="return_version" type="s" direction="out"/>
+ </method>
+ <signal name="NotificationClosed">
+ <arg name="id" type="u" direction="out"/>
+ <arg name="reason" type="u" direction="out"/>
+ </signal>
+ <signal name="ActionInvoked">
+ <arg name="id" type="u" direction="out"/>
+ <arg name="action_key" type="s" direction="out"/>
+ </signal>
+ </interface>
+ </node>]=]
+ return "s", xml
+ end
+end)
+
+-- listen for dbus notification requests
+capi.dbus.request_name("session", "org.freedesktop.Notifications")
+
+return dbus
+
+-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
diff --git a/lib/naughty/init.lua b/lib/naughty/init.lua
new file mode 100644
index 0000000..71dee9f
--- /dev/null
+++ b/lib/naughty/init.lua
@@ -0,0 +1,14 @@
+---------------------------------------------------------------------------
+-- @author Uli Schlachter &lt;psychon@znc.in&gt;
+-- @copyright 2014 Uli Schlachter
+-- @module naughty
+---------------------------------------------------------------------------
+
+local naughty = require("naughty.core")
+if dbus then
+ naughty.dbus = require("naughty.dbus")
+end
+
+return naughty
+
+-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80