--------------------------------------------------------------------------- -- 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 gtable = require("gears.table") local gsurface = require("gears.surface") 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") local cst = require("naughty.constants") local nnotif = require("naughty.notification") local naction = require("naughty.action") --- Notification library, dbus bindings local dbus = { config = {} } -- DBUS Notification constants -- https://developer.gnome.org/notification-spec/#urgency-levels 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}, cst.config.presets.low}, {{urgency = urgency.normal}, cst.config.presets.normal}, {{urgency = urgency.critical}, cst.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 -- This allow notification to be upadated later. local counter = 1 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 local pixels = tcat(rows) local surf = cairo.ImageSurface.create_for_data(pixels, format, w, h, stride) -- The surface refers to 'pixels', which can be freed by the GC. Thus, -- duplicate the surface to create a copy of the data owned by cairo. local res = gsurface.duplicate_surface(surf) surf:finish() return res 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.message = text if title ~= "" then args.title = title end else if title ~= "" then args.message = 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 = gtable.join(args.preset, preset) end end local preset = args.preset or cst.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") notification:destroy(cst.notification_closed_reason.dismissed_by_user) end elseif action_id ~= nil and action_text ~= nil then local a = naction { name = action_text, position = action_id, } a:connect_signal("invoked", function() sendActionInvoked(notification.id, action_id) notification:destroy(cst.notification_closed_reason.dismissed_by_user) end) table.insert(args.actions, a) 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 args.freedesktop_hints = hints -- Try to update existing objects when possible notification = naughty.get_by_id(replaces_id) if notification then for k, v in pairs(args) do notification[k] = v end else counter = counter+1 args.id = counter notification = nnotif(args) end return "u", notification.id end counter = counter+1 return "u", counter elseif data.member == "CloseNotification" then local obj = naughty.get_by_id(appname) if obj then obj:destroy(cst.notification_closed_reason.dismissed_by_command) 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 , -- and 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 = [=[ ]=] 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