diff --git a/helpers/color.lua b/helpers/color.lua new file mode 100644 index 0000000..09e003b --- /dev/null +++ b/helpers/color.lua @@ -0,0 +1,59 @@ + + +local _color = {} + + + +--- Try to guess if a color is dark or light. +-- +-- @string color The color with hexadecimal HTML format `"#RRGGBB"`. +-- @treturn bool `true` if the color is dark, `false` if it is light. +function _color.is_dark(color) + -- Try to determine if the color is dark or light + local numeric_value = 0; + for s in color:gmatch("[a-fA-F0-9][a-fA-F0-9]") do + numeric_value = numeric_value + tonumber("0x"..s); + end + return (numeric_value < 383) +end + + +--- Lighten a color. +-- +-- @string color The color to lighten with hexadecimal HTML format `"#RRGGBB"`. +-- @int[opt=26] amount How much light from 0 to 255. Default is around 10%. +-- @treturn string The lighter color +function _color.lighten(color, amount) + amount = amount or 26 + local c = { + r = tonumber("0x"..color:sub(2,3)), + g = tonumber("0x"..color:sub(4,5)), + b = tonumber("0x"..color:sub(6,7)), + } + + c.r = c.r + amount + c.r = c.r < 0 and 0 or c.r + c.r = c.r > 255 and 255 or c.r + c.g = c.g + amount + c.g = c.g < 0 and 0 or c.g + c.g = c.g > 255 and 255 or c.g + c.b = c.b + amount + c.b = c.b < 0 and 0 or c.b + c.b = c.b > 255 and 255 or c.b + + return string.format('#%02x%02x%02x', c.r, c.g, c.b) +end + +--- Darken a color. +-- +-- @string color The color to darken with hexadecimal HTML format `"#RRGGBB"`. +-- @int[opt=26] amount How much dark from 0 to 255. Default is around 10%. +-- @treturn string The darker color +function _color.darken(color, amount) + amount = amount or 26 + return _color.lighten(color, -amount) +end + + + +return _color diff --git a/helpers/filesystem.lua b/helpers/filesystem.lua new file mode 100644 index 0000000..0d8ed11 --- /dev/null +++ b/helpers/filesystem.lua @@ -0,0 +1,39 @@ +local Gio = require("lgi").Gio + +local _filesystem = {} + +--- Get a list of files from a given directory. +-- @string path The directory to search. +-- @tparam[opt] table exts Specific extensions to limit the search to. eg:`{ "jpg", "png" }` +-- If ommited, all files are considered. +-- @bool[opt=false] recursive List files from subdirectories +-- @staticfct bling.helpers.filesystem.get_random_file_from_dir +function _filesystem.list_directory_files(path, exts, recursive) + recursive = recursive or false + local files, valid_exts = {}, {} + + -- Transforms { "jpg", ... } into { [jpg] = #, ... } + if exts then for i, j in ipairs(exts) do valid_exts[j:lower()] = i end end + + -- Build a table of files from the path with the required extensions + local file_list = Gio.File.new_for_path(path):enumerate_children("standard::*", 0) + if file_list then + for file in function() return file_list:next_file() end do + local file_type = file:get_file_type() + if file_type == "REGULAR" then + local file_name = file:get_display_name() + if not exts or valid_exts[file_name:lower():match(".+%.(.*)$") or ""] then + table.insert(files, file_name) + end + elseif recursive and file_type == "DIRECTORY" then + local file_name = file:get_display_name() + files = gears.table.join(files, list_directory_files(file_name, exts, recursive)) + end + end + end + + return files +end + + +return _filesystem diff --git a/helpers/time.lua b/helpers/time.lua new file mode 100644 index 0000000..dc2a9d4 --- /dev/null +++ b/helpers/time.lua @@ -0,0 +1,29 @@ + + +local time = {} + + +--- Parse a time string to seconds (from midnight) +-- +-- @string time The time (`HH:MM:SS`) +-- @treturn int The number of seconds since 00:00:00 +function time.hhmmss_to_seconds(time) + hour_sec = tonumber(string.sub(time, 1, 2)) * 3600 + min_sec = tonumber(string.sub(time, 4, 5)) * 60 + get_sec = tonumber(string.sub(time, 7, 8)) + return (hour_sec + min_sec + get_sec) +end + + +--- Get time difference in seconds. +-- +-- @tparam string base The time to compare from (`HH:MM:SS`). +-- @tparam string base The time to compare to (`HH:MM:SS`). +-- @treturn int Number of seconds between the two times. +function time.time_diff(base, compare) + local diff = time.hhmmss_to_seconds(base) - time.hhmmss_to_seconds(compare) + return diff +end + + +return time diff --git a/init.lua b/init.lua index 8a64ee0..38c84df 100644 --- a/init.lua +++ b/init.lua @@ -7,5 +7,6 @@ return { layout = require(... .. ".layout"), module = require(... .. ".module"), + helpers = require(... .. ".helpers"), } diff --git a/module/init.lua b/module/init.lua index f0d0e32..953e24a 100644 --- a/module/init.lua +++ b/module/init.lua @@ -1,6 +1,7 @@ return { window_swallowing = require(... .. ".window_swallowing"), tiled_wallpaper = require(... .. ".tiled_wallpaper"), + wallpaper = require(... .. ".wallpaper"), flash_focus = require(... .. ".flash_focus"), tabbed = require(... .. ".tabbed") } diff --git a/module/wallpaper.lua b/module/wallpaper.lua new file mode 100644 index 0000000..69a8147 --- /dev/null +++ b/module/wallpaper.lua @@ -0,0 +1,293 @@ +--------------------------------------------------------------------------- +-- High-level declarative function for setting your wallpaper. +-- +-- +-- An easy way to setup a complex wallpaper with slideshow, random, schedule, extensibility. +-- +-- @usage +-- local wallpaper = require("wallpaper") +-- -- A silly example +-- wallpaper.setup { -- I want a wallpaper +-- change_timer = 500, -- changing every 5 minutes +-- set_function = wallpaper.setters.random, -- in a random way +-- wallpaper = {"#abcdef", +-- "~/Pictures", +-- wallpaper.setters.awesome}, -- from this list (a color, a directory with pictures and the Awesome wallpaper) +-- recursive = false, -- do not read subfolders of "~/Pictures" +-- position = "centered", -- center it on the screen (for pictures) +-- scale = 2, -- 2 time bigger (for pictures) +-- } +-- +-- @author Grumph +-- @copyright 2021 Grumph +-- +--------------------------------------------------------------------------- + + +local beautiful = require("beautiful") +local gears = require("gears") +local helpers = require(tostring(...):match(".*bling") .. ".helpers") + +local setters = {} + +--- Apply a wallpaper. +-- +-- This function is a helper that will apply a wallpaper_object, +-- either using gears.wallpaper.set or gears.wallpaper.* higher level functions when applicable. +-- @param wallpaper_object A wallpaper object, either +-- a `pattern` (see `gears.wallpaper.set`) +-- a `surf` (see `gears.wallpaper.centered`) +-- a function that actually sets the wallpaper. +-- @tparam table args The argument table containing any of the arguments below. +-- @int[opt=nil] args.screen The screen to use (as used in `gears.wallpaper` functions) +-- @string[opt=nil or "centered"] args.position The `gears.wallpaper` position function to use. +-- Must be set when wallpaper is a file. +-- It can be `"centered"`, `"fit"`, `"tiled"` or `"maximized"`. +-- @string[opt=beautiful.bg_normal or "black"] args.background See `gears.wallpaper`. +-- @bool[opt=false] args.ignore_aspect See `gears.wallpaper`. +-- @tparam[opt={x=0,y=0}] table args.offset See `gears.wallpaper`. +-- @int[opt=1] args.scale See `gears.wallpaper`. +function apply(wallpaper_object, args) + args.background = args.background or beautiful.bg_normal or "black" + args.ignore_aspect = args.ignore_aspect or false -- false = keep aspect ratio + args.offset = args.offset or {x = 0, y = 0} + args.scale = args.scale or 1 + local positions = { + ["centered"] = function() gears.wallpaper.centered(wallpaper_object, args.screen, args.background, args.scale) end, + ["tiled"] = function() gears.wallpaper.tiled(wallpaper_object, args.screen, args.offset) end, + ["maximized"] = function() gears.wallpaper.maximized(wallpaper_object, args.screen, args.ignore_aspect, args.offset) end, + ["fit"] = function() gears.wallpaper.fit(wallpaper_object, args.screen, args.background) end, + } + if type(wallpaper_object) == "string" and gears.filesystem.file_readable(wallpaper_object) then + -- path of an image file, we use a position function + local p = args.position or "centered" + positions[p]() + elseif type(wallpaper_object) == "function" then + -- function + wallpaper_object(args) + elseif (not gears.color.ensure_pango_color(wallpaper_object, nil)) and args.position then + -- if the user sets a position function, wallpaper_object should be a cairo surface + positions[args.position]() + else + gears.wallpaper.set(wallpaper_object) + end +end + +--- Converts `args.wallpaper` to a list of `wallpaper_objects` readable by `apply` function). +-- +-- @tparam table args The argument table containing the argument below. +-- @param[opt=`beautiful.wallpaper_path` or `"black"`] args.wallpaper A wallpaper object. +-- It can be a color or a cairo pattern (what `gears.wallpaper.set` understands), +-- a cairo suface (set with gears.wallpaper.set if `args.position` is nil, or with +-- `gears.wallpaper` position functions, see `args.position`), +-- a function similar to args.set_function that will effectively set a wallpaper (usually +-- with `gears.wallpaper` functions), +-- a path to a file, +-- path to a directory containing images, +-- or a list with any of the previous choices. +-- @tparam[opt=`{"jpg", "jpeg", "png", "bmp"}`] table args.image_formats A list of +-- file extensions to filter when `args.wallpaper` is a directory. +-- @bool[opt=true] args.recursive Either to recurse or not when `args.wallpaper` is a directory. +-- @treturn table A list of `wallpaper_objects` (what `apply` can read). +-- @see apply +function prepare_list(args) + args.image_formats = args.image_formats or {"jpg", "jpeg", "png", "bmp"} + args.recursive = args.recursive or true + + local wallpapers = (args.wallpaper + or beautiful.wallpaper_path + or "black") + local res = {} + if type(wallpapers) ~= "table" then + wallpapers = {wallpapers} + end + for _, w in ipairs(wallpapers) do + -- w is either: + -- - a directory path (string) + -- - an image path or a color (string) + -- - a cairo surface or a cairo pattern + -- - a function for setting the wallpaper + if type(w) == "string" and gears.filesystem.dir_readable(w) then + local file_list = helpers.filesystem.list_directory_files(w, args.image_formats, args.recursive) + for _, f in ipairs(file_list) do + res[#res + 1] = w .. "/" .. f + end + else + res[#res + 1] = w + end + end + return res +end + +local simple_index = 0 +--- Set the next wallpaper in a list. +-- +-- @tparam table args See `prepare_list` and `apply` arguments +-- @see apply +-- @see prepare_list +function setters.simple(args) + local wallpapers = prepare_list(args) + simple_index = (simple_index % #wallpapers) + 1 + apply(wallpapers[simple_index], args) +end + +--- Set a random wallpaper from a list. +-- +-- @tparam table args See `prepare_list` and `apply` arguments +-- @see apply +-- @see prepare_list +function setters.random(args) + local wallpapers = prepare_list(args) + apply(wallpapers[math.random(#wallpapers)], args) +end + + +local simple_schedule_object = nil +--- A schedule setter. +-- +-- This simple schedule setter was freely inspired by [dynamic-wallpaper](https://github.com/manilarome/awesome-glorious-widgets/blob/master/dynamic-wallpaper/init.lua). +-- @tparam table args The argument table containing any of the arguments below. +-- @tparam table args.wallpaper The schedule table, with the form +-- { +-- ["HH:MM:SS"] = wallpaper, +-- ["HH:MM:SS"] = wallpaper2, +-- } +-- The wallpapers definition can be anything the `schedule_set_function` can read +-- (what you would place in `args.wallpaper` for this function), +-- @tparam[opt=`setters.simple`] function args.wallpaper_set_function The set_function used by default +function setters.simple_schedule(args) + local function update_wallpaper() + local fake_args = gears.table.join(args, {wallpaper = args.wallpaper[simple_schedule_object.closest_lower_time]}) + simple_schedule_object.schedule_set_function(fake_args) + end + if not simple_schedule_object then + simple_schedule_object = {} + -- initialize the schedule object, so we don't do it for every call + simple_schedule_object.schedule_set_function = args.schedule_set_function or setters.simple + -- we get the sorted time keys + simple_schedule_object.times = {} + for k in pairs(args.wallpaper) do table.insert(simple_schedule_object.times, k) end + table.sort(simple_schedule_object.times) + -- now we get the closest time which is below current time (the current applicable period) + local function update_timer() + local current_time = os.date("%H:%M:%S") + local next_time = simple_schedule_object.times[1] + simple_schedule_object.closest_lower_time = simple_schedule_object.times[#(simple_schedule_object.times)] + for _, k in ipairs(simple_schedule_object.times) do + if k > current_time then + next_time = k + break + end + simple_schedule_object.closest_lower_time = k + end + simple_schedule_object.timer.timeout = helpers.time.time_diff(next_time, current_time) + if simple_schedule_object.timer.timeout < 0 then + -- the next_time is the day after, so we add 24 hours to the timer + simple_schedule_object.timer.timeout = simple_schedule_object.timer.timeout + 86400 + end + simple_schedule_object.timer:again() + update_wallpaper() + end + simple_schedule_object.timer = gears.timer { + callback = update_timer, + } + update_timer() + else + -- if called again (usually when the change_timer is set), we just change the wallpaper depending on current parameters + update_wallpaper() + end +end + + + + + +--- Set the AWESOME wallpaper. +-- +-- @tparam table args The argument table containing the argument below. +-- @param[opt=`beautiful.bg_normal`] args.colors.bg The bg color. +-- If the default is used, the color is darkened if `beautiful.bg_normal` is light +-- or lightned if `beautiful.bg_normal` is dark. +-- @param[opt=`beautiful.fg_normal`] args.colors.fg The fg color. +-- @param[opt=`beautiful.fg_focus`] args.colors.alt_fg The alt_fg color. +-- +-- see beautiful.theme_assets.wallpaper +function setters.awesome_wallpaper(args) + local colors = {bg = beautiful.bg_normal, fg = beautiful.fg_normal, alt_fg = beautiful.bg_focus } + colors.bg = helpers.color.is_dark(beautiful.bg_normal) + and helpers.color.lighten(colors.bg) + or helpers.color.darken(colors.bg) + if (type(args.colors) == "table") then + colors.bg = args.colors.bg or colors.bg + colors.fg = args.colors.fg or colors.fg + colors.alt_fg = args.colors.alt_fg or colors.alt_fg + end + -- Generate wallpaper: + if not args.screen then + for s in screen do + gears.wallpaper.set( + beautiful.theme_assets.wallpaper(colors.bg, colors.fg, colors.alt_fg, s) + ) + end + else + gears.wallpaper.set( + beautiful.theme_assets.wallpaper(colors.bg, colors.fg, colors.alt_fg, args.screen) + ) + end +end + + + +--- Setup a wallpaper. +-- +-- @tparam table args Parameters for the wallpaper. It may also contain all parameters your `args.set_function` needs +-- @int[opt=nil] args.screen The screen to use (as used in `gears.wallpaper` functions) +-- @int[opt=nil] args.change_timer Time in seconds for wallpaper changes +-- @tparam[opt=`setters.awesome` or `setters.simple`] function args.set_function A function to set the wallpaper +-- It takes args as parameter (the same args as the setup function). +-- This function is called at `"request::wallpaper"` `screen` signals and at `args.change_timer` timeouts. +-- There is no obligation, but for consistency, the function should use `args.wallpaper` as a feeder. +-- If `args.wallpaper` is defined, the default function is `setters.simple`, else it will be `setters.awesome`. +-- +-- @usage +-- local wallpaper = require("wallpaper") +-- wallpaper.setup { +-- change_timer = 631, -- Prime number is better +-- set_function = wallpaper.setters.random, +-- -- parameters for the random setter +-- wallpaper = '/data/pictures/wallpapers', +-- position = "maximized", +-- } +-- +-- @see apply +-- @see prepare_list +-- @see setters.simple +function setup(args) + local config = args or {} + config.set_function = config.set_function or (config.wallpaper and setters.simple or setters.awesome_wallpaper) + local function set_wallpaper(s) + config.screen = s or config.screen + config.set_function(config) + end + + if config.change_timer and config.change_timer > 0 then + gears.timer { + timeout = config.change_timer, + call_now = false, + autostart = true, + callback = function() set_wallpaper() end + } + end + + screen.connect_signal("request::wallpaper", set_wallpaper) +end + + + + +return { + setup = setup, + setters = setters, + apply = apply, + prepare_list = prepare_list, +}