diff options
Diffstat (limited to 'awesome/lib/naughty/core.lua')
-rw-r--r-- | awesome/lib/naughty/core.lua | 688 |
1 files changed, 0 insertions, 688 deletions
diff --git a/awesome/lib/naughty/core.lua b/awesome/lib/naughty/core.lua deleted file mode 100644 index 764afe2..0000000 --- a/awesome/lib/naughty/core.lua +++ /dev/null @@ -1,688 +0,0 @@ ----------------------------------------------------------------------------- ---- 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 |