diff options
author | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
---|---|---|
committer | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
commit | 22d656903563f75678f3634964731ccf93355dfd (patch) | |
tree | e3cb6279d95c9764093072d5e946566ea6533799 /lib/naughty |
Init commit
Diffstat (limited to 'lib/naughty')
-rw-r--r-- | lib/naughty/core.lua | 688 | ||||
-rw-r--r-- | lib/naughty/dbus.lua | 261 | ||||
-rw-r--r-- | lib/naughty/init.lua | 14 |
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 <gkusnierz@gmail.com> +-- @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 = { ['<'] = "<", ['>'] = ">", ['&'] = "&" } + + 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><Invalid markup or UTF8, cannot display message></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 <gkusnierz@gmail.com> +-- @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 <psychon@znc.in> +-- @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 |