From 36bfe02555a111d9104f0b7489d425721e6777e3 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Wed, 15 Dec 2021 13:09:25 +0000 Subject: [PATCH] Add awesome extras --- .config/awesome/extras/bling/.editorconfig | 24 + .../awesome/extras/bling/helpers/client.lua | 127 ++ .../awesome/extras/bling/helpers/color.lua | 158 +++ .../extras/bling/helpers/filesystem.lua | 53 + .../extras/bling/helpers/icon_theme.lua | 134 +++ .config/awesome/extras/bling/helpers/init.lua | 7 + .../awesome/extras/bling/helpers/shape.lua | 30 + .config/awesome/extras/bling/helpers/time.lua | 24 + .../extras/bling/icons/layouts/centered.png | 3 + .../extras/bling/icons/layouts/deck.png | 3 + .../extras/bling/icons/layouts/equalarea.png | 3 + .../extras/bling/icons/layouts/horizontal.png | 3 + .../extras/bling/icons/layouts/mstab.png | 3 + .../extras/bling/icons/layouts/vertical.png | 3 + .../extras/bling/images/bling_banner-2x.png | 3 + .../extras/bling/images/bling_banner.png | 3 + .config/awesome/extras/bling/init.lua | 7 + .../awesome/extras/bling/layout/centered.lua | 84 ++ .config/awesome/extras/bling/layout/deck.lua | 37 + .../awesome/extras/bling/layout/equalarea.lua | 77 ++ .../extras/bling/layout/horizontal.lua | 56 + .config/awesome/extras/bling/layout/init.lua | 44 + .config/awesome/extras/bling/layout/mstab.lua | 249 ++++ .../awesome/extras/bling/layout/vertical.lua | 56 + .../extras/bling/module/flash_focus.lua | 44 + .config/awesome/extras/bling/module/init.lua | 8 + .../extras/bling/module/scratchpad.lua | 374 ++++++ .../awesome/extras/bling/module/tabbed.lua | 274 +++++ .../extras/bling/module/tiled_wallpaper.lua | 56 + .../awesome/extras/bling/module/wallpaper.lua | 339 ++++++ .../extras/bling/module/window_swallowing.lua | 88 ++ .config/awesome/extras/bling/signal/init.lua | 1 + .../extras/bling/signal/playerctl/init.lua | 19 + .../bling/signal/playerctl/playerctl_cli.lua | 151 +++ .../bling/signal/playerctl/playerctl_lib.lua | 350 ++++++ .../extras/bling/widget/app_launcher/init.lua | 1053 +++++++++++++++++ .../bling/widget/app_launcher/prompt.lua | 656 ++++++++++ .config/awesome/extras/bling/widget/init.lua | 7 + .../extras/bling/widget/tabbar/boxes.lua | 57 + .../extras/bling/widget/tabbar/default.lua | 60 + .../extras/bling/widget/tabbar/modern.lua | 271 +++++ .../extras/bling/widget/tabbar/pure.lua | 81 ++ .../widget/tabbed_misc/custom_tasklist.lua | 51 + .../extras/bling/widget/tabbed_misc/init.lua | 9 + .../widget/tabbed_misc/titlebar_indicator.lua | 133 +++ .../extras/bling/widget/tag_preview.lua | 246 ++++ .../extras/bling/widget/task_preview.lua | 199 ++++ .../extras/bling/widget/window_switcher.lua | 454 +++++++ .config/awesome/extras/nice | 1 + 49 files changed, 6173 insertions(+) create mode 100755 .config/awesome/extras/bling/.editorconfig create mode 100755 .config/awesome/extras/bling/helpers/client.lua create mode 100755 .config/awesome/extras/bling/helpers/color.lua create mode 100755 .config/awesome/extras/bling/helpers/filesystem.lua create mode 100755 .config/awesome/extras/bling/helpers/icon_theme.lua create mode 100755 .config/awesome/extras/bling/helpers/init.lua create mode 100755 .config/awesome/extras/bling/helpers/shape.lua create mode 100755 .config/awesome/extras/bling/helpers/time.lua create mode 100755 .config/awesome/extras/bling/icons/layouts/centered.png create mode 100755 .config/awesome/extras/bling/icons/layouts/deck.png create mode 100755 .config/awesome/extras/bling/icons/layouts/equalarea.png create mode 100755 .config/awesome/extras/bling/icons/layouts/horizontal.png create mode 100755 .config/awesome/extras/bling/icons/layouts/mstab.png create mode 100755 .config/awesome/extras/bling/icons/layouts/vertical.png create mode 100755 .config/awesome/extras/bling/images/bling_banner-2x.png create mode 100755 .config/awesome/extras/bling/images/bling_banner.png create mode 100755 .config/awesome/extras/bling/init.lua create mode 100755 .config/awesome/extras/bling/layout/centered.lua create mode 100755 .config/awesome/extras/bling/layout/deck.lua create mode 100755 .config/awesome/extras/bling/layout/equalarea.lua create mode 100755 .config/awesome/extras/bling/layout/horizontal.lua create mode 100755 .config/awesome/extras/bling/layout/init.lua create mode 100755 .config/awesome/extras/bling/layout/mstab.lua create mode 100755 .config/awesome/extras/bling/layout/vertical.lua create mode 100755 .config/awesome/extras/bling/module/flash_focus.lua create mode 100755 .config/awesome/extras/bling/module/init.lua create mode 100755 .config/awesome/extras/bling/module/scratchpad.lua create mode 100755 .config/awesome/extras/bling/module/tabbed.lua create mode 100755 .config/awesome/extras/bling/module/tiled_wallpaper.lua create mode 100755 .config/awesome/extras/bling/module/wallpaper.lua create mode 100755 .config/awesome/extras/bling/module/window_swallowing.lua create mode 100755 .config/awesome/extras/bling/signal/init.lua create mode 100755 .config/awesome/extras/bling/signal/playerctl/init.lua create mode 100755 .config/awesome/extras/bling/signal/playerctl/playerctl_cli.lua create mode 100755 .config/awesome/extras/bling/signal/playerctl/playerctl_lib.lua create mode 100755 .config/awesome/extras/bling/widget/app_launcher/init.lua create mode 100755 .config/awesome/extras/bling/widget/app_launcher/prompt.lua create mode 100755 .config/awesome/extras/bling/widget/init.lua create mode 100755 .config/awesome/extras/bling/widget/tabbar/boxes.lua create mode 100755 .config/awesome/extras/bling/widget/tabbar/default.lua create mode 100755 .config/awesome/extras/bling/widget/tabbar/modern.lua create mode 100755 .config/awesome/extras/bling/widget/tabbar/pure.lua create mode 100755 .config/awesome/extras/bling/widget/tabbed_misc/custom_tasklist.lua create mode 100755 .config/awesome/extras/bling/widget/tabbed_misc/init.lua create mode 100755 .config/awesome/extras/bling/widget/tabbed_misc/titlebar_indicator.lua create mode 100755 .config/awesome/extras/bling/widget/tag_preview.lua create mode 100755 .config/awesome/extras/bling/widget/task_preview.lua create mode 100755 .config/awesome/extras/bling/widget/window_switcher.lua create mode 160000 .config/awesome/extras/nice diff --git a/.config/awesome/extras/bling/.editorconfig b/.config/awesome/extras/bling/.editorconfig new file mode 100755 index 0000000..33997d4 --- /dev/null +++ b/.config/awesome/extras/bling/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +trim_trailing_whitespace = true + +[*.lua] +indent_size = 4 +indent_style = space +max_line_length = 80 + +[*.yml] +indent_size = 2 +indent_style = space + +[*.{html,css}] +indent_size = 2 +indent_style = space + +[*.md] +trim_trailing_whitespace = false diff --git a/.config/awesome/extras/bling/helpers/client.lua b/.config/awesome/extras/bling/helpers/client.lua new file mode 100755 index 0000000..0e14c51 --- /dev/null +++ b/.config/awesome/extras/bling/helpers/client.lua @@ -0,0 +1,127 @@ +local awful = require("awful") +local gears = require("gears") + +local _client = {} + +--- Turn off passed client +-- Remove current tag from window's tags +-- +-- @param c A client +function _client.turn_off(c, current_tag) + if current_tag == nil then + current_tag = c.screen.selected_tag + end + local ctags = {} + for k, tag in pairs(c:tags()) do + if tag ~= current_tag then + table.insert(ctags, tag) + end + end + c:tags(ctags) + c.sticky = false +end + +--- Turn on passed client (add current tag to window's tags) +-- +-- @param c A client +function _client.turn_on(c) + local current_tag = c.screen.selected_tag + ctags = { current_tag } + for k, tag in pairs(c:tags()) do + if tag ~= current_tag then + table.insert(ctags, tag) + end + end + c:tags(ctags) + c:raise() + client.focus = c +end + +--- Sync two clients +-- +-- @param to_c The client to which to write all properties +-- @param from_c The client from which to read all properties +function _client.sync(to_c, from_c) + if not from_c or not to_c then + return + end + if not from_c.valid or not to_c.valid then + return + end + if from_c.modal then + return + end + to_c.floating = from_c.floating + to_c.maximized = from_c.maximized + to_c.above = from_c.above + to_c.below = from_c.below + to_c:geometry(from_c:geometry()) + -- TODO: Should also copy over the position in a tiling layout +end + +--- Checks whether the passed client is a childprocess of a given process ID +-- +-- @param c A client +-- @param pid The process ID +-- @return True if the passed client is a childprocess of the given PID otherwise false +function _client.is_child_of(c, pid) + -- io.popen is normally discouraged. Should probably be changed + if not c or not c.valid then + return false + end + if tostring(c.pid) == tostring(pid) then + return true + end + local pid_cmd = [[pstree -T -p -a -s ]] + .. tostring(c.pid) + .. [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']] + local handle = io.popen(pid_cmd) + local parent_pid = handle:read("*a") + handle:close() + return tostring(parent_pid) == tostring(pid) + or tostring(parent_pid) == tostring(c.pid) +end + +--- Finds all clients that satisfy the passed rule +-- +-- @param rule The rule to be searched for +-- @retrun A list of clients that match the given rule +function _client.find(rule) + local function matcher(c) + return awful.rules.match(c, rule) + end + local clients = client.get() + local findex = gears.table.hasitem(clients, client.focus) or 1 + local start = gears.math.cycle(#clients, findex + 1) + + local matches = {} + for c in awful.client.iterate(matcher, start) do + matches[#matches + 1] = c + end + + return matches +end + +--- Gets the next client by direction from the focused one +-- +-- @param direction it the direction as a string ("up", "down", "left" or "right") +-- @retrun the client in the given direction starting at the currently focused one, nil otherwise +function _client.get_by_direction(direction) + local sel = client.focus + if not sel then + return nil + end + local cltbl = sel.screen:get_clients() + local geomtbl = {} + for i, cl in ipairs(cltbl) do + geomtbl[i] = cl:geometry() + end + local target = gears.geometry.rectangle.get_in_direction( + direction, + geomtbl, + sel:geometry() + ) + return cltbl[target] +end + +return _client diff --git a/.config/awesome/extras/bling/helpers/color.lua b/.config/awesome/extras/bling/helpers/color.lua new file mode 100755 index 0000000..4042360 --- /dev/null +++ b/.config/awesome/extras/bling/helpers/color.lua @@ -0,0 +1,158 @@ +local tonumber = tonumber +local string = string +local math = math +local floor = math.floor +local max = math.max +local min = math.min +local abs = math.abs +local format = string.format + +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 + +function _color.is_opaque(color) + if type(color) == "string" then + color = _color.hex_to_rgba(color) + end + + return color.a < 0.01 +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 + +-- Returns a value that is clipped to interval edges if it falls outside the interval +function _color.clip(num, min_num, max_num) + return max(min(num, max_num), min_num) +end + +-- Converts the given hex color to rgba +function _color.hex_to_rgba(color) + color = color:gsub("#", "") + return { r = tonumber("0x" .. color:sub(1, 2)), + g = tonumber("0x" .. color:sub(3, 4)), + b = tonumber("0x" .. color:sub(5, 6)), + a = #color == 8 and tonumber("0x" .. color:sub(7, 8)) or 255 } +end + +-- Converts the given rgba color to hex +function _color.rgba_to_hex(color) + local r = _color.clip(color.r or color[1], 0, 255) + local g = _color.clip(color.g or color[2], 0, 255) + local b = _color.clip(color.b or color[3], 0, 255) + local a = _color.clip(color.a or color[4] or 255, 0, 255) + return "#" .. format("%02x%02x%02x%02x", + floor(r), + floor(g), + floor(b), + floor(a)) +end + +-- Converts the given hex color to hsv +function _color.hex_to_hsv(color) + local color = _color.hex2rgb(color) + local C_max = max(color.r, color.g, color.b) + local C_min = min(color.r, color.g, color.b) + local delta = C_max - C_min + local H, S, V + if delta == 0 then + H = 0 + elseif C_max == color.r then + H = 60 * (((color.g - color.b) / delta) % 6) + elseif C_max == color.g then + H = 60 * (((color.b - color.r) / delta) + 2) + elseif C_max == color.b then + H = 60 * (((color.r - color.g) / delta) + 4) + end + if C_max == 0 then + S = 0 + else + S = delta / C_max + end + V = C_max + + return { h = H, + s = S * 100, + v = V * 100 } +end + +-- Converts the given hsv color to hex +function _color.hsv_to_hex(H, S, V) + S = S / 100 + V = V / 100 + if H > 360 then H = 360 end + if H < 0 then H = 0 end + local C = V * S + local X = C * (1 - abs(((H / 60) % 2) - 1)) + local m = V - C + local r_, g_, b_ = 0, 0, 0 + if H >= 0 and H < 60 then + r_, g_, b_ = C, X, 0 + elseif H >= 60 and H < 120 then + r_, g_, b_ = X, C, 0 + elseif H >= 120 and H < 180 then + r_, g_, b_ = 0, C, X + elseif H >= 180 and H < 240 then + r_, g_, b_ = 0, X, C + elseif H >= 240 and H < 300 then + r_, g_, b_ = X, 0, C + elseif H >= 300 and H < 360 then + r_, g_, b_ = C, 0, X + end + local r, g, b = (r_ + m) * 255, (g_ + m) * 255, (b_ + m) * 255 + return ("#%02x%02x%02x"):format(floor(r), floor(g), floor(b)) +end + +function _color.multiply(color, amount) + return { _color.clip(color.r * amount, 0, 255), + _color.clip(color.g * amount, 0, 255), + _color.clip(color.b * amount, 0, 255), + 255 } +end + +return _color diff --git a/.config/awesome/extras/bling/helpers/filesystem.lua b/.config/awesome/extras/bling/helpers/filesystem.lua new file mode 100755 index 0000000..9f65d0e --- /dev/null +++ b/.config/awesome/extras/bling/helpers/filesystem.lua @@ -0,0 +1,53 @@ +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/.config/awesome/extras/bling/helpers/icon_theme.lua b/.config/awesome/extras/bling/helpers/icon_theme.lua new file mode 100755 index 0000000..4a1db92 --- /dev/null +++ b/.config/awesome/extras/bling/helpers/icon_theme.lua @@ -0,0 +1,134 @@ +local Gio = require("lgi").Gio +local Gtk = require("lgi").Gtk +local gobject = require("gears.object") +local gtable = require("gears.table") +local helpers = require("helpers") +local setmetatable = setmetatable +local ipairs = ipairs + +local icon_theme = { mt = {} } + +function icon_theme:get_client_icon_path(client) + local function find_icon(class) + if self._private.client_icon_cache[class] ~= nil then + return self._private.client_icon_cache[class] + end + + for _, app in ipairs(Gio.AppInfo.get_all()) do + local id = Gio.AppInfo.get_id(app) + if id:match(helpers.misc.case_insensitive_pattern(class)) then + self._private.client_icon_cache[class] = self:get_gicon_path(Gio.AppInfo.get_icon(app)) + return self._private.client_icon_cache[class] + end + end + + return nil + end + + local class = client.class + if class == "jetbrains-studio" then + class = "android-studio" + end + + local icon = self:get_icon_path("gnome-window-manager") + + if class ~= nil then + class = class:gsub("[%-]", "%%%0") + icon = find_icon(class) or icon + + class = client.class + class = class:gsub("[%-]", "") + icon = find_icon(class) or icon + + class = client.class + class = class:gsub("[%-]", ".") + icon = find_icon(class) or icon + + class = client.class + class = class:match("(.-)-") or class + class = class:match("(.-)%.") or class + class = class:match("(.-)%s+") or class + class = class:gsub("[%-]", "%%%0") + icon = find_icon(class) or icon + end + + return icon +end + +function icon_theme:choose_icon(icons_names) + local icon_info = Gtk.IconTheme.choose_icon(self.gtk_theme, icons_names, self.icon_size, 0); + if icon_info then + local icon_path = Gtk.IconInfo.get_filename(icon_info) + if icon_path then + return icon_path + end + end + + return "" +end + + +function icon_theme:get_gicon_path(gicon) + if gicon == nil then + return "" + end + + if self._private.icon_cache[gicon] ~= nil then + return self._private.icon_cache[gicon] + end + + local icon_info = Gtk.IconTheme.lookup_by_gicon(self.gtk_theme, gicon, self.icon_size, 0); + if icon_info then + local icon_path = Gtk.IconInfo.get_filename(icon_info) + if icon_path then + self._private.icon_cache[gicon] = icon_path + return icon_path + end + end + + return "" +end + +function icon_theme:get_icon_path(icon_name) + if self._private.icon_cache[icon_name] ~= nil then + return self._private.icon_cache[icon_name] + end + + local icon_info = Gtk.IconTheme.lookup_icon(self.gtk_theme, icon_name, self.icon_size, 0); + if icon_info then + local icon_path = Gtk.IconInfo.get_filename(icon_info) + if icon_path then + self._private.icon_cache[icon_name] = icon_path + return icon_path + end + end + + return "" +end + +local function new(theme_name, icon_size) + local ret = gobject{} + gtable.crush(ret, icon_theme, true) + + ret._private = {} + ret._private.client_icon_cache = {} + ret._private.icon_cache = {} + + ret.name = theme_name or nil + ret.icon_size = icon_size or 48 + + if theme_name then + ret.gtk_theme = Gtk.IconTheme.new() + Gtk.IconTheme.set_custom_theme(ret.gtk_theme, theme_name); + else + ret.gtk_theme = Gtk.IconTheme.get_default() + end + + return ret +end + +function icon_theme.mt:__call(...) + return new(...) +end + +return setmetatable(icon_theme, icon_theme.mt) diff --git a/.config/awesome/extras/bling/helpers/init.lua b/.config/awesome/extras/bling/helpers/init.lua new file mode 100755 index 0000000..f2c898e --- /dev/null +++ b/.config/awesome/extras/bling/helpers/init.lua @@ -0,0 +1,7 @@ +return { + client = require(... .. ".client"), + color = require(... .. ".color"), + filesystem = require(... .. ".filesystem"), + shape = require(... .. ".shape"), + time = require(... .. ".time"), +} diff --git a/.config/awesome/extras/bling/helpers/shape.lua b/.config/awesome/extras/bling/helpers/shape.lua new file mode 100755 index 0000000..9c96d83 --- /dev/null +++ b/.config/awesome/extras/bling/helpers/shape.lua @@ -0,0 +1,30 @@ +local gears = require("gears") + +shape = {} + +-- Create rounded rectangle shape (in one line) + +function shape.rrect(radius) + return function(cr, width, height) + gears.shape.rounded_rect(cr, width, height, radius) + end +end + +-- Create partially rounded rect + +function shape.prrect(radius, tl, tr, br, bl) + return function(cr, width, height) + gears.shape.partially_rounded_rect( + cr, + width, + height, + tl, + tr, + br, + bl, + radius + ) + end +end + +return shape diff --git a/.config/awesome/extras/bling/helpers/time.lua b/.config/awesome/extras/bling/helpers/time.lua new file mode 100755 index 0000000..5ab0f25 --- /dev/null +++ b/.config/awesome/extras/bling/helpers/time.lua @@ -0,0 +1,24 @@ +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/.config/awesome/extras/bling/icons/layouts/centered.png b/.config/awesome/extras/bling/icons/layouts/centered.png new file mode 100755 index 0000000..4f8fd1e --- /dev/null +++ b/.config/awesome/extras/bling/icons/layouts/centered.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1482e0113a3a8cf874b654a57b30940226ad9fa23cceeea2f8843bc6e549c6f9 +size 2638 diff --git a/.config/awesome/extras/bling/icons/layouts/deck.png b/.config/awesome/extras/bling/icons/layouts/deck.png new file mode 100755 index 0000000..92a89cb --- /dev/null +++ b/.config/awesome/extras/bling/icons/layouts/deck.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd01bcf3202b7b36aaedc71c4bc4b555b208fa2f3c5b3a8893938bed098a6285 +size 1497 diff --git a/.config/awesome/extras/bling/icons/layouts/equalarea.png b/.config/awesome/extras/bling/icons/layouts/equalarea.png new file mode 100755 index 0000000..f975d0d --- /dev/null +++ b/.config/awesome/extras/bling/icons/layouts/equalarea.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e48b23f81ab78ee03f324828fe4e10547c46d915e71d6a8641b67675ee4433c +size 5502 diff --git a/.config/awesome/extras/bling/icons/layouts/horizontal.png b/.config/awesome/extras/bling/icons/layouts/horizontal.png new file mode 100755 index 0000000..6cc516f --- /dev/null +++ b/.config/awesome/extras/bling/icons/layouts/horizontal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:283539b71115938588a599bdf57bc5834af19cf8f533c40490cce1bc6de8a4be +size 768 diff --git a/.config/awesome/extras/bling/icons/layouts/mstab.png b/.config/awesome/extras/bling/icons/layouts/mstab.png new file mode 100755 index 0000000..b31e2bb --- /dev/null +++ b/.config/awesome/extras/bling/icons/layouts/mstab.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79416ab37ce777f4d8d9c6ba9d574e2560ba710ab98da7f4b91048cd1902f173 +size 1706 diff --git a/.config/awesome/extras/bling/icons/layouts/vertical.png b/.config/awesome/extras/bling/icons/layouts/vertical.png new file mode 100755 index 0000000..744f65f --- /dev/null +++ b/.config/awesome/extras/bling/icons/layouts/vertical.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c36f4ea31746a4a79714a44716123e08c6383bbd542de6c76a5c4b0a7fa9a47b +size 767 diff --git a/.config/awesome/extras/bling/images/bling_banner-2x.png b/.config/awesome/extras/bling/images/bling_banner-2x.png new file mode 100755 index 0000000..d965af7 --- /dev/null +++ b/.config/awesome/extras/bling/images/bling_banner-2x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bff27d7ab0238857258834e6ec2abbdc4ba61e765463d3f620b4f5158cd95ac +size 253618 diff --git a/.config/awesome/extras/bling/images/bling_banner.png b/.config/awesome/extras/bling/images/bling_banner.png new file mode 100755 index 0000000..e29541a --- /dev/null +++ b/.config/awesome/extras/bling/images/bling_banner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67f7c79d1cc9e5bab5229c5f502d4079f7fdce321663ac74303092116f060bd2 +size 78099 diff --git a/.config/awesome/extras/bling/init.lua b/.config/awesome/extras/bling/init.lua new file mode 100755 index 0000000..93bde56 --- /dev/null +++ b/.config/awesome/extras/bling/init.lua @@ -0,0 +1,7 @@ +return { + layout = require(... .. ".layout"), + --module = require(... .. ".module"), + --helpers = require(... .. ".helpers"), + --signal = require(... .. ".signal"), + --widget = require(... .. ".widget"), +} diff --git a/.config/awesome/extras/bling/layout/centered.lua b/.config/awesome/extras/bling/layout/centered.lua new file mode 100755 index 0000000..4e1ad24 --- /dev/null +++ b/.config/awesome/extras/bling/layout/centered.lua @@ -0,0 +1,84 @@ +local awful = require("awful") +local math = math + +local mylayout = {} + +mylayout.name = "centered" + +function mylayout.arrange(p) + local area = p.workarea + local t = p.tag or screen[p.screen].selected_tag + local mwfact = t.master_width_factor + local nmaster = math.min(t.master_count, #p.clients) + local nslaves = #p.clients - nmaster + + local master_area_width = area.width * mwfact + local slave_area_width = area.width - master_area_width + local master_area_x = area.x + 0.5 * slave_area_width + + local number_of_left_sided_slaves = math.floor(nslaves / 2) + local number_of_right_sided_slaves = nslaves - number_of_left_sided_slaves + local left_iterator = 0 + local right_iterator = 0 + + -- Special case: no maters -> rrelapse into awesomes fair layout + if t.master_count == 0 then + awful.layout.suit.fair.arrange(p) + return + end + + -- Special case: one slave -> relapse into awesomes masterstack tile layout + if nslaves == 1 then + awful.layout.suit.tile.right.arrange(p) + return + end + + -- Special case: no slaves -> fullscreen master area + if nslaves < 1 then + master_area_width = area.width + master_area_x = area.x + end + + -- iterate through masters + for idx = 1, nmaster do + local c = p.clients[idx] + local g + g = { + x = master_area_x, + y = area.y + (nmaster - idx) * (area.height / nmaster), + width = master_area_width, + height = area.height / nmaster, + } + p.geometries[c] = g + end + + -- iterate through slaves + for idx = 1, nslaves do -- idx=nmaster+1,#p.clients do + local c = p.clients[idx + nmaster] + local g + if idx % 2 == 0 then + g = { + x = area.x, + y = area.y + + left_iterator + * (area.height / number_of_left_sided_slaves), + width = slave_area_width / 2, + height = area.height / number_of_left_sided_slaves, + } + left_iterator = left_iterator + 1 + else + g = { + x = area.x + master_area_width + slave_area_width / 2, + y = area.y + + right_iterator + * (area.height / number_of_right_sided_slaves), + width = slave_area_width / 2, + height = area.height / number_of_right_sided_slaves, + } + right_iterator = right_iterator + 1 + end + p.geometries[c] = g + end +end + +return mylayout diff --git a/.config/awesome/extras/bling/layout/deck.lua b/.config/awesome/extras/bling/layout/deck.lua new file mode 100755 index 0000000..e0500b9 --- /dev/null +++ b/.config/awesome/extras/bling/layout/deck.lua @@ -0,0 +1,37 @@ +local mylayout = {} + +mylayout.name = "deck" + +function mylayout.arrange(p) + local area = p.workarea + local t = p.tag or screen[p.screen].selected_tag + local client_count = #p.clients + + if client_count == 1 then + local c = p.clients[1] + local g = { + x = area.x, + y = area.y, + width = area.width, + height = area.height, + } + p.geometries[c] = g + return + end + + local xoffset = area.width * 0.1 / (client_count - 1) + local yoffset = area.height * 0.1 / (client_count - 1) + + for idx = 1, client_count do + local c = p.clients[idx] + local g = { + x = area.x + (idx - 1) * xoffset, + y = area.y + (idx - 1) * yoffset, + width = area.width - (xoffset * (client_count - 1)), + height = area.height - (yoffset * (client_count - 1)), + } + p.geometries[c] = g + end +end + +return mylayout diff --git a/.config/awesome/extras/bling/layout/equalarea.lua b/.config/awesome/extras/bling/layout/equalarea.lua new file mode 100755 index 0000000..37e972d --- /dev/null +++ b/.config/awesome/extras/bling/layout/equalarea.lua @@ -0,0 +1,77 @@ +local math = math +local screen = screen +local mylayout = {} +mylayout.name = "equalarea" + +local function divide(p, g, low, high, cls, mwfact, mcount) + if low == high then + p.geometries[cls[low]] = g + else + local masters = math.max(0, math.min(mcount, high) - low + 1) + local numblock = high - low + 1 + local slaves = numblock - masters + local smalldiv + if numblock > 5 and (numblock % 5) == 0 then + smalldiv = math.floor(numblock / 5) + else + if (numblock % 3) == 0 then + smalldiv = math.floor(numblock / 3) + else + smalldiv = math.floor(numblock / 2) + end + end + local bigdiv = numblock - smalldiv + local smallmasters = math.min(masters, smalldiv) + local bigmasters = masters - smallmasters + local smallg = {} + local bigg = {} + smallg.x = g.x + smallg.y = g.y + if g.width > (g.height * 1.3) then + smallg.height = g.height + bigg.height = g.height + bigg.width = math.floor( + g.width + * (bigmasters * (mwfact - 1) + bigdiv) + / (slaves + mwfact * masters) + ) + smallg.width = g.width - bigg.width + bigg.y = g.y + bigg.x = g.x + smallg.width + else + smallg.width = g.width + bigg.width = g.width + bigg.height = math.floor( + g.height + * (bigmasters * (mwfact - 1) + bigdiv) + / (slaves + mwfact * masters) + ) + smallg.height = g.height - bigg.height + bigg.x = g.x + bigg.y = g.y + smallg.height + end + divide(p, smallg, low, high - bigdiv, cls, mwfact, mcount) + divide(p, bigg, low + smalldiv, high, cls, mwfact, mcount) + end + return +end + +function mylayout.arrange(p) + local t = p.tag or screen[p.screen].selected_tag + local wa = p.workarea + local cls = p.clients + + if #cls == 0 then + return + end + local mwfact = t.master_width_factor * 2 + local mcount = t.master_count + local g = {} + g.height = wa.height + g.width = wa.width + g.x = wa.x + g.y = wa.y + divide(p, g, 1, #cls, cls, mwfact, mcount) +end + +return mylayout diff --git a/.config/awesome/extras/bling/layout/horizontal.lua b/.config/awesome/extras/bling/layout/horizontal.lua new file mode 100755 index 0000000..23f9c9e --- /dev/null +++ b/.config/awesome/extras/bling/layout/horizontal.lua @@ -0,0 +1,56 @@ +local math = math + +local mylayout = {} + +mylayout.name = "horizontal" + +function mylayout.arrange(p) + local area = p.workarea + local t = p.tag or screen[p.screen].selected_tag + local mwfact = t.master_width_factor + local nmaster = math.min(t.master_count, #p.clients) + local nslaves = #p.clients - nmaster + + local master_area_height = area.height * mwfact + local slave_area_height = area.height - master_area_height + + -- Special case: no slaves + if nslaves == 0 then + master_area_height = area.height + slave_area_height = 0 + end + + -- Special case: no masters + if nmaster == 0 then + master_area_height = 0 + slave_area_height = area.height + end + + -- itearte through masters + for idx = 1, nmaster do + local c = p.clients[idx] + local g = { + x = area.x + (idx - 1) * (area.width / nmaster), + y = area.y, + width = area.width / nmaster, + height = master_area_height, + } + p.geometries[c] = g + end + + -- iterate through slaves + for idx = 1, nslaves do + local c = p.clients[idx + nmaster] + local g = { + x = area.x, + y = area.y + + master_area_height + + (idx - 1) * (slave_area_height / nslaves), + width = area.width, + height = slave_area_height / nslaves, + } + p.geometries[c] = g + end +end + +return mylayout diff --git a/.config/awesome/extras/bling/layout/init.lua b/.config/awesome/extras/bling/layout/init.lua new file mode 100755 index 0000000..a57dd6a --- /dev/null +++ b/.config/awesome/extras/bling/layout/init.lua @@ -0,0 +1,44 @@ +local beautiful = require("beautiful") +local gears = require("gears") + +local M = {} +local relative_lua_path = tostring(...) + +local function get_layout_icon_path(name) + local relative_icon_path = relative_lua_path + :match("^.*bling"):gsub("%.", "/") + .. "/icons/layouts/" .. name .. ".png" + + for p in package.path:gmatch('([^;]+)') do + p = p:gsub("?.*", "") + local absolute_icon_path = p .. relative_icon_path + if gears.filesystem.file_readable(absolute_icon_path) then + return absolute_icon_path + end + end +end + +local function get_icon(icon_raw) + if icon_raw ~= nil then + return gears.color.recolor_image(icon_raw, "#e6e6e6") + else + return nil + end +end + +local layouts = { + "mstab", + "vertical", + "horizontal", + "centered", + "equalarea", + "deck" +} + +for _, layout_name in ipairs(layouts) do + local icon_raw = get_layout_icon_path(layout_name) + beautiful["layout_" .. layout_name] = get_icon(icon_raw) + M[layout_name] = require(... .. "." .. layout_name) +end + +return M diff --git a/.config/awesome/extras/bling/layout/mstab.lua b/.config/awesome/extras/bling/layout/mstab.lua new file mode 100755 index 0000000..93ceb0e --- /dev/null +++ b/.config/awesome/extras/bling/layout/mstab.lua @@ -0,0 +1,249 @@ +local awful = require("awful") +local gears = require("gears") +local wibox = require("wibox") +local beautiful = require("beautiful") + +local mylayout = {} + +mylayout.name = "mstab" + +local tabbar_disable = beautiful.mstab_bar_disable or false +local tabbar_ontop = beautiful.mstab_bar_ontop or false +local tabbar_padding = beautiful.mstab_bar_padding or "default" +local border_radius = beautiful.mstab_border_radius + or beautiful.border_radius + or 0 +local tabbar_position = beautiful.mstab_tabbar_position + or beautiful.tabbar_position + or "top" + +local bar_style = beautiful.mstab_tabbar_style + or beautiful.tabbar_style + or "default" +local bar = require( + tostring(...):match(".*bling") .. ".widget.tabbar." .. bar_style +) +local tabbar_size = bar.size + or beautiful.mstab_bar_height + or beautiful.tabbar_size + or 40 +local dont_resize_slaves = beautiful.mstab_dont_resize_slaves or false + +-- The top_idx is the idx of the slave clients (excluding all master clients) +-- that should be on top of all other slave clients ("the focused slave") +-- by creating a variable outside of the arrange function, this layout can "remember" that client +-- by creating it as a new property of every tag, this layout can be active on different tags and +-- still have different "focused slave clients" +for idx, tag in ipairs(root.tags()) do + tag.top_idx = 1 +end + +-- Haven't found a signal that is emitted when a new tag is added. That should work though +-- since you can't use a layout on a tag that you haven't selected previously +tag.connect_signal("property::selected", function(t) + if not t.top_idx then + t.top_idx = 1 + end +end) + +function update_tabbar( + clients, + t, + top_idx, + area, + master_area_width, + slave_area_width +) + local s = t.screen + + -- create the list of clients for the tabbar + local clientlist = bar.layout() + for idx, c in ipairs(clients) do + -- focus with right click, kill with mid click, minimize with left click + local buttons = gears.table.join( + awful.button({}, 1, function() + c:raise() + client.focus = c + end), + awful.button({}, 2, function() + c:kill() + end), + awful.button({}, 3, function() + c.minimized = true + end) + ) + local client_box = bar.create(c, (idx == top_idx), buttons) + clientlist:add(client_box) + end + + -- if no tabbar exists, create one + if not s.tabbar then + s.tabbar = wibox({ + ontop = tabbar_ontop, + shape = function(cr, width, height) + gears.shape.rounded_rect(cr, width, height, border_radius) + end, + bg = bar.bg_normal, + visible = true, + }) + + -- Change visibility of the tab bar when layout, selected tag or number of clients (visible, master, slave) changes + local function adjust_visiblity(t) + s.tabbar.visible = (#t:clients() - t.master_count > 1) + and (t.layout.name == mylayout.name) + end + + tag.connect_signal("property::selected", function(t) + adjust_visiblity(t) + end) + tag.connect_signal("property::layout", function(t, layout) + adjust_visiblity(t) + end) + tag.connect_signal("tagged", function(t, c) + adjust_visiblity(t) + end) + tag.connect_signal("untagged", function(t, c) + adjust_visiblity(t) + end) + tag.connect_signal("property::master_count", function(t) + adjust_visiblity(t) + end) + client.connect_signal("property::minimized", function(c) + local t = c.first_tag + adjust_visiblity(t) + end) + end + + -- update the tabbar size and position (to support gap size change on the fly) + if tabbar_position == "top" then + s.tabbar.x = area.x + master_area_width + t.gap + s.tabbar.y = area.y + t.gap + s.tabbar.width = slave_area_width - 2 * t.gap + s.tabbar.height = tabbar_size + elseif tabbar_position == "bottom" then + s.tabbar.x = area.x + master_area_width + t.gap + s.tabbar.y = area.y + area.height - tabbar_size - t.gap + s.tabbar.width = slave_area_width - 2 * t.gap + s.tabbar.height = tabbar_size + elseif tabbar_position == "left" then + s.tabbar.x = area.x + master_area_width + t.gap + s.tabbar.y = area.y + t.gap + s.tabbar.width = tabbar_size + s.tabbar.height = area.height - 2 * t.gap + elseif tabbar_position == "right" then + s.tabbar.x = area.x + + master_area_width + + slave_area_width + - tabbar_size + - t.gap + s.tabbar.y = area.y + t.gap + s.tabbar.width = tabbar_size + s.tabbar.height = area.height - 2 * t.gap + end + + -- update clientlist + s.tabbar:setup({ layout = wibox.layout.flex.horizontal, clientlist }) +end + +function mylayout.arrange(p) + local area = p.workarea + local t = p.tag or screen[p.screen].selected_tag + local s = t.screen + local mwfact = t.master_width_factor + local nmaster = math.min(t.master_count, #p.clients) + local nslaves = #p.clients - nmaster + + local master_area_width = area.width * mwfact + local slave_area_width = area.width - master_area_width + + -- "default" means that it uses standard useless gap size + if tabbar_padding == "default" then + tabbar_padding = 2 * t.gap + end + + -- Special case: No masters -> full screen slave width + if nmaster == 0 then + master_area_width = 1 + slave_area_width = area.width + end + + -- Special case: One or zero slaves -> no tabbar (essentially tile right) + if nslaves <= 1 then + -- since update_tabbar isnt called that way we have to hide it manually + if s.tabbar then + s.tabbar.visible = false + end + -- otherwise just do tile right + awful.layout.suit.tile.right.arrange(p) + return + end + + -- Iterate through masters + for idx = 1, nmaster do + local c = p.clients[idx] + local g = { + x = area.x, + y = area.y + (idx - 1) * (area.height / nmaster), + width = master_area_width, + height = area.height / nmaster, + } + p.geometries[c] = g + end + + local tabbar_size_change = 0 + local tabbar_width_change = 0 + local tabbar_y_change = 0 + local tabbar_x_change = 0 + if not tabbar_disable then + if tabbar_position == "top" then + tabbar_size_change = tabbar_size + tabbar_padding + tabbar_y_change = tabbar_size + tabbar_padding + elseif tabbar_position == "bottom" then + tabbar_size_change = tabbar_size + tabbar_padding + elseif tabbar_position == "left" then + tabbar_width_change = tabbar_size + tabbar_padding + tabbar_x_change = tabbar_size + tabbar_padding + elseif tabbar_position == "right" then + tabbar_width_change = tabbar_size + tabbar_padding + end + end + + -- Iterate through slaves + -- (also creates a list of all slave clients for update_tabbar) + local slave_clients = {} + for idx = 1, nslaves do + local c = p.clients[idx + nmaster] + slave_clients[#slave_clients + 1] = c + if c == client.focus then + t.top_idx = #slave_clients + end + local g = { + x = area.x + master_area_width + tabbar_x_change, + y = area.y + tabbar_y_change, + width = slave_area_width - tabbar_width_change, + height = area.height - tabbar_size_change, + } + if not dont_resize_slaves and idx ~= t.top_idx then + g = { + x = area.x + master_area_width + slave_area_width / 4, + y = area.y + tabbar_size + area.height / 4, + width = slave_area_width / 2, + height = area.height / 4 - tabbar_size, + } + end + p.geometries[c] = g + end + + if not tabbar_disable then + update_tabbar( + slave_clients, + t, + t.top_idx, + area, + master_area_width, + slave_area_width + ) + end +end + +return mylayout diff --git a/.config/awesome/extras/bling/layout/vertical.lua b/.config/awesome/extras/bling/layout/vertical.lua new file mode 100755 index 0000000..8b6811e --- /dev/null +++ b/.config/awesome/extras/bling/layout/vertical.lua @@ -0,0 +1,56 @@ +local math = math + +local mylayout = {} + +mylayout.name = "vertical" + +function mylayout.arrange(p) + local area = p.workarea + local t = p.tag or screen[p.screen].selected_tag + local mwfact = t.master_width_factor + local nmaster = math.min(t.master_count, #p.clients) + local nslaves = #p.clients - nmaster + + local master_area_width = area.width * mwfact + local slave_area_width = area.width - master_area_width + + -- Special case: no slaves + if nslaves == 0 then + master_area_width = area.width + slave_area_width = 0 + end + + -- Special case: no masters + if nmaster == 0 then + master_area_width = 0 + slave_area_width = area.width + end + + -- iterate through masters + for idx = 1, nmaster do + local c = p.clients[idx] + local g = { + x = area.x, + y = area.y + (idx - 1) * (area.height / nmaster), + width = master_area_width, + height = area.height / nmaster, + } + p.geometries[c] = g + end + + -- itearte through slaves + for idx = 1, nslaves do + local c = p.clients[idx + nmaster] + local g = { + x = area.x + + master_area_width + + (idx - 1) * (slave_area_width / nslaves), + y = area.y, + width = slave_area_width / nslaves, + height = area.height, + } + p.geometries[c] = g + end +end + +return mylayout diff --git a/.config/awesome/extras/bling/module/flash_focus.lua b/.config/awesome/extras/bling/module/flash_focus.lua new file mode 100755 index 0000000..2fe3f47 --- /dev/null +++ b/.config/awesome/extras/bling/module/flash_focus.lua @@ -0,0 +1,44 @@ +local gears = require("gears") +local beautiful = require("beautiful") + +local op = beautiful.flash_focus_start_opacity or 0.6 +local stp = beautiful.flash_focus_step or 0.01 + +local flashfocus = function(c) + if c then + c.opacity = op + local q = op + local g = gears.timer({ + timeout = stp, + call_now = false, + autostart = true, + }) + + g:connect_signal("timeout", function() + if not c.valid then + return + end + if q >= 1 then + c.opacity = 1 + g:stop() + else + c.opacity = q + q = q + stp + end + end) + end + + -- Bring the focused client to the top + if c then + c:raise() + end +end + +local enable = function() + client.connect_signal("focus", flashfocus) +end +local disable = function() + client.disconnect_signal("focus", flashfocus) +end + +return { enable = enable, disable = disable, flashfocus = flashfocus } diff --git a/.config/awesome/extras/bling/module/init.lua b/.config/awesome/extras/bling/module/init.lua new file mode 100755 index 0000000..ed127f6 --- /dev/null +++ b/.config/awesome/extras/bling/module/init.lua @@ -0,0 +1,8 @@ +return { + window_swallowing = require(... .. ".window_swallowing"), + tiled_wallpaper = require(... .. ".tiled_wallpaper"), + wallpaper = require(... .. ".wallpaper"), + flash_focus = require(... .. ".flash_focus"), + tabbed = require(... .. ".tabbed"), + scratchpad = require(... .. ".scratchpad"), +} diff --git a/.config/awesome/extras/bling/module/scratchpad.lua b/.config/awesome/extras/bling/module/scratchpad.lua new file mode 100755 index 0000000..6f1bf8b --- /dev/null +++ b/.config/awesome/extras/bling/module/scratchpad.lua @@ -0,0 +1,374 @@ +local awful = require("awful") +local gears = require("gears") +local naughty = require("naughty") +local helpers = require(tostring(...):match(".*bling") .. ".helpers") +local capi = { awesome = awesome, client = client } +local ruled = capi.awesome.version ~= "v4.3" and require("ruled") or nil +local pairs = pairs + +local Scratchpad = { mt = {} } + +--- Called when the turn off animation has ended +local function on_animate_turn_off_end(self, tag) + -- When toggling off a scratchpad that's present on multiple tags + -- depsite still being unminizmied on the other tags it will become invisible + -- as it's position could be outside the screen from the animation + self.client:geometry({ + x = self.geometry.x + self.client.screen.geometry.x, + y = self.geometry.y + self.client.screen.geometry.y, + width = self.geometry.width, + height = self.geometry.height, + }) + + helpers.client.turn_off(self.client, tag) + + self.turning_off = false + + self:emit_signal("turn_off", self.client) +end + +--- The turn off animation +local function animate_turn_off(self, anim, axis) + self.screen_on_toggled_scratchpad = self.client.screen + self.tag_on_toggled_scratchpad = self.screen_on_toggled_scratchpad.selected_tag + + if self.client.floating == false then + -- Save the client geometry before floating it + local non_floating_x = self.client.x + local non_floating_y = self.client.y + local non_floating_width = self.client.width + local non_floating_height = self.client.height + + -- Can't animate non floating clients + self.client.floating = true + + -- Set the client geometry back to what it was before floating it + self.client:geometry({ + x = non_floating_x, + y = non_floating_y, + width = non_floating_width, + height = non_floating_height, + }) + end + + if axis == "x" then + anim.pos = self.client.x + else + anim.pos = self.client.y + end + + anim:set(anim:initial()) +end + +-- Handles changing tag mid animation +local function abort_if_tag_was_switched(self) + -- Check for the following scenerio: + -- Toggle on scratchpad at tag 1 + -- Toggle on scratchpad at tag 2 + -- Toggle off scratchpad at tag 1 + -- Switch to tag 2 + -- Outcome: The client will remain on tag 1 and will instead be removed from tag 2 + if (self.turning_off) and (self.screen_on_toggled_scratchpad and + self.screen_on_toggled_scratchpad.selected_tag) ~= self.tag_on_toggled_scratchpad + then + if self.rubato.x then + self.rubato.x:abort() + end + if self.rubato.y then + self.rubato.y:abort() + end + on_animate_turn_off_end(self, self.tag_on_toggled_scratchpad) + self.screen_on_toggled_scratchpad.selected_tag = nil + self.tag_on_toggled_scratchpad = nil + end +end + +--- The turn on animation +local function animate_turn_on(self, anim, axis) + -- Check for the following scenerio: + -- Toggle on scratchpad at tag 1 + -- Toggle on scratchpad at tag 2 + -- The animation will instantly end + -- as the timer pos is already at the on position + -- from toggling on the scratchpad at tag 1 + if axis == "x" and anim.pos == self.geometry.x then + anim.pos = anim:initial() + else + if anim.pos == self.geometry.y then + anim.pos = anim:initial() + end + end + + if axis == "x" then + anim:set(self.geometry.x) + else + anim:set(self.geometry.y) + end +end + +--- Creates a new scratchpad object based on the argument +-- +-- @param args A table of possible arguments +-- @return The new scratchpad object +function Scratchpad:new(args) + args = args or {} + if args.awestore then + naughty.notify({ + title = "Bling Error", + text = "Awestore is no longer supported! Please take a look at the scratchpad documentation and use rubato for animations instead.", + }) + end + + args.rubato = args.rubato or {} + + local ret = gears.object{} + gears.table.crush(ret, Scratchpad) + gears.table.crush(ret, args) + + if ret.rubato.x then + ret.rubato.x:subscribe(function(pos) + if ret.client and ret.client.valid then + ret.client.x = pos + end + abort_if_tag_was_switched(ret) + end) + + ret.rubato.x.ended:subscribe(function() + if ((ret.rubato.y and ret.rubato.y.state == false) or (ret.rubato.y == nil)) and ret.turning_off == true then + on_animate_turn_off_end(ret) + end + end) + end + if ret.rubato.y then + ret.rubato.y:subscribe(function(pos) + if ret.client and ret.client.valid then + ret.client.y = pos + end + abort_if_tag_was_switched(ret) + end) + + ret.rubato.y.ended:subscribe(function() + if ((ret.rubato.x and ret.rubato.x.state == false) or (ret.rubato.x == nil)) and ret.turning_off == true then + on_animate_turn_off_end(ret) + end + end) + end + + return ret +end + +--- Find all clients that satisfy the the rule +-- +-- @return A list of all clients that satisfy the rule +function Scratchpad:find() + return helpers.client.find(self.rule) +end + +--- Applies the objects scratchpad properties to a given client +-- +-- @param c A client to which to apply the properties +function Scratchpad:apply(c) + if not c or not c.valid then + return + end + c.floating = self.floating + c.sticky = self.sticky + c.fullscreen = false + c.maximized = false + c:geometry({ + x = self.geometry.x + awful.screen.focused().geometry.x, + y = self.geometry.y + awful.screen.focused().geometry.y, + width = self.geometry.width, + height = self.geometry.height, + }) + + if self.autoclose then + c:connect_signal("unfocus", function(c1) + c1.sticky = false -- client won't turn off if sticky + helpers.client.turn_off(c1) + end) + end +end + +--- Turns the scratchpad on +function Scratchpad:turn_on() + self.client = self:find()[1] + + local anim_x = self.rubato.x + local anim_y = self.rubato.y + + local in_anim = false + if (anim_x and anim_x.state == true) or (anim_y and anim_y.state == true) then + in_anim = true + end + + if self.client and not in_anim and self.client.first_tag and self.client.first_tag.selected then + self.client:raise() + capi.client.focus = self.client + return + end + if self.client and not in_anim then + -- if a client was found, turn it on + if self.reapply then + self:apply(self.client) + end + -- c.sticky was set to false in turn_off so it has to be reapplied anyway + self.client.sticky = self.sticky + + if anim_x then + animate_turn_on(self, anim_x, "x") + end + if anim_y then + animate_turn_on(self, anim_y, "y") + end + + helpers.client.turn_on(self.client) + self:emit_signal("turn_on", self.client) + + return + end + if not self.client then + -- if no client was found, spawn one, find the corresponding window, + -- apply the properties only once (until the next closing) + local pid = awful.spawn.with_shell(self.command) + if capi.awesome.version ~= "v4.3" then + ruled.client.append_rule({ + id = "scratchpad", + rule = self.rule, + properties = { + -- If a scratchpad is opened it should spawn at the current tag + -- the same way it will behave if the client was already open + tag = awful.screen.focused().selected_tag, + switch_to_tags = false, + -- Hide the client until the gemoetry rules are applied + hidden = true, + minimized = true, + }, + callback = function(c) + -- For a reason I can't quite get the gemotery rules will fail to apply unless we use this timer + gears.timer({ + timeout = 0.15, + autostart = true, + single_shot = true, + callback = function() + self.client = c + + self:apply(c) + c.hidden = false + c.minimized = false + -- Some clients fail to gain focus + c:activate({}) + + if anim_x then + animate_turn_on(self, anim_x, "x") + end + if anim_y then + animate_turn_on(self, anim_y, "y") + end + + self:emit_signal("inital_apply", c) + + -- Discord spawns 2 windows, so keep the rule until the 2nd window shows + if c.name ~= "Discord Updater" then + ruled.client.remove_rule("scratchpad") + end + -- In a case Discord is killed before the second window spawns + c:connect_signal("request::unmanage", function() + ruled.client.remove_rule("scratchpad") + end) + end, + }) + end, + }) + else + local function inital_apply(c1) + if helpers.client.is_child_of(c1, pid) then + self.client = c1 + + self:apply(c1) + if anim_x then + animate_turn_on(self, anim_x, "x") + end + if anim_y then + animate_turn_on(self, anim_y, "y") + end + self:emit_signal("inital_apply", c1) + self.client.disconnect_signal("manage", inital_apply) + end + end + self.client.connect_signal("manage", inital_apply) + end + end +end + +--- Turns the scratchpad off +function Scratchpad:turn_off() + self.client = self:find()[1] + + -- Get the tweens + local anim_x = self.rubato.x + local anim_y = self.rubato.y + + local in_anim = false + if (anim_x and anim_x.state == true) or (anim_y and anim_y.state == true) then + in_anim = true + end + + if self.client and not in_anim then + if anim_x then + self.turning_off = true + animate_turn_off(self, anim_x, "x") + end + if anim_y then + self.turning_off = true + animate_turn_off(self, anim_y, "y") + end + + if not anim_x and not anim_y then + helpers.client.turn_off(self.client) + self:emit_signal("turn_off", self.client) + end + end +end + +--- Turns the scratchpad off if it is focused otherwise it raises the scratchpad +function Scratchpad:toggle() + local is_turn_off = false + local c = self:find()[1] + if self.dont_focus_before_close then + if c then + if c.sticky and #c:tags() > 0 then + is_turn_off = true + else + local current_tag = c.screen.selected_tag + for k, tag in pairs(c:tags()) do + if tag == current_tag then + is_turn_off = true + break + else + is_turn_off = false + end + end + end + end + else + is_turn_off = capi.client.focus + and awful.rules.match(capi.client.focus, self.rule) + end + + if is_turn_off then + self:turn_off() + else + self:turn_on() + end +end + +--- Make the module callable without putting a `:new` at the end of it +-- +-- @param args A table of possible arguments +-- @return The new scratchpad object +function Scratchpad.mt:__call(...) + return Scratchpad:new(...) +end + +return setmetatable(Scratchpad, Scratchpad.mt) diff --git a/.config/awesome/extras/bling/module/tabbed.lua b/.config/awesome/extras/bling/module/tabbed.lua new file mode 100755 index 0000000..c53ec03 --- /dev/null +++ b/.config/awesome/extras/bling/module/tabbed.lua @@ -0,0 +1,274 @@ +--[[ + +This module currently works by adding a new property to each client that is tabbed. +That new property is called bling_tabbed. +So each client in a tabbed state has the property "bling_tabbed" which is a table. +Each client that is not tabbed doesn't have that property. +In the function themselves, the same object is refered to as "tabobj" which is why +you will often see something like: "local tabobj = some_client.bling_tabbed" at the beginning +of a function. + +--]] + +local awful = require("awful") +local wibox = require("wibox") +local gears = require("gears") +local beautiful = require("beautiful") +local helpers = require(tostring(...):match(".*bling") .. ".helpers") + +local bar_style = beautiful.tabbar_style or "default" +local bar = require( + tostring(...):match(".*bling") .. ".widget.tabbar." .. bar_style +) + +tabbed = {} + +-- helper function to connect to the (un)focus signals +local function update_tabbar_from(c) + if not c or not c.bling_tabbed then + return + end + tabbed.update_tabbar(c.bling_tabbed) +end + +-- used to change focused tab relative to the currently focused one +tabbed.iter = function(idx) + if not idx then + idx = 1 + end + if not client.focus or not client.focus.bling_tabbed then + return + end + local tabobj = client.focus.bling_tabbed + local new_idx = (tabobj.focused_idx + idx) % #tabobj.clients + if new_idx == 0 then + new_idx = #tabobj.clients + end + tabbed.switch_to(tabobj, new_idx) +end + +-- removes a given client from its tab object +tabbed.remove = function(c) + if not c or not c.bling_tabbed then + return + end + local tabobj = c.bling_tabbed + table.remove(tabobj.clients, tabobj.focused_idx) + if not beautiful.tabbar_disable then + awful.titlebar.hide(c, bar.position) + end + c.bling_tabbed = nil + c:disconnect_signal("focus", update_tabbar_from) + c:disconnect_signal("unfocus", update_tabbar_from) + awesome.emit_signal("bling::tabbed::client_removed", tabobj, c) + tabbed.switch_to(tabobj, 1) +end + +-- removes the currently focused client from the tab object +tabbed.pop = function() + if not client.focus or not client.focus.bling_tabbed then + return + end + tabbed.remove(client.focus) +end + +-- adds a client to a given tabobj +tabbed.add = function(c, tabobj) + if c.bling_tabbed then + tabbed.remove(c) + end + c:connect_signal("focus", update_tabbar_from) + c:connect_signal("unfocus", update_tabbar_from) + helpers.client.sync(c, tabobj.clients[tabobj.focused_idx]) + tabobj.clients[#tabobj.clients + 1] = c + tabobj.focused_idx = #tabobj.clients + -- calls update even though switch_to calls update again + -- but the new client needs to have the tabobj property + -- before a clean switch can happen + tabbed.update(tabobj) + awesome.emit_signal("bling::tabbed::client_added", tabobj, c) + tabbed.switch_to(tabobj, #tabobj.clients) +end + +-- use xwininfo to select one client and make it tab in the currently focused tab +tabbed.pick = function() + if not client.focus then + return + end + -- this function uses xwininfo to grab a client window id which is then + -- compared to all other clients window ids + + local xwininfo_cmd = + [[ xwininfo | grep 'xwininfo: Window id:' | cut -d " " -f 4 ]] + awful.spawn.easy_async_with_shell(xwininfo_cmd, function(output) + for _, c in ipairs(client.get()) do + if tonumber(c.window) == tonumber(output) then + if not client.focus.bling_tabbed and not c.bling_tabbed then + tabbed.init(client.focus) + tabbed.add(c, client.focus.bling_tabbed) + end + if not client.focus.bling_tabbed and c.bling_tabbed then + tabbed.add(client.focus, c.bling_tabbed) + end + if client.focus.bling_tabbed and not c.bling_tabbed then + tabbed.add(c, client.focus.bling_tabbed) + end + -- TODO: Should also merge tabs when focus and picked + -- both are tab groups + end + end + end) +end + +-- select a client by direction and make it tab in the currently focused tab +tabbed.pick_by_direction = function(direction) + local sel = client.focus + if not sel then + return + end + if not sel.bling_tabbed then + tabbed.init(sel) + end + local c = helpers.client.get_by_direction(direction) + if not c then + return + end + tabbed.add(c, sel.bling_tabbed) +end + +-- use dmenu to select a client and make it tab in the currently focused tab +tabbed.pick_with_dmenu = function(dmenu_command) + if not client.focus then + return + end + + if not dmenu_command then + dmenu_command = "rofi -dmenu -i" + end + + -- get all clients from the current tag + -- ignores the case where multiple tags are selected + local t = awful.screen.focused().selected_tag + local list_clients = {} + local list_clients_string = "" + for idx, c in ipairs(t:clients()) do + if c.window ~= client.focus.window then + list_clients[#list_clients + 1] = c + if #list_clients ~= 1 then + list_clients_string = list_clients_string .. "\\n" + end + list_clients_string = list_clients_string + .. tostring(c.window) + .. " " + .. c.name + end + end + + if #list_clients == 0 then + return + end + -- calls the actual dmenu + local xprop_cmd = [[ echo -e "]] + .. list_clients_string + .. [[" | ]] + .. dmenu_command + .. [[ | awk '{ print $1 }' ]] + awful.spawn.easy_async_with_shell(xprop_cmd, function(output) + for _, c in ipairs(list_clients) do + if tonumber(c.window) == tonumber(output) then + if not client.focus.bling_tabbed then + tabbed.init(client.focus) + end + local tabobj = client.focus.bling_tabbed + tabbed.add(c, tabobj) + end + end + end) +end + +-- update everything about one tab object +tabbed.update = function(tabobj) + local currently_focused_c = tabobj.clients[tabobj.focused_idx] + -- update tabobj of each client and other things + for idx, c in ipairs(tabobj.clients) do + if c.valid then + c.bling_tabbed = tabobj + helpers.client.sync(c, currently_focused_c) + -- the following handles killing a client while the client is tabbed + c:connect_signal("unmanage", function(c) + tabbed.remove(c) + end) + end + end + + -- Maybe remove if I'm the only one using it? + awesome.emit_signal("bling::tabbed::update", tabobj) + if not beautiful.tabbar_disable then + tabbed.update_tabbar(tabobj) + end +end + +-- change focused tab by absolute index +tabbed.switch_to = function(tabobj, new_idx) + local old_focused_c = tabobj.clients[tabobj.focused_idx] + tabobj.focused_idx = new_idx + for idx, c in ipairs(tabobj.clients) do + if idx ~= new_idx then + helpers.client.turn_off(c) + else + helpers.client.turn_on(c) + c:raise() + if old_focused_c and old_focused_c.valid then + c:swap(old_focused_c) + end + helpers.client.sync(c, old_focused_c) + end + end + awesome.emit_signal("bling::tabbed::changed_focus", tabobj) + tabbed.update(tabobj) +end + +tabbed.update_tabbar = function(tabobj) + local flexlist = bar.layout() + local tabobj_focused_client = tabobj.clients[tabobj.focused_idx] + local tabobj_is_focused = (client.focus == tabobj_focused_client) + -- itearte over all tabbed clients to create the widget tabbed list + for idx, c in ipairs(tabobj.clients) do + local buttons = gears.table.join(awful.button({}, 1, function() + tabbed.switch_to(tabobj, idx) + end)) + local wid_temp = bar.create(c, (idx == tabobj.focused_idx), buttons, + not tabobj_is_focused) + flexlist:add(wid_temp) + end + -- add tabbar to each tabbed client (clients will be hided anyway) + for _, c in ipairs(tabobj.clients) do + local titlebar = awful.titlebar(c, { + bg = bar.bg_normal, + size = bar.size, + position = bar.position, + }) + titlebar:setup({ layout = wibox.layout.flex.horizontal, flexlist }) + end +end + +tabbed.init = function(c) + local tabobj = {} + tabobj.clients = { c } + c:connect_signal("focus", update_tabbar_from) + c:connect_signal("unfocus", update_tabbar_from) + tabobj.focused_idx = 1 + tabbed.update(tabobj) +end + +if beautiful.tabbed_spawn_in_tab then + client.connect_signal("manage", function(c) + local s = awful.screen.focused() + local previous_client = awful.client.focus.history.get(s, 1) + if previous_client and previous_client.bling_tabbed then + tabbed.add(c, previous_client.bling_tabbed) + end + end) +end + +return tabbed diff --git a/.config/awesome/extras/bling/module/tiled_wallpaper.lua b/.config/awesome/extras/bling/module/tiled_wallpaper.lua new file mode 100755 index 0000000..75014cf --- /dev/null +++ b/.config/awesome/extras/bling/module/tiled_wallpaper.lua @@ -0,0 +1,56 @@ +--[[ + This module makes use of cairo surfaces + For documentation take a look at the C docs: + https://www.cairographics.org/ + They can be applied to lua by changing the naming conventions + and adjusting for the missing namespaces (and classes) + for example: + cairo_rectangle(cr, 1, 1, 1, 1) in C would be written as + cr:rectangle(1, 1, 1, 1) in lua + and + cairo_fill(cr) in C would be written as + cr:fill() in lua +--]] + +local cairo = require("lgi").cairo +local gears = require("gears") + +function create_tiled_wallpaper(str, s, args_table) + -- user input + args_table = args_table or {} + local fg = args_table.fg or "#ff0000" + local bg = args_table.bg or "#00ffff" + local offset_x = args_table.offset_x + local offset_y = args_table.offset_y + local font = args_table.font or "Hack" + local font_size = tonumber(args_table.font_size) or 16 + local zickzack_bool = args_table.zickzack or false + local padding = args_table.padding or 100 + + -- create cairo image wallpaper + local img = cairo.ImageSurface(cairo.Format.RGB24, padding, padding) + cr = cairo.Context(img) + + cr:set_source(gears.color(bg)) + cr:paint() + + cr:set_source(gears.color(fg)) + + cr:set_font_size(font_size) + cr:select_font_face(font) + + if zickzack_bool then + cr:set_source(gears.color(fg)) + cr:move_to(padding / 2 + font_size, padding / 2 + font_size) + cr:show_text(str) + end + + cr:set_source(gears.color(fg)) + cr:move_to(font_size, font_size) + cr:show_text(str) + + -- tile cairo image + gears.wallpaper.tiled(img, s, { x = offset_x, y = offset_y }) +end + +return create_tiled_wallpaper diff --git a/.config/awesome/extras/bling/module/wallpaper.lua b/.config/awesome/extras/bling/module/wallpaper.lua new file mode 100755 index 0000000..b8344ff --- /dev/null +++ b/.config/awesome/extras/bling/module/wallpaper.lua @@ -0,0 +1,339 @@ +--------------------------------------------------------------------------- +-- 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 awful = require("awful") +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 + if awesome.version == "v4.3" then + awful.screen.connect_for_each_screen(set_wallpaper) + else + screen.connect_signal("request::wallpaper", set_wallpaper) + end +end + +return { + setup = setup, + setters = setters, + apply = apply, + prepare_list = prepare_list, +} diff --git a/.config/awesome/extras/bling/module/window_swallowing.lua b/.config/awesome/extras/bling/module/window_swallowing.lua new file mode 100755 index 0000000..368541b --- /dev/null +++ b/.config/awesome/extras/bling/module/window_swallowing.lua @@ -0,0 +1,88 @@ +local awful = require("awful") +local gears = require("gears") +local beautiful = require("beautiful") + +local helpers = require(tostring(...):match(".*bling") .. ".helpers") + +-- It might actually swallow too much, that's why there is a filter option by classname +-- without the don't-swallow-list it would also swallow for example +-- file pickers or new firefox windows spawned by an already existing one + +local window_swallowing_activated = false + +-- you might want to add or remove applications here +local dont_swallow_classname_list = beautiful.dont_swallow_classname_list + or { "firefox", "Gimp", "Google-chrome" } +local activate_dont_swallow_filter = beautiful.dont_swallow_filter_activated + or true + +-- checks if client classname matches with any entry of the dont-swallow-list +local function check_if_swallow(c) + if not activate_dont_swallow_filter then + return true + end + for _, classname in ipairs(dont_swallow_classname_list) do + if classname == c.class then + return false + end + end + return true +end + +-- the function that will be connected to / disconnected from the spawn client signal +local function manage_clientspawn(c) + -- get the last focused window to check if it is a parent window + local parent_client = awful.client.focus.history.get(c.screen, 1) + if not parent_client then + return + end + + -- io.popen is normally discouraged. Should probably be changed + local handle = io.popen( + [[pstree -T -p -a -s ]] + .. tostring(c.pid) + .. [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']] + ) + local parent_pid = handle:read("*a") + handle:close() + + if + (tostring(parent_pid) == tostring(parent_client.pid)) + and check_if_swallow(c) + then + c:connect_signal("unmanage", function() + helpers.client.turn_on(parent_client) + helpers.client.sync(parent_client, c) + end) + + helpers.client.sync(c, parent_client) + helpers.client.turn_off(parent_client) + end +end + +-- without the following functions that module would be autoloaded by require("bling") +-- a toggle window swallowing hotkey is also possible that way + +local function start() + client.connect_signal("manage", manage_clientspawn) + window_swallowing_activated = true +end + +local function stop() + client.disconnect_signal("manage", manage_clientspawn) + window_swallowing_activated = false +end + +local function toggle() + if window_swallowing_activated then + stop() + else + start() + end +end + +return { + start = start, + stop = stop, + toggle = toggle, +} diff --git a/.config/awesome/extras/bling/signal/init.lua b/.config/awesome/extras/bling/signal/init.lua new file mode 100755 index 0000000..a953d59 --- /dev/null +++ b/.config/awesome/extras/bling/signal/init.lua @@ -0,0 +1 @@ +return { playerctl = require(... .. ".playerctl") } diff --git a/.config/awesome/extras/bling/signal/playerctl/init.lua b/.config/awesome/extras/bling/signal/playerctl/init.lua new file mode 100755 index 0000000..1d586ed --- /dev/null +++ b/.config/awesome/extras/bling/signal/playerctl/init.lua @@ -0,0 +1,19 @@ +local beautiful = require("beautiful") + +-- Use CLI backend as default as it is supported on most if not all systems +local backend_config = beautiful.playerctl_backend or "playerctl_cli" +local backends = { + playerctl_cli = require(... .. ".playerctl_cli"), + playerctl_lib = require(... .. ".playerctl_lib"), +} + +local function enable_wrapper(args) + backend_config = (args and args.backend) or backend_config + backends[backend_config].enable(args) +end + +local function disable_wrapper() + backends[backend_config].disable() +end + +return { enable = enable_wrapper, disable = disable_wrapper } diff --git a/.config/awesome/extras/bling/signal/playerctl/playerctl_cli.lua b/.config/awesome/extras/bling/signal/playerctl/playerctl_cli.lua new file mode 100755 index 0000000..93959f7 --- /dev/null +++ b/.config/awesome/extras/bling/signal/playerctl/playerctl_cli.lua @@ -0,0 +1,151 @@ +-- +-- Provides: +-- bling::playerctl::status +-- playing (boolean) +-- bling::playerctl::title_artist_album +-- title (string) +-- artist (string) +-- album_path (string) +-- bling::playerctl::position +-- interval_sec (number) +-- length_sec (number) +-- bling::playerctl::no_players +-- +local awful = require("awful") +local beautiful = require("beautiful") + +local interval = beautiful.playerctl_position_update_interval or 1 + +local function emit_player_status() + local status_cmd = "playerctl status -F" + + -- Follow status + awful.spawn.easy_async({ + "pkill", + "--full", + "--uid", + os.getenv("USER"), + "^playerctl status", + }, function() + awful.spawn.with_line_callback(status_cmd, { + stdout = function(line) + local playing = false + if line:find("Playing") then + playing = true + else + playing = false + end + awesome.emit_signal("bling::playerctl::status", playing) + end, + }) + collectgarbage("collect") + end) +end + +local function emit_player_info() + local art_script = [[ +sh -c ' + +tmp_dir="$XDG_CACHE_HOME/awesome/" + +if [ -z ${XDG_CACHE_HOME} ]; then + tmp_dir="$HOME/.cache/awesome/" +fi + +tmp_cover_path=${tmp_dir}"cover.png" + +if [ ! -d $tmp_dir ]; then + mkdir -p $tmp_dir +fi + +link="$(playerctl metadata mpris:artUrl)" + +curl -s "$link" --output $tmp_cover_path + +echo "$tmp_cover_path" +']] + + -- Command that lists artist and title in a format to find and follow + local song_follow_cmd = + "playerctl metadata --format 'artist_{{artist}}title_{{title}}' -F" + + -- Progress Cmds + local prog_cmd = "playerctl position" + local length_cmd = "playerctl metadata mpris:length" + + awful.widget.watch(prog_cmd, interval, function(_, interval) + awful.spawn.easy_async_with_shell(length_cmd, function(length) + local length_sec = tonumber(length) -- in microseconds + local interval_sec = tonumber(interval) -- in seconds + if length_sec and interval_sec then + if interval_sec >= 0 and length_sec > 0 then + awesome.emit_signal( + "bling::playerctl::position", + interval_sec, + length_sec / 1000000 + ) + end + end + end) + collectgarbage("collect") + end) + + -- Follow title + awful.spawn.easy_async({ + "pkill", + "--full", + "--uid", + os.getenv("USER"), + "^playerctl metadata", + }, function() + awful.spawn.with_line_callback(song_follow_cmd, { + stdout = function(line) + local album_path = "" + awful.spawn.easy_async_with_shell(art_script, function(out) + -- Get album path + album_path = out:gsub("%\n", "") + -- Get title and artist + local artist = line:match("artist_(.*)title_") + local title = line:match("title_(.*)") + -- If the title is nil or empty then the players stopped + if title and title ~= "" then + awesome.emit_signal( + "bling::playerctl::title_artist_album", + title, + artist, + album_path + ) + else + awesome.emit_signal("bling::playerctl::no_players") + end + end) + collectgarbage("collect") + end, + }) + collectgarbage("collect") + end) +end + +-- Emit info +-- emit_player_status() +-- emit_player_info() + +local enable = function(args) + interval = (args and args.interval) or interval + emit_player_status() + emit_player_info() +end + +local disable = function() + awful.spawn.with_shell( + "pkill --full --uid " .. os.getenv("USER") .. " '^playerctl status -F'" + ) + + awful.spawn.with_shell( + "pkill --full --uid " + .. os.getenv("USER") + .. " '^playerctl metadata --format'" + ) +end + +return { enable = enable, disable = disable } diff --git a/.config/awesome/extras/bling/signal/playerctl/playerctl_lib.lua b/.config/awesome/extras/bling/signal/playerctl/playerctl_lib.lua new file mode 100755 index 0000000..5e67f1f --- /dev/null +++ b/.config/awesome/extras/bling/signal/playerctl/playerctl_lib.lua @@ -0,0 +1,350 @@ +-- Playerctl signals +-- +-- Provides: +-- bling::playerctl::status +-- playing (boolean) +-- player_name (string) +-- bling::playerctl::title_artist_album +-- title (string) +-- artist (string) +-- album_path (string) +-- player_name (string) +-- bling::playerctl::position +-- interval_sec (number) +-- length_sec (number) +-- player_name (string) +-- bling::playerctl::no_players +-- (No parameters) + +local gears = require("gears") +local awful = require("awful") +local beautiful = require("beautiful") +local Playerctl = nil + +local manager = nil +local metadata_timer = nil +local position_timer = nil + +local ignore = {} +local priority = {} +local update_on_activity = true +local interval = 1 + +-- Track position callback +local last_position = -1 +local last_length = -1 +local function position_cb() + local player = manager.players[1] + if player then + local position = player:get_position() / 1000000 + local length = (player.metadata.value["mpris:length"] or 0) / 1000000 + if position ~= last_position or length ~= last_length then + awesome.emit_signal( + "bling::playerctl::position", + position, + length, + player.player_name + ) + last_position = position + last_length = length + end + end +end + +local function get_album_art(url) + return awful.util.shell + .. [[ -c ' + +tmp_dir="$XDG_CACHE_HOME/awesome/" + +if [ -z "$XDG_CACHE_HOME" ]; then + tmp_dir="$HOME/.cache/awesome/" +fi + +tmp_cover_path="${tmp_dir}cover.png" + +if [ ! -d "$tmp_dir" ]; then + mkdir -p $tmp_dir +fi + +curl -s ']] + .. url + .. [[' --output $tmp_cover_path + +echo "$tmp_cover_path" +']] +end + +-- Metadata callback for title, artist, and album art +local last_player = nil +local last_title = "" +local last_artist = "" +local last_artUrl = "" +local function metadata_cb(player, metadata) + if update_on_activity then + manager:move_player_to_top(player) + end + + local data = metadata.value + + local title = data["xesam:title"] or "" + local artist = data["xesam:artist"][1] or "" + for i = 2, #data["xesam:artist"] do + artist = artist .. ", " .. data["xesam:artist"][i] + end + local artUrl = data["mpris:artUrl"] or "" + -- Spotify client doesn't report its art URL's correctly... + if player.player_name == "spotify" then + artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co") + end + + if player == manager.players[1] then + -- Callback can be called even though values we care about haven't + -- changed, so check to see if they have + if + player ~= last_player + or title ~= last_title + or artist ~= last_artist + or artUrl ~= last_artUrl + then + if title == "" and artist == "" and artUrl == "" then + return + end + + if metadata_timer ~= nil then + if metadata_timer.started then + metadata_timer:stop() + end + end + + metadata_timer = gears.timer({ + timeout = 0.3, + autostart = true, + single_shot = true, + callback = function() + if artUrl ~= "" then + awful.spawn.with_line_callback(get_album_art(artUrl), { + stdout = function(line) + awesome.emit_signal( + "bling::playerctl::title_artist_album", + title, + artist, + line, + player.player_name + ) + end, + }) + else + awesome.emit_signal( + "bling::playerctl::title_artist_album", + title, + artist, + "", + player.player_name + ) + end + end, + }) + + -- Re-sync with position timer when track changes + position_timer:again() + last_player = player + last_title = title + last_artist = artist + last_artUrl = artUrl + end + end +end + +-- Playback status callback +-- Reported as PLAYING, PAUSED, or STOPPED +local function playback_status_cb(player, status) + if update_on_activity then + manager:move_player_to_top(player) + end + + if player == manager.players[1] then + if status == "PLAYING" then + awesome.emit_signal( + "bling::playerctl::status", + true, + player.player_name + ) + else + awesome.emit_signal( + "bling::playerctl::status", + false, + player.player_name + ) + end + end +end + +-- Determine if player should be managed +local function name_is_selected(name) + if ignore[name.name] then + return false + end + + if #priority > 0 then + for _, arg in pairs(priority) do + if arg == name.name or arg == "%any" then + return true + end + end + return false + end + + return true +end + +-- Create new player and connect it to callbacks +local function init_player(name) + if name_is_selected(name) then + local player = Playerctl.Player.new_from_name(name) + manager:manage_player(player) + player.on_playback_status = playback_status_cb + player.on_metadata = metadata_cb + + -- Start position timer if its not already running + if not position_timer.started then + position_timer:again() + end + end +end + +-- Determine if a player name comes before or after another according to the +-- priority order +local function player_compare_name(name_a, name_b) + local any_index = math.huge + local a_match_index = nil + local b_match_index = nil + + if name_a == name_b then + return 0 + end + + for index, name in ipairs(priority) do + if name == "%any" then + any_index = (any_index == math.huge) and index or any_index + elseif name == name_a then + a_match_index = a_match_index or index + elseif name == name_b then + b_match_index = b_match_index or index + end + end + + if not a_match_index and not b_match_index then + return 0 + elseif not a_match_index then + return (b_match_index < any_index) and 1 or -1 + elseif not b_match_index then + return (a_match_index < any_index) and -1 or 1 + elseif a_match_index == b_match_index then + return 0 + else + return (a_match_index < b_match_index) and -1 or 1 + end +end + +-- Sorting function used by manager if a priority order is specified +local function player_compare(a, b) + local player_a = Playerctl.Player(a) + local player_b = Playerctl.Player(b) + return player_compare_name(player_a.player_name, player_b.player_name) +end + +local function start_manager() + manager = Playerctl.PlayerManager() + if #priority > 0 then + manager:set_sort_func(player_compare) + end + + -- Timer to update track position at specified interval + position_timer = gears.timer({ + timeout = interval, + callback = position_cb, + }) + + -- Manage existing players on startup + for _, name in ipairs(manager.player_names) do + init_player(name) + end + + -- Callback to manage new players + function manager:on_name_appeared(name) + init_player(name) + end + + -- Callback to check if all players have exited + function manager:on_name_vanished(name) + if #manager.players == 0 then + metadata_timer:stop() + position_timer:stop() + awesome.emit_signal("bling::playerctl::no_players") + end + end +end + +-- Parse arguments +local function parse_args(args) + if args then + update_on_activity = args.update_on_activity or update_on_activity + interval = args.interval or interval + + if type(args.ignore) == "string" then + ignore[args.ignore] = true + elseif type(args.ignore) == "table" then + for _, name in pairs(args.ignore) do + ignore[name] = true + end + end + + if type(args.player) == "string" then + priority[1] = args.player + elseif type(args.player) == "table" then + priority = args.player + end + end +end + +local function playerctl_enable(args) + args = args or {} + -- Grab settings from beautiful variables if not set explicitly + args.ignore = args.ignore or beautiful.playerctl_ignore + args.player = args.player or beautiful.playerctl_player + args.update_on_activity = args.update_on_activity + or beautiful.playerctl_update_on_activity + args.interval = args.interval + or beautiful.playerctl_position_update_interval + parse_args(args) + + -- Grab playerctl library + Playerctl = require("lgi").Playerctl + + -- Ensure main event loop has started before starting player manager + gears.timer.delayed_call(start_manager) +end + +local function playerctl_disable() + -- Remove manager and timer + manager = nil + metadata_timer:stop() + metadata_timer = nil + position_timer:stop() + position_timer = nil + -- Restore default settings + ignore = {} + priority = {} + update_on_activity = true + interval = 1 + -- Reset default values + last_position = -1 + last_length = -1 + last_player = nil + last_title = "" + last_artist = "" + last_artUrl = "" +end + +return { enable = playerctl_enable, disable = playerctl_disable } diff --git a/.config/awesome/extras/bling/widget/app_launcher/init.lua b/.config/awesome/extras/bling/widget/app_launcher/init.lua new file mode 100755 index 0000000..7eeaf17 --- /dev/null +++ b/.config/awesome/extras/bling/widget/app_launcher/init.lua @@ -0,0 +1,1053 @@ +local Gio = require("lgi").Gio +local awful = require("awful") +local gobject = require("gears.object") +local gtable = require("gears.table") +local gtimer = require("gears.timer") +local gfilesystem = require("gears.filesystem") +local wibox = require("wibox") +local beautiful = require("beautiful") +local color = require(tostring(...):match(".*bling") .. ".helpers.color") +local prompt = require(... .. ".prompt") +local dpi = beautiful.xresources.apply_dpi +local string = string +local table = table +local math = math +local ipairs = ipairs +local pairs = pairs +local root = root +local capi = { screen = screen, mouse = mouse } +local path = ... + +local app_launcher = { mt = {} } + +local terminal_commands_lookup = +{ + alacritty = "alacritty -e", + termite = "termite -e", + rxvt = "rxvt -e", + terminator = "terminator -e" +} + +local function string_levenshtein(str1, str2) + local len1 = string.len(str1) + local len2 = string.len(str2) + local matrix = {} + local cost = 0 + + -- quick cut-offs to save time + if (len1 == 0) then + return len2 + elseif (len2 == 0) then + return len1 + elseif (str1 == str2) then + return 0 + end + + -- initialise the base matrix values + for i = 0, len1, 1 do + matrix[i] = {} + matrix[i][0] = i + end + for j = 0, len2, 1 do + matrix[0][j] = j + end + + -- actual Levenshtein algorithm + for i = 1, len1, 1 do + for j = 1, len2, 1 do + if (str1:byte(i) == str2:byte(j)) then + cost = 0 + else + cost = 1 + end + + matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) + end + end + + -- return the last value - this is the Levenshtein distance + return matrix[len1][len2] +end + +local function case_insensitive_pattern(pattern) + -- find an optional '%' (group 1) followed by any character (group 2) + local p = pattern:gsub("(%%?)(.)", function(percent, letter) + if percent ~= "" or not letter:match("%a") then + -- if the '%' matched, or `letter` is not a letter, return "as is" + return percent .. letter + else + -- else, return a case-insensitive character class of the matched letter + return string.format("[%s%s]", letter:lower(), letter:upper()) + end + end) + + return p +end + +local function has_value(tab, val) + for index, value in pairs(tab) do + if val:find(case_insensitive_pattern(value)) then + return true + end + end + return false +end + +local function select_app(self, x, y) + local widgets = self._private.grid:get_widgets_at(x, y) + if widgets then + self._private.active_widget = widgets[1] + if self._private.active_widget ~= nil then + self._private.active_widget.selected = true + self._private.active_widget:get_children_by_id("background")[1].bg = self.app_selected_color + local name_widget = self._private.active_widget:get_children_by_id("name")[1] + if name_widget then + name_widget.markup = string.format("%s", self.app_name_selected_color, name_widget.text) + end + local generic_name_widget = self._private.active_widget:get_children_by_id("generic_name")[1] + if generic_name_widget then + generic_name_widget.markup = string.format("%s", self.app_name_selected_color, generic_name_widget.text) + end + end + end +end + +local function unselect_app(self) + if self._private.active_widget ~= nil then + self._private.active_widget.selected = false + self._private.active_widget:get_children_by_id("background")[1].bg = self.app_normal_color + local name_widget = self._private.active_widget:get_children_by_id("name")[1] + if name_widget then + name_widget.markup = string.format("%s", self.app_name_normal_color, name_widget.text) + end + local generic_name_widget = self._private.active_widget:get_children_by_id("generic_name")[1] + if generic_name_widget then + generic_name_widget.markup = string.format("%s", self.app_name_normal_color, generic_name_widget.text) + end + self._private.active_widget = nil + end +end + +local function create_app_widget(self, entry) + local icon = self.app_show_icon == true and + { + widget = wibox.widget.imagebox, + halign = self.app_icon_halign, + forced_width = self.app_icon_width, + forced_height = self.app_icon_height, + image = entry.icon + } or nil + + local name = self.app_show_name == true and + { + widget = wibox.widget.textbox, + id = "name", + font = self.app_name_font, + markup = entry.name + } or nil + + local generic_name = entry.generic_name ~= nil and self.app_show_generic_name == true and + { + widget = wibox.widget.textbox, + id = "generic_name", + font = self.app_name_font, + markup = entry.generic_name ~= "" and " (" .. entry.generic_name .. ")" or "" + } or nil + + local app = wibox.widget + { + widget = wibox.container.background, + id = "background", + forced_width = self.app_width, + forced_height = self.app_height, + shape = self.app_shape, + bg = self.app_normal_color, + { + widget = wibox.container.margin, + margins = self.app_content_padding, + { + -- Using this hack instead of container.place because that will fuck with the name/icon halign + layout = wibox.layout.align.vertical, + expand = "outside", + nil, + { + layout = wibox.layout.fixed.vertical, + spacing = self.app_content_spacing, + icon, + { + widget = wibox.container.place, + halign = self.app_name_halign, + { + layout = wibox.layout.fixed.horizontal, + spacing = self.app_name_generic_name_spacing, + name, + generic_name + } + } + }, + nil + } + } + } + + function app.spawn() + if entry.terminal == true then + if self.terminal ~= nil then + local terminal_command = terminal_commands_lookup[self.terminal] or self.terminal + awful.spawn(terminal_command .. " " .. entry.executable) + else + awful.spawn.easy_async("gtk-launch " .. entry.executable, function(stdout, stderr) + if stderr then + awful.spawn(entry.executable) + end + end) + end + else + awful.spawn(entry.executable) + end + + if self.hide_on_launch then + self:hide() + end + end + + app:connect_signal("mouse::enter", function(_self) + local widget = capi.mouse.current_wibox + if widget then + widget.cursor = "hand2" + end + + local app = _self + if app.selected then + app:get_children_by_id("background")[1].bg = self.app_selected_hover_color + else + local is_opaque = color.is_opaque(self.app_normal_color) + local is_dark = color.is_dark(self.app_normal_color) + local app_normal_color = color.hex_to_rgba(self.app_normal_color) + local hover_color = (is_dark or is_opaque) and + color.rgba_to_hex(color.multiply(app_normal_color, 2.5)) or + color.rgba_to_hex(color.multiply(app_normal_color, 0.5)) + app:get_children_by_id("background")[1].bg = self.app_normal_hover_color + end + end) + + app:connect_signal("mouse::leave", function(_self) + local widget = capi.mouse.current_wibox + if widget then + widget.cursor = "left_ptr" + end + + local app = _self + if app.selected then + app:get_children_by_id("background")[1].bg = self.app_selected_color + else + app:get_children_by_id("background")[1].bg = self.app_normal_color + end + end) + + app:connect_signal("button::press", function(_self, lx, ly, button, mods, find_widgets_result) + if button == 1 then + local app = _self + if self._private.active_widget == app or not self.select_before_spawn then + app.spawn() + else + -- Unmark the previous app + unselect_app(self) + + -- Mark this app + local pos = self._private.grid:get_widget_position(app) + select_app(self, pos.row, pos.col) + end + end + end) + + return app +end + +local function search(self, text) + unselect_app(self) + + local pos = self._private.grid:get_widget_position(self._private.active_widget) + + -- Reset all the matched entries + self._private.matched_entries = {} + -- Remove all the grid widgets + self._private.grid:reset() + + if text == "" then + self._private.matched_entries = self._private.all_entries + else + for index, entry in pairs(self._private.all_entries) do + text = text:gsub( "%W", "" ) + + -- Check if there's a match by the app name or app command + if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or + self.search_commands and string.find(entry.commandline, case_insensitive_pattern(text)) ~= nil + then + table.insert(self._private.matched_entries, { + name = entry.name, + generic_name = entry.generic_name, + commandline = entry.commandline, + executable = entry.executable, + terminal = entry.terminal, + icon = entry.icon + }) + end + end + + -- Sort by string similarity + table.sort(self._private.matched_entries, function(a, b) + return string_levenshtein(text, a.name) < string_levenshtein(text, b.name) + end) + end + for index, entry in pairs(self._private.matched_entries) do + -- Only add the widgets for apps that are part of the first page + if #self._private.grid.children + 1 <= self._private.max_apps_per_page then + self._private.grid:add(create_app_widget(self, entry)) + end + end + + -- Recalculate the apps per page based on the current matched entries + self._private.apps_per_page = math.min(#self._private.matched_entries, self._private.max_apps_per_page) + + -- Recalculate the pages count based on the current apps per page + self._private.pages_count = math.ceil(math.max(1, #self._private.matched_entries) / math.max(1, self._private.apps_per_page)) + + -- Page should be 1 after a search + self._private.current_page = 1 + + -- This is an option to mimic rofi behaviour where after a search + -- it will reselect the app whose index is the same as the app index that was previously selected + -- and if matched_entries.length < current_index it will instead select the app with the greatest index + if self.try_to_keep_index_after_searching then + if self._private.grid:get_widgets_at(pos.row, pos.col) == nil then + local app = self._private.grid.children[#self._private.grid.children] + pos = self._private.grid:get_widget_position(app) + end + select_app(self, pos.row, pos.col) + -- Otherwise select the first app on the list + else + select_app(self, 1, 1) + end +end + +local function page_backward(self, direction) + if self._private.current_page > 1 then + self._private.current_page = self._private.current_page - 1 + elseif self.wrap_page_scrolling and #self._private.matched_entries >= self._private.max_apps_per_page then + self._private.current_page = self._private.pages_count + elseif self.wrap_app_scrolling then + local rows, columns = self._private.grid:get_dimension() + unselect_app(self) + select_app(self, math.min(rows, #self._private.grid.children % self.apps_per_row), columns) + return + else + return + end + + local pos = self._private.grid:get_widget_position(self._private.active_widget) + + -- Remove the current page apps from the grid + self._private.grid:reset() + + local max_app_index_to_include = self._private.apps_per_page * self._private.current_page + local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page + + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry)) + end + end + + local rows, columns = self._private.grid:get_dimension() + if self._private.current_page < self._private.pages_count then + if direction == "up" then + select_app(self, rows, columns) + else + -- Keep the same row from last page + select_app(self, pos.row, columns) + end + elseif self.wrap_page_scrolling then + if direction == "up" then + select_app(self, math.min(rows, #self._private.grid.children % self.apps_per_row), columns) + else + -- Keep the same row from last page + select_app(self, math.min(pos.row, #self._private.grid.children % self.apps_per_row), columns) + end + end +end + +local function page_forward(self, direction) + local min_app_index_to_include = 0 + local max_app_index_to_include = self._private.apps_per_page + + if self._private.current_page < self._private.pages_count then + min_app_index_to_include = self._private.apps_per_page * self._private.current_page + self._private.current_page = self._private.current_page + 1 + max_app_index_to_include = self._private.apps_per_page * self._private.current_page + elseif self.wrap_page_scrolling and #self._private.matched_entries >= self._private.max_apps_per_page then + self._private.current_page = 1 + min_app_index_to_include = 0 + max_app_index_to_include = self._private.apps_per_page + elseif self.wrap_app_scrolling then + unselect_app(self) + select_app(self, 1, 1) + return + else + return + end + + local pos = self._private.grid:get_widget_position(self._private.active_widget) + + -- Remove the current page apps from the grid + self._private.grid:reset() + + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry)) + end + end + + if self._private.current_page > 1 or self.wrap_page_scrolling then + if direction == "down" then + select_app(self, 1, 1) + else + local last_col_max_row = math.min(pos.row, #self._private.grid.children % self.apps_per_row) + if last_col_max_row ~= 0 then + select_app(self, last_col_max_row, 1) + else + select_app(self, pos.row, 1) + end + end + end +end + +local function scroll_up(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local rows, columns = self._private.grid:get_dimension() + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_bigger_than_first_app = pos.col > 1 or pos.row > 1 + + -- Check if the current marked app is not the first + if is_bigger_than_first_app then + unselect_app(self) + if pos.row == 1 then + select_app(self, rows, pos.col - 1) + else + select_app(self, pos.row - 1, pos.col) + end + else + page_backward(self, "up") + end +end + +local function scroll_down(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local rows, columns = self._private.grid:get_dimension() + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_less_than_max_app = self._private.grid:index(self._private.active_widget) < #self._private.grid.children + + -- Check if we can scroll down the app list + if is_less_than_max_app then + -- Unmark the previous app + unselect_app(self) + if pos.row == rows then + select_app(self, 1, pos.col + 1) + else + select_app(self, pos.row + 1, pos.col) + end + else + page_forward(self, "down") + end +end + +local function scroll_left(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_bigger_than_first_column = pos.col > 1 + + -- Check if the current marked app is not the first + if is_bigger_than_first_column then + unselect_app(self) + select_app(self, pos.row, pos.col - 1) + else + page_backward(self, "left") + end +end + +local function scroll_right(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local rows, columns = self._private.grid:get_dimension() + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_less_than_max_column = pos.col < columns + local is_less_than_max_page = self._private.current_page < self._private.pages_count + + -- Check if we can scroll down the app list + if is_less_than_max_column then + -- Unmark the previous app + unselect_app(self) + + -- Scroll up to the max app if there are directly to the right of previous app + if self._private.grid:get_widgets_at(pos.row, pos.col + 1) == nil then + local app = self._private.grid.children[#self._private.grid.children] + pos = self._private.grid:get_widget_position(app) + select_app(self, pos.row, pos.col) + else + select_app(self, pos.row, pos.col + 1) + end + + else + page_forward(self, "right") + end +end + +local function reset(self) + self._private.grid:reset() + self._private.matched_entries = self._private.all_entries + self._private.apps_per_page = self._private.max_apps_per_page + self._private.pages_count = math.ceil(#self._private.all_entries / self._private.apps_per_page) + self._private.current_page = 1 + + for index, entry in pairs(self._private.all_entries) do + -- Only add the apps that are part of the first page + if index <= self._private.apps_per_page then + self._private.grid:add(create_app_widget(self, entry)) + else + break + end + end + + select_app(self, 1, 1) +end + +local function generate_apps(self) + self._private.all_entries = {} + self._private.matched_entries = {} + + local app_info = Gio.AppInfo + local apps = app_info.get_all() + if self.sort_alphabetically then + table.sort(apps, function(a, b) + local app_a_score = app_info.get_name(a):lower() + if has_value(self.favorites, app_info.get_name(a)) then + app_a_score = "aaaaaaaaaaa" .. app_a_score + end + local app_b_score = app_info.get_name(b):lower() + if has_value(self.favorites, app_info.get_name(b)) then + app_b_score = "aaaaaaaaaaa" .. app_b_score + end + + return app_a_score < app_b_score + end) + elseif self.reverse_sort_alphabetically then + table.sort(apps, function(a, b) + local app_a_score = app_info.get_name(a):lower() + if has_value(self.favorites, app_info.get_name(a)) then + app_a_score = "zzzzzzzzzzz" .. app_a_score + end + local app_b_score = app_info.get_name(b):lower() + if has_value(self.favorites, app_info.get_name(b)) then + app_b_score = "zzzzzzzzzzz" .. app_b_score + end + + return app_a_score > app_b_score + end) + else + table.sort(apps, function(a, b) + local app_a_favorite = has_value(self.favorites, app_info.get_name(a)) + local app_b_favorite = has_value(self.favorites, app_info.get_name(b)) + + if app_a_favorite and not app_b_favorite then + return true + elseif app_b_favorite and not app_a_favorite then + return false + elseif app_a_favorite and app_b_favorite then + return app_info.get_name(a):lower() < app_info.get_name(b):lower() + else + return false + end + end) + end + + local icon_theme = require(tostring(path):match(".*bling") .. ".helpers.icon_theme")(self.icon_theme, self.icon_size) + + for _, app in ipairs(apps) do + if app.should_show(app) then + local name = app_info.get_name(app) + local commandline = app_info.get_commandline(app) + local executable = app_info.get_executable(app) + local icon = icon_theme:get_gicon_path(app_info.get_icon(app)) + + -- Check if this app should be skipped, depanding on the skip_names / skip_commands table + if not has_value(self.skip_names, name) and not has_value(self.skip_commands, commandline) then + -- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons + if icon ~= "" or self.skip_empty_icons == false then + if icon == "" then + if self.default_app_icon_name ~= nil then + icon = icon_theme:get_icon_path(self.default_app_icon_name) + elseif self.default_app_icon_path ~= nil then + icon = self.default_app_icon_path + else + icon = icon_theme:choose_icon({"application-all", "application", "application-default-icon", "app"}) + end + end + + local desktop_app_info = Gio.DesktopAppInfo.new(app_info.get_id(app)) + local terminal = Gio.DesktopAppInfo.get_string(desktop_app_info, "Terminal") == "true" and true or false + local generic_name = Gio.DesktopAppInfo.get_string(desktop_app_info, "GenericName") or nil + + table.insert(self._private.all_entries, { + name = name, + generic_name = generic_name, + commandline = commandline, + executable = executable, + terminal = terminal, + icon = icon + }) + end + end + end + end +end + +--- Shows the app launcher +function app_launcher:show() + local screen = self.screen + if self.show_on_focused_screen then + screen = awful.screen.focused() + end + + screen.app_launcher = self._private.widget + screen.app_launcher.screen = screen + self._private.prompt:start() + + local animation = self.rubato + if animation ~= nil then + if self._private.widget.goal_x == nil then + self._private.widget.goal_x = self._private.widget.x + end + if self._private.widget.goal_y == nil then + self._private.widget.goal_y = self._private.widget.y + self._private.widget.placement = nil + end + + if animation.x then + animation.x.ended:unsubscribe() + animation.x:set(self._private.widget.goal_x) + gtimer { + timeout = 0.01, + call_now = false, + autostart = true, + single_shot = true, + callback = function() + screen.app_launcher.visible = true + end + } + end + if animation.y then + animation.y.ended:unsubscribe() + animation.y:set(self._private.widget.goal_y) + gtimer { + timeout = 0.01, + call_now = false, + autostart = true, + single_shot = true, + callback = function() + screen.app_launcher.visible = true + end + } + end + else + screen.app_launcher.visible = true + end + + self:emit_signal("bling::app_launcher::visibility", true) +end + +--- Hides the app launcher +function app_launcher:hide() + local screen = self.screen + if self.show_on_focused_screen then + screen = awful.screen.focused() + end + + if screen.app_launcher == nil or screen.app_launcher.visible == false then + return + end + + self._private.prompt:stop() + + local animation = self.rubato + if animation ~= nil then + if animation.x then + animation.x:set(animation.x:initial()) + end + if animation.y then + animation.y:set(animation.y:initial()) + end + + local anim_x_duration = (animation.x and animation.x.duration) or 0 + local anim_y_duration = (animation.y and animation.y.duration) or 0 + local turn_off_on_anim_x_end = (anim_x_duration >= anim_y_duration) and true or false + + if turn_off_on_anim_x_end then + animation.x.ended:subscribe(function() + if self.reset_on_hide == true then reset(self) end + screen.app_launcher.visible = false + screen.app_launcher = nil + animation.x.ended:unsubscribe() + end) + else + animation.y.ended:subscribe(function() + if self.reset_on_hide == true then reset(self) end + screen.app_launcher.visible = false + screen.app_launcher = nil + animation.y.ended:unsubscribe() + end) + end + else + if self.reset_on_hide == true then reset(self) end + screen.app_launcher.visible = false + screen.app_launcher = nil + end + + self:emit_signal("bling::app_launcher::visibility", false) +end + +--- Toggles the app launcher +function app_launcher:toggle() + local screen = self.screen + if self.show_on_focused_screen then + screen = awful.screen.focused() + end + + if screen.app_launcher and screen.app_launcher.visible then + self:hide() + else + self:show() + end +end + +-- Returns a new app launcher +local function new(args) + args = args or {} + + args.terminal = args.terminal or nil + args.favorites = args.favorites or {} + args.search_commands = args.search_commands == nil and true or args.search_commands + args.skip_names = args.skip_names or {} + args.skip_commands = args.skip_commands or {} + args.skip_empty_icons = args.skip_empty_icons ~= nil and args.skip_empty_icons or false + args.sort_alphabetically = args.sort_alphabetically == nil and true or args.sort_alphabetically + args.reverse_sort_alphabetically = args.reverse_sort_alphabetically ~= nil and args.reverse_sort_alphabetically or false + args.select_before_spawn = args.select_before_spawn == nil and true or args.select_before_spawn + args.hide_on_left_clicked_outside = args.hide_on_left_clicked_outside == nil and true or args.hide_on_left_clicked_outside + args.hide_on_right_clicked_outside = args.hide_on_right_clicked_outside == nil and true or args.hide_on_right_clicked_outside + args.hide_on_launch = args.hide_on_launch == nil and true or args.hide_on_launch + args.try_to_keep_index_after_searching = args.try_to_keep_index_after_searching ~= nil and args.try_to_keep_index_after_searching or false + args.reset_on_hide = args.reset_on_hide == nil and true or args.reset_on_hide + args.save_history = args.save_history == nil and true or args.save_history + args.wrap_page_scrolling = args.wrap_page_scrolling == nil and true or args.wrap_page_scrolling + args.wrap_app_scrolling = args.wrap_app_scrolling == nil and true or args.wrap_app_scrolling + + args.default_app_icon_name = args.default_app_icon_name or nil + args.default_app_icon_path = args.default_app_icon_path or nil + args.icon_theme = args.icon_theme or nil + args.icons_size = args.icons_size or nil + + args.show_on_focused_screen = args.show_on_focused_screen == nil and true or args.show_on_focused_screen + args.screen = args.screen or capi.screen.primary + args.placement = args.placement or awful.placement.centered + args.rubato = args.rubato or nil + args.shirnk_width = args.shirnk_width ~= nil and args.shirnk_width or false + args.shrink_height = args.shrink_height ~= nil and args.shrink_height or false + args.background = args.background or "#000000" + args.shape = args.shape or nil + + args.prompt_height = args.prompt_height or dpi(100) + args.prompt_margins = args.prompt_margins or dpi(0) + args.prompt_paddings = args.prompt_paddings or dpi(30) + args.prompt_shape = args.prompt_shape or nil + args.prompt_color = args.prompt_color or beautiful.fg_normal or "#FFFFFF" + args.prompt_border_width = args.prompt_border_width or beautiful.border_width or dpi(0) + args.prompt_border_color = args.prompt_border_color or beautiful.border_color or args.prompt_color + args.prompt_text_halign = args.prompt_text_halign or "left" + args.prompt_text_valign = args.prompt_text_valign or "center" + args.prompt_icon_text_spacing = args.prompt_icon_text_spacing or dpi(10) + args.prompt_show_icon = args.prompt_show_icon == nil and true or args.prompt_show_icon + args.prompt_icon_font = args.prompt_icon_font or beautiful.font + args.prompt_icon_color = args.prompt_icon_color or beautiful.bg_normal or "#000000" + args.prompt_icon = args.prompt_icon or "" + args.prompt_icon_markup = args.prompt_icon_markup or string.format("%s", args.prompt_icon_color, args.prompt_icon) + args.prompt_text = args.prompt_text or "Search: " + args.prompt_start_text = args.prompt_start_text or "" + args.prompt_font = args.prompt_font or beautiful.font + args.prompt_text_color = args.prompt_text_color or beautiful.bg_normal or "#000000" + args.prompt_cursor_color = args.prompt_cursor_color or beautiful.bg_normal or "#000000" + + args.apps_per_row = args.apps_per_row or 5 + args.apps_per_column = args.apps_per_column or 3 + args.apps_margin = args.apps_margin or dpi(30) + args.apps_spacing = args.apps_spacing or dpi(30) + + args.expand_apps = args.expand_apps == nil and true or args.expand_apps + args.app_width = args.app_width or dpi(300) + args.app_height = args.app_height or dpi(120) + args.app_shape = args.app_shape or nil + args.app_normal_color = args.app_normal_color or beautiful.bg_normal or "#000000" + args.app_normal_hover_color = args.app_normal_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_normal_color), 2.5)) or + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_normal_color), 0.5)) + args.app_selected_color = args.app_selected_color or beautiful.fg_normal or "#FFFFFF" + args.app_selected_hover_color = args.app_selected_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 2.5)) or + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 0.5)) + args.app_content_padding = args.app_content_padding or dpi(10) + args.app_content_spacing = args.app_content_spacing or dpi(10) + args.app_show_icon = args.app_show_icon == nil and true or args.app_show_icon + args.app_icon_halign = args.app_icon_halign or "center" + args.app_icon_width = args.app_icon_width or dpi(70) + args.app_icon_height = args.app_icon_height or dpi(70) + args.app_show_name = args.app_show_name == nil and true or args.app_show_name + args.app_name_generic_name_spacing = args.app_name_generic_name_spacing or dpi(0) + args.app_name_halign = args.app_name_halign or "center" + args.app_name_font = args.app_name_font or beautiful.font + args.app_name_normal_color = args.app_name_normal_color or beautiful.fg_normal or "#FFFFFF" + args.app_name_selected_color = args.app_name_selected_color or beautiful.bg_normal or "#000000" + args.app_show_generic_name = args.app_show_generic_name ~= nil and args.app_show_generic_name or false + + local ret = gobject({}) + ret._private = {} + ret._private.text = "" + + gtable.crush(ret, app_launcher) + gtable.crush(ret, args) + + -- Calculate the grid width and height + local grid_width = ret.shirnk_width == false + and dpi((ret.app_width * ret.apps_per_column) + ((ret.apps_per_column - 1) * ret.apps_spacing)) + or nil + local grid_height = ret.shrink_height == false + and dpi((ret.app_height * ret.apps_per_row) + ((ret.apps_per_row - 1) * ret.apps_spacing)) + or nil + + -- These widgets need to be later accessed + ret._private.prompt = prompt + { + prompt = ret.prompt_text, + text = ret.prompt_start_text, + font = ret.prompt_font, + reset_on_stop = ret.reset_on_hide, + bg_cursor = ret.prompt_cursor_color, + history_path = ret.save_history == true and gfilesystem.get_cache_dir() .. "/history" or nil, + changed_callback = function(text) + if text == ret._private.text then + return + end + + if ret._private.search_timer ~= nil and ret._private.search_timer.started then + ret._private.search_timer:stop() + end + + ret._private.search_timer = gtimer { + timeout = 0.05, + autostart = true, + single_shot = true, + callback = function() + search(ret, text) + end + } + + ret._private.text = text + end, + keypressed_callback = function(mod, key, cmd) + if key == "Escape" then + ret:hide() + end + if key == "Return" then + if ret._private.active_widget ~= nil then + ret._private.active_widget.spawn() + end + end + if key == "Up" then + scroll_up(ret) + end + if key == "Down" then + scroll_down(ret) + end + if key == "Left" then + scroll_left(ret) + end + if key == "Right" then + scroll_right(ret) + end + end + } + ret._private.grid = wibox.widget + { + layout = wibox.layout.grid, + forced_width = grid_width, + forced_height = grid_height, + orientation = "horizontal", + homogeneous = true, + expand = ret.expand_apps, + spacing = ret.apps_spacing, + forced_num_rows = ret.apps_per_row, + buttons = + { + awful.button({}, 4, function() scroll_up(ret) end), + awful.button({}, 5, function() scroll_down(ret) end) + } + } + ret._private.widget = awful.popup + { + type = "dock", + visible = false, + ontop = true, + placement = ret.placement, + shape = ret.shape, + bg = ret.background, + widget = + { + layout = wibox.layout.fixed.vertical, + { + widget = wibox.container.margin, + margins = ret.prompt_margins, + { + widget = wibox.container.background, + forced_height = ret.prompt_height, + shape = ret.prompt_shape, + bg = ret.prompt_color, + fg = ret.prompt_text_color, + border_width = ret.prompt_border_width, + border_color = ret.prompt_border_color, + { + widget = wibox.container.margin, + margins = ret.prompt_paddings, + { + widget = wibox.container.place, + halign = ret.prompt_text_halign, + valign = ret.prompt_text_valign, + { + layout = wibox.layout.fixed.horizontal, + spacing = ret.prompt_icon_text_spacing, + { + widget = wibox.widget.textbox, + font = ret.prompt_icon_font, + markup = ret.prompt_icon_markup + }, + ret._private.prompt.textbox + } + } + } + } + }, + { + widget = wibox.container.margin, + margins = ret.apps_margin, + ret._private.grid + } + } + } + + -- Private variables to be used to be used by the scrolling and searching functions + ret._private.max_apps_per_page = ret.apps_per_column * ret.apps_per_row + ret._private.apps_per_page = ret._private.max_apps_per_page + ret._private.pages_count = 0 + ret._private.current_page = 1 + + generate_apps(ret) + reset(ret) + + if ret.rubato and ret.rubato.x then + ret.rubato.x:subscribe(function(pos) + ret._private.widget.x = pos + end) + end + if ret.rubato and ret.rubato.y then + ret.rubato.y:subscribe(function(pos) + ret._private.widget.y = pos + end) + end + + if ret.hide_on_left_clicked_outside then + awful.mouse.append_client_mousebinding( + awful.button({ }, 1, function (c) + ret:hide() + end) + ) + + awful.mouse.append_global_mousebinding( + awful.button({ }, 1, function (c) + ret:hide() + end) + ) + end + if ret.hide_on_right_clicked_outside then + awful.mouse.append_client_mousebinding( + awful.button({ }, 3, function (c) + ret:hide() + end) + ) + + awful.mouse.append_global_mousebinding( + awful.button({ }, 3, function (c) + ret:hide() + end) + ) + end + + local kill_old_inotify_process_script = [[ ps x | grep "inotifywait -e modify /usr/share/applications" | grep -v grep | awk '{print $1}' | xargs kill ]] + local subscribe_script = [[ bash -c "while (inotifywait -e modify /usr/share/applications -qq) do echo; done" ]] + + awful.spawn.easy_async_with_shell(kill_old_inotify_process_script, function() + awful.spawn.with_line_callback(subscribe_script, {stdout = function(_) + generate_apps(ret) + end}) + end) + + return ret +end + +function app_launcher.text(args) + args = args or {} + + args.prompt_height = args.prompt_height or dpi(50) + args.prompt_margins = args.prompt_margins or dpi(30) + args.prompt_paddings = args.prompt_paddings or dpi(15) + args.app_width = args.app_width or dpi(400) + args.app_height = args.app_height or dpi(40) + args.apps_spacing = args.apps_spacing or dpi(10) + args.apps_per_row = args.apps_per_row or 15 + args.apps_per_column = args.apps_per_column or 1 + args.app_name_halign = args.app_name_halign or "left" + args.app_show_icon = args.app_show_icon ~= nil and args.app_show_icon or false + args.app_show_generic_name = args.app_show_generic_name == nil and true or args.app_show_generic_name + args.apps_margin = args.apps_margin or { left = dpi(40), right = dpi(40), bottom = dpi(30) } + + return new(args) +end + +function app_launcher.mt:__call(...) + return new(...) +end + +return setmetatable(app_launcher, app_launcher.mt) diff --git a/.config/awesome/extras/bling/widget/app_launcher/prompt.lua b/.config/awesome/extras/bling/widget/app_launcher/prompt.lua new file mode 100755 index 0000000..fae3b86 --- /dev/null +++ b/.config/awesome/extras/bling/widget/app_launcher/prompt.lua @@ -0,0 +1,656 @@ +--------------------------------------------------------------------------- +--- Modified Prompt module. +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +--------------------------------------------------------------------------- + +local akey = require("awful.key") +local keygrabber = require("awful.keygrabber") +local gobject = require("gears.object") +local gdebug = require('gears.debug') +local gtable = require("gears.table") +local gcolor = require("gears.color") +local gstring = require("gears.string") +local gfs = require("gears.filesystem") +local wibox = require("wibox") +local beautiful = require("beautiful") +local io = io +local table = table +local math = math +local ipairs = ipairs +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local capi = { selection = selection } + +local prompt = { mt = {} } + +--- Private data +local data = {} +data.history = {} + +local function itera(inc,a, i) + i = i + inc + local v = a[i] + if v then return i,v end +end + +local function history_check_load(id, max) + if id and id ~= "" and not data.history[id] then + data.history[id] = { max = 50, table = {} } + + if max then + data.history[id].max = max + end + + local f = io.open(id, "r") + if not f then return end + + -- Read history file + for line in f:lines() do + if gtable.hasitem(data.history[id].table, line) == nil then + table.insert(data.history[id].table, line) + if #data.history[id].table >= data.history[id].max then + break + end + end + end + f:close() + end +end + +local function is_word_char(c) + if string.find(c, "[{[(,.:;_-+=@/ ]") then + return false + else + return true + end +end + +local function cword_start(s, pos) + local i = pos + if i > 1 then + i = i - 1 + end + while i >= 1 and not is_word_char(s:sub(i, i)) do + i = i - 1 + end + while i >= 1 and is_word_char(s:sub(i, i)) do + i = i - 1 + end + if i <= #s then + i = i + 1 + end + return i +end + +local function cword_end(s, pos) + local i = pos + while i <= #s and not is_word_char(s:sub(i, i)) do + i = i + 1 + end + while i <= #s and is_word_char(s:sub(i, i)) do + i = i + 1 + end + return i +end + +local function history_save(id) + if data.history[id] then + gfs.make_parent_directories(id) + local f = io.open(id, "w") + if not f then + gdebug.print_warning("Failed to write the history to "..id) + return + end + for i = 1, math.min(#data.history[id].table, data.history[id].max) do + f:write(data.history[id].table[i] .. "\n") + end + f:close() + end +end + +local function history_items(id) + if data.history[id] then + return #data.history[id].table + else + return -1 + end +end + +local function history_add(id, command) + if data.history[id] and command ~= "" then + local index = gtable.hasitem(data.history[id].table, command) + if index == nil then + table.insert(data.history[id].table, command) + + -- Do not exceed our max_cmd + if #data.history[id].table > data.history[id].max then + table.remove(data.history[id].table, 1) + end + + history_save(id) + else + -- Bump this command to the end of history + table.remove(data.history[id].table, index) + table.insert(data.history[id].table, command) + history_save(id) + end + end +end + +local function have_multibyte_char_at(text, position) + return text:sub(position, position):wlen() == -1 +end + +local function prompt_text_with_cursor(args) + local char, spacer, text_start, text_end, ret + local text = args.text or "" + local _prompt = args.prompt or "" + local underline = args.cursor_ul or "none" + + if args.select_all then + if #text == 0 then char = " " else char = gstring.xml_escape(text) end + spacer = " " + text_start = "" + text_end = "" + elseif #text < args.cursor_pos then + char = " " + spacer = "" + text_start = gstring.xml_escape(text) + text_end = "" + else + local offset = 0 + if have_multibyte_char_at(text, args.cursor_pos) then + offset = 1 + end + char = gstring.xml_escape(text:sub(args.cursor_pos, args.cursor_pos + offset)) + spacer = " " + text_start = gstring.xml_escape(text:sub(1, args.cursor_pos - 1)) + text_end = gstring.xml_escape(text:sub(args.cursor_pos + 1 + offset)) + end + + local cursor_color = gcolor.ensure_pango_color(args.cursor_color) + local text_color = gcolor.ensure_pango_color(args.text_color) + + if args.highlighter then + text_start, text_end = args.highlighter(text_start, text_end) + end + + ret = _prompt .. text_start .. "" .. char .. "" .. text_end .. spacer + + return ret +end + +local function update(self) + self.textbox:set_font(self.font) + self.textbox:set_markup(prompt_text_with_cursor{ + text = self.command, text_color = self.fg_cursor, cursor_color = self.bg_cursor, + cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all, + prompt = self.prompt, highlighter = self.highlighter }) +end + +local function exec(self, cb, command_to_history) + self.textbox:set_markup("") + history_add(self.history_path, command_to_history) + keygrabber.stop(self._private.grabber) + if cb then cb(self.command) end + if self.done_callback then + self.done_callback() + end +end + +function prompt:start() + -- The cursor position + if self.reset_on_stop == true or self._private_cur_pos == nil then + self._private_cur_pos = (self.select_all and 1) or self.text:wlen() + 1 + end + if self.reset_on_stop == true then self.text = "" self.command = "" end + + self.textbox:set_font(self.font) + self.textbox:set_markup(prompt_text_with_cursor{ + text = self.reset_on_stop and self.text or self.command, text_color = self.fg_cursor, cursor_color = self.bg_cursor, + cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all, + prompt = self.prompt, highlighter = self.highlighter}) + + self._private.search_term = nil + + history_check_load(self.history_path, self.history_max) + local history_index = history_items(self.history_path) + 1 + + -- The completion element to use on completion request. + local ncomp = 1 + + local command_before_comp + local cur_pos_before_comp + + self._private.grabber = keygrabber.run(function(modifiers, key, event) + -- Convert index array to hash table + local mod = {} + for _, v in ipairs(modifiers) do mod[v] = true end + + if event ~= "press" then + if self.keyreleased_callback then + self.keyreleased_callback(mod, key, self.command) + end + return + end + + -- Call the user specified callback. If it returns true as + -- the first result then return from the function. Treat the + -- second and third results as a new command and new prompt + -- to be set (if provided) + if self.keypressed_callback then + local user_catched, new_command, new_prompt = + self.keypressed_callback(mod, key, self.command) + if new_command or new_prompt then + if new_command then + self.command = new_command + end + if new_prompt then + self.prompt = new_prompt + end + update(self) + end + if user_catched then + if self.changed_callback then + self.changed_callback(self.command) + end + return + end + end + + local filtered_modifiers = {} + + -- User defined cases + if self.hooks[key] then + -- Remove caps and num lock + for _, m in ipairs(modifiers) do + if not gtable.hasitem(akey.ignore_modifiers, m) then + table.insert(filtered_modifiers, m) + end + end + + for _,v in ipairs(self.hooks[key]) do + if #filtered_modifiers == #v[1] then + local match = true + for _,v2 in ipairs(v[1]) do + match = match and mod[v2] + end + if match then + local cb + local ret, quit = v[3](self.command) + local original_command = self.command + + -- Support both a "simple" and a "complex" way to + -- control if the prompt should quit. + quit = quit == nil and (ret ~= true) or (quit~=false) + + -- Allow the callback to change the command + self.command = (ret ~= true) and ret or self.command + + -- Quit by default, but allow it to be disabled + if ret and type(ret) ~= "boolean" then + cb = self.exe_callback + if not quit then + self._private_cur_pos = ret:wlen() + 1 + update(self) + end + elseif quit then + -- No callback. + cb = function() end + end + + -- Execute the callback + if cb then + exec(self, cb, original_command) + end + + return + end + end + end + end + + -- Get out cases + if (mod.Control and (key == "c" or key == "g")) + or (not mod.Control and key == "Escape") then + self:stop() + return false + elseif (mod.Control and (key == "j" or key == "m")) + -- or (not mod.Control and key == "Return") + -- or (not mod.Control and key == "KP_Enter") + then + exec(self, self.exe_callback, self.command) + -- We already unregistered ourselves so we don't want to return + -- true, otherwise we may unregister someone else. + return + end + + -- Control cases + if mod.Control then + self.select_all = nil + if key == "v" then + local selection = capi.selection() + if selection then + -- Remove \n + local n = selection:find("\n") + if n then + selection = selection:sub(1, n - 1) + end + self.command = self.command:sub(1, self._private_cur_pos - 1) .. selection .. self.command:sub(self._private_cur_pos) + self._private_cur_pos = self._private_cur_pos + #selection + end + elseif key == "a" then + self._private_cur_pos = 1 + elseif key == "b" then + if self._private_cur_pos > 1 then + self._private_cur_pos = self._private_cur_pos - 1 + if have_multibyte_char_at(self.command, self._private_cur_pos) then + self._private_cur_pos = self._private_cur_pos - 1 + end + end + elseif key == "d" then + if self._private_cur_pos <= #self.command then + self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(self._private_cur_pos + 1) + end + elseif key == "p" then + if history_index > 1 then + history_index = history_index - 1 + + self.command = data.history[self.history_path].table[history_index] + self._private_cur_pos = #self.command + 2 + end + elseif key == "n" then + if history_index < history_items(self.history_path) then + history_index = history_index + 1 + + self.command = data.history[self.history_path].table[history_index] + self._private_cur_pos = #self.command + 2 + elseif history_index == history_items(self.history_path) then + history_index = history_index + 1 + + self.command = "" + self._private_cur_pos = 1 + end + elseif key == "e" then + self._private_cur_pos = #self.command + 1 + elseif key == "r" then + self._private.search_term = self._private.search_term or self.command:sub(1, self._private_cur_pos - 1) + for i,v in (function(a,i) return itera(-1,a,i) end), data.history[self.history_path].table, history_index do + if v:find(self._private.search_term,1,true) ~= nil then + self.command=v + history_index=i + self._private_cur_pos=#self.command+1 + break + end + end + elseif key == "s" then + self._private.search_term = self._private.search_term or self.command:sub(1, self._private_cur_pos - 1) + for i,v in (function(a,i) return itera(1,a,i) end), data.history[self.history_path].table, history_index do + if v:find(self._private.search_term,1,true) ~= nil then + self.command=v + history_index=i + self._private_cur_pos=#self.command+1 + break + end + end + elseif key == "f" then + if self._private_cur_pos <= #self.command then + if have_multibyte_char_at(self.command, self._private_cur_pos) then + self._private_cur_pos = self._private_cur_pos + 2 + else + self._private_cur_pos = self._private_cur_pos + 1 + end + end + elseif key == "h" then + if self._private_cur_pos > 1 then + local offset = 0 + if have_multibyte_char_at(self.command, self._private_cur_pos - 1) then + offset = 1 + end + self.command = self.command:sub(1, self._private_cur_pos - 2 - offset) .. self.command:sub(self._private_cur_pos) + self._private_cur_pos = self._private_cur_pos - 1 - offset + end + elseif key == "k" then + self.command = self.command:sub(1, self._private_cur_pos - 1) + elseif key == "u" then + self.command = self.command:sub(self._private_cur_pos, #self.command) + self._private_cur_pos = 1 + elseif key == "Prior" then + self._private.search_term = self.command:sub(1, self._private_cur_pos - 1) or "" + for i,v in (function(a,i) return itera(-1,a,i) end), data.history[self.history_path].table, history_index do + if v:find(self._private.search_term,1,true) == 1 then + self.command=v + history_index=i + break + end + end + elseif key == "Next" then + self._private.search_term = self.command:sub(1, self._private_cur_pos - 1) or "" + for i,v in (function(a,i) return itera(1,a,i) end), data.history[self.history_path].table, history_index do + if v:find(self._private.search_term,1,true) == 1 then + self.command=v + history_index=i + break + end + end + elseif key == "w" or key == "BackSpace" then + local wstart = 1 + local wend = 1 + local cword_start_pos = 1 + local cword_end_pos = 1 + while wend < self._private_cur_pos do + wend = self.command:find("[{[(,.:;_-+=@/ ]", wstart) + if not wend then wend = #self.command + 1 end + if self._private_cur_pos >= wstart and self._private_cur_pos <= wend + 1 then + cword_start_pos = wstart + cword_end_pos = self._private_cur_pos - 1 + break + end + wstart = wend + 1 + end + self.command = self.command:sub(1, cword_start_pos - 1) .. self.command:sub(cword_end_pos + 1) + self._private_cur_pos = cword_start_pos + elseif key == "Delete" then + -- delete from history only if: + -- we are not dealing with a new command + -- the user has not edited an existing entry + if self.command == data.history[self.history_path].table[history_index] then + table.remove(data.history[self.history_path].table, history_index) + if history_index <= history_items(self.history_path) then + self.command = data.history[self.history_path].table[history_index] + self._private_cur_pos = #self.command + 2 + elseif history_index > 1 then + history_index = history_index - 1 + + self.command = data.history[self.history_path].table[history_index] + self._private_cur_pos = #self.command + 2 + else + self.command = "" + self._private_cur_pos = 1 + end + end + end + elseif mod.Mod1 or mod.Mod3 then + if key == "b" then + self._private_cur_pos = cword_start(self.command, self._private_cur_pos) + elseif key == "f" then + self._private_cur_pos = cword_end(self.command, self._private_cur_pos) + elseif key == "d" then + self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(cword_end(self.command, self._private_cur_pos)) + elseif key == "BackSpace" then + local wstart = cword_start(self.command, self._private_cur_pos) + self.command = self.command:sub(1, wstart - 1) .. self.command:sub(self._private_cur_pos) + self._private_cur_pos = wstart + end + else + if self.completion_callback then + if key == "Tab" or key == "ISO_Left_Tab" then + if key == "ISO_Left_Tab" or mod.Shift then + if ncomp == 1 then return end + if ncomp == 2 then + self.command = command_before_comp + self.textbox:set_font(self.font) + self.textbox:set_markup(prompt_text_with_cursor{ + text = command_before_comp, text_color = self.fg_cursor, cursor_color = self.bg_cursor, + cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all, + prompt = self.prompt }) + self._private_cur_pos = cur_pos_before_comp + ncomp = 1 + return + end + + ncomp = ncomp - 2 + elseif ncomp == 1 then + command_before_comp = self.command + cur_pos_before_comp = self._private_cur_pos + end + local matches + self.command, self._private_cur_pos, matches = self.completion_callback(command_before_comp, cur_pos_before_comp, ncomp) + ncomp = ncomp + 1 + key = "" + -- execute if only one match found and autoexec flag set + if matches and #matches == 1 and args.autoexec then + exec(self, self.exe_callback) + return + end + elseif key ~= "Shift_L" and key ~= "Shift_R" then + ncomp = 1 + end + end + + -- Typin cases + if mod.Shift and key == "Insert" then + local selection = capi.selection() + if selection then + -- Remove \n + local n = selection:find("\n") + if n then + selection = selection:sub(1, n - 1) + end + self.command = self.command:sub(1, self._private_cur_pos - 1) .. selection .. self.command:sub(self._private_cur_pos) + self._private_cur_pos = self._private_cur_pos + #selection + end + elseif key == "Home" then + self._private_cur_pos = 1 + elseif key == "End" then + self._private_cur_pos = #self.command + 1 + elseif key == "BackSpace" then + if self._private_cur_pos > 1 then + local offset = 0 + if have_multibyte_char_at(self.command, self._private_cur_pos - 1) then + offset = 1 + end + self.command = self.command:sub(1, self._private_cur_pos - 2 - offset) .. self.command:sub(self._private_cur_pos) + self._private_cur_pos = self._private_cur_pos - 1 - offset + end + elseif key == "Delete" then + self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(self._private_cur_pos + 1) + elseif key == "Left" then + self._private_cur_pos = self._private_cur_pos - 1 + elseif key == "Right" then + self._private_cur_pos = self._private_cur_pos + 1 + elseif key == "Prior" then + if history_index > 1 then + history_index = history_index - 1 + + self.command = data.history[self.history_path].table[history_index] + self._private_cur_pos = #self.command + 2 + end + elseif key == "Next" then + if history_index < history_items(self.history_path) then + history_index = history_index + 1 + + self.command = data.history[self.history_path].table[history_index] + self._private_cur_pos = #self.command + 2 + elseif history_index == history_items(self.history_path) then + history_index = history_index + 1 + + self.command = "" + self._private_cur_pos = 1 + end + else + -- wlen() is UTF-8 aware but #key is not, + -- so check that we have one UTF-8 char but advance the cursor of # position + if key:wlen() == 1 then + if self.select_all then self.command = "" end + self.command = self.command:sub(1, self._private_cur_pos - 1) .. key .. self.command:sub(self._private_cur_pos) + self._private_cur_pos = self._private_cur_pos + #key + end + end + if self._private_cur_pos < 1 then + self._private_cur_pos = 1 + elseif self._private_cur_pos > #self.command + 1 then + self._private_cur_pos = #self.command + 1 + end + self.select_all = nil + end + + update(self) + if self.changed_callback then + self.changed_callback(self.command) + end + end) +end + +function prompt:stop() + keygrabber.stop(self._private.grabber) + history_save(self.history_path) + if self.done_callback then self.done_callback() end + return false +end + +local function new(args) + args = args or {} + + args.command = args.text or "" + args.prompt = args.prompt or "" + args.text = args.text or "" + args.font = args.font or beautiful.prompt_font or beautiful.font + args.bg_cursor = args.bg_cursor or beautiful.prompt_bg_cursor or beautiful.bg_focus or "white" + args.fg_cursor = args.fg_cursor or beautiful.prompt_fg_cursor or beautiful.fg_focus or "black" + args.ul_cursor = args.ul_cursor or nil + args.reset_on_stop = args.reset_on_stop == nil and true or args.reset_on_stop + args.select_all = args.select_all or nil + args.highlighter = args.highlighter or nil + args.hooks = args.hooks or {} + args.keypressed_callback = args.keypressed_callback or nil + args.changed_callback = args.changed_callback or nil + args.done_callback = args.done_callback or nil + args.history_max = args.history_max or nil + args.history_path = args.history_path or nil + args.completion_callback = args.completion_callback or nil + args.exe_callback = args.exe_callback or nil + args.textbox = args.textbox or wibox.widget.textbox() + + -- Build the hook map + local hooks = {} + for _,v in ipairs(args.hooks) do + if #v == 3 then + local _,key,callback = unpack(v) + if type(callback) == "function" then + hooks[key] = hooks[key] or {} + hooks[key][#hooks[key]+1] = v + else + gdebug.print_warning("The hook's 3rd parameter has to be a function.") + end + else + gdebug.print_warning("The hook has to have 3 parameters.") + end + end + args.hooks = hooks + + local ret = gobject({}) + ret._private = {} + gtable.crush(ret, prompt) + gtable.crush(ret, args) + + return ret +end + +function prompt.mt:__call(...) + return new(...) +end + +return setmetatable(prompt, prompt.mt) \ No newline at end of file diff --git a/.config/awesome/extras/bling/widget/init.lua b/.config/awesome/extras/bling/widget/init.lua new file mode 100755 index 0000000..d3c6ebd --- /dev/null +++ b/.config/awesome/extras/bling/widget/init.lua @@ -0,0 +1,7 @@ +return { + tag_preview = require(... .. ".tag_preview"), + task_preview = require(... .. ".task_preview"), + window_switcher = require(... .. ".window_switcher"), + tabbed_misc = require(... .. ".tabbed_misc"), + app_launcher = require(... .. ".app_launcher"), +} diff --git a/.config/awesome/extras/bling/widget/tabbar/boxes.lua b/.config/awesome/extras/bling/widget/tabbar/boxes.lua new file mode 100755 index 0000000..720f420 --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbar/boxes.lua @@ -0,0 +1,57 @@ +local awful = require("awful") +local gears = require("gears") +local wibox = require("wibox") + +local beautiful = require("beautiful") + +local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" +local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" +local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" +local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal +local font = beautiful.tabbar_font or beautiful.font or "Hack 15" +local size = beautiful.tabbar_size or 40 +local position = beautiful.tabbar_position or "bottom" + +local function create(c, focused_bool, buttons, inactive_bool) + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal + if focused_bool then + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus + end + local wid_temp = wibox.widget({ + { + { + awful.widget.clienticon(c), + left = 10, + right = 10, + bottom = 10, + top = 10, + widget = wibox.container.margin(), + }, + widget = wibox.container.place(), + }, + buttons = buttons, + bg = bg_temp, + widget = wibox.container.background(), + }) + return wid_temp +end + +local layout = wibox.layout.fixed.horizontal +if position == "left" or position == "right" then + layout = wibox.layout.fixed.vertical +end + +return { + layout = layout, + create = create, + position = position, + size = size, + bg_normal = bg_normal, + bg_focus = bg_normal, +} diff --git a/.config/awesome/extras/bling/widget/tabbar/default.lua b/.config/awesome/extras/bling/widget/tabbar/default.lua new file mode 100755 index 0000000..ad6b0b1 --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbar/default.lua @@ -0,0 +1,60 @@ +local gears = require("gears") +local wibox = require("wibox") + +local beautiful = require("beautiful") + +local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" +local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" +local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" +local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal +local font = beautiful.tabbar_font or beautiful.font or "Hack 15" +local size = beautiful.tabbar_size or 20 +local position = beautiful.tabbar_position or "top" + +local function create(c, focused_bool, buttons, inactive_bool) + local flexlist = wibox.layout.flex.horizontal() + local title_temp = c.name or c.class or "-" + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal + if focused_bool then + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus + end + local text_temp = wibox.widget.textbox() + text_temp.align = "center" + text_temp.valign = "center" + text_temp.font = font + text_temp.markup = "" + .. title_temp + .. "" + c:connect_signal("property::name", function(_) + local title_temp = c.name or c.class or "-" + text_temp.markup = "" + .. title_temp + .. "" + end) + local wid_temp = wibox.widget({ + text_temp, + buttons = buttons, + bg = bg_temp, + widget = wibox.container.background(), + }) + return wid_temp +end + +return { + layout = wibox.layout.flex.horizontal, + create = create, + position = position, + size = size, + bg_normal = bg_normal, + bg_focus = bg_focus, +} diff --git a/.config/awesome/extras/bling/widget/tabbar/modern.lua b/.config/awesome/extras/bling/widget/tabbar/modern.lua new file mode 100755 index 0000000..5f48066 --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbar/modern.lua @@ -0,0 +1,271 @@ +local awful = require("awful") +local gears = require("gears") +local wibox = require("wibox") +local beautiful = require("beautiful") +local xresources = require("beautiful.xresources") +local dpi = xresources.apply_dpi +local helpers = require(tostring(...):match(".*bling") .. ".helpers") + +local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" +local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" +local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" +local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal +local font = beautiful.tabbar_font or beautiful.font or "Hack 15" +local size = beautiful.tabbar_size or dpi(40) +local border_radius = beautiful.mstab_border_radius + or beautiful.border_radius + or 6 +local position = beautiful.tabbar_position or "top" +local close_color = beautiful.tabbar_color_close + or beautiful.xcolor1 + or "#f9929b" +local min_color = beautiful.tabbar_color_min or beautiful.xcolor3 or "#fbdf90" +local float_color = beautiful.tabbar_color_float + or beautiful.xcolor5 + or "#ccaced" + +-- Helper to create buttons +local function create_title_button(c, color_focus, color_unfocus) + local tb_color = wibox.widget({ + wibox.widget.textbox(), + forced_width = dpi(8), + forced_height = dpi(8), + bg = color_focus, + shape = gears.shape.circle, + widget = wibox.container.background, + }) + + local tb = wibox.widget({ + tb_color, + width = dpi(25), + height = dpi(25), + strategy = "min", + layout = wibox.layout.constraint, + }) + + local function update() + if client.focus == c then + tb_color.bg = color_focus + else + tb_color.bg = color_unfocus + end + end + update() + c:connect_signal("focus", update) + c:connect_signal("unfocus", update) + + tb:connect_signal("mouse::enter", function() + tb_color.bg = color_focus .. "70" + end) + + tb:connect_signal("mouse::leave", function() + tb_color.bg = color_focus + end) + + tb.visible = true + return tb +end + +local function create(c, focused_bool, buttons, inactive_bool) + -- local flexlist = wibox.layout.flex.horizontal() + local title_temp = c.name or c.class or "-" + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal + if focused_bool then + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus + end + local text_temp = wibox.widget.textbox() + text_temp.align = "center" + text_temp.valign = "center" + text_temp.font = font + text_temp.markup = "" + .. title_temp + .. "" + c:connect_signal("property::name", function(_) + local title_temp = c.name or c.class or "-" + text_temp.markup = "" + .. title_temp + .. "" + end) + + local tab_content = wibox.widget({ + { + awful.widget.clienticon(c), + top = dpi(6), + left = dpi(15), + bottom = dpi(6), + widget = wibox.container.margin, + }, + text_temp, + nill, + expand = "none", + layout = wibox.layout.align.horizontal, + }) + + local close = create_title_button(c, close_color, bg_normal) + close:connect_signal("button::press", function() + c:kill() + end) + + local floating = create_title_button(c, float_color, bg_normal) + floating:connect_signal("button::press", function() + c.floating = not c.floating + end) + + local min = create_title_button(c, min_color, bg_normal) + min:connect_signal("button::press", function() + c.minimized = true + end) + + if focused_bool then + tab_content = wibox.widget({ + { + awful.widget.clienticon(c), + top = dpi(10), + left = dpi(15), + bottom = dpi(10), + widget = wibox.container.margin, + }, + text_temp, + { + { min, floating, close, layout = wibox.layout.fixed.horizontal }, + top = dpi(10), + right = dpi(10), + bottom = dpi(10), + widget = wibox.container.margin, + }, + expand = "none", + layout = wibox.layout.align.horizontal, + }) + end + + local main_content = nil + local left_shape = nil + local right_shape = nil + + if position == "top" then + main_content = wibox.widget({ + { + tab_content, + bg = bg_temp, + shape = helpers.shape.prrect( + border_radius, + true, + true, + false, + false + ), + widget = wibox.container.background, + }, + top = dpi(8), + widget = wibox.container.margin, + }) + + left_shape = helpers.shape.prrect( + border_radius, + false, + false, + true, + false + ) + right_shape = helpers.shape.prrect( + border_radius, + false, + false, + false, + true + ) + else + main_content = wibox.widget({ + { + tab_content, + bg = bg_temp, + shape = helpers.shape.prrect( + border_radius, + false, + false, + true, + true + ), + widget = wibox.container.background, + }, + bottom = dpi(8), + widget = wibox.container.margin, + }) + + left_shape = helpers.shape.prrect( + border_radius, + false, + true, + false, + false + ) + right_shape = helpers.shape.prrect( + border_radius, + true, + false, + false, + false + ) + end + + local wid_temp = wibox.widget({ + buttons = buttons, + { + { + { + wibox.widget.textbox(), + bg = bg_normal, + shape = left_shape, + widget = wibox.container.background, + }, + bg = bg_temp, + shape = gears.rectangle, + widget = wibox.container.background, + }, + width = border_radius + (border_radius / 2), + height = size, + strategy = "exact", + layout = wibox.layout.constraint, + }, + main_content, + { + { + { + wibox.widget.textbox(), + bg = bg_normal, + shape = right_shape, + widget = wibox.container.background, + }, + bg = bg_temp, + shape = gears.rectangle, + widget = wibox.container.background, + }, + width = border_radius + (border_radius / 2), + height = size, + strategy = "exact", + layout = wibox.layout.constraint, + }, + + layout = wibox.layout.align.horizontal, + }) + return wid_temp +end + +return { + layout = wibox.layout.flex.horizontal, + create = create, + position = position, + size = size, + bg_normal = bg_normal, + bg_focus = bg_focus, +} diff --git a/.config/awesome/extras/bling/widget/tabbar/pure.lua b/.config/awesome/extras/bling/widget/tabbar/pure.lua new file mode 100755 index 0000000..5be82e5 --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbar/pure.lua @@ -0,0 +1,81 @@ +local awful = require("awful") +local gears = require("gears") +local wibox = require("wibox") +local gcolor = require("gears.color") +local beautiful = require("beautiful") + +local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" +local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" +local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" +local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal +local font = beautiful.tabbar_font or beautiful.font or "Hack 15" +local size = beautiful.tabbar_size or 20 +local position = beautiful.tabbar_position or "top" + +local function create(c, focused_bool, buttons, inactive_bool) + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal + if focused_bool then + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus + end + + local wid_temp = wibox.widget({ + { + { -- Left + wibox.widget.base.make_widget( + awful.titlebar.widget.iconwidget(c) + ), + buttons = buttons, + layout = wibox.layout.fixed.horizontal, + }, + { -- Title + wibox.widget.base.make_widget( + awful.titlebar.widget.titlewidget(c) + ), + buttons = buttons, + widget = wibox.container.place, + }, + { -- Right + focused_bool and wibox.widget.base.make_widget( + awful.titlebar.widget.floatingbutton(c) + ) or nil, + focused_bool and wibox.widget.base.make_widget( + awful.titlebar.widget.stickybutton(c) + ) or nil, + focused_bool and wibox.widget.base.make_widget( + awful.titlebar.widget.ontopbutton(c) + ) or nil, + focused_bool and wibox.widget.base.make_widget( + awful.titlebar.widget.maximizedbutton(c) + ) or nil, + focused_bool and wibox.widget.base.make_widget( + awful.titlebar.widget.minimizebutton(c) + ) or nil, + focused_bool and wibox.widget.base.make_widget( + awful.titlebar.widget.closebutton(c) + ) or nil, + layout = wibox.layout.fixed.horizontal, + }, + layout = wibox.layout.align.horizontal, + }, + bg = bg_temp, + fg = fg_temp, + widget = wibox.container.background, + }) + + return wid_temp +end + +return { + layout = wibox.layout.flex.horizontal, + create = create, + position = position, + size = size, + bg_normal = bg_normal, + bg_focus = bg_focus, +} diff --git a/.config/awesome/extras/bling/widget/tabbed_misc/custom_tasklist.lua b/.config/awesome/extras/bling/widget/tabbed_misc/custom_tasklist.lua new file mode 100755 index 0000000..ac22377 --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbed_misc/custom_tasklist.lua @@ -0,0 +1,51 @@ +local wibox = require("wibox") +local awful = require("awful") +local gears = require("gears") +local beautiful = require("beautiful") +local dpi = require("beautiful.xresources").apply_dpi + +local function tabobj_support(self, c, index, clients) + -- Self is the background widget in this context + if not c.bling_tabbed and #c.bling_tabbed.clients > 1 then + return + end + + local group = c.bling_tabbed + + -- TODO: Allow customization here + local layout_v = wibox.widget { + vertical_spacing = dpi(2), + horizontal_spacing = dpi(2), + layout = wibox.layout.grid.horizontal, + forced_num_rows = 2, + forced_num_cols = 2, + homogeneous = true + } + + local wrapper = wibox.widget({ + layout_v, + id = "click_role", + widget = wibox.container.margin, + margins = dpi(5), + }) + + -- To get the ball rolling. + for idx, c in ipairs(group.clients) do + if not (c and c.icon) then goto skip end + + -- Add to the last layout + layout_v:add(wibox.widget { + { + widget = awful.widget.clienticon, + client = c + }, + widget = wibox.container.constraint, + width = dpi(24), + height = dpi(24) + }) + ::skip:: + end + self.widget = wrapper +end + +return tabobj_support diff --git a/.config/awesome/extras/bling/widget/tabbed_misc/init.lua b/.config/awesome/extras/bling/widget/tabbed_misc/init.lua new file mode 100755 index 0000000..1de3fbb --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbed_misc/init.lua @@ -0,0 +1,9 @@ +return { + titlebar_indicator = require( + tostring(...):match(".*bling") + .. ".widget.tabbed_misc.titlebar_indicator" + ), + custom_tasklist = require( + tostring(...):match(".*bling") .. ".widget.tabbed_misc.custom_tasklist" + ), +} diff --git a/.config/awesome/extras/bling/widget/tabbed_misc/titlebar_indicator.lua b/.config/awesome/extras/bling/widget/tabbed_misc/titlebar_indicator.lua new file mode 100755 index 0000000..46959cd --- /dev/null +++ b/.config/awesome/extras/bling/widget/tabbed_misc/titlebar_indicator.lua @@ -0,0 +1,133 @@ +local wibox = require("wibox") +local awful = require("awful") +local gears = require("gears") +local beautiful = require("beautiful") +local dpi = require("beautiful.xresources").apply_dpi +local tabbed_module = require( + tostring(...):match(".*bling") .. ".module.tabbed" +) + +-- Just check if a table contains a value. +local function tbl_contains(tbl, item) + for _, v in ipairs(tbl) do + if v == item then + return true + end + end + return false +end + +-- Needs to be run, every time a new titlbear is created +return function(c, opts) + -- Args & Fallback -- Widget templates are in their original loactions + opts = gears.table.crush({ + layout_spacing = dpi(4), + icon_size = dpi(20), + icon_margin = dpi(4), + bg_color_focus = "#ff0000", + bg_color = "#00000000", + fg_color = "#fafafa", + fg_color_focus = "#e0e0e0", + icon_shape = function(cr, w, h) + gears.shape.rounded_rect(cr, w, h, 0) + end, + layout = wibox.layout.fixed.horizontal, + }, gears.table.join( + opts, + beautiful.bling_tabbed_misc_titlebar_indicator + )) + + -- Container to store icons + local tabbed_icons = wibox.widget({ + layout = opts.layout, + spacing = opts.layout_spacing, + }) + + awesome.connect_signal("bling::tabbed::client_removed", function(_, removed_c) + -- Remove from list + for idx, icon in ipairs(tabbed_icons.children) do + if icon._client == removed_c then + tabbed_icons:remove(idx) + end + end + + -- Empty list + if removed_c == c then + tabbed_icons:reset() + end + end) + + local function recreate(group) + if tbl_contains(group.clients, c) then + tabbed_icons:reset() + local focused = group.clients[group.focused_idx] + + -- Autohide? + if #group.clients == 1 then + return + end + + for idx, client in ipairs(group.clients) do + local widget = wibox.widget( + opts.widget_template or { + { + { + { + id = "icon_role", + forced_width = opts.icon_size, + forced_height = opts.icon_size, + widget = awful.widget.clienticon, + }, + margins = opts.icon_margin, + widget = wibox.container.margin, + }, + shape = opts.icon_shape, + id = "bg_role", + widget = wibox.container.background, + }, + halign = "center", + valign = "center", + widget = wibox.container.place, + }) + + widget._client = client + + -- No creation call back since this would be called on creation & every time the widget updated. + if opts.widget_template and opts.widget_template.update_callback then + opts.widget_template.update_callback(widget, client, group) + end + + -- Add icons & etc + for _, w in ipairs(widget:get_children_by_id("icon_role")) do + -- TODO: Allow fallback icon? + w.image = client.icon + w.client = client + end + + for _, w in ipairs(widget:get_children_by_id("bg_role")) do + w:add_button(awful.button({}, 1, function() + tabbed_module.switch_to(group, idx) + end)) + if client == focused then + w.bg = opts.bg_color_focus + w.fg = opts.fg_color_focus + else + w.bg = opts.bg_color + w.fg = opts.fg_color + end + end + + for _, w in ipairs(widget:get_children_by_id("text_role")) do + w.text = client.name + end + + tabbed_icons:add(widget) + end + end + end + + awesome.connect_signal("bling::tabbed::client_added", recreate) + awesome.connect_signal("bling::tabbed::changed_focus", recreate) + + return tabbed_icons +end diff --git a/.config/awesome/extras/bling/widget/tag_preview.lua b/.config/awesome/extras/bling/widget/tag_preview.lua new file mode 100755 index 0000000..2a181b4 --- /dev/null +++ b/.config/awesome/extras/bling/widget/tag_preview.lua @@ -0,0 +1,246 @@ +-- +-- Provides: +-- bling::tag_preview::update -- first line is the signal +-- t (tag) -- indented lines are function parameters +-- bling::tag_preview::visibility +-- s (screen) +-- v (boolean) +-- +local awful = require("awful") +local wibox = require("wibox") +local helpers = require(tostring(...):match(".*bling") .. ".helpers") +local gears = require("gears") +local beautiful = require("beautiful") +local dpi = beautiful.xresources.apply_dpi +local cairo = require("lgi").cairo + +local function draw_widget( + t, + tag_preview_image, + scale, + screen_radius, + client_radius, + client_opacity, + client_bg, + client_border_color, + client_border_width, + widget_bg, + widget_border_color, + widget_border_width, + geo, + margin, + background_image +) + local client_list = wibox.layout.manual() + client_list.forced_height = geo.height + client_list.forced_width = geo.width + local tag_screen = t.screen + for i, c in ipairs(t:clients()) do + if not c.hidden and not c.minimized then + + + local img_box = wibox.widget ({ + resize = true, + forced_height = 100 * scale, + forced_width = 100 * scale, + widget = wibox.widget.imagebox, + }) + + -- If fails to set image, fallback to a awesome icon + if not pcall(function() img_box.image = gears.surface.load(c.icon) end) then + img_box.image = beautiful.theme_assets.awesome_icon (24, "#222222", "#fafafa") + end + + if tag_preview_image then + if c.prev_content or t.selected then + local content + if t.selected then + content = gears.surface(c.content) + else + content = gears.surface(c.prev_content) + end + local cr = cairo.Context(content) + local x, y, w, h = cr:clip_extents() + local img = cairo.ImageSurface.create( + cairo.Format.ARGB32, + w - x, + h - y + ) + cr = cairo.Context(img) + cr:set_source_surface(content, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + + img_box = wibox.widget({ + image = gears.surface.load(img), + resize = true, + opacity = client_opacity, + forced_height = math.floor(c.height * scale), + forced_width = math.floor(c.width * scale), + widget = wibox.widget.imagebox, + }) + end + end + + local client_box = wibox.widget({ + { + nil, + { + nil, + img_box, + nil, + expand = "outside", + layout = wibox.layout.align.horizontal, + }, + nil, + expand = "outside", + widget = wibox.layout.align.vertical, + }, + forced_height = math.floor(c.height * scale), + forced_width = math.floor(c.width * scale), + bg = client_bg, + shape_border_color = client_border_color, + shape_border_width = client_border_width, + shape = helpers.shape.rrect(client_radius), + widget = wibox.container.background, + }) + + client_box.point = { + x = math.floor((c.x - geo.x) * scale), + y = math.floor((c.y - geo.y) * scale), + } + + client_list:add(client_box) + end + end + + return wibox.widget { + { + background_image, + { + { + { + { + client_list, + forced_height = geo.height, + forced_width = geo.width, + widget = wibox.container.place, + }, + layout = wibox.layout.align.horizontal, + }, + layout = wibox.layout.align.vertical, + }, + margins = margin, + widget = wibox.container.margin, + }, + layout = wibox.layout.stack + }, + bg = widget_bg, + shape_border_width = widget_border_width, + shape_border_color = widget_border_color, + shape = helpers.shape.rrect(screen_radius), + widget = wibox.container.background, + } +end + +local enable = function(opts) + local opts = opts or {} + + local tag_preview_image = opts.show_client_content or false + local widget_x = opts.x or dpi(20) + local widget_y = opts.y or dpi(20) + local scale = opts.scale or 0.2 + local work_area = opts.honor_workarea or false + local padding = opts.honor_padding or false + local placement_fn = opts.placement_fn or nil + local background_image = opts.background_widget or nil + + local margin = beautiful.tag_preview_widget_margin or dpi(0) + local screen_radius = beautiful.tag_preview_widget_border_radius or dpi(0) + local client_radius = beautiful.tag_preview_client_border_radius or dpi(0) + local client_opacity = beautiful.tag_preview_client_opacity or 0.5 + local client_bg = beautiful.tag_preview_client_bg or "#000000" + local client_border_color = beautiful.tag_preview_client_border_color + or "#ffffff" + local client_border_width = beautiful.tag_preview_client_border_width + or dpi(3) + local widget_bg = beautiful.tag_preview_widget_bg or "#000000" + local widget_border_color = beautiful.tag_preview_widget_border_color + or "#ffffff" + local widget_border_width = beautiful.tag_preview_widget_border_width + or dpi(3) + + local tag_preview_box = awful.popup({ + type = "dropdown_menu", + visible = false, + ontop = true, + placement = placement_fn, + widget = wibox.container.background, + input_passthrough = true, + bg = "#00000000", + }) + + tag.connect_signal("property::selected", function(t) + -- Awesome switches up tags on startup really fast it seems, probably depends on what rules you have set + -- which can cause the c.content to not show the correct image + gears.timer + { + timeout = 0.1, + call_now = false, + autostart = true, + single_shot = true, + callback = function() + if t.selected == true then + for _, c in ipairs(t:clients()) do + c.prev_content = gears.surface.duplicate_surface(c.content) + end + end + end + } + end) + + awesome.connect_signal("bling::tag_preview::update", function(t) + local geo = t.screen:get_bounding_geometry({ + honor_padding = padding, + honor_workarea = work_area, + }) + + tag_preview_box.maximum_width = scale * geo.width + margin * 2 + tag_preview_box.maximum_height = scale * geo.height + margin * 2 + + + tag_preview_box.widget = draw_widget( + t, + tag_preview_image, + scale, + screen_radius, + client_radius, + client_opacity, + client_bg, + client_border_color, + client_border_width, + widget_bg, + widget_border_color, + widget_border_width, + geo, + margin, + background_image + ) + end) + + awesome.connect_signal("bling::tag_preview::visibility", function(s, v) + if not placement_fn then + tag_preview_box.x = s.geometry.x + widget_x + tag_preview_box.y = s.geometry.y + widget_y + end + + if v == false then + tag_preview_box.widget = nil + collectgarbage("collect") + end + + tag_preview_box.visible = v + end) +end + +return {enable = enable, draw_widget = draw_widget} diff --git a/.config/awesome/extras/bling/widget/task_preview.lua b/.config/awesome/extras/bling/widget/task_preview.lua new file mode 100755 index 0000000..3b54867 --- /dev/null +++ b/.config/awesome/extras/bling/widget/task_preview.lua @@ -0,0 +1,199 @@ +-- +-- Provides: +-- bling::task_preview::visibility +-- s (screen) +-- v (boolean) +-- c (client) +-- +local awful = require("awful") +local wibox = require("wibox") +local helpers = require(tostring(...):match(".*bling") .. ".helpers") +local gears = require("gears") +local beautiful = require("beautiful") +local dpi = beautiful.xresources.apply_dpi +local cairo = require("lgi").cairo + +-- TODO: rename structure to something better? +local function draw_widget( + c, + widget_template, + screen_radius, + widget_bg, + widget_border_color, + widget_border_width, + margin, + widget_width, + widget_height +) + if not pcall(function() + return type(c.content) + end) then + return + end + + local content = nil + if c.active then + content = gears.surface(c.content) + elseif c.prev_content then + content = gears.surface(c.prev_content) + end + + local img = nil + if content ~= nil then + local cr = cairo.Context(content) + local x, y, w, h = cr:clip_extents() + img = cairo.ImageSurface.create(cairo.Format.ARGB32, w - x, h - y) + cr = cairo.Context(img) + cr:set_source_surface(content, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + end + + local widget = wibox.widget({ + (widget_template or { + { + { + { + { + id = "icon_role", + resize = true, + forced_height = dpi(20), + forced_width = dpi(20), + widget = wibox.widget.imagebox, + }, + { + { + id = "name_role", + align = "center", + widget = wibox.widget.textbox, + }, + left = dpi(4), + right = dpi(4), + widget = wibox.container.margin, + }, + layout = wibox.layout.align.horizontal, + }, + { + { + { + id = "image_role", + resize = true, + clip_shape = helpers.shape.rrect(screen_radius), + widget = wibox.widget.imagebox, + }, + valign = "center", + halign = "center", + widget = wibox.container.place, + }, + top = margin * 0.25, + widget = wibox.container.margin, + }, + fill_space = true, + layout = wibox.layout.fixed.vertical, + }, + margins = margin, + widget = wibox.container.margin, + }, + bg = widget_bg, + shape_border_width = widget_border_width, + shape_border_color = widget_border_color, + shape = helpers.shape.rrect(screen_radius), + widget = wibox.container.background, + }), + width = widget_width, + height = widget_height, + widget = wibox.container.constraint, + }) + + -- TODO: have something like a create callback here? + + for _, w in ipairs(widget:get_children_by_id("image_role")) do + w.image = img -- TODO: copy it with gears.surface.xxx or something + end + + for _, w in ipairs(widget:get_children_by_id("name_role")) do + w.text = c.name + end + + for _, w in ipairs(widget:get_children_by_id("icon_role")) do + w.image = c.icon -- TODO: detect clienticon + end + + return widget +end + +local enable = function(opts) + local opts = opts or {} + + local widget_x = opts.x or dpi(20) + local widget_y = opts.y or dpi(20) + local widget_height = opts.height or dpi(200) + local widget_width = opts.width or dpi(200) + local placement_fn = opts.placement_fn or nil + + local margin = beautiful.task_preview_widget_margin or dpi(0) + local screen_radius = beautiful.task_preview_widget_border_radius or dpi(0) + local widget_bg = beautiful.task_preview_widget_bg or "#000000" + local widget_border_color = beautiful.task_preview_widget_border_color + or "#ffffff" + local widget_border_width = beautiful.task_preview_widget_border_width + or dpi(3) + + local task_preview_box = awful.popup({ + type = "dropdown_menu", + visible = false, + ontop = true, + placement = placement_fn, + widget = wibox.container.background, -- A dummy widget to make awful.popup not scream + input_passthrough = true, + bg = "#00000000", + }) + + tag.connect_signal("property::selected", function(t) + -- Awesome switches up tags on startup really fast it seems, probably depends on what rules you have set + -- which can cause the c.content to not show the correct image + gears.timer + { + timeout = 0.1, + call_now = false, + autostart = true, + single_shot = true, + callback = function() + if t.selected == true then + for _, c in ipairs(t:clients()) do + c.prev_content = gears.surface.duplicate_surface(c.content) + end + end + end + } + end) + + awesome.connect_signal("bling::task_preview::visibility", function(s, v, c) + if v then + -- Update task preview contents + task_preview_box.widget = draw_widget( + c, + opts.structure, + screen_radius, + widget_bg, + widget_border_color, + widget_border_width, + margin, + widget_width, + widget_height + ) + else + task_preview_box.widget = nil + collectgarbage("collect") + end + + if not placement_fn then + task_preview_box.x = s.geometry.x + widget_x + task_preview_box.y = s.geometry.y + widget_y + end + + task_preview_box.visible = v + end) +end + +return { enable = enable, draw_widget = draw_widget } diff --git a/.config/awesome/extras/bling/widget/window_switcher.lua b/.config/awesome/extras/bling/widget/window_switcher.lua new file mode 100755 index 0000000..ac835a5 --- /dev/null +++ b/.config/awesome/extras/bling/widget/window_switcher.lua @@ -0,0 +1,454 @@ +local cairo = require("lgi").cairo +local awful = require("awful") +local gears = require("gears") +local wibox = require("wibox") +local beautiful = require("beautiful") +local helpers = require(tostring(...):match(".*bling") .. ".helpers") +local dpi = beautiful.xresources.apply_dpi + +local window_switcher_first_client -- The client that was focused when the window_switcher was activated +local window_switcher_minimized_clients = {} -- The clients that were minimized when the window switcher was activated +local window_switcher_grabber + +local get_num_clients = function() + local minimized_clients_in_tag = 0 + local matcher = function(c) + return awful.rules.match( + c, + { + minimized = true, + skip_taskbar = false, + hidden = false, + first_tag = awful.screen.focused().selected_tag, + } + ) + end + for c in awful.client.iterate(matcher) do + minimized_clients_in_tag = minimized_clients_in_tag + 1 + end + return minimized_clients_in_tag + #awful.screen.focused().clients +end + +local window_switcher_hide = function(window_switcher_box) + -- Add currently focused client to history + if client.focus then + local window_switcher_last_client = client.focus + awful.client.focus.history.add(window_switcher_last_client) + -- Raise client that was focused originally + -- Then raise last focused client + if + window_switcher_first_client and window_switcher_first_client.valid + then + window_switcher_first_client:raise() + window_switcher_last_client:raise() + end + end + + -- Minimize originally minimized clients + local s = awful.screen.focused() + for _, c in pairs(window_switcher_minimized_clients) do + if c and c.valid and not (client.focus and client.focus == c) then + c.minimized = true + end + end + -- Reset helper table + window_switcher_minimized_clients = {} + + -- Resume recording focus history + awful.client.focus.history.enable_tracking() + -- Stop and hide window_switcher + awful.keygrabber.stop(window_switcher_grabber) + window_switcher_box.visible = false + window_switcher_box.widget = nil + collectgarbage("collect") +end + +local function draw_widget( + type, + background, + border_width, + border_radius, + border_color, + clients_spacing, + client_icon_horizontal_spacing, + client_width, + client_height, + client_margins, + thumbnail_margins, + thumbnail_scale, + name_margins, + name_valign, + name_forced_width, + name_font, + name_normal_color, + name_focus_color, + icon_valign, + icon_width, + mouse_keys +) + local tasklist_widget = type == "thumbnail" + and awful.widget.tasklist({ + screen = awful.screen.focused(), + filter = awful.widget.tasklist.filter.currenttags, + buttons = mouse_keys, + style = { + font = name_font, + fg_normal = name_normal_color, + fg_focus = name_focus_color, + }, + layout = { + layout = wibox.layout.flex.horizontal, + spacing = clients_spacing, + }, + widget_template = { + widget = wibox.container.background, + id = "bg_role", + forced_width = client_width, + forced_height = client_height, + create_callback = function(self, c, _, __) + local content = gears.surface(c.content) + local cr = cairo.Context(content) + local x, y, w, h = cr:clip_extents() + local img = cairo.ImageSurface.create( + cairo.Format.ARGB32, + w - x, + h - y + ) + cr = cairo.Context(img) + cr:set_source_surface(content, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + self:get_children_by_id("thumbnail")[1].image = + gears.surface.load( + img + ) + end, + { + { + { + horizontal_fit_policy = thumbnail_scale == true + and "fit" + or "auto", + vertical_fit_policy = thumbnail_scale == true + and "fit" + or "auto", + id = "thumbnail", + widget = wibox.widget.imagebox, + }, + margins = thumbnail_margins, + widget = wibox.container.margin, + }, + { + { + { + id = "icon_role", + widget = wibox.widget.imagebox, + }, + forced_width = icon_width, + valign = icon_valign, + widget = wibox.container.place, + }, + { + { + forced_width = name_forced_width, + valign = name_valign, + id = "text_role", + widget = wibox.widget.textbox, + }, + margins = name_margins, + widget = wibox.container.margin, + }, + spacing = client_icon_horizontal_spacing, + layout = wibox.layout.fixed.horizontal, + }, + layout = wibox.layout.flex.vertical, + }, + }, + }) + or awful.widget.tasklist({ + screen = awful.screen.focused(), + filter = awful.widget.tasklist.filter.currenttags, + buttons = mouse_keys, + style = { + font = name_font, + fg_normal = name_normal_color, + fg_focus = name_focus_color, + }, + layout = { + layout = wibox.layout.fixed.vertical, + spacing = clients_spacing, + }, + widget_template = { + widget = wibox.container.background, + id = "bg_role", + forced_width = client_width, + forced_height = client_height, + { + { + { + id = "icon_role", + widget = wibox.widget.imagebox, + }, + forced_width = icon_width, + valign = icon_valign, + widget = wibox.container.place, + }, + { + { + forced_width = name_forced_width, + valign = name_valign, + id = "text_role", + widget = wibox.widget.textbox, + }, + margins = name_margins, + widget = wibox.container.margin, + }, + spacing = client_icon_horizontal_spacing, + layout = wibox.layout.fixed.horizontal, + }, + }, + }) + + return wibox.widget({ + { + tasklist_widget, + margins = client_margins, + widget = wibox.container.margin, + }, + shape_border_width = border_width, + shape_border_color = border_color, + bg = background, + shape = helpers.shape.rrect(border_radius), + widget = wibox.container.background, + }) +end + +local enable = function(opts) + local opts = opts or {} + + local type = opts.type or "thumbnail" + local background = beautiful.window_switcher_widget_bg or "#000000" + local border_width = beautiful.window_switcher_widget_border_width or dpi(3) + local border_radius = beautiful.window_switcher_widget_border_radius + or dpi(0) + local border_color = beautiful.window_switcher_widget_border_color + or "#ffffff" + local clients_spacing = beautiful.window_switcher_clients_spacing or dpi(20) + local client_icon_horizontal_spacing = beautiful.window_switcher_client_icon_horizontal_spacing + or dpi(5) + local client_width = beautiful.window_switcher_client_width + or dpi(type == "thumbnail" and 150 or 500) + local client_height = beautiful.window_switcher_client_height + or dpi(type == "thumbnail" and 250 or 50) + local client_margins = beautiful.window_switcher_client_margins or dpi(10) + local thumbnail_margins = beautiful.window_switcher_thumbnail_margins + or dpi(5) + local thumbnail_scale = beautiful.thumbnail_scale or false + local name_margins = beautiful.window_switcher_name_margins or dpi(10) + local name_valign = beautiful.window_switcher_name_valign or "center" + local name_forced_width = beautiful.window_switcher_name_forced_width + or dpi(type == "thumbnail" and 200 or 550) + local name_font = beautiful.window_switcher_name_font or beautiful.font + local name_normal_color = beautiful.window_switcher_name_normal_color + or "#FFFFFF" + local name_focus_color = beautiful.window_switcher_name_focus_color + or "#FF0000" + local icon_valign = beautiful.window_switcher_icon_valign or "center" + local icon_width = beautiful.window_switcher_icon_width or dpi(40) + + local hide_window_switcher_key = opts.hide_window_switcher_key or "Escape" + + local select_client_key = opts.select_client_key or 1 + local minimize_key = opts.minimize_key or "n" + local unminimize_key = opts.unminimize_key or "N" + local kill_client_key = opts.kill_client_key or "q" + + local cycle_key = opts.cycle_key or "Tab" + + local previous_key = opts.previous_key or "Left" + local next_key = opts.next_key or "Right" + + local vim_previous_key = opts.vim_previous_key or "h" + local vim_next_key = opts.vim_next_key or "l" + + local scroll_previous_key = opts.scroll_previous_key or 4 + local scroll_next_key = opts.scroll_next_key or 5 + + local window_switcher_box = awful.popup({ + bg = "#00000000", + visible = false, + ontop = true, + placement = awful.placement.centered, + screen = awful.screen.focused(), + widget = wibox.container.background, -- A dummy widget to make awful.popup not scream + widget = { + { + draw_widget(), + margins = client_margins, + widget = wibox.container.margin, + }, + shape_border_width = border_width, + shape_border_color = border_color, + bg = background, + shape = helpers.shape.rrect(border_radius), + widget = wibox.container.background, + }, + }) + + local mouse_keys = gears.table.join( + awful.button({ + modifiers = { "Any" }, + button = select_client_key, + on_press = function(c) + client.focus = c + end, + }), + + awful.button({ + modifiers = { "Any" }, + button = scroll_previous_key, + on_press = function() + awful.client.focus.byidx(-1) + end, + }), + + awful.button({ + modifiers = { "Any" }, + button = scroll_next_key, + on_press = function() + awful.client.focus.byidx(1) + end, + }) + ) + + local keyboard_keys = { + [hide_window_switcher_key] = function() + window_switcher_hide(window_switcher_box) + end, + + [minimize_key] = function() + if client.focus then + client.focus.minimized = true + end + end, + [unminimize_key] = function() + if awful.client.restore() then + client.focus = awful.client.restore() + end + end, + [kill_client_key] = function() + if client.focus then + client.focus:kill() + end + end, + + [cycle_key] = function() + awful.client.focus.byidx(1) + end, + + [previous_key] = function() + awful.client.focus.byidx(1) + end, + [next_key] = function() + awful.client.focus.byidx(-1) + end, + + [vim_previous_key] = function() + awful.client.focus.byidx(1) + end, + [vim_next_key] = function() + awful.client.focus.byidx(-1) + end, + } + + window_switcher_box:connect_signal("property::width", function() + if window_switcher_box.visible and get_num_clients() == 0 then + window_switcher_hide(window_switcher_box) + end + end) + + window_switcher_box:connect_signal("property::height", function() + if window_switcher_box.visible and get_num_clients() == 0 then + window_switcher_hide(window_switcher_box) + end + end) + + awesome.connect_signal("bling::window_switcher::turn_on", function() + local number_of_clients = get_num_clients() + if number_of_clients == 0 then + return + end + + -- Store client that is focused in a variable + window_switcher_first_client = client.focus + + -- Stop recording focus history + awful.client.focus.history.disable_tracking() + + -- Go to previously focused client (in the tag) + awful.client.focus.history.previous() + + -- Track minimized clients + -- Unminimize them + -- Lower them so that they are always below other + -- originally unminimized windows + local clients = awful.screen.focused().selected_tag:clients() + for _, c in pairs(clients) do + if c.minimized then + table.insert(window_switcher_minimized_clients, c) + c.minimized = false + c:lower() + end + end + + -- Start the keygrabber + window_switcher_grabber = awful.keygrabber.run(function(_, key, event) + if event == "release" then + -- Hide if the modifier was released + -- We try to match Super or Alt or Control since we do not know which keybind is + -- used to activate the window switcher (the keybind is set by the user in keys.lua) + if + key:match("Super") + or key:match("Alt") + or key:match("Control") + then + window_switcher_hide(window_switcher_box) + end + -- Do nothing + return + end + + -- Run function attached to key, if it exists + if keyboard_keys[key] then + keyboard_keys[key]() + end + end) + + window_switcher_box.widget = draw_widget( + type, + background, + border_width, + border_radius, + border_color, + clients_spacing, + client_icon_horizontal_spacing, + client_width, + client_height, + client_margins, + thumbnail_margins, + thumbnail_scale, + name_margins, + name_valign, + name_forced_width, + name_font, + name_normal_color, + name_focus_color, + icon_valign, + icon_width, + mouse_keys + ) + window_switcher_box.visible = true + end) +end + +return { enable = enable } diff --git a/.config/awesome/extras/nice b/.config/awesome/extras/nice new file mode 160000 index 0000000..810aa72 --- /dev/null +++ b/.config/awesome/extras/nice @@ -0,0 +1 @@ +Subproject commit 810aa72bbebfee15d3375fdcb8d8a09f5c7741c8