+
+Set of widgets compatible with Awesome Window Manager v.4.3+.
+
+## Screenshots
+
+Spotify, CPU, RAM, brightness-arc, volume-arc and battery-arc widgets:
+
+
+
+
+
+Brightness, volume and battery widgets:
+
+
+
+
+
+
+
+Some more screenshots in this reddit [post](https://www.reddit.com/r/unixporn/comments/8qijmx/awesomewm_dark_theme/)
+
+# Installation
+
+Clone the repo under **~/.config/awesome/**, then follow an Installation section of widget's readme file.
+
+# Stargazers
+
+[](https://starchart.cc/streetturtle/awesome-wm-widgets)
+
+# Troubleshooting
+
+In case of any doubts/questions/problems:
+ - create an [issue](https://github.com/streetturtle/awesome-wm-widgets/issues/new/choose)
+ - raise a question on [Discussions](https://github.com/streetturtle/awesome-wm-widgets/discussions)!
+ - ping me on AwesomeWM's discord, here's an [invite](https://discord.gg/BPat4F87dg)
+
+# Support
+
+If you find anything useful here, you can:
+ - star a repo - this really motivates me to work on this project
+ - or
+ - or even become a [sponsor](https://github.com/sponsors/streetturtle)
+
+# Contributors
+
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/Screenshot from 2019-03-01 14-28-18.png b/.config/awesome/awesome-wm-widgets/Screenshot from 2019-03-01 14-28-18.png
new file mode 100755
index 0000000..4c9bd87
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/Screenshot from 2019-03-01 14-28-18.png differ
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/README.md b/.config/awesome/awesome-wm-widgets/apt-widget/README.md
new file mode 100755
index 0000000..f3ac47a
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/README.md
@@ -0,0 +1,27 @@
+# APT widget
+
+Widget which shows a list of APT packages to be updated:
+
+
+
+Features:
+ - scrollable list !!! (thanks to this [post](https://www.reddit.com/r/awesomewm/comments/isx89x/scrolling_a_layout_fixed_flexed_layout_widget/) of reddit)
+ - update single package
+ - update multiple packages
+
+## Installation
+
+Clone the repo under ~/.config/awesome/ folder, then in rc.lua add the following:
+
+```lua
+local apt_widget = require("awesome-wm-widgets.apt-widget.apt-widget")
+
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ apt_widget(),
+ ...
+```
+
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/apt-widget.lua b/.config/awesome/awesome-wm-widgets/apt-widget/apt-widget.lua
new file mode 100755
index 0000000..c15c32f
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/apt-widget.lua
@@ -0,0 +1,349 @@
+-------------------------------------------------
+-- APT Widget for Awesome Window Manager
+-- Lists containers and allows to manage them
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/apt-widget
+
+-- @author Pavel Makhov
+-- @copyright 2021 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/apt-widget'
+local ICONS_DIR = WIDGET_DIR .. '/icons/'
+
+local LIST_PACKAGES = [[sh -c "LC_ALL=c apt list --upgradable 2>/dev/null"]]
+
+--- Utility function to show warning messages
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Docker Widget',
+ text = message}
+end
+
+local wibox_popup = wibox {
+ ontop = true,
+ visible = false,
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ max_widget_size = 500,
+ height = 500,
+ width = 300,
+}
+
+local apt_widget = wibox.widget {
+ {
+ {
+ id = 'icon',
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_icon = function(self, new_icon)
+ self:get_children_by_id("icon")[1].image = new_icon
+ end
+}
+
+--- Parses the line and creates the package table out of it
+--- yaru-theme-sound/focal-updates,focal-updates 20.04.10.1 all [upgradable from: 20.04.8]
+local parse_package = function(line)
+ local name,_,nv,type,ov = line:match('(.*)%/(.*)%s(.*)%s(.*)%s%[upgradable from: (.*)]')
+
+ if name == nil then return nil end
+
+ local package = {
+ name = name,
+ new_version = nv,
+ type = type,
+ old_version = ov
+ }
+ return package
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or ICONS_DIR .. 'white-black.svg'
+
+ apt_widget:set_icon(icon)
+
+ local pointer = 0
+ local min_widgets = 5
+ local carousel = false
+
+ local function rebuild_widget(containers, errors, _, _)
+
+ local to_update = {}
+
+ if errors ~= '' then
+ show_warning(errors)
+ return
+ end
+
+ local rows = wibox.layout.fixed.vertical()
+ rows:connect_signal("button::press", function(_,_,_,button)
+ if carousel then
+ if button == 4 then -- up scrolling
+ local cnt = #rows.children
+ local first_widget = rows.children[1]
+ rows:insert(cnt+1, first_widget)
+ rows:remove(1)
+ elseif button == 5 then -- down scrolling
+ local cnt = #rows.children
+ local last_widget = rows.children[cnt]
+ rows:insert(1, last_widget)
+ rows:remove(cnt+1)
+ end
+ else
+ if button == 5 then -- up scrolling
+ if pointer < #rows.children and ((#rows.children - pointer) >= min_widgets) then
+ pointer = pointer + 1
+ rows.children[pointer].visible = false
+ end
+ elseif button == 4 then -- down scrolling
+ if pointer > 0 then
+ rows.children[pointer].visible = true
+ pointer = pointer - 1
+ end
+ end
+ end
+ end)
+
+ for line in containers:gmatch("[^\r\n]+") do
+ local package = parse_package(line)
+
+ if package ~= nil then
+
+ local refresh_button = wibox.widget {
+ {
+ {
+ id = 'icon',
+ image = ICONS_DIR .. 'refresh-cw.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ widget = wibox.container.margin
+ },
+ shape = gears.shape.circle,
+ opacity = 0.5,
+ widget = wibox.container.background
+ }
+ local old_cursor, old_wibox
+ refresh_button:connect_signal("mouse::enter", function(c)
+ c:set_opacity(1)
+ c:emit_signal('widget::redraw_needed')
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ refresh_button:connect_signal("mouse::leave", function(c)
+ c:set_opacity(0.5)
+ c:emit_signal('widget::redraw_needed')
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ id = 'checkbox',
+ checked = false,
+ color = beautiful.bg_normal,
+ paddings = 2,
+ shape = gears.shape.circle,
+ forced_width = 20,
+ forced_height = 20,
+ check_color = beautiful.fg_urgent,
+ border_color = beautiful.bg_urgent,
+ border_width = 1,
+ widget = wibox.widget.checkbox
+ },
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ {
+ id = 'name',
+ markup = '' .. package['name'] .. '',
+ widget = wibox.widget.textbox
+ },
+ halign = 'left',
+ layout = wibox.container.place
+ },
+ {
+ refresh_button,
+ halign = 'right',
+ valign = 'center',
+ fill_horizontal = true,
+ layout = wibox.container.place,
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ id = 'row',
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background,
+ click = function(self, checked)
+ local a = self:get_children_by_id('checkbox')[1]
+ if checked == nil then
+ a:set_checked(not a.checked)
+ else
+ a:set_checked(checked)
+ end
+
+ if a.checked then
+ to_update[package['name']] = self
+ else
+ to_update[package['name']] = false
+ end
+ end,
+ update = function(self)
+ refresh_button:get_children_by_id('icon')[1]:set_image(ICONS_DIR .. 'watch.svg')
+ self:get_children_by_id('name')[1]:set_opacity(0.4)
+ self:get_children_by_id('name')[1]:emit_signal('widget::redraw_needed')
+
+ spawn.easy_async(
+ string.format([[sh -c 'yes | aptdcon --hide-terminal -u %s']], package['name']),
+ function(stdout, stderr) -- luacheck:ignore 212
+ rows:remove_widgets(self)
+ end)
+
+ end
+ }
+
+ row:connect_signal("mouse::enter", function(c)
+ c:set_bg(beautiful.bg_focus)
+ end)
+ row:connect_signal("mouse::leave", function(c)
+ c:set_bg(beautiful.bg_normal)
+ end)
+
+ row:connect_signal("button::press", function(c, _, _, button)
+ if button == 1 then c:click() end
+ end)
+
+ refresh_button:buttons(awful.util.table.join(awful.button({}, 1, function()
+ row:update()
+ end)))
+
+ rows:add(row)
+ end
+ end
+
+
+ local header_checkbox = wibox.widget {
+ checked = false,
+ color = beautiful.bg_normal,
+ paddings = 2,
+ shape = gears.shape.circle,
+ forced_width = 20,
+ forced_height = 20,
+ check_color = beautiful.fg_urgent,
+ border_color = beautiful.bg_urgent,
+ border_width = 1,
+ widget = wibox.widget.checkbox
+ }
+ header_checkbox:connect_signal("button::press", function(c)
+ c:set_checked(not c.checked)
+ local cbs = rows.children
+ for _,v in ipairs(cbs) do
+ v:click(c.checked)
+ end
+ end)
+
+ local header_refresh_icon = wibox.widget {
+ image = ICONS_DIR .. 'refresh-cw.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ }
+ header_refresh_icon:buttons(awful.util.table.join(awful.button({}, 1, function()
+ print(#to_update)
+ for _,v in pairs(to_update) do
+ if v ~= nil then
+ v:update()
+ end
+ end
+ end)))
+
+ local header_row = wibox.widget {
+ {
+ {
+ {
+ header_checkbox,
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ {
+ id = 'name',
+ markup = '' .. #rows.children .. ' packages to update',
+ widget = wibox.widget.textbox
+ },
+ halign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ header_refresh_icon,
+ halign = 'right',
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ wibox_popup:setup {
+ header_row,
+ rows,
+ layout = wibox.layout.fixed.vertical
+ }
+ end
+
+ apt_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if wibox_popup.visible then
+ wibox_popup.visible = not wibox_popup.visible
+ else
+ spawn.easy_async(LIST_PACKAGES,
+ function(stdout, stderr)
+ rebuild_widget(stdout, stderr)
+ wibox_popup.visible = true
+ awful.placement.top(wibox_popup, { margins = { top = 20 }, parent = mouse})
+ end)
+ end
+ end)
+ )
+ )
+
+ return apt_widget
+end
+
+return setmetatable(apt_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/black.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/black.svg
new file mode 100755
index 0000000..95c8410
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/black.svg
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/help-circle.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/help-circle.svg
new file mode 100755
index 0000000..51fddd8
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/help-circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/orange.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/orange.svg
new file mode 100755
index 0000000..0ec1388
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/orange.svg
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/refresh-cw.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/refresh-cw.svg
new file mode 100755
index 0000000..39f52a5
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/refresh-cw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/watch.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/watch.svg
new file mode 100755
index 0000000..661a560
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/watch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/white-black.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/white-black.svg
new file mode 100755
index 0000000..dc7ee55
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/white-black.svg
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/icons/white-orange.svg b/.config/awesome/awesome-wm-widgets/apt-widget/icons/white-orange.svg
new file mode 100755
index 0000000..c353bb5
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/apt-widget/icons/white-orange.svg
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/apt-widget/screenshots/screenshot.gif b/.config/awesome/awesome-wm-widgets/apt-widget/screenshots/screenshot.gif
new file mode 100755
index 0000000..6ffe2aa
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/apt-widget/screenshots/screenshot.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/awesome-o.png b/.config/awesome/awesome-wm-widgets/awesome-o.png
new file mode 100755
index 0000000..424d008
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/awesome-o.png differ
diff --git a/.config/awesome/awesome-wm-widgets/awesome.png b/.config/awesome/awesome-wm-widgets/awesome.png
new file mode 100755
index 0000000..6960955
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/awesome.png differ
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/README.md b/.config/awesome/awesome-wm-widgets/battery-widget/README.md
new file mode 100755
index 0000000..b15aac6
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/battery-widget/README.md
@@ -0,0 +1,75 @@
+# Battery widget
+
+Simple and easy-to-install widget for Awesome Window Manager.
+
+This widget consists of:
+
+ - an icon which shows the battery level:
+ 
+ - a pop-up window, which shows up when you hover over an icon:
+ 
+ Alternatively you can use a tooltip (check the code):
+ 
+ - a pop-up warning message which appears on bottom right corner when battery level is less that 15% (you can get the image [here](https://vk.com/images/stickers/1933/512.png)):
+ 
+
+Note that widget uses the Arc icon theme, so it should be [installed](https://github.com/horst3180/arc-icon-theme#installation) first under **/usr/share/icons/Arc/** folder.
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `font` | Play 8 | Fond |
+| `path_to_icons` | `/usr/share/icons/Arc/status/symbolic/` | Path to the folder with icons* |
+| `show_current_level`| false | Show current charge level |
+| `margin_right`|0| The right margin of the widget|
+| `margin_left`|0| The left margin of the widget|
+| `display_notification` | `false` | Display a notification on mouseover |
+| `notification_position` | `top_right` | The notification position |
+| `timeout` | 10 | How often in seconds the widget refreshes |
+| `warning_msg_title` | _Huston, we have a problem_ | Title of the warning popup |
+| `warning_msg_text` | _Battery is dying_ | Text of the warning popup |
+| `warning_msg_position` | `bottom_right` | Position of the warning popup |
+| `warning_msg_icon` | ~/.config/awesome/awesome-wm-widgets/battery-widget/spaceman.jpg | Icon of the warning popup |
+| `enable_battery_warning` | `true` | Display low battery warning |
+
+*Note: the widget expects following icons to be present in the folder:
+
+ - battery-caution-charging-symbolic.svg
+ - battery-empty-charging-symbolic.svg
+ - battery-full-charged-symbolic.svg
+ - battery-full-symbolic.svg
+ - battery-good-symbolic.svg
+ - battery-low-symbolic.svg
+ - battery-caution-symbolic.svg
+ - battery-empty-symbolic.svg
+ - battery-full-charging-symbolic.svg
+ - battery-good-charging-symbolic.svg
+ - battery-low-charging-symbolic.svg
+ - battery-missing-symbolic.svg
+
+## Installation
+
+This widget reads the output of acpi tool.
+
+- install `acpi` and check the output:
+
+```bash
+$ sudo apt-get install acpi
+$ acpi
+Battery 0: Discharging, 66%, 02:34:06 remaining
+```
+
+```lua
+local battery_widget = require("awesome-wm-widgets.battery-widget.battery")
+
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ battery_widget(),
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-1.png b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-1.png
new file mode 100755
index 0000000..00e1618
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-1.png differ
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-2.png b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-2.png
new file mode 100755
index 0000000..ae20af2
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-2.png differ
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-22.png b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-22.png
new file mode 100755
index 0000000..38761f7
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-22.png differ
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-3.png b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-3.png
new file mode 100755
index 0000000..352b496
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/battery-widget/bat-wid-3.png differ
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/battery.lua b/.config/awesome/awesome-wm-widgets/battery-widget/battery.lua
new file mode 100755
index 0000000..452d7ef
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/battery-widget/battery.lua
@@ -0,0 +1,200 @@
+-------------------------------------------------
+-- Battery Widget for Awesome Window Manager
+-- Shows the battery status using the ACPI tool
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/battery-widget
+
+-- @author Pavel Makhov
+-- @copyright 2017 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local naughty = require("naughty")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+local gfs = require("gears.filesystem")
+local dpi = require('beautiful').xresources.apply_dpi
+
+-- acpi sample outputs
+-- Battery 0: Discharging, 75%, 01:51:38 remaining
+-- Battery 0: Charging, 53%, 00:57:43 until charged
+
+local HOME = os.getenv("HOME")
+local WIDGET_DIR = HOME .. '/.config/awesome/awesome-wm-widgets/battery-widget'
+
+local battery_widget = {}
+local function worker(user_args)
+ local args = user_args or {}
+
+ local font = args.font or 'Play 8'
+ local path_to_icons = args.path_to_icons or "/usr/share/icons/Arc/status/symbolic/"
+ local show_current_level = args.show_current_level or false
+ local margin_left = args.margin_left or 0
+ local margin_right = args.margin_right or 0
+
+ local display_notification = args.display_notification or false
+ local display_notification_onClick = args.display_notification_onClick or true
+ local position = args.notification_position or "top_right"
+ local timeout = args.timeout or 10
+
+ local warning_msg_title = args.warning_msg_title or 'Huston, we have a problem'
+ local warning_msg_text = args.warning_msg_text or 'Battery is dying'
+ local warning_msg_position = args.warning_msg_position or 'bottom_right'
+ local warning_msg_icon = args.warning_msg_icon or WIDGET_DIR .. '/spaceman.jpg'
+ local enable_battery_warning = args.enable_battery_warning
+ if enable_battery_warning == nil then
+ enable_battery_warning = true
+ end
+
+ if not gfs.dir_readable(path_to_icons) then
+ naughty.notify{
+ title = "Battery Widget",
+ text = "Folder with icons doesn't exist: " .. path_to_icons,
+ preset = naughty.config.presets.critical
+ }
+ end
+
+ local icon_widget = wibox.widget {
+ {
+ id = "icon",
+ widget = wibox.widget.imagebox,
+ resize = false
+ },
+ valign = 'center',
+ layout = wibox.container.place,
+ }
+ local level_widget = wibox.widget {
+ font = font,
+ widget = wibox.widget.textbox
+ }
+
+ battery_widget = wibox.widget {
+ icon_widget,
+ level_widget,
+ layout = wibox.layout.fixed.horizontal,
+ }
+ -- Popup with battery info
+ -- One way of creating a pop-up notification - naughty.notify
+ local notification
+ local function show_battery_status(batteryType)
+ awful.spawn.easy_async([[bash -c 'acpi']],
+ function(stdout, _, _, _)
+ naughty.destroy(notification)
+ notification = naughty.notify{
+ text = stdout,
+ title = "Battery status",
+ icon = path_to_icons .. batteryType .. ".svg",
+ icon_size = dpi(16),
+ position = position,
+ timeout = 5, hover_timeout = 0.5,
+ width = 200,
+ screen = mouse.screen
+ }
+ end
+ )
+ end
+
+ -- Alternative to naughty.notify - tooltip. You can compare both and choose the preferred one
+ --battery_popup = awful.tooltip({objects = {battery_widget}})
+
+ -- To use colors from beautiful theme put
+ -- following lines in rc.lua before require("battery"):
+ -- beautiful.tooltip_fg = beautiful.fg_normal
+ -- beautiful.tooltip_bg = beautiful.bg_normal
+
+ local function show_battery_warning()
+ naughty.notify {
+ icon = warning_msg_icon,
+ icon_size = 100,
+ text = warning_msg_text,
+ title = warning_msg_title,
+ timeout = 25, -- show the warning for a longer time
+ hover_timeout = 0.5,
+ position = warning_msg_position,
+ bg = "#F06060",
+ fg = "#EEE9EF",
+ width = 300,
+ screen = mouse.screen
+ }
+ end
+ local last_battery_check = os.time()
+ local batteryType = "battery-good-symbolic"
+
+ watch("acpi -i", timeout,
+ function(widget, stdout)
+ local battery_info = {}
+ local capacities = {}
+ for s in stdout:gmatch("[^\r\n]+") do
+ local status, charge_str, _ = string.match(s, '.+: ([%a%s]+), (%d?%d?%d)%%,?(.*)')
+ if status ~= nil then
+ table.insert(battery_info, {status = status, charge = tonumber(charge_str)})
+ else
+ local cap_str = string.match(s, '.+:.+last full capacity (%d+)')
+ table.insert(capacities, tonumber(cap_str))
+ end
+ end
+
+ local capacity = 0
+ for _, cap in ipairs(capacities) do
+ capacity = capacity + cap
+ end
+
+ local charge = 0
+ local status
+ for i, batt in ipairs(battery_info) do
+ if capacities[i] ~= nil then
+ if batt.charge >= charge then
+ status = batt.status -- use most charged battery status
+ -- this is arbitrary, and maybe another metric should be used
+ end
+
+ charge = charge + batt.charge * capacities[i]
+ end
+ end
+ charge = charge / capacity
+
+ if show_current_level then
+ level_widget.text = string.format('%d%%', charge)
+ end
+
+ if (charge >= 1 and charge < 15) then
+ batteryType = "battery-empty%s-symbolic"
+ if enable_battery_warning and status ~= 'Charging' and os.difftime(os.time(), last_battery_check) > 300 then
+ -- if 5 minutes have elapsed since the last warning
+ last_battery_check = os.time()
+
+ show_battery_warning()
+ end
+ elseif (charge >= 15 and charge < 40) then batteryType = "battery-caution%s-symbolic"
+ elseif (charge >= 40 and charge < 60) then batteryType = "battery-low%s-symbolic"
+ elseif (charge >= 60 and charge < 80) then batteryType = "battery-good%s-symbolic"
+ elseif (charge >= 80 and charge <= 100) then batteryType = "battery-full%s-symbolic"
+ end
+
+ if status == 'Charging' then
+ batteryType = string.format(batteryType, '-charging')
+ else
+ batteryType = string.format(batteryType, '')
+ end
+
+ widget.icon:set_image(path_to_icons .. batteryType .. ".svg")
+
+ -- Update popup text
+ -- battery_popup.text = string.gsub(stdout, "\n$", "")
+ end,
+ icon_widget)
+
+ if display_notification then
+ battery_widget:connect_signal("mouse::enter", function() show_battery_status(batteryType) end)
+ battery_widget:connect_signal("mouse::leave", function() naughty.destroy(notification) end)
+ elseif display_notification_onClick then
+ battery_widget:connect_signal("button::press", function(_,_,_,button)
+ if (button == 3) then show_battery_status(batteryType) end
+ end)
+ battery_widget:connect_signal("mouse::leave", function() naughty.destroy(notification) end)
+ end
+
+ return wibox.container.margin(battery_widget, margin_left, margin_right)
+end
+
+return setmetatable(battery_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/battery-widget/spaceman.jpg b/.config/awesome/awesome-wm-widgets/battery-widget/spaceman.jpg
new file mode 100755
index 0000000..73ddaf3
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/battery-widget/spaceman.jpg differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/10_c.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/10_c.png
new file mode 100755
index 0000000..3faf753
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/10_c.png differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/10_d.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/10_d.png
new file mode 100755
index 0000000..c9aa8d3
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/10_d.png differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/20_c.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/20_c.png
new file mode 100755
index 0000000..f0a191d
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/20_c.png differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/20_d.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/20_d.png
new file mode 100755
index 0000000..15fabed
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/20_d.png differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/80_c.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/80_c.png
new file mode 100755
index 0000000..e6dae75
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/80_c.png differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/80_d.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/80_d.png
new file mode 100755
index 0000000..220c8e3
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/80_d.png differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/README.md b/.config/awesome/awesome-wm-widgets/batteryarc-widget/README.md
new file mode 100755
index 0000000..98a2956
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/batteryarc-widget/README.md
@@ -0,0 +1,73 @@
+# Batteryarc widget
+
+[](https://github.com/streetturtle/awesome-wm-widgets/labels/batteryarc)
+
+This widget is more informative version of [battery widget](https://github.com/streetturtle/awesome-wm-widgets/tree/master/battery-widget).
+
+Depending of the battery status it could look following ways:
+
+ -  - less than 15 percent
+ -  - less than 15 percent, charging
+ -  - between 15 and 40 percent
+ -  - between 15 and 40 percent, charging
+ -  - more than 40 percent
+ -  - more than 40 percent, charging
+
+If a battery level is low then warning popup will show up:
+
+
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `font` | Play 6 | Font |
+| `arc_thickness` | 2 | Thickness of the arc |
+| `show_current_level`| false | Show current charge level |
+| `size`| 18 | Size of the widget |
+| `timeout` | 10 | How often in seconds the widget refreshes |
+| `main_color` | `beautiful.fg_color` | Color of the text with the current charge level and the arc |
+| `bg_color` | `#ffffff11` | Color of the charge level background |
+| `low_level_color` | `#e53935` | Arc color when battery charge is less that 15% |
+| `medium_level_color` | `#c0ca33` | Arc color when battery charge is between 15% and 40% |
+| `charging_color` | `#43a047` | Color of the circle inside the arc when charging |
+| `warning_msg_title` | _Huston, we have a problem_ | Title of the warning popup |
+| `warning_msg_text` | _Battery is dying_ | Text of the warning popup |
+| `warning_msg_position` | `bottom_right` | Position of the warning popup |
+| `warning_msg_icon` | ~/.config/awesome/awesome-wm-widgets/batteryarc-widget/spaceman.jpg | Icon of the warning popup |
+| `enable_battery_warning` | `true` | Display low battery warning |
+| `show_notification_mode` | `on_hover` | How to trigger a notification with the battery status: `on_hover`, `on_click` or `off` |
+| `notification_position` | `top_left` | Where to show she notification when triggered. Values: `top_right`, `top_left`, `bottom_left`, `bottom_right`, `top_middle`, `bottom_middle`. (default `top_right`) |
+
+## Requirements
+
+This widget requires the `acpi` command to be available to retrieve battery and
+power information.
+
+## Installation
+
+Clone repo, include widget and use it in **rc.lua**:
+
+```lua
+local batteryarc_widget = require("awesome-wm-widgets.batteryarc-widget.batteryarc")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ --[[default]]
+ batteryarc_widget(),
+ --[[or customized]]
+ batteryarc_widget({
+ show_current_level = true,
+ arc_thickness = 1,
+ }),
+ }
+ ...
+```
+
+## Troubleshooting
+
+In case of any doubts or questions please raise an [issue](https://github.com/streetturtle/awesome-wm-widgets/issues/new).
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/batteryarc.lua b/.config/awesome/awesome-wm-widgets/batteryarc-widget/batteryarc.lua
new file mode 100755
index 0000000..55a7694
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/batteryarc-widget/batteryarc.lua
@@ -0,0 +1,170 @@
+-------------------------------------------------
+-- Battery Arc Widget for Awesome Window Manager
+-- Shows the battery level of the laptop
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/batteryarc-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local beautiful = require("beautiful")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+
+local HOME = os.getenv("HOME")
+local WIDGET_DIR = HOME .. '/.config/awesome/awesome-wm-widgets/batteryarc-widget'
+
+local batteryarc_widget = {}
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local font = args.font or 'Play 6'
+ local arc_thickness = args.arc_thickness or 2
+ local show_current_level = args.show_current_level or false
+ local size = args.size or 18
+ local timeout = args.timeout or 10
+ local show_notification_mode = args.show_notification_mode or 'on_hover' -- on_hover / on_click
+ local notification_position = args.notification_position or 'top_right' -- see naughty.notify position argument
+
+ local main_color = args.main_color or beautiful.fg_color
+ local bg_color = args.bg_color or '#ffffff11'
+ local low_level_color = args.low_level_color or '#e53935'
+ local medium_level_color = args.medium_level_color or '#c0ca33'
+ local charging_color = args.charging_color or '#43a047'
+
+ local warning_msg_title = args.warning_msg_title or 'Houston, we have a problem'
+ local warning_msg_text = args.warning_msg_text or 'Battery is dying'
+ local warning_msg_position = args.warning_msg_position or 'bottom_right'
+ local warning_msg_icon = args.warning_msg_icon or WIDGET_DIR .. '/spaceman.jpg'
+ local enable_battery_warning = args.enable_battery_warning
+ if enable_battery_warning == nil then
+ enable_battery_warning = true
+ end
+
+ local text = wibox.widget {
+ font = font,
+ align = 'center',
+ valign = 'center',
+ widget = wibox.widget.textbox
+ }
+
+ local text_with_background = wibox.container.background(text)
+
+ batteryarc_widget = wibox.widget {
+ text_with_background,
+ max_value = 100,
+ rounded_edge = true,
+ thickness = arc_thickness,
+ start_angle = 4.71238898, -- 2pi*3/4
+ forced_height = size,
+ forced_width = size,
+ bg = bg_color,
+ paddings = 2,
+ widget = wibox.container.arcchart
+ }
+
+ local last_battery_check = os.time()
+
+ --[[ Show warning notification ]]
+ local function show_battery_warning()
+ naughty.notify {
+ icon = warning_msg_icon,
+ icon_size = 100,
+ text = warning_msg_text,
+ title = warning_msg_title,
+ timeout = 25, -- show the warning for a longer time
+ hover_timeout = 0.5,
+ position = warning_msg_position,
+ bg = "#F06060",
+ fg = "#EEE9EF",
+ width = 300,
+ }
+ end
+
+ local function update_widget(widget, stdout)
+ local charge = 0
+ local status
+ for s in stdout:gmatch("[^\r\n]+") do
+ local cur_status, charge_str, _ = string.match(s, '.+: ([%a%s]+), (%d?%d?%d)%%,?(.*)')
+ if cur_status ~= nil and charge_str ~=nil then
+ local cur_charge = tonumber(charge_str)
+ if cur_charge > charge then
+ status = cur_status
+ charge = cur_charge
+ end
+ end
+ end
+
+ widget.value = charge
+
+ if status == 'Charging' then
+ text_with_background.bg = charging_color
+ text_with_background.fg = '#000000'
+ else
+ text_with_background.bg = '#00000000'
+ text_with_background.fg = main_color
+ end
+
+ if show_current_level == true then
+ --- if battery is fully charged (100) there is not enough place for three digits, so we don't show any text
+ text.text = charge == 100
+ and ''
+ or string.format('%d', charge)
+ else
+ text.text = ''
+ end
+
+ if charge < 15 then
+ widget.colors = { low_level_color }
+ if enable_battery_warning and status ~= 'Charging' and os.difftime(os.time(), last_battery_check) > 300 then
+ -- if 5 minutes have elapsed since the last warning
+ last_battery_check = os.time()
+
+ show_battery_warning()
+ end
+ elseif charge > 15 and charge < 40 then
+ widget.colors = { medium_level_color }
+ else
+ widget.colors = { main_color }
+ end
+ end
+
+ watch("acpi", timeout, update_widget, batteryarc_widget)
+
+ -- Popup with battery info
+ local notification
+ local function show_battery_status()
+ awful.spawn.easy_async([[bash -c 'acpi']],
+ function(stdout, _, _, _)
+ naughty.destroy(notification)
+ notification = naughty.notify {
+ text = stdout,
+ title = "Battery status",
+ timeout = 5,
+ width = 200,
+ position = notification_position,
+ }
+ end)
+ end
+
+ if show_notification_mode == 'on_hover' then
+ batteryarc_widget:connect_signal("mouse::enter", function() show_battery_status() end)
+ batteryarc_widget:connect_signal("mouse::leave", function() naughty.destroy(notification) end)
+ elseif show_notification_mode == 'on_click' then
+ batteryarc_widget:connect_signal('button::press', function(_, _, _, button)
+ if (button == 1) then show_battery_status() end
+ end)
+ end
+
+ return batteryarc_widget
+
+end
+
+return setmetatable(batteryarc_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/spaceman.jpg b/.config/awesome/awesome-wm-widgets/batteryarc-widget/spaceman.jpg
new file mode 100755
index 0000000..73ddaf3
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/spaceman.jpg differ
diff --git a/.config/awesome/awesome-wm-widgets/batteryarc-widget/warning.png b/.config/awesome/awesome-wm-widgets/batteryarc-widget/warning.png
new file mode 100755
index 0000000..55ca790
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/batteryarc-widget/warning.png differ
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/README.md b/.config/awesome/awesome-wm-widgets/bitbucket-widget/README.md
new file mode 100755
index 0000000..197a765
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/README.md
@@ -0,0 +1,69 @@
+# Bitbucket widget
+
+The widget shows the number of pull requests assigned to the user and when clicked shows them in the list with some additional information. When item in the list is clicked - it opens the pull request in the browser.
+
+## How it works
+
+Widget uses cURL to query Bitbucket's [REST API](https://developer.atlassian.com/bitbucket/api/2/reference/). In order to be authenticated, widget uses a [netrc](https://ec.haxx.se/usingcurl/usingcurl-netrc) feature of the cURL, which is basically allows storing basic auth credentials in a **.netrc** file in home folder.
+
+Bitbucket allows using [App Passwords](https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html) (available in the account settings) - simply generate one for the widget and use it as password in **.netrc** file.
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `icon` | `~/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket-icon-gradient-blue.svg` | Path to the icon |
+| `host` | Required | e.g _http://api.bitbucket.org_ |
+| `uuid` | Required | e.g _{123e4567-e89b-12d3-a456-426614174000}_ |
+| `workspace` | Required | Workspace ID|
+| `repo_slug` | Required | Repository slug |
+| `timeout` | 60 | How often in seconds the widget refreshes |
+
+Note:
+ - host most likely should start with _api._
+ - to get your UUID you may call `curl -s -n 'https://api.bitbucket.org/2.0/user'`
+
+## Installation
+
+Create a **.netrc** file in you home directory with following content:
+
+```bash
+machine api.bitbucket.org
+login mikey@tmnt.com
+password cowabunga
+```
+
+Then change file's permissions to 600 (so only you can read/write it):
+
+```bash
+chmod 600 ~/.netrc
+```
+And test if it works by calling the API:
+
+```bash
+curl -s -n 'https://api.bitbucket.org/2.0/repositories/'
+```
+
+Also, to properly setup required parameters you can use `test_bitbucket_api.sh` script - it uses the same curl call as widget.
+
+Then clone/download repo and use widget in **rc.lua**:
+
+```lua
+local bitbucket_widget = require("awesome-wm-widgets.bitbucket-widget.bitbucket")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ bitbucket_widget({
+ host = 'https://api.bitbucket.org',
+ uuid = '{123e4567-e89b-12d3-a456-426614174000}',
+ workspace = 'workspace',
+ repo_slug = 'slug'
+
+ }}),
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket-icon-gradient-blue.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket-icon-gradient-blue.svg
new file mode 100755
index 0000000..ea700ea
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket-icon-gradient-blue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket.lua b/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket.lua
new file mode 100755
index 0000000..b85e653
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/bitbucket.lua
@@ -0,0 +1,371 @@
+-------------------------------------------------
+-- Bitbucket Widget for Awesome Window Manager
+-- Shows the number of currently assigned pull requests
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/bitbucket-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local json = require("json")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/bitbucket-widget/'
+
+local GET_PRS_CMD= [[bash -c "curl -s --show-error -n ]]
+ .. [['%s/2.0/repositories/%s/%s/pullrequests]]
+ .. [[?fields=values.participants.approved,values.title,values.links.html,values.author.display_name,]]
+ .. [[values.author.uuid,values.author.links.avatar,values.source.branch,values.destination.branch,]]
+ .. [[values.comment_count,values.created_on&q=reviewers.uuid+%%3D+%%22%s%%22+AND+state+%%3D+%%22OPEN%%22']]
+ .. [[ | jq '.[] '"]]
+local DOWNLOAD_AVATAR_CMD = [[bash -c "curl -L -n --create-dirs -o %s/.cache/awmw/bitbucket-widget/avatars/%s %s"]]
+
+local bitbucket_widget = wibox.widget {
+ {
+ {
+ id = 'icon',
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ {
+ id = "new_pr",
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_text = function(self, new_value)
+ self.txt.text = new_value
+ end,
+ set_icon = function(self, new_value)
+ self:get_children_by_id('icon')[1]:set_image(new_value)
+ end
+}
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Bitbucket Widget',
+ text = message}
+end
+
+local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+--- Converts string representation of date (2020-06-02T11:25:27Z) to date
+local function parse_date(date_str)
+ local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)%Z"
+ local y, m, d, h, min, sec, _ = date_str:match(pattern)
+
+ return os.time{year = y, month = m, day = d, hour = h, min = min, sec = sec}
+end
+
+--- Converts seconds to "time ago" represenation, like '1 hour ago'
+local function to_time_ago(seconds)
+ local days = seconds / 86400
+ if days > 1 then
+ days = math.floor(days + 0.5)
+ return days .. (days == 1 and ' day' or ' days') .. ' ago'
+ end
+
+ local hours = (seconds % 86400) / 3600
+ if hours > 1 then
+ hours = math.floor(hours + 0.5)
+ return hours .. (hours == 1 and ' hour' or ' hours') .. ' ago'
+ end
+
+ local minutes = ((seconds % 86400) % 3600) / 60
+ if minutes > 1 then
+ minutes = math.floor(minutes + 0.5)
+ return minutes .. (minutes == 1 and ' minute' or ' minutes') .. ' ago'
+ end
+end
+
+local function ellipsize(text, length)
+ return (text:len() > length and length > 0)
+ and text:sub(0, length - 3) .. '...'
+ or text
+end
+
+local function count_approves(participants)
+ local res = 0
+ for i = 1, #participants do
+ if participants[i]['approved'] then res = res + 1 end
+ end
+ return res
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or WIDGET_DIR .. '/bitbucket-icon-gradient-blue.svg'
+ local host = args.host or show_warning('Bitbucket host is not set')
+ local uuid = args.uuid or show_warning('UUID is not set')
+ local workspace = args.workspace or show_warning('Workspace is not set')
+ local repo_slug = args.repo_slug or show_warning('Repo slug is not set')
+ local timeout = args.timeout or 60
+
+ local current_number_of_prs
+
+ local to_review_rows = {layout = wibox.layout.fixed.vertical}
+ local my_review_rows = {layout = wibox.layout.fixed.vertical}
+ local rows = {layout = wibox.layout.fixed.vertical}
+
+ bitbucket_widget:set_icon(icon)
+
+ local update_widget = function(widget, stdout, stderr, _, _)
+ if stderr ~= '' then
+ show_warning(stderr)
+ return
+ end
+
+ local result = json.decode(stdout)
+
+ current_number_of_prs = rawlen(result)
+
+ if current_number_of_prs == 0 then
+ widget:set_visible(false)
+ return
+ end
+
+ widget:set_visible(true)
+ widget:set_text(current_number_of_prs)
+
+ for i = 0, #rows do rows[i]=nil end
+
+ for i = 0, #to_review_rows do to_review_rows[i]=nil end
+ table.insert(to_review_rows, {
+ {
+ markup = 'PRs to review',
+ align = 'center',
+ forced_height = 20,
+ widget = wibox.widget.textbox
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+
+ for i = 0, #my_review_rows do my_review_rows[i]=nil end
+ table.insert(my_review_rows, {
+ {
+ markup = 'My PRs',
+ align = 'center',
+ forced_height = 20,
+ widget = wibox.widget.textbox
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+ local current_time = os.time(os.date("!*t"))
+
+ for _, pr in ipairs(result) do
+ local path_to_avatar = os.getenv("HOME") ..'/.cache/awmw/bitbucket-widget/avatars/' .. pr.author.uuid
+ local number_of_approves = count_approves(pr.participants)
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ resize = true,
+ image = path_to_avatar,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+ },
+ id = 'avatar',
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ {
+ {
+ id = 'title',
+ markup = '' .. ellipsize(pr.title, 50) .. '',
+ widget = wibox.widget.textbox,
+ forced_width = 400
+ },
+ {
+ {
+ {
+ {
+ text = ellipsize(pr.source.branch.name, 30),
+ widget = wibox.widget.textbox
+ },
+ {
+ text = '->',
+ widget = wibox.widget.textbox
+ },
+ {
+ text = pr.destination.branch.name,
+ widget = wibox.widget.textbox
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ {
+ {
+ text = pr.author.display_name,
+ widget = wibox.widget.textbox
+ },
+ {
+ text = to_time_ago(os.difftime(current_time, parse_date(pr.created_on))),
+ widget = wibox.widget.textbox
+ },
+ spacing = 8,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+ },
+ forced_width = 285,
+ layout = wibox.layout.fixed.vertical
+ },
+ {
+ {
+ {
+ image = WIDGET_DIR .. '/check.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = number_of_approves,
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+ {
+ {
+ image = WIDGET_DIR .. '/message-circle.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = pr.comment_count,
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+ layout = wibox.layout.fixed.vertical
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+
+ spacing = 8,
+ layout = wibox.layout.fixed.vertical
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ if not gfs.file_readable(path_to_avatar) then
+ local cmd = string.format(DOWNLOAD_AVATAR_CMD, HOME_DIR, pr.author.uuid, pr.author.links.avatar.href)
+ spawn.easy_async(cmd, function() row:get_children_by_id('avatar')[1]:set_image(path_to_avatar) end)
+ end
+
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ row:get_children_by_id('title')[1]:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. pr.links.html.href)
+ popup.visible = false
+ end)
+ )
+ )
+ row:get_children_by_id('avatar')[1]:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell(
+ string.format('xdg-open "https://bitbucket.org/%s/%s/pull-requests?state=OPEN&author=%s"',
+ workspace, repo_slug, pr.author.uuid)
+ )
+ popup.visible = false
+ end)
+ )
+ )
+
+ local old_cursor, old_wibox
+ row:get_children_by_id('title')[1]:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:get_children_by_id('title')[1]:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:get_children_by_id('avatar')[1]:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:get_children_by_id('avatar')[1]:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ if (pr.author.uuid == '{' .. uuid .. '}') then
+ table.insert(my_review_rows, row)
+ else
+ table.insert(to_review_rows, row)
+ end
+ end
+
+ table.insert(rows, to_review_rows)
+ if (#my_review_rows > 1) then
+ table.insert(rows, my_review_rows)
+ end
+ popup:setup(rows)
+ end
+
+ bitbucket_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ watch(string.format(GET_PRS_CMD, host, workspace, repo_slug, uuid, uuid),
+ timeout, update_widget, bitbucket_widget)
+ return bitbucket_widget
+end
+
+return setmetatable(bitbucket_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/check.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/check.svg
new file mode 100755
index 0000000..e9e44ac
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/clipboard.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/clipboard.svg
new file mode 100755
index 0000000..5c6dfd3
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/clipboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/copy.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/copy.svg
new file mode 100755
index 0000000..bab2098
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/git-pull-request.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/git-pull-request.svg
new file mode 100755
index 0000000..c2e2867
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/git-pull-request.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/message-circle.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/message-circle.svg
new file mode 100755
index 0000000..43eacbb
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/message-circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/test_bitbucket_api.sh b/.config/awesome/awesome-wm-widgets/bitbucket-widget/test_bitbucket_api.sh
new file mode 100755
index 0000000..378b3ef
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/test_bitbucket_api.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+HOST='https://api.bitbucket.org'
+ACCOUNT_ID=''
+WORKSPACE=''
+REPO_SLUG=''
+
+curl -s -n "${HOST}/2.0/repositories/${WORKSPACE}/${REPO_SLUG}/pullrequests?fields=values.title,values.links.html,values.author.display_name,values.author.links.avatar&q=reviewers.account_id+%3D+%22${ACCOUNT_ID}%22"
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/bitbucket-widget/user.svg b/.config/awesome/awesome-wm-widgets/bitbucket-widget/user.svg
new file mode 100755
index 0000000..4058dee
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/bitbucket-widget/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/brightness-widget/README.md b/.config/awesome/awesome-wm-widgets/brightness-widget/README.md
new file mode 100755
index 0000000..0cdb774
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/brightness-widget/README.md
@@ -0,0 +1,94 @@
+# Brightness widget
+
+This widget represents current brightness level, depending on config parameters could be an arcchart or icon with text: 
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `type`| `arc` | The widget type. Could be `arc` or `icon_and_text` |
+| `program` | `light` | The program used to control the brightness, either `light`, `xbacklight`, or `brightnessctl`. |
+| `step` | 5 | Step |
+| `base` | 20 | Base level to set brightness to on left click. |
+| `path_to_icon` | `/usr/share/icons/Arc/status/symbolic/display-brightness-symbolic.svg` | Path to the icon |
+| `font` | `beautiful.font` | Font name and size, like `Play 12` |
+| `timeout` | 1 | How often in seconds the widget refreshes. Check the note below |
+| `tooltip` | false | Display brightness level in a tooltip when the mouse cursor hovers the widget |
+| `percentage` | false | Display a '%' character after the brightness level |
+
+_Note:_ If brightness is controlled only by the widget (either by a mouse, or by a shortcut, then the `timeout` could be quite big, as there is no reason to synchronize the brightness level).
+
+## Installation
+
+To choose the right `program` argument, first you need to check which of them works better for you.
+
+ - using `xbacklight`:
+
+ Install (on Ubuntu it's available in the apt repository) it and check if it works by running:
+
+ ```bash
+ xbacklight -get
+ ```
+
+ If there is no output it means that it doesn't work, you can either try to fix it, or try to use `light`.
+
+ - using `light` command:
+
+ Install (on Ubuntu it's available in the apt repository) from the repo: [github.com/haikarainen/light](https://github.com/haikarainen/light) and check if it works by running
+
+ ```bash
+ light -G
+ 49.18
+ light -A 5
+ ```
+ If you're on Ubuntu/debian and if the brightness level doesn't change, try to do this: https://github.com/haikarainen/light/issues/113#issuecomment-632638436.
+
+ - using `brightnessctl`:
+
+ On Ubuntu it is available in the apt repository. Install and check the ouptut of the following command.
+ ```bash
+ brightnessctl --list
+ ```
+
+Then clone this repo under **~/.config/awesome/**:
+
+```bash
+git clone https://github.com/streetturtle/awesome-wm-widgets.git ~/.config/awesome/awesome-wm-widgets
+```
+
+Require widget at the beginning of **rc.lua**:
+
+```lua
+local brightness_widget = require("awesome-wm-widgets.brightness-widget.brightness")
+```
+
+Add the widget to the tasklist:
+
+```lua
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ brightness_widget(),
+ -- or customized
+ brightness_widget{
+ type = 'icon_and_text',
+ program = 'xbacklight',
+ step = 2,
+ }
+ }
+ ...
+```
+
+## Controls
+
+In order to change brightness by shortcuts you can add them to the `globalkeys` table in the **rc.lua**:
+
+```lua
+awful.key({ modkey }, ";", function () brightness_widget:inc() end, {description = "increase brightness", group = "custom"}),
+awful.key({ modkey, "Shift"}, ";", function () brightness_widget:dec() end, {description = "decrease brightness", group = "custom"}),
+```
+On a laptop you can use `XF86MonBrightnessUp` and `XF86MonBrightnessDown` keys.
diff --git a/.config/awesome/awesome-wm-widgets/brightness-widget/br-wid-1.png b/.config/awesome/awesome-wm-widgets/brightness-widget/br-wid-1.png
new file mode 100755
index 0000000..b00b0e6
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/brightness-widget/br-wid-1.png differ
diff --git a/.config/awesome/awesome-wm-widgets/brightness-widget/brightness.lua b/.config/awesome/awesome-wm-widgets/brightness-widget/brightness.lua
new file mode 100755
index 0000000..b1cb107
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/brightness-widget/brightness.lua
@@ -0,0 +1,195 @@
+-------------------------------------------------
+-- Brightness Widget for Awesome Window Manager
+-- Shows the brightness level of the laptop display
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/brightness-widget
+
+-- @author Pavel Makhov
+-- @copyright 2021 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local spawn = require("awful.spawn")
+local gfs = require("gears.filesystem")
+local naughty = require("naughty")
+local beautiful = require("beautiful")
+
+local ICON_DIR = gfs.get_configuration_dir() .. "awesome-wm-widgets/brightness-widget/"
+local get_brightness_cmd
+local set_brightness_cmd
+local inc_brightness_cmd
+local dec_brightness_cmd
+
+local brightness_widget = {}
+
+local function show_warning(message)
+ naughty.notify({
+ preset = naughty.config.presets.critical,
+ title = "Brightness Widget",
+ text = message,
+ })
+end
+
+local function worker(user_args)
+ local args = user_args or {}
+
+ local type = args.type or 'arc' -- arc or icon_and_text
+ local path_to_icon = args.path_to_icon or ICON_DIR .. 'brightness.svg'
+ local font = args.font or beautiful.font
+ local timeout = args.timeout or 100
+
+ local program = args.program or 'light'
+ local step = args.step or 5
+ local base = args.base or 20
+ local current_level = 0 -- current brightness value
+ local tooltip = args.tooltip or false
+ local percentage = args.percentage or false
+ if program == 'light' then
+ get_brightness_cmd = 'light -G'
+ set_brightness_cmd = 'light -S %d' --
+ inc_brightness_cmd = 'light -A ' .. step
+ dec_brightness_cmd = 'light -U ' .. step
+ elseif program == 'xbacklight' then
+ get_brightness_cmd = 'xbacklight -get'
+ set_brightness_cmd = 'xbacklight -set %d' --
+ inc_brightness_cmd = 'xbacklight -inc ' .. step
+ dec_brightness_cmd = 'xbacklight -dec ' .. step
+ elseif program == 'brightnessctl' then
+ get_brightness_cmd = "brightnessctl get"
+ set_brightness_cmd = "brightnessctl set %d%%" --
+ inc_brightness_cmd = "brightnessctl set +" .. step .. "%"
+ dec_brightness_cmd = "brightnessctl set " .. step .. "-%"
+ else
+ show_warning(program .. " command is not supported by the widget")
+ return
+ end
+
+ if type == 'icon_and_text' then
+ brightness_widget.widget = wibox.widget {
+ {
+ {
+ image = path_to_icon,
+ resize = false,
+ widget = wibox.widget.imagebox,
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ id = 'txt',
+ font = font,
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ set_value = function(self, level)
+ local display_level = level
+ if percentage then
+ display_level = display_level .. '%'
+ end
+ self:get_children_by_id('txt')[1]:set_text(display_level)
+ end
+ }
+ elseif type == 'arc' then
+ brightness_widget.widget = wibox.widget {
+ {
+ {
+ image = path_to_icon,
+ resize = true,
+ widget = wibox.widget.imagebox,
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ max_value = 100,
+ thickness = 2,
+ start_angle = 4.71238898, -- 2pi*3/4
+ forced_height = 18,
+ forced_width = 18,
+ paddings = 2,
+ widget = wibox.container.arcchart,
+ set_value = function(self, level)
+ self:set_value(level)
+ end
+ }
+ else
+ show_warning(type .. " type is not supported by the widget")
+ return
+
+ end
+
+ local update_widget = function(widget, stdout, _, _, _)
+ local brightness_level = tonumber(string.format("%.0f", stdout))
+ current_level = brightness_level
+ widget:set_value(brightness_level)
+ end
+
+ function brightness_widget:set(value)
+ current_level = value
+ spawn.easy_async(string.format(set_brightness_cmd, value), function()
+ spawn.easy_async(get_brightness_cmd, function(out)
+ update_widget(brightness_widget.widget, out)
+ end)
+ end)
+ end
+ local old_level = 0
+ function brightness_widget:toggle()
+ if old_level < 0.1 then
+ -- avoid toggling between '0' and 'almost 0'
+ old_level = 1
+ end
+ if current_level < 0.1 then
+ -- restore previous level
+ current_level = old_level
+ else
+ -- save current brightness for later
+ old_level = current_level
+ current_level = 0
+ end
+ brightness_widget:set(current_level)
+ end
+ function brightness_widget:inc()
+ spawn.easy_async(inc_brightness_cmd, function()
+ spawn.easy_async(get_brightness_cmd, function(out)
+ update_widget(brightness_widget.widget, out)
+ end)
+ end)
+ end
+ function brightness_widget:dec()
+ spawn.easy_async(dec_brightness_cmd, function()
+ spawn.easy_async(get_brightness_cmd, function(out)
+ update_widget(brightness_widget.widget, out)
+ end)
+ end)
+ end
+
+ brightness_widget.widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function() brightness_widget:set(base) end),
+ awful.button({}, 3, function() brightness_widget:toggle() end),
+ awful.button({}, 4, function() brightness_widget:inc() end),
+ awful.button({}, 5, function() brightness_widget:dec() end)
+ )
+ )
+
+ watch(get_brightness_cmd, timeout, update_widget, brightness_widget.widget)
+
+ if tooltip then
+ awful.tooltip {
+ objects = { brightness_widget.widget },
+ timer_function = function()
+ return current_level .. " %"
+ end,
+ }
+ end
+
+ return brightness_widget.widget
+end
+
+return setmetatable(brightness_widget, {
+ __call = function(_, ...)
+ return worker(...)
+ end,
+})
diff --git a/.config/awesome/awesome-wm-widgets/brightness-widget/brightness.svg b/.config/awesome/awesome-wm-widgets/brightness-widget/brightness.svg
new file mode 100755
index 0000000..d334372
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/brightness-widget/brightness.svg
@@ -0,0 +1,153 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/README.md b/.config/awesome/awesome-wm-widgets/calendar-widget/README.md
new file mode 100755
index 0000000..b663a18
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/calendar-widget/README.md
@@ -0,0 +1,89 @@
+# Calendar Widget
+
+Calendar widget for Awesome WM - slightly improved version of the `wibox.widget.calendar`.
+
+## Features
+
+
+### Customization
+
+| Name | Default | Description |
+|---|---|---|
+| theme | `naughty` | The theme to use |
+| placement | `top` | The position of the popup |
+| radius | 8 | The popup radius |
+| start_sunday | false | Start the week on Sunday |
+
+ - themes:
+
+ | Name | Screenshot |
+ |---|---|
+ | nord |  |
+ | outrun |  |
+ | light |  |
+ | dark |  |
+ | naughty (default) | from local theme |
+
+ - setup widget placement
+
+ top center - in case you clock is centered:
+
+ 
+
+ top right - for default awesome config:
+
+ 
+
+ bottom right - in case your wibar at the bottom:
+
+ 
+
+ - setup first day of week
+
+ By setting `start_sunday` to true:
+ 
+
+ - mouse support:
+ move to the next and previous month. Using mouse buttons or scroll wheel.
+
+ You can configure this by specifying the button to move to next/previous.
+ Usually these are configured as follows. If you want to use other mouse buttons, you can find their number using `xev`.
+
+ | number | button |
+ |--------|---------------|
+ | 4 | scroll up |
+ | 5 | scroll down |
+ | 1 | left click |
+ | 2 | right click |
+ | 3 | middles click |
+
+ By default `previous_month_button` is 5, `next_month_button` is 4.
+
+
+## How to use
+
+This widget needs an 'anchor' - another widget which triggers visibility of the calendar. Default `mytextclock` is the perfect candidate!
+Just after mytextclock is instantiated, create the widget and add the mouse listener to it.
+
+```lua
+local calendar_widget = require("awesome-wm-widgets.calendar-widget.calendar")
+-- ...
+-- Create a textclock widget
+mytextclock = wibox.widget.textclock()
+-- default
+local cw = calendar_widget()
+-- or customized
+local cw = calendar_widget({
+ theme = 'outrun',
+ placement = 'bottom_right',
+ start_sunday = true,
+ radius = 8,
+-- with customized next/previous (see table above)
+ previous_month_button = 1,
+ next_month_button = 3,
+})
+mytextclock:connect_signal("button::press",
+ function(_, _, _, button)
+ if button == 1 then cw.toggle() end
+ end)
+```
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/calendar.lua b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar.lua
new file mode 100755
index 0000000..bc4a877
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar.lua
@@ -0,0 +1,258 @@
+-------------------------------------------------
+-- Calendar Widget for Awesome Window Manager
+-- Shows the current month and supports scroll up/down to switch month
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/calendar-widget
+
+-- @author Pavel Makhov
+-- @copyright 2019 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local beautiful = require("beautiful")
+local wibox = require("wibox")
+local gears = require("gears")
+local naughty = require("naughty")
+
+local calendar_widget = {}
+
+local function worker(user_args)
+
+ local calendar_themes = {
+ nord = {
+ bg = '#2E3440',
+ fg = '#D8DEE9',
+ focus_date_bg = '#88C0D0',
+ focus_date_fg = '#000000',
+ weekend_day_bg = '#3B4252',
+ weekday_fg = '#88C0D0',
+ header_fg = '#E5E9F0',
+ border = '#4C566A'
+ },
+ outrun = {
+ bg = '#0d0221',
+ fg = '#D8DEE9',
+ focus_date_bg = '#650d89',
+ focus_date_fg = '#2de6e2',
+ weekend_day_bg = '#261447',
+ weekday_fg = '#2de6e2',
+ header_fg = '#f6019d',
+ border = '#261447'
+ },
+ dark = {
+ bg = '#000000',
+ fg = '#ffffff',
+ focus_date_bg = '#ffffff',
+ focus_date_fg = '#000000',
+ weekend_day_bg = '#444444',
+ weekday_fg = '#ffffff',
+ header_fg = '#ffffff',
+ border = '#333333'
+ },
+ light = {
+ bg = '#ffffff',
+ fg = '#000000',
+ focus_date_bg = '#000000',
+ focus_date_fg = '#ffffff',
+ weekend_day_bg = '#AAAAAA',
+ weekday_fg = '#000000',
+ header_fg = '#000000',
+ border = '#CCCCCC'
+ },
+ monokai = {
+ bg = '#272822',
+ fg = '#F8F8F2',
+ focus_date_bg = '#AE81FF',
+ focus_date_fg = '#ffffff',
+ weekend_day_bg = '#75715E',
+ weekday_fg = '#FD971F',
+ header_fg = '#F92672',
+ border = '#75715E'
+ },
+ naughty = {
+ bg = beautiful.notification_bg or beautiful.bg,
+ fg = beautiful.notification_fg or beautiful.fg,
+ focus_date_bg = beautiful.notification_fg or beautiful.fg,
+ focus_date_fg = beautiful.notification_bg or beautiful.bg,
+ weekend_day_bg = beautiful.bg_focus,
+ weekday_fg = beautiful.fg,
+ header_fg = beautiful.fg,
+ border = beautiful.border_normal
+ }
+
+ }
+
+ local args = user_args or {}
+
+ if args.theme ~= nil and calendar_themes[args.theme] == nil then
+ naughty.notify({
+ preset = naughty.config.presets.critical,
+ title = 'Calendar Widget',
+ text = 'Theme "' .. args.theme .. '" not found, fallback to default'})
+ args.theme = 'naughty'
+ end
+
+ local theme = args.theme or 'naughty'
+ local placement = args.placement or 'top'
+ local radius = args.radius or 8
+ local next_month_button = args.next_month_button or 4
+ local previous_month_button = args.previous_month_button or 5
+ local start_sunday = args.start_sunday or false
+
+ local styles = {}
+ local function rounded_shape(size)
+ return function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, size)
+ end
+ end
+
+ styles.month = {
+ padding = 4,
+ bg_color = calendar_themes[theme].bg,
+ border_width = 0,
+ }
+
+ styles.normal = {
+ markup = function(t) return t end,
+ shape = rounded_shape(4)
+ }
+
+ styles.focus = {
+ fg_color = calendar_themes[theme].focus_date_fg,
+ bg_color = calendar_themes[theme].focus_date_bg,
+ markup = function(t) return '' .. t .. '' end,
+ shape = rounded_shape(4)
+ }
+
+ styles.header = {
+ fg_color = calendar_themes[theme].header_fg,
+ bg_color = calendar_themes[theme].bg,
+ markup = function(t) return '' .. t .. '' end
+ }
+
+ styles.weekday = {
+ fg_color = calendar_themes[theme].weekday_fg,
+ bg_color = calendar_themes[theme].bg,
+ markup = function(t) return '' .. t .. '' end,
+ }
+
+ local function decorate_cell(widget, flag, date)
+ if flag == 'monthheader' and not styles.monthheader then
+ flag = 'header'
+ end
+
+ -- highlight only today's day
+ if flag == 'focus' then
+ local today = os.date('*t')
+ if not (today.month == date.month and today.year == date.year) then
+ flag = 'normal'
+ end
+ end
+
+ local props = styles[flag] or {}
+ if props.markup and widget.get_text and widget.set_markup then
+ widget:set_markup(props.markup(widget:get_text()))
+ end
+ -- Change bg color for weekends
+ local d = { year = date.year, month = (date.month or 1), day = (date.day or 1) }
+ local weekday = tonumber(os.date('%w', os.time(d)))
+ local default_bg = (weekday == 0 or weekday == 6)
+ and calendar_themes[theme].weekend_day_bg
+ or calendar_themes[theme].bg
+ local ret = wibox.widget {
+ {
+ {
+ widget,
+ halign = 'center',
+ widget = wibox.container.place
+ },
+ margins = (props.padding or 2) + (props.border_width or 0),
+ widget = wibox.container.margin
+ },
+ shape = props.shape,
+ shape_border_color = props.border_color or '#000000',
+ shape_border_width = props.border_width or 0,
+ fg = props.fg_color or calendar_themes[theme].fg,
+ bg = props.bg_color or default_bg,
+ widget = wibox.container.background
+ }
+
+ return ret
+ end
+
+ local cal = wibox.widget {
+ date = os.date('*t'),
+ font = beautiful.get_font(),
+ fn_embed = decorate_cell,
+ long_weekdays = true,
+ start_sunday = start_sunday,
+ widget = wibox.widget.calendar.month
+ }
+
+ local popup = awful.popup {
+ ontop = true,
+ visible = false,
+ shape = rounded_shape(radius),
+ offset = { y = 5 },
+ border_width = 1,
+ border_color = calendar_themes[theme].border,
+ widget = cal
+ }
+
+ popup:buttons(
+ awful.util.table.join(
+ awful.button({}, next_month_button, function()
+ local a = cal:get_date()
+ a.month = a.month + 1
+ cal:set_date(nil)
+ cal:set_date(a)
+ popup:set_widget(cal)
+ end),
+ awful.button({}, previous_month_button, function()
+ local a = cal:get_date()
+ a.month = a.month - 1
+ cal:set_date(nil)
+ cal:set_date(a)
+ popup:set_widget(cal)
+ end)
+ )
+ )
+
+ function calendar_widget.toggle()
+
+ if popup.visible then
+ -- to faster render the calendar refresh it and just hide
+ cal:set_date(nil) -- the new date is not set without removing the old one
+ cal:set_date(os.date('*t'))
+ popup:set_widget(nil) -- just in case
+ popup:set_widget(cal)
+ popup.visible = not popup.visible
+ else
+ if placement == 'top' then
+ awful.placement.top(popup, { margins = { top = 30 }, parent = awful.screen.focused() })
+ elseif placement == 'top_right' then
+ awful.placement.top_right(popup, { margins = { top = 30, right = 10}, parent = awful.screen.focused() })
+ elseif placement == 'top_left' then
+ awful.placement.top_left(popup, { margins = { top = 30, left = 10}, parent = awful.screen.focused() })
+ elseif placement == 'bottom_right' then
+ awful.placement.bottom_right(popup, { margins = { bottom = 30, right = 10},
+ parent = awful.screen.focused() })
+ elseif placement == 'bottom_left' then
+ awful.placement.bottom_left(popup, { margins = { bottom = 30, left = 10},
+ parent = awful.screen.focused() })
+ else
+ awful.placement.top(popup, { margins = { top = 30 }, parent = awful.screen.focused() })
+ end
+
+ popup.visible = true
+
+ end
+ end
+
+ return calendar_widget
+
+end
+
+return setmetatable(calendar_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_bottom_right.png b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_bottom_right.png
new file mode 100755
index 0000000..2bc2e82
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_bottom_right.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_start_sunday.png b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_start_sunday.png
new file mode 100755
index 0000000..126a218
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_start_sunday.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_top.png b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_top.png
new file mode 100755
index 0000000..3e6b66b
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_top.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_top_right.png b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_top_right.png
new file mode 100755
index 0000000..4a29022
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/calendar_top_right.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/dark.png b/.config/awesome/awesome-wm-widgets/calendar-widget/dark.png
new file mode 100755
index 0000000..540289f
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/dark.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/light.png b/.config/awesome/awesome-wm-widgets/calendar-widget/light.png
new file mode 100755
index 0000000..ab675d1
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/light.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/nord.png b/.config/awesome/awesome-wm-widgets/calendar-widget/nord.png
new file mode 100755
index 0000000..94f9f7e
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/nord.png differ
diff --git a/.config/awesome/awesome-wm-widgets/calendar-widget/outrun.png b/.config/awesome/awesome-wm-widgets/calendar-widget/outrun.png
new file mode 100755
index 0000000..d59c123
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/calendar-widget/outrun.png differ
diff --git a/.config/awesome/awesome-wm-widgets/cmus-widget/README.md b/.config/awesome/awesome-wm-widgets/cmus-widget/README.md
new file mode 100755
index 0000000..e33655e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/cmus-widget/README.md
@@ -0,0 +1,53 @@
+# Cmus widget
+
+Cmus widget that shows the current playing track.
+
+
+
+Left click toggles playback.
+
+## Installation
+
+Clone the repo under **~/.config/awesome/** and add widget in **rc.lua**:
+
+```lua
+local cmus_widget = require('awesome-wm-widgets.cmus-widget.cmus')
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ cmus_widget(),
+ -- customized
+ cmus_widget{
+ space = 5,
+ timeout = 5
+ },
+```
+
+### Shortcuts
+
+To improve responsiveness of the widget when playback is changed by a shortcut use corresponding methods of the widget:
+
+```lua
+awful.key({ modkey, "Shift" }, "p", function () cmus_widget:play_pause() end, {description = "toggle track", group = "cmus"}),
+awful.key({ }, "XF86AudioPlay", function () cmus_widget:play() end, {description = "play track", group = "cmus"}),
+awful.key({ }, "XF86AudioPause", function () cmus_widget:play() end, {description = "pause track", group = "cmus"}),
+awful.key({ }, "XF86AudioNext", function () cmus_widget:next_track() end, {description = "next track", group = "cmus"}),
+awful.key({ }, "XF86AudioPrev", function () cmus_widget:prev_track() end, {description = "previous track", group = "cmus"}),
+awful.key({ }, "XF86AudioStop", function () cmus_widget:stop() end, {description = "stop cmus", group = "cmus"}),
+```
+
+## Customization
+
+It is possible to customize the widget by providing a table with all or some of the following config parameters:
+
+### Generic parameter
+
+| Name | Default | Description |
+|---|---|---|
+| `font` | `beautiful.font` | Font name and size, like `Play 12` |
+| `path_to_icons` | `/usr/share/icons/Arc/actions/symbolic/` | Alternative path for the icons |
+| `timeout`| `10` | Refresh cooldown |
+| `space` | `3` | Space between icon and track title |
diff --git a/.config/awesome/awesome-wm-widgets/cmus-widget/cmus.lua b/.config/awesome/awesome-wm-widgets/cmus-widget/cmus.lua
new file mode 100755
index 0000000..b1287c5
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/cmus-widget/cmus.lua
@@ -0,0 +1,149 @@
+-------------------------------------------------
+-- Cmus Widget for Awesome Window Manager
+-- Show what's playing, play/pause, etc
+
+-- @author Augusto Gunsch
+-- @copyright 2022 Augusto Gunsch
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local spawn = require("awful.spawn")
+local beautiful = require('beautiful')
+
+local cmus_widget = {}
+
+local function worker(user_args)
+
+ local args = user_args or {}
+ local font = args.font or beautiful.font
+
+ local path_to_icons = args.path_to_icons or "/usr/share/icons/Arc/actions/symbolic/"
+ local timeout = args.timeout or 10
+ local space = args.space or 3
+
+ cmus_widget.widget = wibox.widget {
+ {
+ {
+ id = "playback_icon",
+ resize = false,
+ widget = wibox.widget.imagebox,
+ },
+ layout = wibox.container.place
+ },
+ {
+ id = "text",
+ font = font,
+ widget = wibox.widget.textbox
+ },
+ spacing = space,
+ layout = wibox.layout.fixed.horizontal,
+ update_icon = function(self, name)
+ self:get_children_by_id("playback_icon")[1]:set_image(path_to_icons .. name)
+ end,
+ set_title = function(self, title)
+ self:get_children_by_id("text")[1]:set_text(title)
+ end
+ }
+
+ local function update_widget(widget, stdout, _, _, code)
+ if code == 0 then
+ local cmus_info = {}
+
+ for s in stdout:gmatch("[^\r\n]+") do
+ local key, val = string.match(s, "^tag (%a+) (.+)$")
+
+ if key and val then
+ cmus_info[key] = val
+ else
+ key, val = string.match(s, "^set (%a+) (.+)$")
+
+ if key and val then
+ cmus_info[key] = val
+ else
+ key, val = string.match(s, "^(%a+) (.+)$")
+ if key and val then
+ cmus_info[key] = val
+ end
+ end
+ end
+ end
+
+ local title = cmus_info.title
+
+ if not title and cmus_info.file then
+ title = cmus_info.file:gsub("%..-$", "")
+ title = title:gsub("^.+/", "")
+ end
+
+ if title then
+ if cmus_info["status"] == "playing" then
+ widget:update_icon("media-playback-start-symbolic.svg")
+ elseif cmus_info["status"] == "paused" then
+ widget:update_icon("media-playback-pause-symbolic.svg")
+ else
+ widget:update_icon("media-playback-stop-symbolic.svg")
+ end
+
+ widget:set_title(title)
+ widget.visible = true
+ else
+ widget.visible = false
+ end
+ else
+ widget.visible = false
+ end
+ end
+
+ function cmus_widget:update()
+ spawn.easy_async("cmus-remote -Q",
+ function(stdout, _, _, code)
+ update_widget(cmus_widget.widget, stdout, _, _, code)
+ end)
+ end
+
+ function cmus_widget:play_pause()
+ spawn("cmus-remote -u")
+ cmus_widget.update()
+ end
+
+ function cmus_widget:pause()
+ spawn("cmus-remote -U")
+ cmus_widget.update()
+ end
+
+ function cmus_widget:play()
+ spawn("cmus-remote -p")
+ cmus_widget.update()
+ end
+
+ function cmus_widget:next_track()
+ spawn("cmus-remote -n")
+ cmus_widget.update()
+ end
+
+ function cmus_widget:prev_track()
+ spawn("cmus-remote -r")
+ cmus_widget.update()
+ end
+
+ function cmus_widget:stop()
+ spawn("cmus-remote -s")
+ cmus_widget.update()
+ end
+
+ cmus_widget.widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function() cmus_widget:play_pause() end)
+ )
+ )
+
+ watch("cmus-remote -Q", timeout, update_widget, cmus_widget.widget)
+
+ return cmus_widget.widget
+end
+
+return setmetatable(cmus_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/cmus-widget/screenshots/cmus-widget.png b/.config/awesome/awesome-wm-widgets/cmus-widget/screenshots/cmus-widget.png
new file mode 100755
index 0000000..ba04401
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/cmus-widget/screenshots/cmus-widget.png differ
diff --git a/.config/awesome/awesome-wm-widgets/cpu-widget/README.md b/.config/awesome/awesome-wm-widgets/cpu-widget/README.md
new file mode 100755
index 0000000..b323f9b
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/cpu-widget/README.md
@@ -0,0 +1,71 @@
+# CPU widget
+
+[](https://github.com/streetturtle/awesome-wm-widgets/labels/cpu)
+
+This widget shows the average CPU load among all cores of the machine:
+
+
+
+## How it works
+
+To measure the load I took Paul Colby's bash [script](http://colby.id.au/calculating-cpu-usage-from-proc-stat/) and rewrote it in Lua, which was quite simple.
+So awesome simply reads the first line of /proc/stat:
+
+```bash
+$ cat /proc/stat | grep '^cpu '
+cpu 197294 718 50102 2002182 3844 0 2724 0 0 0
+```
+
+and calculates the percentage.
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `width` | 50 | Width of the widget |
+| `step_width` | 2 | Width of the step |
+| `step_spacing` | 1 | Space size between steps |
+| `color` | `beautiful.fg_normal` | Color of the graph |
+| `enable_kill_button` | `false` | Show button which kills the process |
+| `process_info_max_length` | `-1` | Truncate the process information. Some processes may have a very long list of parameters which won't fit in the screen, this options allows to truncate it to the given length. |
+| `timeout` | 1 | How often in seconds the widget refreshes |
+
+### Example
+
+```lua
+cpu_widget({
+ width = 70,
+ step_width = 2,
+ step_spacing = 0,
+ color = '#434c5e'
+})
+```
+
+The config above results in the following widget:
+
+
+
+## Installation
+
+Clone/download repo and use widget in **rc.lua**:
+
+```lua
+local cpu_widget = require("awesome-wm-widgets.cpu-widget.cpu-widget")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ cpu_widget(),
+ -- or custom
+ cpu_widget({
+ width = 70,
+ step_width = 2,
+ step_spacing = 0,
+ color = '#434c5e'
+ })
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/cpu-widget/cpu-widget.lua b/.config/awesome/awesome-wm-widgets/cpu-widget/cpu-widget.lua
new file mode 100755
index 0000000..11debe8
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/cpu-widget/cpu-widget.lua
@@ -0,0 +1,339 @@
+-------------------------------------------------
+-- CPU Widget for Awesome Window Manager
+-- Shows the current CPU utilization
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/cpu-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+local beautiful = require("beautiful")
+local gears = require("gears")
+
+local CMD = [[sh -c "grep '^cpu.' /proc/stat; ps -eo '%p|%c|%C|' -o "%mem" -o '|%a' --sort=-%cpu ]]
+ .. [[| head -11 | tail -n +2"]]
+
+-- A smaller command, less resource intensive, used when popup is not shown.
+local CMD_slim = [[grep --max-count=1 '^cpu.' /proc/stat]]
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/cpu-widget'
+
+local cpu_widget = {}
+local cpu_rows = {
+ spacing = 4,
+ layout = wibox.layout.fixed.vertical,
+}
+local is_update = true
+local process_rows = {
+ layout = wibox.layout.fixed.vertical,
+}
+
+-- Splits the string by separator
+-- @return table with separated substrings
+local function split(string_to_split, separator)
+ if separator == nil then separator = "%s" end
+ local t = {}
+
+ for str in string.gmatch(string_to_split, "([^".. separator .."]+)") do
+ table.insert(t, str)
+ end
+
+ return t
+end
+
+-- Checks if a string starts with a another string
+local function starts_with(str, start)
+ return str:sub(1, #start) == start
+end
+
+
+local function create_textbox(args)
+ return wibox.widget{
+ text = args.text,
+ align = args.align or 'left',
+ markup = args.markup,
+ forced_width = args.forced_width or 40,
+ widget = wibox.widget.textbox
+ }
+end
+
+local function create_process_header(params)
+ local res = wibox.widget{
+ create_textbox{markup = 'PID'},
+ create_textbox{markup = 'Name'},
+ {
+ create_textbox{markup = '%CPU'},
+ create_textbox{markup = '%MEM'},
+ params.with_action_column and create_textbox{forced_width = 20} or nil,
+ layout = wibox.layout.align.horizontal
+ },
+ layout = wibox.layout.ratio.horizontal
+ }
+ res:ajust_ratio(2, 0.2, 0.47, 0.33)
+
+ return res
+end
+
+local function create_kill_process_button()
+ return wibox.widget{
+ {
+ id = "icon",
+ image = WIDGET_DIR .. '/window-close-symbolic.svg',
+ resize = false,
+ opacity = 0.1,
+ widget = wibox.widget.imagebox
+ },
+ widget = wibox.container.background
+ }
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local width = args.width or 50
+ local step_width = args.step_width or 2
+ local step_spacing = args.step_spacing or 1
+ local color = args.color or beautiful.fg_normal
+ local background_color = args.background_color or "#00000000"
+ local enable_kill_button = args.enable_kill_button or false
+ local process_info_max_length = args.process_info_max_length or -1
+ local timeout = args.timeout or 1
+
+ local cpugraph_widget = wibox.widget {
+ max_value = 100,
+ background_color = background_color,
+ forced_width = width,
+ step_width = step_width,
+ step_spacing = step_spacing,
+ widget = wibox.widget.graph,
+ color = "linear:0,0:0,20:0,#FF0000:0.3,#FFFF00:0.6," .. color
+ }
+
+ -- This timer periodically executes the heavy command while the popup is open.
+ -- It is stopped when the popup is closed and only the slim command is run then.
+ -- This greatly improves performance while the popup is closed at the small cost
+ -- of a slightly longer popup opening time.
+ local popup_timer = gears.timer {
+ timeout = timeout
+ }
+
+ local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_normal,
+ maximum_width = 300,
+ offset = { y = 5 },
+ widget = {}
+ }
+
+ -- Do not update process rows when mouse cursor is over the widget
+ popup:connect_signal("mouse::enter", function() is_update = false end)
+ popup:connect_signal("mouse::leave", function() is_update = true end)
+
+ cpugraph_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ -- When the popup is not visible, stop the timer
+ popup_timer:stop()
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ -- Restart the timer, when the popup becomes visible
+ -- Emit the signal to start the timer directly and not wait the timeout first
+ popup_timer:start()
+ popup_timer:emit_signal("timeout")
+ end
+ end)
+ )
+ )
+
+ --- By default graph widget goes from left to right, so we mirror it and push up a bit
+ cpu_widget = wibox.widget {
+ {
+ cpugraph_widget,
+ reflection = {horizontal = true},
+ layout = wibox.container.mirror
+ },
+ bottom = 2,
+ color = background_color,
+ widget = wibox.container.margin
+ }
+
+ -- This part runs constantly, also when the popup is closed.
+ -- It updates the graph widget in the bar.
+ local maincpu = {}
+ watch(CMD_slim, timeout, function(widget, stdout)
+
+ local _, user, nice, system, idle, iowait, irq, softirq, steal, _, _ =
+ stdout:match('(%w+)%s+(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)')
+
+ local total = user + nice + system + idle + iowait + irq + softirq + steal
+
+ local diff_idle = idle - tonumber(maincpu['idle_prev'] == nil and 0 or maincpu['idle_prev'])
+ local diff_total = total - tonumber(maincpu['total_prev'] == nil and 0 or maincpu['total_prev'])
+ local diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10
+
+ maincpu['total_prev'] = total
+ maincpu['idle_prev'] = idle
+
+ widget:add_value(diff_usage)
+ end,
+ cpugraph_widget
+ )
+
+ -- This part runs whenever the timer is fired.
+ -- It therefore only runs when the popup is open.
+ local cpus = {}
+ popup_timer:connect_signal('timeout', function()
+ awful.spawn.easy_async(CMD, function(stdout, _, _, _)
+ local i = 1
+ local j = 1
+ for line in stdout:gmatch("[^\r\n]+") do
+ if starts_with(line, 'cpu') then
+
+ if cpus[i] == nil then cpus[i] = {} end
+
+ local name, user, nice, system, idle, iowait, irq, softirq, steal, _, _ =
+ line:match('(%w+)%s+(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)')
+
+ local total = user + nice + system + idle + iowait + irq + softirq + steal
+
+ local diff_idle = idle - tonumber(cpus[i]['idle_prev'] == nil and 0 or cpus[i]['idle_prev'])
+ local diff_total = total - tonumber(cpus[i]['total_prev'] == nil and 0 or cpus[i]['total_prev'])
+ local diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10
+
+ cpus[i]['total_prev'] = total
+ cpus[i]['idle_prev'] = idle
+
+ local row = wibox.widget
+ {
+ create_textbox{text = name},
+ create_textbox{text = math.floor(diff_usage) .. '%'},
+ {
+ max_value = 100,
+ value = diff_usage,
+ forced_height = 20,
+ forced_width = 150,
+ paddings = 1,
+ margins = 4,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ background_color = beautiful.bg_normal,
+ bar_border_width = 1,
+ bar_border_color = beautiful.bg_focus,
+ color = "linear:150,0:0,0:0,#D08770:0.3,#BF616A:0.6," .. beautiful.fg_normal,
+ widget = wibox.widget.progressbar,
+
+ },
+ layout = wibox.layout.ratio.horizontal
+ }
+ row:ajust_ratio(2, 0.15, 0.15, 0.7)
+ cpu_rows[i] = row
+ i = i + 1
+ else
+ if is_update == true then
+
+ local columns = split(line, '|')
+
+ local pid = columns[1]
+ local comm = columns[2]
+ local cpu = columns[3]
+ local mem = columns[4]
+ local cmd = columns[5]
+
+ local kill_proccess_button = enable_kill_button and create_kill_process_button() or nil
+
+ local pid_name_rest = wibox.widget{
+ create_textbox{text = pid},
+ create_textbox{text = comm},
+ {
+ create_textbox{text = cpu, align = 'center'},
+ create_textbox{text = mem, align = 'center'},
+ kill_proccess_button,
+ layout = wibox.layout.fixed.horizontal
+ },
+ layout = wibox.layout.ratio.horizontal
+ }
+ pid_name_rest:ajust_ratio(2, 0.2, 0.47, 0.33)
+
+ local row = wibox.widget {
+ {
+ pid_name_rest,
+ top = 4,
+ bottom = 4,
+ widget = wibox.container.margin
+ },
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ if enable_kill_button then
+ row:connect_signal("mouse::enter", function() kill_proccess_button.icon.opacity = 1 end)
+ row:connect_signal("mouse::leave", function() kill_proccess_button.icon.opacity = 0.1 end)
+
+ kill_proccess_button:buttons(
+ awful.util.table.join( awful.button({}, 1, function()
+ row:set_bg('#ff0000')
+ awful.spawn.with_shell('kill -9 ' .. pid)
+ end) ) )
+ end
+
+ awful.tooltip {
+ objects = { row },
+ mode = 'outside',
+ preferred_positions = {'bottom'},
+ timer_function = function()
+ local text = cmd
+ if process_info_max_length > 0 and text:len() > process_info_max_length then
+ text = text:sub(0, process_info_max_length - 3) .. '...'
+ end
+
+ return text
+ :gsub('%s%-', '\n\t-') -- put arguments on a new line
+ :gsub(':/', '\n\t\t:/') -- java classpath uses : to separate jars
+ end,
+ }
+
+ process_rows[j] = row
+
+ j = j + 1
+ end
+
+ end
+ end
+ popup:setup {
+ {
+ cpu_rows,
+ {
+ orientation = 'horizontal',
+ forced_height = 15,
+ color = beautiful.bg_focus,
+ widget = wibox.widget.separator
+ },
+ create_process_header{with_action_column = enable_kill_button},
+ process_rows,
+ layout = wibox.layout.fixed.vertical,
+ },
+ margins = 8,
+ widget = wibox.container.margin
+ }
+ end)
+ end)
+
+ return cpu_widget
+end
+
+return setmetatable(cpu_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/cpu-widget/cpu.gif b/.config/awesome/awesome-wm-widgets/cpu-widget/cpu.gif
new file mode 100755
index 0000000..cb97262
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/cpu-widget/cpu.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/cpu-widget/cpu.png b/.config/awesome/awesome-wm-widgets/cpu-widget/cpu.png
new file mode 100755
index 0000000..96ba29f
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/cpu-widget/cpu.png differ
diff --git a/.config/awesome/awesome-wm-widgets/cpu-widget/custom.png b/.config/awesome/awesome-wm-widgets/cpu-widget/custom.png
new file mode 100755
index 0000000..be275e4
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/cpu-widget/custom.png differ
diff --git a/.config/awesome/awesome-wm-widgets/cpu-widget/window-close-symbolic.svg b/.config/awesome/awesome-wm-widgets/cpu-widget/window-close-symbolic.svg
new file mode 100755
index 0000000..46ff888
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/cpu-widget/window-close-symbolic.svg
@@ -0,0 +1,95 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/README.md b/.config/awesome/awesome-wm-widgets/docker-widget/README.md
new file mode 100755
index 0000000..01c1fbf
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/README.md
@@ -0,0 +1,38 @@
+# Docker Widget
+
+[](https://github.com/streetturtle/awesome-wm-widgets/labels/docker)
+
+
+The widget allows to manage docker containers, namely start/stop/pause/unpause:
+
+
+
+
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `icon` | `./docker-widget/icons/docker.svg` | Path to the icon |
+| `number_of_containers` | `-1` | Number of last created containers to show |
+
+## Installation
+
+Clone the repo under **~/.config/awesome/** and add widget in **rc.lua**:
+
+```lua
+local docker_widget = require("awesome-wm-widgets.docker-widget.docker")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ docker_widget(),
+ -- customized
+ docker_widget{
+ number_of_containers = 5
+ },
+```
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/docker.gif b/.config/awesome/awesome-wm-widgets/docker-widget/docker.gif
new file mode 100755
index 0000000..3b39b5f
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/docker-widget/docker.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/docker.lua b/.config/awesome/awesome-wm-widgets/docker-widget/docker.lua
new file mode 100755
index 0000000..f5ce7fa
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/docker.lua
@@ -0,0 +1,377 @@
+-------------------------------------------------
+-- Docker Widget for Awesome Window Manager
+-- Lists containers and allows to manage them
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/docker-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/docker-widget'
+local ICONS_DIR = WIDGET_DIR .. '/icons/'
+
+local LIST_CONTAINERS_CMD = [[bash -c "docker container ls -a -s -n %s]]
+ .. [[ --format '{{.Names}}::{{.ID}}::{{.Image}}::{{.Status}}::{{.Size}}'"]]
+
+--- Utility function to show warning messages
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Docker Widget',
+ text = message}
+end
+
+local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local docker_widget = wibox.widget {
+ {
+ {
+ id = 'icon',
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+ set_icon = function(self, new_icon)
+ self:get_children_by_id("icon")[1].image = new_icon
+ end
+}
+
+local parse_container = function(line)
+ local name, id, image, status, how_long, size = line:match('(.*)::(.*)::(.*)::(%w*) (.*)::(.*)')
+ local actual_status
+ if status == 'Up' and how_long:find('Paused') then actual_status = 'Paused'
+ else actual_status = status end
+
+ how_long = how_long:gsub('%s?%(.*%)%s?', '')
+
+ local container = {
+ name = name,
+ id = id,
+ image = image,
+ status = actual_status,
+ how_long = how_long,
+ size = size,
+ is_up = function() return status == 'Up' end,
+ is_paused = function() return actual_status:find('Paused') end,
+ is_exited = function() return status == 'Exited' end
+ }
+ return container
+end
+
+local status_to_icon_name = {
+ Up = ICONS_DIR .. 'play.svg',
+ Exited = ICONS_DIR .. 'square.svg',
+ Paused = ICONS_DIR .. 'pause.svg'
+}
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or ICONS_DIR .. 'docker.svg'
+ local number_of_containers = args.number_of_containers or -1
+
+ docker_widget:set_icon(icon)
+
+ local rows = {
+ { widget = wibox.widget.textbox },
+ layout = wibox.layout.fixed.vertical,
+ }
+
+ local function rebuild_widget(containers, errors, _, _)
+ if errors ~= '' then
+ show_warning(errors)
+ return
+ end
+
+ for i = 0, #rows do rows[i]=nil end
+
+ for line in containers:gmatch("[^\r\n]+") do
+
+ local container = parse_container(line)
+
+
+ local status_icon = wibox.widget {
+ image = status_to_icon_name[container['status']],
+ resize = false,
+ widget = wibox.widget.imagebox
+ }
+
+
+ local start_stop_button
+ if container.is_up() or container.is_exited() then
+ start_stop_button = wibox.widget {
+ {
+ {
+ id = 'icon',
+ image = ICONS_DIR .. (container:is_up() and 'stop-btn.svg' or 'play-btn.svg'),
+ opacity = 0.4,
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ left = 2,
+ right = 2,
+ layout = wibox.container.margin
+ },
+ shape = gears.shape.circle,
+ bg = '#00000000',
+ widget = wibox.container.background
+ }
+ local old_cursor, old_wibox
+ start_stop_button:connect_signal("mouse::enter", function(c)
+ c:set_bg('#3B4252')
+
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ c:get_children_by_id("icon")[1]:set_opacity(1)
+ c:get_children_by_id("icon")[1]:emit_signal('widget::redraw_needed') end)
+ start_stop_button:connect_signal("mouse::leave", function(c)
+ c:set_bg('#00000000')
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ c:get_children_by_id("icon")[1]:set_opacity(0.4)
+ c:get_children_by_id("icon")[1]:emit_signal('widget::redraw_needed')
+ end)
+
+ start_stop_button:buttons(
+ gears.table.join( awful.button({}, 1, function()
+ local command
+ if container:is_up() then command = 'stop' else command = 'start' end
+
+ status_icon:set_opacity(0.2)
+ status_icon:emit_signal('widget::redraw_needed')
+
+ spawn.easy_async('docker ' .. command .. ' ' .. container['name'], function()
+ if errors ~= '' then show_warning(errors) end
+ spawn.easy_async(string.format(LIST_CONTAINERS_CMD, number_of_containers),
+ function(stdout, stderr)
+ rebuild_widget(stdout, stderr)
+ end)
+ end)
+ end) ) )
+ else
+ start_stop_button = nil
+ end
+
+
+ local pause_unpause_button
+ if container.is_up() then
+ pause_unpause_button = wibox.widget {
+ {
+ {
+ id = 'icon',
+ image = ICONS_DIR .. (container:is_paused() and 'unpause-btn.svg' or 'pause-btn.svg'),
+ opacity = 0.4,
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ left = 2,
+ right = 2,
+ layout = wibox.container.margin
+ },
+ shape = gears.shape.circle,
+ bg = '#00000000',
+ widget = wibox.container.background
+ }
+ local old_cursor, old_wibox
+ pause_unpause_button:connect_signal("mouse::enter", function(c)
+ c:set_bg('#3B4252')
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ c:get_children_by_id("icon")[1]:set_opacity(1)
+ c:get_children_by_id("icon")[1]:emit_signal('widget::redraw_needed')
+ end)
+ pause_unpause_button:connect_signal("mouse::leave", function(c)
+ c:set_bg('#00000000')
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ c:get_children_by_id("icon")[1]:set_opacity(0.4)
+ c:get_children_by_id("icon")[1]:emit_signal('widget::redraw_needed')
+ end)
+
+ pause_unpause_button:buttons(
+ gears.table.join( awful.button({}, 1, function()
+ local command
+ if container:is_paused() then command = 'unpause' else command = 'pause' end
+
+ status_icon:set_opacity(0.2)
+ status_icon:emit_signal('widget::redraw_needed')
+
+ awful.spawn.easy_async('docker ' .. command .. ' ' .. container['name'], function(_, stderr)
+ if stderr ~= '' then show_warning(stderr) end
+ spawn.easy_async(string.format(LIST_CONTAINERS_CMD, number_of_containers),
+ function(stdout, container_errors)
+ rebuild_widget(stdout, container_errors)
+ end)
+ end)
+ end) ) )
+ else
+ pause_unpause_button = nil
+ end
+
+ local delete_button
+ if not container.is_up() then
+ delete_button = wibox.widget {
+ {
+ {
+ id = 'icon',
+ image = ICONS_DIR .. 'trash-btn.svg',
+ opacity = 0.4,
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ shape = gears.shape.circle,
+ bg = '#00000000',
+ widget = wibox.container.background
+ }
+ delete_button:buttons(
+ gears.table.join( awful.button({}, 1, function()
+ awful.spawn.easy_async('docker rm ' .. container['name'], function(_, rm_stderr)
+ if rm_stderr ~= '' then show_warning(rm_stderr) end
+ spawn.easy_async(string.format(LIST_CONTAINERS_CMD, number_of_containers),
+ function(lc_stdout, lc_stderr)
+ rebuild_widget(lc_stdout, lc_stderr) end)
+ end)
+ end)))
+
+ local old_cursor, old_wibox
+ delete_button:connect_signal("mouse::enter", function(c)
+ c:set_bg('#3B4252')
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ c:get_children_by_id("icon")[1]:set_opacity(1)
+ c:get_children_by_id("icon")[1]:emit_signal('widget::redraw_needed')
+ end)
+ delete_button:connect_signal("mouse::leave", function(c)
+ c:set_bg('#00000000')
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ c:get_children_by_id("icon")[1]:set_opacity(0.4)
+ c:get_children_by_id("icon")[1]:emit_signal('widget::redraw_needed')
+ end)
+ else
+ delete_button = nil
+ end
+
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ status_icon,
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ {
+ {
+ markup = '' .. container['name'] .. '',
+ widget = wibox.widget.textbox
+ },
+ {
+ text = container['size'],
+ widget = wibox.widget.textbox
+ },
+ {
+ text = container['how_long'],
+ widget = wibox.widget.textbox
+ },
+ forced_width = 180,
+ layout = wibox.layout.fixed.vertical
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ {
+ start_stop_button,
+ pause_unpause_button,
+ delete_button,
+ layout = wibox.layout.align.horizontal
+ },
+ forced_width = 90,
+ valign = 'center',
+ haligh = 'center',
+ layout = wibox.container.place,
+ },
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ table.insert(rows, row)
+ end
+
+ popup:setup(rows)
+ end
+
+ docker_widget:buttons(
+ gears.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ docker_widget:set_bg('#00000000')
+ popup.visible = not popup.visible
+ else
+ docker_widget:set_bg(beautiful.bg_focus)
+ spawn.easy_async(string.format(LIST_CONTAINERS_CMD, number_of_containers),
+ function(stdout, stderr)
+ rebuild_widget(stdout, stderr)
+ popup:move_next_to(mouse.current_widget_geometry)
+ end)
+ end
+ end)
+ )
+ )
+
+ return docker_widget
+end
+
+return setmetatable(docker_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/docker.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/docker.svg
new file mode 100755
index 0000000..468ce94
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/docker.svg
@@ -0,0 +1 @@
+
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/pause-btn.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/pause-btn.svg
new file mode 100755
index 0000000..ac2900b
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/pause-btn.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/pause.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/pause.svg
new file mode 100755
index 0000000..33f1ad2
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/pause.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/play-btn.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/play-btn.svg
new file mode 100755
index 0000000..573ae7e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/play-btn.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/play-btn.svg- b/.config/awesome/awesome-wm-widgets/docker-widget/icons/play-btn.svg-
new file mode 100755
index 0000000..455a61d
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/play-btn.svg-
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/play.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/play.svg
new file mode 100755
index 0000000..4f0ee04
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/play.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/square.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/square.svg
new file mode 100755
index 0000000..d8424d1
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/square.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/stop-btn.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/stop-btn.svg
new file mode 100755
index 0000000..f676d01
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/stop-btn.svg
@@ -0,0 +1,10 @@
+
+
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/trash-btn.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/trash-btn.svg
new file mode 100755
index 0000000..78d8035
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/trash-btn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/docker-widget/icons/unpause-btn.svg b/.config/awesome/awesome-wm-widgets/docker-widget/icons/unpause-btn.svg
new file mode 100755
index 0000000..db5b25f
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/docker-widget/icons/unpause-btn.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/email-widget/README.md b/.config/awesome/awesome-wm-widgets/email-widget/README.md
new file mode 100755
index 0000000..510792d
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/email-widget/README.md
@@ -0,0 +1,36 @@
+# Email widget
+
+This widget consists of an icon with counter which shows number of unread emails: 
+and a popup message which appears when mouse hovers over an icon: 
+
+Note that widget uses the Arc icon theme, so it should be [installed](https://github.com/horst3180/arc-icon-theme#installation) first under **/usr/share/icons/Arc/** folder.
+
+## Installation
+
+To install it put **email.lua** and **email-widget** folder under **~/.config/awesome**. Then
+
+ - in **email.lua** change path to python scripts;
+ - in python scripts add your credentials (note that password should be encrypted using pgp for example);
+ - add widget to awesome:
+
+```lua
+local email_widget, email_icon = require("email")
+
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ email_icon,
+ email_widget,
+ ...
+```
+
+## How it works
+
+This widget uses the output of two python scripts, first is called every 20 seconds - it returns number of unread emails and second is called when mouse hovers over an icon and displays content of those emails. For both of them you'll need to provide your credentials and imap server. For testing, they can simply be called from console:
+
+``` bash
+python ~/.config/awesome/email/count_unread_emails.py
+python ~/.config/awesome/email/read_emails.py
+```
diff --git a/.config/awesome/awesome-wm-widgets/email-widget/count_unread_emails.py b/.config/awesome/awesome-wm-widgets/email-widget/count_unread_emails.py
new file mode 100755
index 0000000..a843814
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/email-widget/count_unread_emails.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+
+import imaplib
+import re
+
+M=imaplib.IMAP4_SSL("mail.teenagemutantninjaturtles.com", 993)
+M.login("mickey@tmnt.com","cowabunga")
+
+status, counts = M.status("INBOX","(MESSAGES UNSEEN)")
+
+if status == "OK":
+ unread = re.search(r'UNSEEN\s(\d+)', counts[0].decode('utf-8')).group(1)
+else:
+ unread = "N/A"
+
+print(unread)
diff --git a/.config/awesome/awesome-wm-widgets/email-widget/em-wid-1.png b/.config/awesome/awesome-wm-widgets/email-widget/em-wid-1.png
new file mode 100755
index 0000000..5290ea8
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/email-widget/em-wid-1.png differ
diff --git a/.config/awesome/awesome-wm-widgets/email-widget/em-wid-2.png b/.config/awesome/awesome-wm-widgets/email-widget/em-wid-2.png
new file mode 100755
index 0000000..0a0fd3a
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/email-widget/em-wid-2.png differ
diff --git a/.config/awesome/awesome-wm-widgets/email-widget/email.lua b/.config/awesome/awesome-wm-widgets/email-widget/email.lua
new file mode 100755
index 0000000..df80678
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/email-widget/email.lua
@@ -0,0 +1,44 @@
+local wibox = require("wibox")
+local awful = require("awful")
+local naughty = require("naughty")
+local watch = require("awful.widget.watch")
+
+local path_to_icons = "/usr/share/icons/Arc/actions/22/"
+
+local email_widget = wibox.widget.textbox()
+email_widget:set_font('Play 9')
+
+local email_icon = wibox.widget.imagebox()
+email_icon:set_image(path_to_icons .. "/mail-mark-new.png")
+
+watch(
+ "python /home//.config/awesome/email-widget/count_unread_emails.py", 20,
+ function(_, stdout)
+ local unread_emails_num = tonumber(stdout) or 0
+ if (unread_emails_num > 0) then
+ email_icon:set_image(path_to_icons .. "/mail-mark-unread.png")
+ email_widget:set_text(stdout)
+ elseif (unread_emails_num == 0) then
+ email_icon:set_image(path_to_icons .. "/mail-message-new.png")
+ email_widget:set_text("")
+ end
+ end
+)
+
+
+local function show_emails()
+ awful.spawn.easy_async([[bash -c 'python /home//.config/awesome/email-widget/read_unread_emails.py']],
+ function(stdout)
+ naughty.notify{
+ text = stdout,
+ title = "Unread Emails",
+ timeout = 5, hover_timeout = 0.5,
+ width = 400,
+ }
+ end
+ )
+end
+
+email_icon:connect_signal("mouse::enter", function() show_emails() end)
+
+return email_widget, email_icon
diff --git a/.config/awesome/awesome-wm-widgets/email-widget/read_unread_emails.py b/.config/awesome/awesome-wm-widgets/email-widget/read_unread_emails.py
new file mode 100755
index 0000000..fda8188
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/email-widget/read_unread_emails.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+
+import imaplib
+import email
+import datetime
+
+def process_mailbox(M):
+ rv, data = M.search(None, "(UNSEEN)")
+ if rv != 'OK':
+ print "No messages found!"
+ return
+
+ for num in data[0].split():
+ rv, data = M.fetch(num, '(BODY.PEEK[])')
+ if rv != 'OK':
+ print "ERROR getting message", num
+ return
+ msg = email.message_from_bytes(data[0][1])
+ for header in [ 'From', 'Subject', 'Date' ]:
+ hdr = email.header.make_header(email.header.decode_header(msg[header]))
+ if header == 'Date':
+ date_tuple = email.utils.parsedate_tz(str(hdr))
+ if date_tuple:
+ local_date = datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
+ print("{}: {}".format(header, local_date.strftime("%a, %d %b %Y %H:%M:%S")))
+ else:
+ print('{}: {}'.format(header, hdr))
+ # with code below you can process text of email
+ # if msg.is_multipart():
+ # for payload in msg.get_payload():
+ # if payload.get_content_maintype() == 'text':
+ # print payload.get_payload()
+ # else:
+ # print msg.get_payload()
+
+
+M=imaplib.IMAP4_SSL("mail.teenagemutantninjaturtles.com", 993)
+M.login("mickey@tmnt.com","cowabunga")
+
+rv, data = M.select("INBOX")
+if rv == 'OK':
+ process_mailbox(M)
+M.close()
+M.logout()
diff --git a/.config/awesome/awesome-wm-widgets/experiments/spotify-player/README.md b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/README.md
new file mode 100755
index 0000000..b19d5ac
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/README.md
@@ -0,0 +1,5 @@
+# Spotify Player
+
+In progress
+
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-indicator.svg b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-indicator.svg
new file mode 100755
index 0000000..0b96c0a
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-indicator.svg
@@ -0,0 +1,8 @@
+
diff --git a/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-player.lua b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-player.lua
new file mode 100755
index 0000000..981978b
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-player.lua
@@ -0,0 +1,192 @@
+-------------------------------------------------
+-- Spotify Player Widget for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/spotify-player
+
+-- @author Pavel Makhov
+-- @copyright 2021 Pavel Makhov
+-------------------------------------------------
+--luacheck:ignore
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+local gs = require("gears.string")
+local awesomebuttons = require("awesome-buttons.awesome-buttons")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/experiments/spotify-player/'
+local ICON_DIR = WIDGET_DIR
+
+local spotify_player = {}
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Spotify Player Widget',
+ text = message}
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+ local artwork_size = args.artwork_size or 300
+
+ local timeout = args.timeout or 1
+
+ local popup = awful.popup{
+ ontop = true,
+ bg = beautiful.bg_normal .. '88',
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ width = artwork_size,
+ maximum_width = 300,
+ offset = { y = 5 },
+ widget = {}
+ }
+
+ local rows = {
+ expand = 'none',
+ layout = wibox.layout.align.vertical,
+ }
+
+ spotify_player.widget = wibox.widget {
+ image = ICON_DIR .. 'spotify-indicator.svg',
+ widget = wibox.widget.imagebox
+ }
+
+ local artwork_widget = wibox.widget {
+ forced_height = artwork_size,
+ forced_width = artwork_size,
+ widget = wibox.widget.imagebox
+ }
+
+ local artist_w = wibox.widget {
+ align = 'center',
+ widget = wibox.widget.textbox,
+ set_artist = function(self, artist)
+ self:set_markup('' .. artist .. '')
+ end
+ }
+
+ local title_w = wibox.widget {
+ align = 'center',
+ forced_height = 30,
+ widget = wibox.widget.textbox,
+ set_title = function(self, title)
+ self:set_markup('' .. title .. '')
+ end
+ }
+
+ local play_pause_btn = awesomebuttons.with_icon{ type = 'outline', icon = 'play', icon_size = 32, icon_margin = 8, color = '#1DB954', shape = 'circle', onclick = function()
+ spawn.with_shell('sp play')
+ end}
+
+ local buttons_w = wibox.widget {
+ {
+ awesomebuttons.with_icon{ icon = 'rewind', icon_size = 32, icon_margin = 8, color = '#18800000', shape = 'circle', onclick = function()
+ spawn.with_shell('sp prev')
+ end},
+ play_pause_btn,
+ awesomebuttons.with_icon{ icon = 'fast-forward', icon_size = 32, icon_margin = 8, color = '#18800000', shape = 'circle', onclick = function()
+ spawn.with_shell('sp next')
+ end},
+ spacing = 16,
+ layout = wibox.layout.fixed.horizontal
+ },
+ halign = 'center',
+ layout = wibox.container.place,
+ }
+
+ local some_w = wibox.widget {
+ artwork_widget,
+ {
+ {
+ {
+ {
+ title_w,
+ artist_w,
+ buttons_w,
+ layout = wibox.layout.fixed.vertical
+ },
+ top = 8,
+ bottom = 8,
+ widget = wibox.container.margin
+ },
+ bg = '#33333388',
+ widget = wibox.container.background
+ },
+ valign = 'bottom',
+ content_fill_horizontal = true,
+ layout = wibox.container.place,
+ },
+ layout = wibox.layout.stack
+ }
+
+ popup:setup({
+ some_w,
+ layout = wibox.layout.fixed.vertical,
+ })
+
+ local update_widget = function(widget, stdout, stderr, _, _)
+ for i = 0, #rows do rows[i]=nil end
+
+ if string.find(stdout, 'Error: Spotify is not running.') ~= nil then
+ return
+ end
+
+ local track_id, length, art_url, album, album_artist, artist, auto_rating, disc_number, title, track_number, url =
+ string.match(stdout, 'trackid|(.*)\nlength|(.*)\nartUrl|(.*)\nalbum|(.*)\nalbumArtist|(.*)\nartist|(.*)\nautoRating|(.*)\ndiscNumber|(.*)\ntitle|(.*)\ntrackNumber|(.*)\nurl|(.*)')
+
+ title = string.gsub(title, "&", '&')
+ artist_w:set_artist(artist)
+ title_w:set_title(title)
+
+ -- spotify client bug: https://community.spotify.com/t5/Desktop-Linux/MPRIS-cover-art-url-file-not-found/td-p/4920104
+ art_url = art_url:gsub('https://open.spotify.com', 'https://i.scdn.co')
+ if ((art_url ~= nil or art_url ~='') and not gfs.file_readable('/tmp/' .. track_id)) then
+ spawn.easy_async('touch /tmp/' .. track_id, function()
+ spawn.easy_async('curl -L -s --show-error --create-dirs -o /tmp/' .. track_id .. ' '.. art_url, function(stdout, stderr)
+ if stderr ~= '' then
+ show_warning(stderr)
+ return
+ end
+ artwork_widget:set_image('/tmp/' .. track_id)
+ end)
+ end)
+ else
+ artwork_widget:set_image('/tmp/' .. track_id)
+ end
+ end
+
+ function spotify_player:tog()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end
+
+ spotify_player.widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function() spotify_player:tog() end)
+ )
+ )
+
+ watch('sp metadata', timeout, update_widget)
+
+ watch('sp status', 1, function(_, stdout)
+ stdout = string.gsub(stdout, "\n", "")
+ play_pause_btn:set_icon(stdout == 'Playing' and 'pause' or 'play')
+ end)
+
+ return spotify_player
+end
+
+return setmetatable(spotify_player, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-player.png b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-player.png
new file mode 100755
index 0000000..7bfecfc
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/experiments/spotify-player/spotify-player.png differ
diff --git a/.config/awesome/awesome-wm-widgets/fs-widget/README.md b/.config/awesome/awesome-wm-widgets/fs-widget/README.md
new file mode 100755
index 0000000..4657e9e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/fs-widget/README.md
@@ -0,0 +1,29 @@
+# Filesystem Widget
+
+This widget shows file system disk space usage which is based on the `df` output. When clicked another widget appears with more detailed information. By default, it monitors the "/" mount. It can be configured with a list of mounts to monitor though only the first will show in the wibar. To have multiple mounts displayed on the wibar simply define multiple `fs_widgets` with different mounts as arguments.
+
+
+
+## Customizations
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `mounts` | `{'/'}` | Table with mounts to monitor, check the output from a `df` command for available options (column 'Mounted on') |
+| `timeout` | 60 | How often in seconds the widget refreshes |
+
+## Installation
+
+Clone/download repo and use the widget in **rc.lua**:
+
+```lua
+ local fs_widget = require("awesome-wm-widgets.fs-widget.fs-widget")
+ ...
+ s.mywibox:setup {
+ s.mytasklist, -- Middle widget
+ { -- Right widgets
+ fs_widget(), --default
+ fs_widget({ mounts = { '/', '/mnt/music' } }), -- multiple mounts
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/fs-widget/fs-widget.lua b/.config/awesome/awesome-wm-widgets/fs-widget/fs-widget.lua
new file mode 100755
index 0000000..ca76193
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/fs-widget/fs-widget.lua
@@ -0,0 +1,190 @@
+local awful = require("awful")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+local beautiful = require("beautiful")
+local gears = require("gears")
+
+local storage_bar_widget = {}
+
+--- Table with widget configuration, consists of three sections:
+--- - general - general configuration
+--- - widget - configuration of the widget displayed on the wibar
+--- - popup - configuration of the popup
+local config = {}
+
+-- general
+config.mounts = { '/' }
+config.refresh_rate = 60
+
+-- wibar widget
+config.widget_width = 40
+config.widget_bar_color = '#aaaaaa'
+config.widget_onclick_bg = '#ff0000'
+config.widget_border_color = '#535d6c66'
+config.widget_background_color = '#22222233'
+
+-- popup
+config.popup_bg = '#22222233'
+config.popup_border_width = 1
+config.popup_border_color = '#535d6c66'
+config.popup_bar_color = '#aaaaaa'
+config.popup_bar_background_color = '#22222233'
+config.popup_bar_border_color = '#535d6c66'
+
+local function worker(user_args)
+ local args = user_args or {}
+
+ -- Setup config for the widget instance.
+ -- The `_config` table will keep the first existing value after checking
+ -- in this order: user parameter > beautiful > module default.
+ local _config = {}
+ for prop, value in pairs(config) do
+ _config[prop] = args[prop] or beautiful[prop] or value
+ end
+
+ storage_bar_widget = wibox.widget {
+ {
+ id = 'progressbar',
+ color = _config.widget_bar_color,
+ max_value = 100,
+ forced_height = 20,
+ forced_width = _config.widget_width,
+ paddings = 2,
+ margins = 4,
+ border_width = 1,
+ border_radius = 2,
+ border_color = _config.widget_border_color,
+ background_color = _config.widget_background_color,
+ widget = wibox.widget.progressbar
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+ set_value = function(self, new_value)
+ self:get_children_by_id("progressbar")[1].value = new_value
+ end
+ }
+
+ local disk_rows = {
+ { widget = wibox.widget.textbox },
+ spacing = 4,
+ layout = wibox.layout.fixed.vertical,
+ }
+
+ local disk_header = wibox.widget {
+ {
+ markup = 'Mount',
+ forced_width = 150,
+ align = 'left',
+ widget = wibox.widget.textbox,
+ },
+ {
+ markup = 'Used',
+ align = 'left',
+ widget = wibox.widget.textbox,
+ },
+ layout = wibox.layout.ratio.horizontal
+ }
+ disk_header:ajust_ratio(1, 0, 0.3, 0.7)
+
+ local popup = awful.popup {
+ bg = _config.popup_bg,
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = _config.popup_border_width,
+ border_color = _config.popup_border_color,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+ }
+
+ storage_bar_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ storage_bar_widget:set_bg('#00000000')
+ else
+ storage_bar_widget:set_bg(_config.widget_background_color)
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ local disks = {}
+ watch([[bash -c "df | tail -n +2"]], _config.refresh_rate,
+ function(widget, stdout)
+ for line in stdout:gmatch("[^\r\n$]+") do
+ local filesystem, size, used, avail, perc, mount =
+ line:match('([%p%w]+)%s+([%d%w]+)%s+([%d%w]+)%s+([%d%w]+)%s+([%d]+)%%%s+([%p%w]+)')
+
+ disks[mount] = {}
+ disks[mount].filesystem = filesystem
+ disks[mount].size = size
+ disks[mount].used = used
+ disks[mount].avail = avail
+ disks[mount].perc = perc
+ disks[mount].mount = mount
+
+ if disks[mount].mount == _config.mounts[1] then
+ widget:set_value(tonumber(disks[mount].perc))
+ end
+ end
+
+ for k, v in ipairs(_config.mounts) do
+
+ local row = wibox.widget {
+ {
+ text = disks[v].mount,
+ forced_width = 150,
+ widget = wibox.widget.textbox
+ },
+ {
+ color = _config.popup_bar_color,
+ max_value = 100,
+ value = tonumber(disks[v].perc),
+ forced_height = 20,
+ paddings = 1,
+ margins = 4,
+ border_width = 1,
+ border_color = _config.popup_bar_border_color,
+ background_color = _config.popup_bar_background_color,
+ bar_border_width = 1,
+ bar_border_color = _config.popup_bar_border_color,
+ widget = wibox.widget.progressbar,
+ },
+ {
+ text = math.floor(disks[v].used / 1024 / 1024)
+ .. '/'
+ .. math.floor(disks[v].size / 1024 / 1024) .. 'GB('
+ .. math.floor(disks[v].perc) .. '%)',
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.ratio.horizontal
+ }
+ row:ajust_ratio(2, 0.3, 0.3, 0.4)
+
+ disk_rows[k] = row
+ end
+ popup:setup {
+ {
+ disk_header,
+ disk_rows,
+ layout = wibox.layout.fixed.vertical,
+ },
+ margins = 8,
+ widget = wibox.container.margin
+ }
+ end,
+ storage_bar_widget
+ )
+
+ return storage_bar_widget
+end
+
+return setmetatable(storage_bar_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/fs-widget/screenshot.png b/.config/awesome/awesome-wm-widgets/fs-widget/screenshot.png
new file mode 100755
index 0000000..41e6ccc
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/fs-widget/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/gerrit-widget/README.md b/.config/awesome/awesome-wm-widgets/gerrit-widget/README.md
new file mode 100755
index 0000000..62c89a1
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gerrit-widget/README.md
@@ -0,0 +1,77 @@
+# Gerrit widget
+
+It shows number of currently assigned reviews in [Gerrit](https://www.gerritcodereview.com/) to the user (by default) :
+
+ 
+
+ when clicked it shows reviews in a list:
+
+ 
+
+ left click on an item will open review in the default browser, right click will copy the review number, which you can use to checkout this review by running `git-review -d `.
+
+ Also, if a new review is assigned to the user, there will be a pop-up:
+
+ 
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `icon`| `/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_icon.svg`| Path to the icon |
+| `host` | Required | Ex https://gerrit.tmnt.com |
+| `query` | `is:reviewer AND status:open AND NOT is:wip` | Query to retrieve reviews |
+| `timeout` | 10 | How often in seconds the widget refreshes |
+
+## Prerequisite
+
+ - [curl](https://curl.haxx.se/) - is used to communicate with gerrit's [REST API](https://gerrit-review.googlesource.com/Documentation/rest-api.html)
+ - setup [netrc](https://ec.haxx.se/usingcurl-netrc.html) which is used to store username and password in order to call API's endpoints.
+
+## Installation
+
+1. This widget relies on Gerrit [REST API](https://gerrit-review.googlesource.com/Documentation/rest-api.html), so you need to have a permission to access it. You also need to setup [netrc](https://ec.haxx.se/usingcurl-netrc.html), as widget uses curl to communicate with API and you have to be authenticated.
+To test if you have access to API and netrc setup is correct run following command, you should have a json response:
+
+ ```bash
+ curl -s --request GET --netrc https://gerrit-host.com/a/changes/\?q\=status:open+AND+NOT+is:wip+AND+is:reviewer | tail -n +2
+ ```
+ Note: `tail -n +2` is needed to skip first line of the response, as gerrit returns some characters there in order to prevent XSS hacks.
+
+1. Download json parser for lua from [github.com/rxi/json.lua](https://github.com/rxi/json.lua) and place it under **~/.config/awesome/** (don't forget to star a repo):
+
+ ```bash
+ wget -P ~/.config/awesome/ https://raw.githubusercontent.com/rxi/json.lua/master/json.lua
+ ```
+
+1. Clone this repo (if not cloned yet) under **~/.config/awesome/**:
+
+ ```bash
+ git clone https://github.com/streetturtle/awesome-wm-widgets.git ~/.config/awesome/
+ ```
+
+1. Require widget at the top of the **rc.lua**:
+
+ ```lua
+ local gerrit_widget = require("awesome-wm-widgets.gerrit-widget.gerrit")
+ ```
+
+1. Add widget to the tasklist:
+
+ ```lua
+ s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ --default
+ gerrit_widget({host = 'https://gerrit.tmnt.com'}),
+ --customized
+ gerrit_widget({
+ host = 'https://gerrit.tmnt.com',
+ query = 'is:reviewer AND is:wip'
+ })
+ ...
+ ```
+
diff --git a/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit.lua b/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit.lua
new file mode 100755
index 0000000..682eb0f
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit.lua
@@ -0,0 +1,230 @@
+-------------------------------------------------
+-- Gerrit Widget for Awesome Window Manager
+-- Shows the number of currently assigned reviews
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/gerrit-widget
+
+-- @author Pavel Makhov
+-- @copyright 2019 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local json = require("json")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+
+local HOME_DIR = os.getenv("HOME")
+local PATH_TO_AVATARS = HOME_DIR .. '/.cache/awmw/gerrit-widget/avatars/'
+
+local GET_CHANGES_CMD = [[bash -c "curl -s -X GET -n %s/a/changes/\\?q\\=%s | tail -n +2"]]
+local GET_USER_CMD = [[bash -c "curl -s -X GET -n %s/accounts/%s/ | tail -n +2"]]
+local DOWNLOAD_AVATAR_CMD = [[bash -c "curl --create-dirs -o %s %s"]]
+
+local gerrit_widget = {}
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icons or HOME_DIR .. '/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_icon.svg'
+ local host = args.host or naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Gerrit Widget',
+ text = 'Gerrit host is unknown'
+ }
+ local query = args.query or 'is:reviewer AND status:open AND NOT is:wip'
+ local timeout = args.timeout or 10
+
+ local current_number_of_reviews
+ local previous_number_of_reviews = 0
+ local name_dict = {}
+
+ local rows = {
+ { widget = wibox.widget.textbox },
+ layout = wibox.layout.fixed.vertical,
+ }
+
+ local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+ }
+
+ gerrit_widget = wibox.widget {
+ {
+ {
+ image = icon,
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ {
+ id = "new_rev",
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_text = function(self, new_value)
+ self.txt.text = new_value
+ end,
+ set_unseen_review = function(self, is_new_review)
+ self.new_rev.text = is_new_review and '*' or ''
+ end
+ }
+
+ local function get_name_by_user_id(user_id)
+ if name_dict[user_id] == nil then
+ name_dict[user_id] = {}
+ end
+
+ if name_dict[user_id].username == nil then
+ name_dict[user_id].username = ''
+ spawn.easy_async(string.format(GET_USER_CMD, host, user_id), function(stdout)
+ local user = json.decode(stdout)
+ name_dict[tonumber(user_id)].username = user.name
+ if not gfs.file_readable(PATH_TO_AVATARS .. user_id) then
+ spawn.easy_async(
+ string.format(DOWNLOAD_AVATAR_CMD, PATH_TO_AVATARS .. user_id, user.avatars[1].url))
+ end
+ end)
+ return name_dict[user_id].username
+ end
+
+ return name_dict[user_id].username
+ end
+
+ local update_widget = function(widget, stdout, _, _, _)
+ local reviews = json.decode(stdout)
+
+ current_number_of_reviews = rawlen(reviews)
+
+ if current_number_of_reviews == 0 then
+ widget:set_visible(false)
+ return
+ else
+ widget:set_visible(true)
+ end
+
+ widget:set_visible(true)
+ if current_number_of_reviews > previous_number_of_reviews then
+ widget:set_unseen_review(true)
+ naughty.notify{
+ icon = HOME_DIR ..'/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_icon.svg',
+ title = 'New Incoming Review',
+ text = reviews[1].project .. '\n' .. get_name_by_user_id(reviews[1].owner._account_id) ..
+ reviews[1].subject .. '\n',
+ run = function() spawn.with_shell("xdg-open https://" .. host .. '/' .. reviews[1]._number) end
+ }
+ end
+
+ previous_number_of_reviews = current_number_of_reviews
+ widget:set_text(current_number_of_reviews)
+
+ for i = 0, #rows do rows[i]=nil end
+ for _, review in ipairs(reviews) do
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ resize = true,
+ image = PATH_TO_AVATARS .. review.owner._account_id,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ {
+ {
+ markup = '' .. review.project .. '',
+ align = 'center',
+ widget = wibox.widget.textbox
+ },
+ {
+ text = ' ' .. review.subject,
+ widget = wibox.widget.textbox
+ },
+ {
+ text = ' ' .. get_name_by_user_id(review.owner._account_id),
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.align.vertical
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("button::release", function()
+ spawn.with_shell("xdg-open " .. host .. '/' .. review._number)
+ end)
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ row:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. host .. '/' .. review._number)
+ popup.visible = false
+ end),
+ awful.button({}, 3, function()
+ spawn.with_shell("echo '" .. review._number .."' | xclip -selection clipboard")
+ popup.visible = false
+ end)
+ )
+ )
+
+ table.insert(rows, row)
+ end
+
+ popup:setup(rows)
+ end
+
+ gerrit_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ gerrit_widget:set_unseen_review(false)
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ --local geo = mouse.current_widget_geometry
+ --if theme.calendar_placement == 'center' then
+ -- local x = geo.x + (geo.width / 2) - (popup:geometry().width / 2) -- align two widgets
+ -- popup:move_next_to({x = x, y = geo.y + 22, width = 0, height = geo.height})
+ --else
+ -- popup:move_next_to(geo)
+ --end
+
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ watch(string.format(GET_CHANGES_CMD, host, query:gsub(" ", "+")), timeout, update_widget, gerrit_widget)
+ return gerrit_widget
+end
+
+return setmetatable(gerrit_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_icon.svg b/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_icon.svg
new file mode 100755
index 0000000..4ac5652
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_icon.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_widget.png b/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_widget.png
new file mode 100755
index 0000000..926bc4b
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/gerrit-widget/gerrit_widget.png differ
diff --git a/.config/awesome/awesome-wm-widgets/gerrit-widget/new_review.png b/.config/awesome/awesome-wm-widgets/gerrit-widget/new_review.png
new file mode 100755
index 0000000..ebc9bad
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/gerrit-widget/new_review.png differ
diff --git a/.config/awesome/awesome-wm-widgets/gerrit-widget/popup.png b/.config/awesome/awesome-wm-widgets/gerrit-widget/popup.png
new file mode 100755
index 0000000..e08879a
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/gerrit-widget/popup.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/README.md b/.config/awesome/awesome-wm-widgets/github-activity-widget/README.md
new file mode 100755
index 0000000..2dbf98b
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/README.md
@@ -0,0 +1,89 @@
+# GitHub Activity Widget
+
+Widget shows recent activities on GitHub. It is very similar to the GitHub's "All activity" feed on the main page:
+
+
+
+
+
+Mouse click on the item opens repo/issue/pr depending on the type of the activity. Mouse click on user's avatar opens user GitHub profile.
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `icon` | github.png from the widget sources | Widget icon displayed on the wibar |
+| `username` | your username | Required parameter |
+| `number_of_events` | 10 | Number of events to display in the list |
+
+## Installation
+
+Clone repo under **~/.config/awesome/** and add widget in **rc.lua**:
+
+```lua
+local github_activity_widget = require("awesome-wm-widgets.github-activity-widget.github-activity-widget")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ github_activity_widget{
+ username = 'streetturtle',
+ },
+ -- customized
+ github_activity_widget{
+ username = 'streetturtle',
+ number_of_events = 5
+ },
+
+```
+
+
+## How it works
+
+Everything starts with this timer, which gets recent activities by calling GitHub [Events API](https://developer.github.com/v3/activity/events/) and stores the response under /.cache/awmw/github-activity-widget/activity.json directory:
+
+```lua
+gears.timer {
+ timeout = 600, -- calls every ten minutes
+ call_now = true,
+ autostart = true,
+ callback = function()
+ spawn.easy_async(string.format(UPDATE_EVENTS_CMD, username, CACHE_DIR), function(stdout, stderr)
+ if stderr ~= '' then show_warning(stderr) return end
+ end)
+ end
+}
+```
+
+There are several reasons to store output in a file and then use it as a source to build the widget, instead of calling it everytime the widget is opened:
+ - activity feed does not update that often
+ - events API doesn't provide filtering of fields, so the output is quite large (300 events)
+ - it's much faster to read file from filesystem
+
+ Next important part is **rebuild_widget** function, which is called when mouse button clicks on the widget on the wibar. It receives a json string which contains first n events from the cache file. Those events are processed by `jq` (get first n events, remove unused fields, slightly change the json structure to simplify serialization to lua table). And then it builds a widget, row by row in a loop. To display the text part of the row we already have all neccessary information in the json string which was converted to lua table. But to show an avatar we should download it first. This is done in the following snippet. First it creates a template and then checks if file already exists, and sets it in template, otherwise, downloads it asynchronously and only then sets in:
+
+ ```lua
+local avatar_img = wibox.widget {
+ resize = true,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+}
+
+if gfs.file_readable(path_to_avatar) then
+ avatar_img:set_image(path_to_avatar)
+else
+ -- download it first
+ spawn.easy_async(string.format(
+ DOWNLOAD_AVATAR_CMD,
+ CACHE_DIR,
+ event.actor.id,
+ event.actor.avatar_url),
+ -- and then set
+ function() avatar_img:set_image(path_to_avatar) end)
+end
+ ```
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/github-activity-widget.lua b/.config/awesome/awesome-wm-widgets/github-activity-widget/github-activity-widget.lua
new file mode 100755
index 0000000..af29b35
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/github-activity-widget.lua
@@ -0,0 +1,294 @@
+-------------------------------------------------
+-- GitHub Widget for Awesome Window Manager
+-- Shows the recent activity from GitHub
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/github-activity-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local json = require("json")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/github-activity-widget'
+local ICONS_DIR = WIDGET_DIR .. '/icons/'
+local CACHE_DIR = HOME_DIR .. '/.cache/awmw/github-activity-widget'
+
+local GET_EVENTS_CMD = [[sh -c "cat %s/activity.json | jq '.[:%d] | [.[] ]]
+ .. [[| {type: .type, actor: .actor, repo: .repo, action: .payload.action, issue_url: .payload.issue.html_url, ]]
+ .. [[pr_url: .payload.pull_request.html_url, created_at: .created_at}]'"]]
+local DOWNLOAD_AVATAR_CMD = [[sh -c "curl -n --create-dirs -o %s/avatars/%s %s"]]
+local UPDATE_EVENTS_CMD = [[sh -c "curl -s --show-error https://api.github.com/users/%s/received_events ]]
+ ..[[> %s/activity.json"]]
+
+--- Utility function to show warning messages
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'GitHub Activity Widget',
+ text = message}
+end
+
+--- Converts string representation of date (2020-06-02T11:25:27Z) to date
+local function parse_date(date_str)
+ local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)%Z"
+ local y, m, d, h, min, sec, _ = date_str:match(pattern)
+
+ return os.time{year = y, month = m, day = d, hour = h, min = min, sec = sec}
+end
+
+--- Converts seconds to "time ago" representation, like '1 hour ago'
+local function to_time_ago(seconds)
+ local days = seconds / 86400
+ if days > 1 then
+ days = math.floor(days + 0.5)
+ return days .. (days == 1 and ' day' or ' days') .. ' ago'
+ end
+
+ local hours = (seconds % 86400) / 3600
+ if hours > 1 then
+ hours = math.floor(hours + 0.5)
+ return hours .. (hours == 1 and ' hour' or ' hours') .. ' ago'
+ end
+
+ local minutes = ((seconds % 86400) % 3600) / 60
+ if minutes > 1 then
+ minutes = math.floor(minutes + 0.5)
+ return minutes .. (minutes == 1 and ' minute' or ' minutes') .. ' ago'
+ end
+end
+
+
+local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 350,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local function generate_action_string(event)
+ local action_string = event.type
+ local icon = 'repo.svg'
+ local link = 'http://github.com/' .. event.repo.name
+
+ if (event.type == "PullRequestEvent") then
+ action_string = event.action .. ' a pull request in'
+ link = event.pr_url
+ icon = 'git-pull-request.svg'
+ elseif (event.type == "IssuesEvent") then
+ action_string = event.action .. ' an issue in'
+ link = event.issue_url
+ icon = 'alert-circle.svg'
+ elseif (event.type == "IssueCommentEvent") then
+ action_string = event.action == 'created' and 'commented in issue' or event.action .. ' a comment in'
+ link = event.issue_url
+ icon = 'message-square.svg'
+ elseif (event.type == "WatchEvent") then
+ action_string = 'starred'
+ icon = 'star.svg'
+ elseif (event.type == "ForkEvent") then
+ action_string = 'forked'
+ icon = 'git-branch.svg'
+ elseif (event.type == "CreateEvent") then
+ action_string = 'created'
+ end
+
+ return { action_string = action_string, link = link, icon = icon }
+end
+
+local github_widget = wibox.widget {
+ {
+ {
+ {
+ id = 'icon',
+ widget = wibox.widget.imagebox
+ },
+ id = "m",
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ layout = wibox.layout.fixed.horizontal,
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+ set_icon = function(self, new_icon)
+ self:get_children_by_id("icon")[1].image = new_icon
+ end
+}
+
+
+local function worker(user_args)
+
+ if not gfs.dir_readable(CACHE_DIR) then
+ gfs.make_directories(CACHE_DIR)
+ end
+
+ local args = user_args or {}
+
+ local icon = args.icon or ICONS_DIR .. 'github.png'
+ local username = args.username or show_warning('No username provided')
+ local number_of_events = args.number_of_events or 10
+
+ github_widget:set_icon(icon)
+
+ local rows = {
+ layout = wibox.layout.fixed.vertical,
+ }
+
+ local rebuild_widget = function(stdout, stderr, _, _)
+ if stderr ~= '' then
+ show_warning(stderr)
+ return
+ end
+
+ local current_time = os.time(os.date("!*t"))
+
+ local events = json.decode(stdout)
+
+ for i = 0, #rows do rows[i]=nil end
+ for _, event in ipairs(events) do
+ local path_to_avatar = CACHE_DIR .. '/avatars/' .. event.actor.id
+
+ local avatar_img = wibox.widget {
+ resize = true,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+ }
+
+ if not gfs.file_readable(path_to_avatar) then
+ -- download it first
+ spawn.easy_async(string.format(
+ DOWNLOAD_AVATAR_CMD,
+ CACHE_DIR,
+ event.actor.id,
+ event.actor.avatar_url), function() avatar_img:set_image(path_to_avatar) end)
+ else
+ avatar_img:set_image(path_to_avatar)
+ end
+
+ local action_and_link = generate_action_string(event)
+
+ local avatar = wibox.widget {
+ avatar_img,
+ margins = 8,
+ layout = wibox.container.margin
+ }
+ avatar:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell('xdg-open http://github.com/' .. event.actor.login)
+ popup.visible = false
+ end)
+ )
+ )
+
+ local repo_info = wibox.widget {
+ {
+ markup = ' ' .. event.actor.display_login .. ' ' .. action_and_link.action_string
+ .. ' ' .. event.repo.name .. '',
+ wrap = 'word',
+ widget = wibox.widget.textbox
+ },
+ {
+ {
+ {
+ image = ICONS_DIR .. action_and_link.icon,
+ resize = true,
+ forced_height = 16,
+ forced_width = 16,
+ widget = wibox.widget.imagebox
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ markup = to_time_ago(os.difftime(current_time, parse_date(event.created_at))),
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ },
+ layout = wibox.layout.align.vertical
+ }
+ repo_info:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. action_and_link.link)
+ popup.visible = false
+ end)
+ )
+ )
+
+ local row = wibox.widget {
+ {
+ {
+ avatar,
+ repo_info,
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ table.insert(rows, row)
+ end
+
+ popup:setup(rows)
+ end
+
+ github_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ github_widget:set_bg('#00000000')
+ else
+ github_widget:set_bg(beautiful.bg_focus)
+ spawn.easy_async(string.format(GET_EVENTS_CMD, CACHE_DIR, number_of_events),
+ function (stdout, stderr)
+ rebuild_widget(stdout, stderr)
+ popup:move_next_to(mouse.current_widget_geometry)
+ end)
+ end
+ end)
+ )
+ )
+
+ -- Calls GitHub event API and stores response in "cache" file
+ gears.timer {
+ timeout = 600,
+ call_now = true,
+ autostart = true,
+ callback = function()
+ spawn.easy_async(string.format(UPDATE_EVENTS_CMD, username, CACHE_DIR), function(_, stderr)
+ if stderr ~= '' then show_warning(stderr) return end
+ end)
+ end
+ }
+
+ return github_widget
+end
+
+return setmetatable(github_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/alert-circle.svg b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/alert-circle.svg
new file mode 100755
index 0000000..1c42eaf
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/alert-circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/git-branch.svg b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/git-branch.svg
new file mode 100755
index 0000000..3f06c34
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/git-branch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/git-pull-request.svg b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/git-pull-request.svg
new file mode 100755
index 0000000..c2e2867
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/git-pull-request.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/github.png b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/github.png
new file mode 100755
index 0000000..628da97
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/github.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/message-square.svg b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/message-square.svg
new file mode 100755
index 0000000..758ba42
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/message-square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/repo.svg b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/repo.svg
new file mode 100755
index 0000000..f74a595
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/repo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/star.svg b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/star.svg
new file mode 100755
index 0000000..0a3d39e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-activity-widget/icons/star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-activity-widget/screenshot.png b/.config/awesome/awesome-wm-widgets/github-activity-widget/screenshot.png
new file mode 100755
index 0000000..f066cbc
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-activity-widget/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/README.md b/.config/awesome/awesome-wm-widgets/github-contributions-widget/README.md
new file mode 100755
index 0000000..7d02008
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-contributions-widget/README.md
@@ -0,0 +1,63 @@
+# Github Contributions Widget
+
+The widget is inspired by the https://github-contributions.now.sh/ and relies on it's API.
+
+It shows the contribution graph, similar to the one on the github profile page: 
+
+You might wonder what could be the reason to have your github's contributions in front of you all day long? The more you contribute, the nicer widget looks! Check out [Thomashighbaugh](https://github.com/Thomashighbaugh)'s graph:
+
+
+
+## Customization
+
+It is possible to customize the widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `username` | `streetturtle` | GitHub username |
+| `days` | `365` | Number of days in the past, more days - wider the widget |
+| `color_of_empty_cells` | Theme's default | Color of the days with no contributions |
+| `with_border` | `true` | Should the graph contains border or not |
+| `margin_top` | `1` | Top margin |
+| `theme` | `standard` | Color theme of the graph, see below |
+
+_Note:_ widget height is 21px (7 rows of 3x3 cells). So it would look nice on the wibar of 22-24px height.
+
+### Themes
+
+Following themes are available:
+
+| Theme name | Preview |
+|---|---|
+| standard |  |
+| classic |  |
+| teal |  |
+| leftpad |  |
+| dracula |  |
+| pink |  |
+
+To add a new theme, simply add a new entry in `themes` table (themes.lua) with the colors of your theme.
+
+### Screenshots
+
+1000 days, with border:
+
+
+365 days, no border:
+
+
+## Installation
+
+Clone/download repo under **~/.config/awesome** and use widget in **rc.lua**:
+
+```lua
+local github_contributions_widget = require("awesome-wm-widgets.github-contributions-widget.github-contributions-widget")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ github_contributions_widget({username = ''}),
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/github-contributions-widget.lua b/.config/awesome/awesome-wm-widgets/github-contributions-widget/github-contributions-widget.lua
new file mode 100755
index 0000000..113d474
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-contributions-widget/github-contributions-widget.lua
@@ -0,0 +1,104 @@
+-------------------------------------------------
+-- Github Contributions Widget for Awesome Window Manager
+-- Shows the contributions graph
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/github-contributions-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local gears = require("gears")
+local widget_themes = require("awesome-wm-widgets.github-contributions-widget.themes")
+
+local GET_CONTRIBUTIONS_CMD = [[bash -c "curl -s https://github-contributions.vercel.app/api/v1/%s]]
+ .. [[ | jq -r '[.contributions[] ]]
+ .. [[ | select ( .date | strptime(\"%%Y-%%m-%%d\") | mktime < now)][:%s]| .[].intensity'"]]
+
+local github_contributions_widget = wibox.widget{
+ reflection = {
+ horizontal = true,
+ vertical = true,
+ },
+ widget = wibox.container.mirror
+}
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Github Contributions Widget',
+ text = message}
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+ local username = args.username or 'streetturtle'
+ local days = args.days or 365
+ local color_of_empty_cells = args.color_of_empty_cells
+ local with_border = args.with_border
+ local margin_top = args.margin_top or 1
+ local theme = args.theme or 'standard'
+
+ if widget_themes[theme] == nil then
+ show_warning('Theme ' .. theme .. ' does not exist')
+ theme = 'standard'
+ end
+
+ if with_border == nil then with_border = true end
+
+ local function get_square(color)
+ if color_of_empty_cells ~= nil and color == widget_themes[theme][0] then
+ color = color_of_empty_cells
+ end
+
+ return wibox.widget{
+ fit = function()
+ return 3, 3
+ end,
+ draw = function(_, _, cr, _, _)
+ cr:set_source(gears.color(color))
+ cr:rectangle(0, 0, with_border and 2 or 3, with_border and 2 or 3)
+ cr:fill()
+ end,
+ layout = wibox.widget.base.make_widget
+ }
+ end
+
+ local col = {layout = wibox.layout.fixed.vertical}
+ local row = {layout = wibox.layout.fixed.horizontal}
+ local day_idx = 5 - os.date('%w')
+ for _ = 0, day_idx do
+ table.insert(col, get_square(color_of_empty_cells))
+ end
+
+ local update_widget = function(_, stdout, _, _, _)
+ for intensity in stdout:gmatch("[^\r\n]+") do
+ if day_idx %7 == 0 then
+ table.insert(row, col)
+ col = {layout = wibox.layout.fixed.vertical}
+ end
+ table.insert(col, get_square(widget_themes[theme][tonumber(intensity)]))
+ day_idx = day_idx + 1
+ end
+ github_contributions_widget:setup(
+ {
+ row,
+ top = margin_top,
+ layout = wibox.container.margin
+ }
+ )
+ end
+
+ awful.spawn.easy_async(string.format(GET_CONTRIBUTIONS_CMD, username, days),
+ function(stdout)
+ update_widget(github_contributions_widget, stdout)
+ end)
+
+ return github_contributions_widget
+end
+
+return setmetatable(github_contributions_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/Thomashighbaugh.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/Thomashighbaugh.png
new file mode 100755
index 0000000..b31245b
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/Thomashighbaugh.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/classic.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/classic.png
new file mode 100755
index 0000000..4652140
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/classic.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/dracula.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/dracula.png
new file mode 100755
index 0000000..65fb769
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/dracula.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/leftpad.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/leftpad.png
new file mode 100755
index 0000000..19e4f64
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/leftpad.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/pink.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/pink.png
new file mode 100755
index 0000000..2fb7bc6
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/pink.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot.jpg b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot.jpg
new file mode 100755
index 0000000..15ad456
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot.jpg differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot1.jpg b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot1.jpg
new file mode 100755
index 0000000..d1eeb44
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot1.jpg differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot2.jpg b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot2.jpg
new file mode 100755
index 0000000..5ce47f2
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/screenshot2.jpg differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/standard.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/standard.png
new file mode 100755
index 0000000..e10479a
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/standard.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/teal.png b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/teal.png
new file mode 100755
index 0000000..f10de7a
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-contributions-widget/screenshots/teal.png differ
diff --git a/.config/awesome/awesome-wm-widgets/github-contributions-widget/themes.lua b/.config/awesome/awesome-wm-widgets/github-contributions-widget/themes.lua
new file mode 100755
index 0000000..574f8fa
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-contributions-widget/themes.lua
@@ -0,0 +1,46 @@
+local themes = {
+ standard = {
+ [4] = '#216e39',
+ [3] = '#30a14e',
+ [2] = '#40c463',
+ [1] = '#9be9a8',
+ [0] = '#ebedf0'
+ },
+ classic = {
+ [4] = '#196127',
+ [3] = '#239a3b',
+ [2] = '#7bc96f',
+ [1] = '#c6e48b',
+ [0] = '#ebedf0',
+ },
+ teal = {
+ [4] = '#458B74',
+ [3] = '#66CDAA',
+ [2] = '#76EEC6',
+ [1] = '#7FFFD4',
+ [0] = '#ebedf0',
+ },
+ leftpad = {
+ [4] = '#F6F6F6',
+ [3] = '#DDDDDD',
+ [2] = '#A5A5A5',
+ [1] = '#646464',
+ [0] = '#2F2F2F',
+ },
+ dracula = {
+ [4] = '#ff79c6',
+ [3] = '#bd93f9',
+ [2] = '#6272a4',
+ [1] = '#44475a',
+ [0] = '#282a36'
+ },
+ pink = {
+ [4] = '#61185f',
+ [3] = '#a74aa8',
+ [2] = '#ca5bcc',
+ [1] = '#e48bdc',
+ [0] = '#ebedf0',
+ }
+}
+
+return themes
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/README.md b/.config/awesome/awesome-wm-widgets/github-prs-widget/README.md
new file mode 100755
index 0000000..1d4c27e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/README.md
@@ -0,0 +1,43 @@
+# GitHub PRs Widget
+
+
+
+
+
+The widget shows the number of pull requests assigned to the user and when clicked shows additional information, such as
+ - author's name and avatar (opens user profile page when clicked);
+ - PR name (opens MR when clicked);
+ - name of the repository;
+ - when was created;
+ - number of comments;
+
+
+
+
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `reviewer` | Required | github user login |
+
+## Installation
+
+Install and setup [GitHub CLI](https://cli.github.com/)
+Clone/download repo and use widget in **rc.lua**:
+
+```lua
+local github_prs_widget = require("awesome-wm-widgets.github-prs-widget")
+...
+s.mytasklist, -- Middle widget
+{ -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ github_prs_widget {
+ reviewer = 'streetturtle'
+ },
+}
+...
+```
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/book.svg b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/book.svg
new file mode 100755
index 0000000..7833095
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/book.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/calendar.svg b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/calendar.svg
new file mode 100755
index 0000000..45a15fe
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/calendar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/git-pull-request.svg b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/git-pull-request.svg
new file mode 100755
index 0000000..54c92b9
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/git-pull-request.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/message-square.svg b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/message-square.svg
new file mode 100755
index 0000000..e37df4b
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/message-square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/user.svg b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/user.svg
new file mode 100755
index 0000000..7704341
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/icons/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/init.lua b/.config/awesome/awesome-wm-widgets/github-prs-widget/init.lua
new file mode 100755
index 0000000..8d59ac8
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/github-prs-widget/init.lua
@@ -0,0 +1,434 @@
+-------------------------------------------------
+-- GitHub Widget for Awesome Window Manager
+-- Shows the number of currently assigned merge requests
+-- and information about them
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/github-prs-widget
+
+-- @author Pavel Makhov
+-- @copyright 2021 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local json = require("json")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+local color = require("gears.color")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/github-prs-widget/'
+local ICONS_DIR = WIDGET_DIR .. 'icons/'
+
+local AVATARS_DIR = HOME_DIR .. '/.cache/awmw/github-widget/avatars/'
+local DOWNLOAD_AVATAR_CMD = [[sh -c "curl -L --create-dirs -o ''\\]] .. AVATARS_DIR .. [[%s %s"]]
+
+local GET_PRS_CMD = "gh api -X GET search/issues "
+ .. "-f 'q=review-requested:%s is:unmerged is:open' "
+ .. "-f per_page=30 "
+ .. "--jq '[.items[] | {url,repository_url,title,html_url,comments,assignees,user,created_at,draft}]'"
+
+local github_widget = wibox.widget {
+ {
+ {
+ {
+ {
+ {
+ id = 'icon',
+ widget = wibox.widget.imagebox
+ },
+ {
+ id = 'error_marker',
+ draw = function(_, _, cr, width, height)
+ cr:set_source(color('#BF616A'))
+ cr:arc(width - height / 6, height / 6, height / 6, 0, math.pi * 2)
+ cr:fill()
+ end,
+ visible = false,
+ layout = wibox.widget.base.make_widget,
+ },
+ layout = wibox.layout.stack
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ {
+ id = "new_pr",
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ },
+ left = 4,
+ right = 4,
+ widget = wibox.container.margin
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+ set_text = function(self, new_value)
+ self:get_children_by_id('txt')[1]:set_text(new_value)
+ end,
+ set_icon = function(self, new_value)
+ self:get_children_by_id('icon')[1]:set_image(new_value)
+ end,
+ is_everything_ok = function(self, is_ok)
+ if is_ok then
+ self:get_children_by_id('error_marker')[1]:set_visible(false)
+ self:get_children_by_id('icon')[1]:set_opacity(1)
+ self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
+ else
+ self.txt:set_text('')
+ self:get_children_by_id('error_marker')[1]:set_visible(true)
+ self:get_children_by_id('icon')[1]:set_opacity(0.2)
+ self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
+ end
+ end
+}
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'GitHub PRs Widget',
+ text = message}
+end
+
+local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+--- Converts string representation of date (2020-06-02T11:25:27Z) to date
+local function parse_date(date_str)
+ local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)%Z"
+ local y, m, d, h, min, sec, _ = date_str:match(pattern)
+
+ return os.time{year = y, month = m, day = d, hour = h, min = min, sec = sec}
+end
+
+--- Converts seconds to "time ago" represenation, like '1 hour ago'
+local function to_time_ago(seconds)
+ local days = seconds / 86400
+ if days > 1 then
+ days = math.floor(days + 0.5)
+ return days .. (days == 1 and ' day' or ' days') .. ' ago'
+ end
+
+ local hours = (seconds % 86400) / 3600
+ if hours > 1 then
+ hours = math.floor(hours + 0.5)
+ return hours .. (hours == 1 and ' hour' or ' hours') .. ' ago'
+ end
+
+ local minutes = ((seconds % 86400) % 3600) / 60
+ if minutes > 1 then
+ minutes = math.floor(minutes + 0.5)
+ return minutes .. (minutes == 1 and ' minute' or ' minutes') .. ' ago'
+ end
+end
+
+local function ellipsize(text, length)
+ return (text:len() > length and length > 0)
+ and text:sub(0, length - 3) .. '...'
+ or text
+end
+
+local warning_shown = false
+local tooltip = awful.tooltip {
+ mode = 'outside',
+ preferred_positions = {'bottom'},
+}
+
+local config = {}
+
+config.reviewer = nil
+
+config.bg_normal = '#aaaaaa'
+config.bg_focus = '#ffffff'
+
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ -- Setup config for the widget instance.
+ -- The `_config` table will keep the first existing value after checking
+ -- in this order: user parameter > beautiful > module default
+ local _config = {}
+ for prop, value in pairs(config) do
+ _config[prop] = args[prop] or beautiful[prop] or value
+ end
+
+ local icon = args.icon or ICONS_DIR .. 'git-pull-request.svg'
+ local reviewer = args.reviewer
+ local timeout = args.timeout or 60
+
+ local current_number_of_prs
+
+ local to_review_rows = {layout = wibox.layout.fixed.vertical}
+ local rows = {layout = wibox.layout.fixed.vertical}
+
+ github_widget:set_icon(icon)
+
+ local update_widget = function(widget, stdout, stderr, _, _)
+
+ if stderr ~= '' then
+ if not warning_shown then
+ show_warning(stderr)
+ warning_shown = true
+ widget:is_everything_ok(false)
+ tooltip:add_to_object(widget)
+
+ widget:connect_signal('mouse::enter', function()
+ tooltip.text = stderr
+ end)
+ end
+ return
+ end
+
+ warning_shown = false
+ tooltip:remove_from_object(widget)
+ widget:is_everything_ok(true)
+
+ local prs = json.decode(stdout)
+
+ current_number_of_prs = #prs
+
+ if current_number_of_prs == 0 then
+ widget:set_visible(false)
+ return
+ end
+
+ widget:set_visible(true)
+ widget:set_text(current_number_of_prs)
+
+ for i = 0, #rows do rows[i]=nil end
+
+ for i = 0, #to_review_rows do to_review_rows[i]=nil end
+ table.insert(to_review_rows, {
+ {
+ markup = 'PRs to review',
+ align = 'center',
+ forced_height = 20,
+ widget = wibox.widget.textbox
+ },
+ bg = _config.bg_normal,
+ widget = wibox.container.background
+ })
+
+ local current_time = os.time(os.date("!*t"))
+
+ for _, pr in ipairs(prs) do
+ local path_to_avatar = AVATARS_DIR .. pr.user.id
+ local index = string.find(pr.repository_url, "/[^/]*$")
+ local repo = string.sub(pr.repository_url, index + 1)
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ resize = true,
+ image = path_to_avatar,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+ },
+ id = 'avatar',
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ {
+ {
+ id = 'title',
+ markup = '' .. ellipsize(pr.title, 60) .. '',
+ widget = wibox.widget.textbox,
+ forced_width = 400
+ },
+ {
+ {
+ {
+ {
+ image = ICONS_DIR .. 'book.svg',
+ forced_width = 12,
+ forced_height = 12,
+ resize = true,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = repo,
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+ },
+ {
+ {
+ image = ICONS_DIR .. 'user.svg',
+ forced_width = 12,
+ forced_height = 12,
+ resize = true,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = pr.user.login,
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+ },
+ spacing = 8,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+ },
+ {
+ {
+ {
+ image = ICONS_DIR .. 'user.svg',
+ forced_width = 12,
+ forced_height = 12,
+ resize = true,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = to_time_ago(os.difftime(current_time, parse_date(pr.created_at))),
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+
+ },
+ {
+ {
+ image = ICONS_DIR .. 'message-square.svg',
+ forced_width = 12,
+ forced_height = 12,
+ resize = true,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = pr.comments,
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ layout = wibox.layout.fixed.vertical
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.vertical
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = _config.bg_normal,
+ widget = wibox.container.background
+ }
+
+ if not gfs.file_readable(path_to_avatar) then
+ spawn.easy_async(string.format(
+ DOWNLOAD_AVATAR_CMD,
+ pr.user.id,
+ pr.user.avatar_url), function()
+ row:get_children_by_id('avatar')[1]:set_image(path_to_avatar)
+ end)
+ end
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(_config.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(_config.bg_normal) end)
+
+ row:get_children_by_id('title')[1]:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. pr.html_url)
+ popup.visible = false
+ end)
+ )
+ )
+ row:get_children_by_id('avatar')[1]:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. pr.user.html_url)
+ popup.visible = false
+ end)
+ )
+ )
+
+ local old_cursor, old_wibox
+ row:get_children_by_id('title')[1]:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:get_children_by_id('title')[1]:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:get_children_by_id('avatar')[1]:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:get_children_by_id('avatar')[1]:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ table.insert(to_review_rows, row)
+ end
+
+ table.insert(rows, to_review_rows)
+ popup:setup(rows)
+ end
+
+ github_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ github_widget:set_bg('#00000000')
+ else
+ github_widget:set_bg(beautiful.bg_focus)
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ watch(string.format(GET_PRS_CMD, reviewer),
+ timeout, update_widget, github_widget)
+
+ return github_widget
+end
+
+return setmetatable(github_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/github-prs-widget/screenshots/screenshot1.png b/.config/awesome/awesome-wm-widgets/github-prs-widget/screenshots/screenshot1.png
new file mode 100755
index 0000000..295b56e
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/github-prs-widget/screenshots/screenshot1.png differ
diff --git a/.config/awesome/awesome-wm-widgets/gitlab-widget/README.md b/.config/awesome/awesome-wm-widgets/gitlab-widget/README.md
new file mode 100755
index 0000000..17007bc
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gitlab-widget/README.md
@@ -0,0 +1,48 @@
+# Gitlab widget
+
+
+
+
+
+The widget shows the number of merge requests assigned to the user and when clicked shows additional information, such as
+ - author's name and avatar (opens user profile page when clicked);
+ - MR name (opens MR when clicked);
+ - source and target branches;
+ - when was created;
+ - number of comments;
+ - number of approvals.
+
+
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `icon` | `./icons/gitlab-icon.svg` | Path to the icon |
+| `host` | Required | e.g _https://gitlab.yourcompany.com_ |
+| `access_token` | Required | e.g _h2v531iYASDz6McxYk4A_ |
+| `timeout` | 60 | How often in seconds the widget should be refreshed |
+
+_Note:_
+ - to get the access token, go to **User Settings** -> **Access Tokens** and generate a token with **api** scope
+
+## Installation
+
+Clone/download repo and use widget in **rc.lua**:
+
+```lua
+local gitlab_widget = require("awesome-wm-widgets.gitlab-widget.gitlab")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ gitlab_widget{
+ host = 'https://gitlab.yourcompany.com',
+ access_token = 'h2v531iYASDz6McxYk4A'
+ },
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/gitlab-widget/gitlab.lua b/.config/awesome/awesome-wm-widgets/gitlab-widget/gitlab.lua
new file mode 100755
index 0000000..c0716d7
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gitlab-widget/gitlab.lua
@@ -0,0 +1,401 @@
+-------------------------------------------------
+-- Gitlab Widget for Awesome Window Manager
+-- Shows the number of currently assigned merge requests
+-- and information about them
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/gitlab-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local json = require("json")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+local color = require("gears.color")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/gitlab-widget/'
+local GET_PRS_CMD= [[sh -c "curl -s --connect-timeout 5 --show-error --header 'PRIVATE-TOKEN: %s']]
+ ..[[ '%s/api/v4/merge_requests?state=opened'"]]
+local DOWNLOAD_AVATAR_CMD = [[sh -c "curl -L --create-dirs -o %s/.cache/awmw/gitlab-widget/avatars/%s %s"]]
+
+local gitlab_widget = wibox.widget {
+ {
+ {
+ {
+ id = 'icon',
+ widget = wibox.widget.imagebox
+ },
+ {
+ id = 'error_marker',
+ draw = function(_, _, cr, width, height)
+ cr:set_source(color(beautiful.fg_urgent))
+ cr:arc(width - height/6, height/6, height/6, 0, math.pi*2)
+ cr:fill()
+ end,
+ visible = false,
+ layout = wibox.widget.base.make_widget,
+ },
+ layout = wibox.layout.stack
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ {
+ id = "new_pr",
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_text = function(self, new_value)
+ self.txt.text = new_value
+ end,
+ set_icon = function(self, new_value)
+ self:get_children_by_id('icon')[1]:set_image(new_value)
+ end,
+ is_everything_ok = function(self, is_ok)
+ if is_ok then
+ self:get_children_by_id('error_marker')[1]:set_visible(false)
+ self:get_children_by_id('icon')[1]:set_opacity(1)
+ self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
+ else
+ self.txt:set_text('')
+ self:get_children_by_id('error_marker')[1]:set_visible(true)
+ self:get_children_by_id('icon')[1]:set_opacity(0.2)
+ self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
+ end
+ end
+}
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Gitlab Widget',
+ text = message}
+end
+
+local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+--- Converts string representation of date (2020-06-02T11:25:27Z) to date
+local function parse_date(date_str)
+ local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)%Z"
+ local y, m, d, h, min, sec, _ = date_str:match(pattern)
+
+ return os.time{year = y, month = m, day = d, hour = h, min = min, sec = sec}
+end
+
+--- Converts seconds to "time ago" represenation, like '1 hour ago'
+local function to_time_ago(seconds)
+ local days = seconds / 86400
+ if days > 1 then
+ days = math.floor(days + 0.5)
+ return days .. (days == 1 and ' day' or ' days') .. ' ago'
+ end
+
+ local hours = (seconds % 86400) / 3600
+ if hours > 1 then
+ hours = math.floor(hours + 0.5)
+ return hours .. (hours == 1 and ' hour' or ' hours') .. ' ago'
+ end
+
+ local minutes = ((seconds % 86400) % 3600) / 60
+ if minutes > 1 then
+ minutes = math.floor(minutes + 0.5)
+ return minutes .. (minutes == 1 and ' minute' or ' minutes') .. ' ago'
+ end
+end
+
+local function ellipsize(text, length)
+ return (text:len() > length and length > 0)
+ and text:sub(0, length - 3) .. '...'
+ or text
+end
+
+local warning_shown = false
+local tooltip = awful.tooltip {
+ mode = 'outside',
+ preferred_positions = {'bottom'},
+ }
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or WIDGET_DIR .. '/icons/gitlab-icon.svg'
+ local access_token = args.access_token or show_warning('API Token is not set')
+ local host = args.host or show_warning('Gitlab host is not set')
+ local timeout = args.timeout or 60
+
+ local current_number_of_prs
+
+ local to_review_rows = {layout = wibox.layout.fixed.vertical}
+ local my_review_rows = {layout = wibox.layout.fixed.vertical}
+ local rows = {layout = wibox.layout.fixed.vertical}
+
+ gitlab_widget:set_icon(icon)
+
+ local update_widget = function(widget, stdout, stderr, _, _)
+
+ if stderr ~= '' then
+ if not warning_shown then
+ show_warning(stderr)
+ warning_shown = true
+ widget:is_everything_ok(false)
+ tooltip:add_to_object(widget)
+
+ widget:connect_signal('mouse::enter', function()
+ tooltip.text = stderr
+ end)
+ end
+ return
+ end
+
+ warning_shown = false
+ tooltip:remove_from_object(widget)
+ widget:is_everything_ok(true)
+
+ local result = json.decode(stdout)
+
+ current_number_of_prs = rawlen(result)
+
+ if current_number_of_prs == 0 then
+ widget:set_visible(false)
+ return
+ end
+
+ widget:set_visible(true)
+ widget:set_text(current_number_of_prs)
+
+ for i = 0, #rows do rows[i]=nil end
+
+ for i = 0, #to_review_rows do to_review_rows[i]=nil end
+ table.insert(to_review_rows, {
+ {
+ markup = 'PRs to review',
+ align = 'center',
+ forced_height = 20,
+ widget = wibox.widget.textbox
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+
+ for i = 0, #my_review_rows do my_review_rows[i]=nil end
+ table.insert(my_review_rows, {
+ {
+ markup = 'My PRs',
+ align = 'center',
+ forced_height = 20,
+ widget = wibox.widget.textbox
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+ local current_time = os.time(os.date("!*t"))
+
+ for _, pr in ipairs(result) do
+ local path_to_avatar = os.getenv("HOME") ..'/.cache/awmw/gitlab-widget/avatars/' .. pr.author.id
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ resize = true,
+ image = path_to_avatar,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+ },
+ id = 'avatar',
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ {
+ {
+ id = 'title',
+ markup = '' .. ellipsize(pr.title, 50) .. '',
+ widget = wibox.widget.textbox,
+ forced_width = 400
+ },
+ {
+ {
+ {
+ {
+ text = pr.source_branch,
+ widget = wibox.widget.textbox
+ },
+ {
+ text = '->',
+ widget = wibox.widget.textbox
+ },
+ {
+ text = pr.target_branch,
+ widget = wibox.widget.textbox
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ {
+ {
+ text = pr.author.name,
+ widget = wibox.widget.textbox
+ },
+ {
+ text = to_time_ago(os.difftime(current_time, parse_date(pr.created_at))),
+ widget = wibox.widget.textbox
+ },
+ spacing = 8,
+ expand = 'none',
+ layout = wibox.layout.fixed.horizontal
+ },
+ forced_width = 285,
+ layout = wibox.layout.fixed.vertical
+ },
+ {
+ {
+ {
+ -- image = number_of_approves > 0 and WIDGET_DIR .. '/check.svg' or '',
+ image = WIDGET_DIR .. '/icons/check.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = pr.upvotes,
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+ {
+ {
+ image = WIDGET_DIR .. '/icons/message-circle.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = pr.user_notes_count,
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+ layout = wibox.layout.fixed.vertical
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+
+ spacing = 8,
+ layout = wibox.layout.fixed.vertical
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ if not gfs.file_readable(path_to_avatar) then
+ spawn.easy_async(string.format(
+ DOWNLOAD_AVATAR_CMD,
+ HOME_DIR,
+ pr.author.id,
+ pr.author.avatar_url), function()
+ row:get_children_by_id('avatar')[1]:set_image(path_to_avatar)
+ end)
+ end
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ row:get_children_by_id('title')[1]:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. pr.web_url)
+ popup.visible = false
+ end)
+ )
+ )
+ row:get_children_by_id('avatar')[1]:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. pr.author.web_url)
+ popup.visible = false
+ end)
+ )
+ )
+
+ local old_cursor, old_wibox
+ row:get_children_by_id('title')[1]:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:get_children_by_id('title')[1]:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:get_children_by_id('avatar')[1]:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:get_children_by_id('avatar')[1]:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ table.insert(to_review_rows, row)
+ end
+
+ table.insert(rows, to_review_rows)
+ if (#my_review_rows > 1) then
+ table.insert(rows, my_review_rows)
+ end
+ popup:setup(rows)
+ end
+
+ gitlab_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ watch(string.format(GET_PRS_CMD, access_token, host),
+ -- string.format(GET_PRS_CMD, host, workspace, repo_slug, uuid, uuid),
+ timeout, update_widget, gitlab_widget)
+ return gitlab_widget
+end
+
+return setmetatable(gitlab_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/check.svg b/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/check.svg
new file mode 100755
index 0000000..e9e44ac
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/gitlab-icon.svg b/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/gitlab-icon.svg
new file mode 100755
index 0000000..abe3f37
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/gitlab-icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/message-circle.svg b/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/message-circle.svg
new file mode 100755
index 0000000..43eacbb
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/gitlab-widget/icons/message-circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/gitlab-widget/screenshot.png b/.config/awesome/awesome-wm-widgets/gitlab-widget/screenshot.png
new file mode 100755
index 0000000..8ab6590
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/gitlab-widget/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/jira-widget/README.md b/.config/awesome/awesome-wm-widgets/jira-widget/README.md
new file mode 100755
index 0000000..85abaf2
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/jira-widget/README.md
@@ -0,0 +1,59 @@
+# Jira widget
+
+The widget shows the number of tickets assigned to the user (or any other result of a JQL query, see customization section) and when clicked shows them in the list, grouped by the ticket status. Left-click on the item opens the issue in the default browser:
+
+
+
+
+
+## How it works
+
+Widget uses cURL to query Jira's [REST API](https://developer.atlassian.com/server/jira/platform/rest-apis/). In order to be authenticated, widget uses a [netrc](https://ec.haxx.se/usingcurl/usingcurl-netrc) feature of the cURL, which is basically to store basic auth credentials in a .netrc file in home folder.
+
+If you are on Atlassian Cloud, then instead of providing a password in netrc file you can set an [API token](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) which is a safer option, as you can revoke/change the token at any time.
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `host` | Required | Ex: _http://jira.tmnt.com_ |
+| `query` | `jql=assignee=currentuser() AND resolution=Unresolved` | JQL query |
+| `icon` | `~/.config/awesome/awesome-wm-widgets/jira-widget/jira-mark-gradient-blue.svg` | Path to the icon |
+| `timeout` | 600 | How often in seconds the widget refreshes |
+
+## Installation
+
+Create a .netrc file in your home directory with following content:
+
+```bash
+machine turtlejira.com
+login mikey@tmnt.com
+password cowabunga
+```
+
+Then change file's permissions to 600 (so only you can read/write it):
+
+```bash
+chmod 600 ~/.netrc
+```
+And test if it works by calling the API (`-n` option is to use the .netrc file for authentication):
+
+```bash
+curl -n 'https://turtleninja.com/rest/api/2/search?jql=assignee=currentuser()+AND+resolution=Unresolved'
+```
+
+Clone/download repo and use the widget in **rc.lua**:
+
+```lua
+local jira_widget = require("awesome-wm-widgets.jira-widget.jira")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ jira_widget({host = 'http://jira.tmnt.com'}),
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/jira-widget/icon/jira-mark-gradient-blue.svg b/.config/awesome/awesome-wm-widgets/jira-widget/icon/jira-mark-gradient-blue.svg
new file mode 100755
index 0000000..74ea729
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/jira-widget/icon/jira-mark-gradient-blue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/jira-widget/jira.lua b/.config/awesome/awesome-wm-widgets/jira-widget/jira.lua
new file mode 100755
index 0000000..f34e951
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/jira-widget/jira.lua
@@ -0,0 +1,305 @@
+-------------------------------------------------
+-- Jira Widget for Awesome Window Manager
+-- Shows the number of currently assigned issues
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/jira-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local json = require("json")
+local spawn = require("awful.spawn")
+local naughty = require("naughty")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+local color = require("gears.color")
+
+local HOME_DIR = os.getenv("HOME")
+
+local GET_ISSUES_CMD =
+ [[bash -c "curl -s --show-error -X GET -n '%s/rest/api/2/search?%s&fields=id,assignee,summary,status'"]]
+local DOWNLOAD_AVATAR_CMD = [[bash -c "curl -n --create-dirs -o %s/.cache/awmw/jira-widget/avatars/%s %s"]]
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Jira Widget',
+ text = message}
+end
+
+local jira_widget = wibox.widget {
+ {
+ {
+ {
+ {
+ id = 'c',
+ widget = wibox.widget.imagebox
+ },
+ {
+ id = 'd',
+ draw = function(_, _, cr, width, height)
+ cr:set_source(color(beautiful.fg_urgent))
+ cr:arc(width - height / 6, height / 6, height / 6, 0, math.pi * 2)
+ cr:fill()
+ end,
+ visible = false,
+ layout = wibox.widget.base.make_widget,
+ },
+ id = 'b',
+ layout = wibox.layout.stack
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+ set_text = function(self, new_value)
+ self:get_children_by_id('txt')[1]:set_text(new_value)
+ --self.txt.text = new_value
+ end,
+ set_icon = function(self, path)
+ self:get_children_by_id('c')[1]:set_image(path)
+ end,
+ is_everything_ok = function(self, is_ok)
+ if is_ok then
+ self:get_children_by_id('d')[1]:set_visible(false)
+ self:get_children_by_id('c')[1]:set_opacity(1)
+ self:get_children_by_id('c')[1]:emit_signal('widget:redraw_needed')
+ else
+ --self.txt:set_text('')
+ self:get_children_by_id('txt')[1]:set_text('')
+ self:get_children_by_id('d')[1]:set_visible(true)
+ self:get_children_by_id('c')[1]:set_opacity(0.2)
+ self:get_children_by_id('c')[1]:emit_signal('widget:redraw_needed')
+ end
+ end
+}
+
+local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local number_of_issues
+
+local warning_shown = false
+local tooltip = awful.tooltip {
+ mode = 'outside',
+ preferred_positions = {'bottom'},
+ }
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or
+ HOME_DIR .. '/.config/awesome/awesome-wm-widgets/jira-widget/icon/jira-mark-gradient-blue.svg'
+ local host = args.host or show_warning('Jira host is unknown')
+ local query = args.query or 'jql=assignee=currentuser() AND resolution=Unresolved'
+ local timeout = args.timeout or 600
+
+ jira_widget:set_icon(icon)
+
+ local separator_widget = {
+ orientation = 'horizontal',
+ forced_height = 1,
+ color = beautiful.bg_focus,
+ widget = wibox.widget.separator
+ }
+
+ local update_widget = function(widget, stdout, stderr, _, _)
+ if stderr ~= '' then
+ if not warning_shown then
+ show_warning(stderr)
+ warning_shown = true
+ widget:is_everything_ok(false)
+ tooltip:add_to_object(widget)
+
+ widget:connect_signal('mouse::enter', function()
+ tooltip.text = stderr
+ end)
+ end
+ return
+ end
+
+ warning_shown = false
+ tooltip:remove_from_object(widget)
+ widget:is_everything_ok(true)
+
+ local result = json.decode(stdout)
+
+ number_of_issues = rawlen(result.issues)
+
+ if number_of_issues == 0 then
+ widget:set_visible(false)
+ return
+ end
+
+ widget:set_visible(true)
+ widget:set_text(number_of_issues)
+
+ local rows = { layout = wibox.layout.fixed.vertical }
+
+ for i = 0, #rows do rows[i]=nil end
+
+ -- sort issues based on the status
+ table.sort(result.issues, function(a,b) return a.fields.status.name > b.fields.status.name end)
+
+ local cur_status = ''
+ for _, issue in ipairs(result.issues) do
+
+ local name
+ if issue.fields.assignee.name == nil then
+ name = issue.fields.assignee.displayName
+ else
+ name = issue.fields.assignee.name
+ end
+
+ local path_to_avatar = HOME_DIR ..'/.cache/awmw/jira-widget/avatars/' .. name
+
+ if not gfs.file_readable(path_to_avatar) then
+ spawn.easy_async(string.format(
+ DOWNLOAD_AVATAR_CMD,
+ HOME_DIR,
+ name,
+ issue.fields.assignee.avatarUrls['48x48']))
+ end
+
+ if (cur_status ~= issue.fields.status.name) then
+ -- do not insert separator before first item
+ if (cur_status ~= '') then
+ table.insert(rows, separator_widget)
+ end
+
+ table.insert(rows, wibox.widget {
+ {
+ {
+ markup = "" .. issue.fields.status.name .. "",
+ widget = wibox.widget.textbox,
+ },
+ left = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+ cur_status = issue.fields.status.name
+ end
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ {
+ resize = true,
+ image = path_to_avatar,
+ forced_width = 40,
+ forced_height = 40,
+ widget = wibox.widget.imagebox
+ },
+ left = 4,
+ layout = wibox.container.margin
+ },
+ {
+ {
+ markup = '' .. issue.fields.summary .. '',
+ widget = wibox.widget.textbox
+ },
+ {
+ {
+ markup = "" .. issue.key .. "",
+ widget = wibox.widget.textbox
+ },
+ {
+ markup = ""
+ .. issue.fields.assignee.displayName .. "",
+ widget = wibox.widget.textbox
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ layout = wibox.layout.align.vertical
+ },
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ local old_cursor, old_wibox
+ row:connect_signal("mouse::enter", function(c)
+ c:set_bg(beautiful.bg_focus)
+ c:set_shape(function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end)
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:connect_signal("mouse::leave", function(c)
+ c:set_bg(beautiful.bg_normal)
+ c:set_shape(nil)
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("xdg-open " .. host .. '/browse/' .. issue.key)
+ popup.visible = false
+ jira_widget:set_bg('#00000000')
+ end)
+ )
+ )
+
+ table.insert(rows, row)
+ end
+
+ popup:setup(rows)
+ end
+
+ jira_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ jira_widget:set_bg('#00000000')
+ popup.visible = not popup.visible
+ else
+ jira_widget:set_bg(beautiful.bg_focus)
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+ watch(string.format(GET_ISSUES_CMD, host, query:gsub(' ', '+')), timeout, update_widget, jira_widget)
+ return jira_widget
+end
+
+return setmetatable(jira_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/jira-widget/screenshot/screenshot.png b/.config/awesome/awesome-wm-widgets/jira-widget/screenshot/screenshot.png
new file mode 100755
index 0000000..7bfe9b6
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/jira-widget/screenshot/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/README.md b/.config/awesome/awesome-wm-widgets/logout-menu-widget/README.md
new file mode 100755
index 0000000..6f8ca27
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/README.md
@@ -0,0 +1,45 @@
+# Logout Menu Widget
+
+This widget shows a menu with options to log out from the current session, lock, reboot, suspend and power off the computer, similar to [logout-popup-widget](https://github.com/streetturtle/awesome-wm-widgets/tree/master/logout-popup-widget):
+
+
+
+## Installation
+
+Clone this repo (if not cloned yet) under **./.config/awesome/**
+
+```bash
+cd ./.config/awesome/
+git clone https://github.com/streetturtle/awesome-wm-widgets
+```
+Then add the widget to the wibar:
+
+```lua
+local logout_menu_widget = require("awesome-wm-widgets.logout-menu-widget.logout-menu")
+
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ logout_menu_widget(),
+ -- custom
+ logout_menu_widget{
+ font = 'Play 14',
+ onlock = function() awful.spawn.with_shell('i3lock-fancy') end
+ }
+ ...
+```
+
+## Customization
+
+It is possible to customize the widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `font` | `beautiful.font` | Font of the menu items |
+| `onlogout` | `function() awesome.quit() end` | Function which is called when the logout item is clicked |
+| `onlock` | `function() awful.spawn.with_shell("i3lock") end` | Function which is called when the lock item is clicked |
+| `onreboot` | `function() awful.spawn.with_shell("reboot") end` | Function which is called when the reboot item is clicked |
+| `onsuspend` | `function() awful.spawn.with_shell("systemctl suspend") end` | Function which is called when the suspend item is clicked |
+| `onpoweroff` | `function() awful.spawn.with_shell("shutdown now") end` | Function which is called when the poweroff item is clicked |
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/lock.svg b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/lock.svg
new file mode 100755
index 0000000..3cfa528
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/log-out.svg b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/log-out.svg
new file mode 100755
index 0000000..77afebb
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/log-out.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/moon.svg b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/moon.svg
new file mode 100755
index 0000000..60e6ce8
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/power.svg b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/power.svg
new file mode 100755
index 0000000..68b1be8
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/power.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/power_w.svg b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/power_w.svg
new file mode 100755
index 0000000..1f9c4e3
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/power_w.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/refresh-cw.svg b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/refresh-cw.svg
new file mode 100755
index 0000000..39f52a5
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/refresh-cw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/logout-menu.gif b/.config/awesome/awesome-wm-widgets/logout-menu-widget/logout-menu.gif
new file mode 100755
index 0000000..9f17b51
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-menu-widget/logout-menu.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-menu-widget/logout-menu.lua b/.config/awesome/awesome-wm-widgets/logout-menu-widget/logout-menu.lua
new file mode 100755
index 0000000..85311b0
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-menu-widget/logout-menu.lua
@@ -0,0 +1,136 @@
+-------------------------------------------------
+-- Logout Menu Widget for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/logout-menu-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local gears = require("gears")
+local beautiful = require("beautiful")
+
+local HOME = os.getenv('HOME')
+local ICON_DIR = HOME .. '/.config/awesome/awesome-wm-widgets/logout-menu-widget/icons/'
+
+local logout_menu_widget = wibox.widget {
+ {
+ {
+ image = ICON_DIR .. 'power_w.svg',
+ resize = true,
+ widget = wibox.widget.imagebox,
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+}
+
+local popup = awful.popup {
+ ontop = true,
+ visible = false,
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local function worker(user_args)
+ local rows = { layout = wibox.layout.fixed.vertical }
+
+ local args = user_args or {}
+
+ local font = args.font or beautiful.font
+
+ local onlogout = args.onlogout or function () awesome.quit() end
+ local onlock = args.onlock or function() awful.spawn.with_shell("i3lock") end
+ local onreboot = args.onreboot or function() awful.spawn.with_shell("reboot") end
+ local onsuspend = args.onsuspend or function() awful.spawn.with_shell("systemctl suspend") end
+ local onpoweroff = args.onpoweroff or function() awful.spawn.with_shell("shutdown now") end
+
+ local menu_items = {
+ { name = 'Log out', icon_name = 'log-out.svg', command = onlogout },
+ { name = 'Lock', icon_name = 'lock.svg', command = onlock },
+ { name = 'Reboot', icon_name = 'refresh-cw.svg', command = onreboot },
+ { name = 'Suspend', icon_name = 'moon.svg', command = onsuspend },
+ { name = 'Power off', icon_name = 'power.svg', command = onpoweroff },
+ }
+
+ for _, item in ipairs(menu_items) do
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ image = ICON_DIR .. item.icon_name,
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ {
+ text = item.name,
+ font = font,
+ widget = wibox.widget.textbox
+ },
+ spacing = 12,
+ layout = wibox.layout.fixed.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ local old_cursor, old_wibox
+ row:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:buttons(awful.util.table.join(awful.button({}, 1, function()
+ popup.visible = not popup.visible
+ item.command()
+ end)))
+
+ table.insert(rows, row)
+ end
+ popup:setup(rows)
+
+ logout_menu_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ logout_menu_widget:set_bg('#00000000')
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ logout_menu_widget:set_bg(beautiful.bg_focus)
+ end
+ end)
+ )
+ )
+
+ return logout_menu_widget
+
+end
+
+return setmetatable(logout_menu_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/README.md b/.config/awesome/awesome-wm-widgets/logout-popup-widget/README.md
new file mode 100755
index 0000000..d95b692
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-popup-widget/README.md
@@ -0,0 +1,84 @@
+# Logout Popup Widget
+
+Widget which allows performing lock, reboot, log out, power off and sleep actions. It can be called either by a shortcut, or by clicking on a widget in wibar.
+
+
+
+
+
+When the widget is shown, following shortcuts can be used:
+ - Escape - hide widget
+ - s - shutdown
+ - r - reboot
+ - u - suspend
+ - k - lock
+ - l - log out
+
+# Installation
+
+Clone this (if not cloned yet) and the [awesome-buttons](https://github.com/streetturtle/awesome-buttons) repos under **./.config/awesome/**
+
+```bash
+cd ./.config/awesome/
+git clone https://github.com/streetturtle/awesome-wm-widgets
+git clone https://github.com/streetturtle/awesome-buttons
+```
+Then
+
+- to show by a shortcut - define a shortcut in `globalkeys`:
+
+ ```lua
+ local logout_popup = require("awesome-wm-widgets.logout-popup-widget.logout-popup")
+ ...
+ globalkeys = gears.table.join(
+ ...
+ awful.key({ modkey }, "l", function() logout_popup.launch() end, {description = "Show logout screen", group = "custom"}),
+ ```
+
+- to show by clicking on a widget in wibar - add widget to the wibar:
+
+ ```lua
+ local logout_popup = require("awesome-wm-widgets.logout-popup-widget.logout-popup")
+
+ s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ logout_popup.widget{},
+ ...
+ ```
+
+# Customisation
+
+| Name | Default | Description |
+|---|---|---|
+| `icon` | `power.svg` | If used as widget - the path to the widget's icon |
+| `icon_size` | `40` | Size of the icon |
+| `icon_margin` | `16` | Margin around the icon |
+| `bg_color` | `beautiful.bg_normal` | The color the background of the |
+| `accent_color` | `beautiful.bg_focus` | The color of the buttons |
+| `text_color` | `beautiful.fg_normal` | The color of text |
+| `label_color` | `beautiful.fg_normal` | The color of the button's label |
+| `phrases` | `{'Goodbye!'}` | The table with phrase(s) to show, if more than one provided, the phrase is chosen randomly. Leave empty (`{}`) to hide the phrase |
+| `onlogout` | `function() awesome.quit() end` | Function which is called when the logout button is pressed |
+| `onlock` | `function() awful.spawn.with_shell("systemctl suspend") end` | Function which is called when the lock button is pressed |
+| `onreboot` | `function() awful.spawn.with_shell("reboot") end` | Function which is called when the reboot button is pressed |
+| `onsuspend` | `function() awful.spawn.with_shell("systemctl suspend") end` | Function which is called when the suspend button is pressed |
+| `onpoweroff` | `function() awful.spawn.with_shell("shutdown now") end` | Function which is called when the poweroff button is pressed |
+
+Some color themes for inspiration:
+
+
+
+
+
+
+```lua
+logout.launch{
+ bg_color = "#261447", accent_color = "#ff4365", text_color = '#f706cf', icon_size = 40, icon_margin = 16, -- outrun
+ -- bg_color = "#0b0c10", accent_color = "#1f2833", text_color = '#66fce1', -- dark
+ -- bg_color = "#3B4252", accent_color = "#88C0D0", text_color = '#D8DEE9', -- nord
+ -- bg_color = "#282a36", accent_color = "#ff79c6", phrases = {}, -- dracula, no phrase
+ phrases = {"exit(0)", "Don't forget to be awesome.", "Yippee ki yay!"},
+}
+```
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-dark.png b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-dark.png
new file mode 100755
index 0000000..06e7c9c
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-dark.png differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-dracula.png b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-dracula.png
new file mode 100755
index 0000000..3c61c46
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-dracula.png differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-nord.png b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-nord.png
new file mode 100755
index 0000000..9ab4b55
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-nord.png differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-outrun.png b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-outrun.png
new file mode 100755
index 0000000..9be68b5
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-outrun.png differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-popup.lua b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-popup.lua
new file mode 100755
index 0000000..efe6882
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-popup-widget/logout-popup.lua
@@ -0,0 +1,187 @@
+-------------------------------------------------
+-- Logout widget for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/logout-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local capi = {keygrabber = keygrabber }
+local wibox = require("wibox")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local awesomebuttons = require("awesome-buttons.awesome-buttons")
+
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/logout-popup-widget'
+
+
+local w = wibox {
+ bg = beautiful.fg_normal,
+ max_widget_size = 500,
+ ontop = true,
+ height = 200,
+ width = 400,
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 8)
+ end
+}
+
+local action = wibox.widget {
+ text = ' ',
+ widget = wibox.widget.textbox
+}
+
+local phrase_widget = wibox.widget{
+ align = 'center',
+ widget = wibox.widget.textbox
+}
+
+local function create_button(icon_name, action_name, accent_color, label_color, onclick, icon_size, icon_margin)
+
+ local button = awesomebuttons.with_icon {
+ type = 'basic',
+ icon = icon_name,
+ color = accent_color,
+ icon_size = icon_size,
+ icon_margin = icon_margin,
+ onclick = function()
+ onclick()
+ w.visible = false
+ capi.keygrabber.stop()
+ end
+ }
+ button:connect_signal("mouse::enter", function()
+ action:set_markup('' .. action_name .. '')
+ end)
+
+ button:connect_signal("mouse::leave", function() action:set_markup('') end)
+
+ return button
+end
+
+local function launch(args)
+ args = args or {}
+
+ local bg_color = args.bg_color or beautiful.bg_normal
+ local accent_color = args.accent_color or beautiful.bg_focus
+ local text_color = args.text_color or beautiful.fg_normal
+ local label_color = args.label_color or beautiful.fg_focus
+ local phrases = args.phrases or {'Goodbye!'}
+ local icon_size = args.icon_size or 40
+ local icon_margin = args.icon_margin or 16
+
+ local onlogout = args.onlogout or function () awesome.quit() end
+ local onlock = args.onlock or function() awful.spawn.with_shell("i3lock") end
+ local onreboot = args.onreboot or function() awful.spawn.with_shell("reboot") end
+ local onsuspend = args.onsuspend or function() awful.spawn.with_shell("systemctl suspend") end
+ local onpoweroff = args.onpoweroff or function() awful.spawn.with_shell("shutdown now") end
+
+ w:set_bg(bg_color)
+ if #phrases > 0 then
+ phrase_widget:set_markup(
+ '' .. phrases[ math.random( #phrases ) ] .. '')
+ end
+
+ w:setup {
+ {
+ phrase_widget,
+ {
+ {
+ create_button('log-out', 'Log Out (l)',
+ accent_color, label_color, onlogout, icon_size, icon_margin),
+ create_button('lock', 'Lock (k)',
+ accent_color, label_color, onlock, icon_size, icon_margin),
+ create_button('refresh-cw', 'Reboot (r)',
+ accent_color, label_color, onreboot, icon_size, icon_margin),
+ create_button('moon', 'Suspend (u)',
+ accent_color, label_color, onsuspend, icon_size, icon_margin),
+ create_button('power', 'Power Off (s)',
+ accent_color, label_color, onpoweroff, icon_size, icon_margin),
+ id = 'buttons',
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ action,
+ haligh = 'center',
+ layout = wibox.container.place
+ },
+ spacing = 32,
+ layout = wibox.layout.fixed.vertical
+ },
+ id = 'a',
+ shape_border_width = 1,
+ valign = 'center',
+ layout = wibox.container.place
+ }
+
+ w.screen = mouse.screen
+ w.visible = true
+
+ awful.placement.centered(w)
+ capi.keygrabber.run(function(_, key, event)
+ if event == "release" then return end
+ if key then
+ if key == 'Escape' then
+ phrase_widget:set_text('')
+ capi.keygrabber.stop()
+ w.visible = false
+ elseif key == 's' then onpoweroff()
+ elseif key == 'r' then onreboot()
+ elseif key == 'u' then onsuspend()
+ elseif key == 'k' then onlock()
+ elseif key == 'l' then onlogout()
+ end
+
+ if key == 'Escape' or string.match("srukl", key) then
+ phrase_widget:set_text('')
+ capi.keygrabber.stop()
+ w.visible = false
+ end
+ end
+ end)
+end
+
+local function widget(args)
+ local icon = args.icon or WIDGET_DIR .. '/power.svg'
+
+ local res = wibox.widget {
+ {
+ {
+ image = icon,
+ widget = wibox.widget.imagebox
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ layout = wibox.layout.fixed.horizontal,
+ }
+
+ res:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if w.visible then
+ phrase_widget:set_text('')
+ capi.keygrabber.stop()
+ w.visible = false
+ else
+ launch(args)
+ end
+ end)
+ ))
+
+ return res
+
+end
+
+return {
+ launch = launch,
+ widget = widget
+}
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/power.svg b/.config/awesome/awesome-wm-widgets/logout-popup-widget/power.svg
new file mode 100755
index 0000000..1f9c4e3
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/logout-popup-widget/power.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/screenshot.gif b/.config/awesome/awesome-wm-widgets/logout-popup-widget/screenshot.gif
new file mode 100755
index 0000000..4975c19
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-popup-widget/screenshot.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/logout-popup-widget/screenshot.png b/.config/awesome/awesome-wm-widgets/logout-popup-widget/screenshot.png
new file mode 100755
index 0000000..74ed7f0
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/logout-popup-widget/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/mpdarc-widget/README.md b/.config/awesome/awesome-wm-widgets/mpdarc-widget/README.md
new file mode 100755
index 0000000..2192410
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/mpdarc-widget/README.md
@@ -0,0 +1,26 @@
+# MPD Widget
+
+Music Player Daemon widget by @raphaelfournier.
+
+# Prerequisite
+
+Install `mpd` (Music Player Daemon itself) and `mpc` (Music Player Client - program for controlling mpd), both should be available in repo, e.g for Ubuntu:
+
+```bash
+sudo apt-get install mpd mpc
+```
+
+## Installation
+
+To use this widget clone repo under **~/.config/awesome/** and then add it in **rc.lua**:
+
+```lua
+local mpdarc_widget = require("awesome-wm-widgets.mpdarc-widget.mpdarc")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ mpdarc_widget,
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/mpdarc-widget/mpdarc.lua b/.config/awesome/awesome-wm-widgets/mpdarc-widget/mpdarc.lua
new file mode 100755
index 0000000..f1d6930
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/mpdarc-widget/mpdarc.lua
@@ -0,0 +1,119 @@
+-------------------------------------------------
+-- mpd Arc Widget for Awesome Window Manager
+-- Modelled after Pavel Makhov's work
+
+-- @author Raphaël Fournier-S'niehotta
+-- @copyright 2018 Raphaël Fournier-S'niehotta
+-------------------------------------------------
+
+local awful = require("awful")
+local beautiful = require("beautiful")
+local spawn = require("awful.spawn")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+local naughty = require("naughty")
+
+local GET_MPD_CMD = "mpc status"
+local TOGGLE_MPD_CMD = "mpc toggle"
+local PAUSE_MPD_CMD = "mpc pause"
+local STOP_MPD_CMD = "mpc stop"
+local NEXT_MPD_CMD = "mpc next"
+local PREV_MPD_CMD = "mpc prev"
+
+local PATH_TO_ICONS = "/usr/share/icons/Arc"
+local PAUSE_ICON_NAME = PATH_TO_ICONS .. "/actions/24/player_pause.png"
+local PLAY_ICON_NAME = PATH_TO_ICONS .. "/actions/24/player_play.png"
+local STOP_ICON_NAME = PATH_TO_ICONS .. "/actions/24/player_stop.png"
+
+local icon = wibox.widget {
+ id = "icon",
+ widget = wibox.widget.imagebox,
+ image = PLAY_ICON_NAME
+ }
+local mirrored_icon = wibox.container.mirror(icon, { horizontal = true })
+
+local mpdarc = wibox.widget {
+ mirrored_icon,
+ max_value = 1,
+ value = 0.75,
+ thickness = 2,
+ start_angle = 4.71238898, -- 2pi*3/4
+ forced_height = 32,
+ forced_width = 32,
+ rounded_edge = true,
+ bg = "#ffffff11",
+ paddings = 0,
+ widget = wibox.container.arcchart
+}
+
+local mpdarc_icon_widget = wibox.container.mirror(mpdarc, { horizontal = true })
+local mpdarc_current_song_widget = wibox.widget {
+ id = 'current_song',
+ widget = wibox.widget.textbox,
+ font = 'Play 9'
+}
+
+local update_graphic = function(widget, stdout, _, _, _)
+ local current_song = string.gmatch(stdout, "[^\r\n]+")()
+ stdout = string.gsub(stdout, "\n", "")
+ local mpdpercent = string.match(stdout, "(%d%d)%%")
+ local mpdstatus = string.match(stdout, "%[(%a+)%]")
+ if mpdstatus == "playing" then
+ icon.image = PLAY_ICON_NAME
+ widget.colors = { beautiful.widget_main_color }
+ widget.value = tonumber((100-mpdpercent)/100)
+ mpdarc_current_song_widget.markup = current_song
+ elseif mpdstatus == "paused" then
+ icon.image = PAUSE_ICON_NAME
+ widget.colors = { beautiful.widget_main_color }
+ widget.value = tonumber(mpdpercent/100)
+ mpdarc_current_song_widget.markup = current_song
+ else
+ icon.image = STOP_ICON_NAME
+ if string.len(stdout) == 0 then -- MPD is not running
+ mpdarc_current_song_widget.markup = "MPD is not running"
+ else
+ widget.colors = { beautiful.widget_red }
+ mpdarc_current_song_widget.markup = ""
+ end
+ end
+end
+
+mpdarc:connect_signal("button::press", function(_, _, _, button)
+ if (button == 1) then awful.spawn(TOGGLE_MPD_CMD, false) -- left click
+ elseif (button == 2) then awful.spawn(STOP_MPD_CMD, false)
+ elseif (button == 3) then awful.spawn(PAUSE_MPD_CMD, false)
+ elseif (button == 4) then awful.spawn(NEXT_MPD_CMD, false) -- scroll up
+ elseif (button == 5) then awful.spawn(PREV_MPD_CMD, false) -- scroll down
+ end
+
+ spawn.easy_async(GET_MPD_CMD, function(stdout, stderr, exitreason, exitcode)
+ update_graphic(mpdarc, stdout, stderr, exitreason, exitcode)
+ end)
+end)
+
+local notification
+local function show_MPD_status()
+ spawn.easy_async(GET_MPD_CMD,
+ function(stdout, _, _, _)
+ notification = naughty.notify {
+ text = stdout,
+ title = "MPD",
+ timeout = 5,
+ hover_timeout = 0.5,
+ width = 600,
+ }
+ end)
+end
+
+mpdarc:connect_signal("mouse::enter", function() show_MPD_status() end)
+mpdarc:connect_signal("mouse::leave", function() naughty.destroy(notification) end)
+
+watch(GET_MPD_CMD, 1, update_graphic, mpdarc)
+
+local mpdarc_widget = wibox.widget{
+ mpdarc_icon_widget,
+ mpdarc_current_song_widget,
+ layout = wibox.layout.align.horizontal,
+ }
+return mpdarc_widget
diff --git a/.config/awesome/awesome-wm-widgets/mpris-widget/README.md b/.config/awesome/awesome-wm-widgets/mpris-widget/README.md
new file mode 100755
index 0000000..7efad78
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/mpris-widget/README.md
@@ -0,0 +1,26 @@
+# MPRIS Widget (In progress)
+
+Music Player Info widget cy @mgabs
+
+# Prerequisite
+
+Install `playerctl` (mpris implementation), should be available in repo, e.g for Ubuntu:
+
+```bash
+sudo apt-get install playerctl
+```
+
+## Installation
+
+To use this widget clone repo under **~/.config/awesome/** and then add it in **rc.lua**:
+
+```lua
+local mpris_widget = require("awesome-wm-widgets.mpris-widget")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ mpris_widget(),
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/mpris-widget/init.lua b/.config/awesome/awesome-wm-widgets/mpris-widget/init.lua
new file mode 100755
index 0000000..5e45ffa
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/mpris-widget/init.lua
@@ -0,0 +1,187 @@
+-------------------------------------------------
+-- mpris based Arc Widget for Awesome Window Manager
+-- Modelled after Pavel Makhov's work
+-- @author Mohammed Gaber
+-- requires - playerctl
+-- @copyright 2020
+-------------------------------------------------
+local awful = require("awful")
+local beautiful = require("beautiful")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+local gears = require("gears")
+
+local GET_MPD_CMD = "playerctl -p %s -f '{{status}};{{xesam:artist}};{{xesam:title}}' metadata"
+
+local TOGGLE_MPD_CMD = "playerctl play-pause"
+local NEXT_MPD_CMD = "playerctl next"
+local PREV_MPD_CMD = "playerctl previous"
+local LIST_PLAYERS_CMD = "playerctl -l"
+
+local PATH_TO_ICONS = "/usr/share/icons/Arc"
+local PAUSE_ICON_NAME = PATH_TO_ICONS .. "/actions/24/player_pause.png"
+local PLAY_ICON_NAME = PATH_TO_ICONS .. "/actions/24/player_play.png"
+local STOP_ICON_NAME = PATH_TO_ICONS .. "/actions/24/player_stop.png"
+local LIBRARY_ICON_NAME = PATH_TO_ICONS .. "/actions/24/music-library.png"
+
+local default_player = ''
+
+local icon = wibox.widget {
+ id = "icon",
+ widget = wibox.widget.imagebox,
+ image = PLAY_ICON_NAME
+}
+
+local mpris_widget = wibox.widget{
+ {
+ id = 'artist',
+ widget = wibox.widget.textbox
+ },
+ {
+ icon,
+ max_value = 1,
+ value = 0,
+ thickness = 2,
+ start_angle = 4.71238898, -- 2pi*3/4
+ forced_height = 24,
+ forced_width = 24,
+ rounded_edge = true,
+ bg = "#ffffff11",
+ paddings = 0,
+ widget = wibox.container.arcchart
+ },
+ {
+ id = 'title',
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_text = function(self, artist, title)
+ self:get_children_by_id('artist')[1]:set_text(artist)
+ self:get_children_by_id('title')[1]:set_text(title)
+ end
+}
+
+local rows = { layout = wibox.layout.fixed.vertical }
+
+local popup = awful.popup{
+ bg = beautiful.bg_normal,
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local function rebuild_popup()
+ awful.spawn.easy_async(LIST_PLAYERS_CMD, function(stdout, _, _, _)
+ for i = 0, #rows do rows[i]=nil end
+ for player_name in stdout:gmatch("[^\r\n]+") do
+ if player_name ~='' and player_name ~=nil then
+
+ local checkbox = wibox.widget{
+ {
+ checked = player_name == default_player,
+ color = beautiful.bg_normal,
+ paddings = 2,
+ shape = gears.shape.circle,
+ forced_width = 20,
+ forced_height = 20,
+ check_color = beautiful.fg_urgent,
+ widget = wibox.widget.checkbox
+ },
+ valign = 'center',
+ layout = wibox.container.place,
+ }
+
+ checkbox:connect_signal("button::press", function()
+ default_player = player_name
+ rebuild_popup()
+ end)
+
+ table.insert(rows, wibox.widget {
+ {
+ {
+ checkbox,
+ {
+ {
+ text = player_name,
+ align = 'left',
+ widget = wibox.widget.textbox
+ },
+ left = 10,
+ layout = wibox.container.margin
+ },
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+ end
+ end
+ end)
+
+ popup:setup(rows)
+end
+
+local function worker()
+
+ -- retrieve song info
+ local current_song, artist, player_status
+
+ local update_graphic = function(widget, stdout, _, _, _)
+ local words = gears.string.split(stdout, ';')
+ player_status = words[1]
+ artist = words[2]
+ current_song = words[3]
+ if current_song ~= nil then
+ if string.len(current_song) > 18 then
+ current_song = string.sub(current_song, 0, 9) .. ".."
+ end
+ end
+
+ if player_status == "Playing" then
+ icon.image = PLAY_ICON_NAME
+ widget.colors = {beautiful.widget_main_color}
+ widget:set_text(artist, current_song)
+ elseif player_status == "Paused" then
+ icon.image = PAUSE_ICON_NAME
+ widget.colors = {beautiful.widget_main_color}
+ widget:set_text(artist, current_song)
+ elseif player_status == "Stopped" then
+ icon.image = STOP_ICON_NAME
+ else -- no player is running
+ icon.image = LIBRARY_ICON_NAME
+ widget.colors = {beautiful.widget_red}
+ end
+ end
+
+ mpris_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 3, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ rebuild_popup()
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end),
+ awful.button({}, 4, function() awful.spawn(NEXT_MPD_CMD, false) end),
+ awful.button({}, 5, function() awful.spawn(PREV_MPD_CMD, false) end),
+ awful.button({}, 1, function() awful.spawn(TOGGLE_MPD_CMD, false) end)
+ )
+ )
+
+ watch(string.format(GET_MPD_CMD, "'" .. default_player .. "'"), 1, update_graphic, mpris_widget)
+
+ return mpris_widget
+
+end
+
+return setmetatable(mpris_widget, {__call = function(_, ...) return worker(...) end})
diff --git a/.config/awesome/awesome-wm-widgets/net-speed-widget/README.md b/.config/awesome/awesome-wm-widgets/net-speed-widget/README.md
new file mode 100755
index 0000000..a09893e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/net-speed-widget/README.md
@@ -0,0 +1,22 @@
+# Net Speed Widget
+
+The widget and readme is in progress
+
+## Installation
+
+Please refer to the [installation](https://github.com/streetturtle/awesome-wm-widgets#installation) section of the repo.
+
+Clone repo, include widget and use it in **rc.lua**:
+
+```lua
+local net_speed_widget = require("awesome-wm-widgets.net-speed-widget.net-speed")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ net_speed_widget(),
+ ...
+ }
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/net-speed-widget/icons/down.svg b/.config/awesome/awesome-wm-widgets/net-speed-widget/icons/down.svg
new file mode 100755
index 0000000..9a98f39
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/net-speed-widget/icons/down.svg
@@ -0,0 +1,10 @@
+
diff --git a/.config/awesome/awesome-wm-widgets/net-speed-widget/icons/up.svg b/.config/awesome/awesome-wm-widgets/net-speed-widget/icons/up.svg
new file mode 100755
index 0000000..e3c12a7
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/net-speed-widget/icons/up.svg
@@ -0,0 +1,10 @@
+
diff --git a/.config/awesome/awesome-wm-widgets/net-speed-widget/net-speed.lua b/.config/awesome/awesome-wm-widgets/net-speed-widget/net-speed.lua
new file mode 100755
index 0000000..6dd3b05
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/net-speed-widget/net-speed.lua
@@ -0,0 +1,126 @@
+-------------------------------------------------
+-- Net Speed Widget for Awesome Window Manager
+-- Shows current upload/download speed
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/net-speed-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/net-speed-widget/'
+local ICONS_DIR = WIDGET_DIR .. 'icons/'
+
+local net_speed_widget = {}
+
+local function convert_to_h(bytes)
+ local speed
+ local dim
+ local bits = bytes * 8
+ if bits < 1000 then
+ speed = bits
+ dim = 'b/s'
+ elseif bits < 1000000 then
+ speed = bits/1000
+ dim = 'kb/s'
+ elseif bits < 1000000000 then
+ speed = bits/1000000
+ dim = 'mb/s'
+ elseif bits < 1000000000000 then
+ speed = bits/1000000000
+ dim = 'gb/s'
+ else
+ speed = tonumber(bits)
+ dim = 'b/s'
+ end
+ return math.floor(speed + 0.5) .. dim
+end
+
+local function split(string_to_split, separator)
+ if separator == nil then separator = "%s" end
+ local t = {}
+
+ for str in string.gmatch(string_to_split, "([^".. separator .."]+)") do
+ table.insert(t, str)
+ end
+
+ return t
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local interface = args.interface or '*'
+ local timeout = args.timeout or 1
+ local width = args.width or 55
+
+ net_speed_widget = wibox.widget {
+ {
+ id = 'rx_speed',
+ forced_width = width,
+ align = 'right',
+ widget = wibox.widget.textbox
+ },
+ {
+ image = ICONS_DIR .. 'down.svg',
+ widget = wibox.widget.imagebox
+ },
+ {
+ image = ICONS_DIR .. 'up.svg',
+ widget = wibox.widget.imagebox
+ },
+ {
+ id = 'tx_speed',
+ forced_width = width,
+ align = 'left',
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_rx_text = function(self, new_rx_speed)
+ self:get_children_by_id('rx_speed')[1]:set_text(tostring(new_rx_speed))
+ end,
+ set_tx_text = function(self, new_tx_speed)
+ self:get_children_by_id('tx_speed')[1]:set_text(tostring(new_tx_speed))
+ end
+ }
+
+ -- make sure these are not shared across different worker/widgets (e.g. two monitors)
+ -- otherwise the speed will be randomly split among the worker in each monitor
+ local prev_rx = 0
+ local prev_tx = 0
+
+ local update_widget = function(widget, stdout)
+
+ local cur_vals = split(stdout, '\r\n')
+
+ local cur_rx = 0
+ local cur_tx = 0
+
+ for i, v in ipairs(cur_vals) do
+ if i%2 == 1 then cur_rx = cur_rx + v end
+ if i%2 == 0 then cur_tx = cur_tx + v end
+ end
+
+ local speed_rx = (cur_rx - prev_rx) / timeout
+ local speed_tx = (cur_tx - prev_tx) / timeout
+
+ widget:set_rx_text(convert_to_h(speed_rx))
+ widget:set_tx_text(convert_to_h(speed_tx))
+
+ prev_rx = cur_rx
+ prev_tx = cur_tx
+ end
+
+ watch(string.format([[bash -c "cat /sys/class/net/%s/statistics/*_bytes"]], interface),
+ timeout, update_widget, net_speed_widget)
+
+ return net_speed_widget
+
+end
+
+return setmetatable(net_speed_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/pomodoroarc-widget/README.md b/.config/awesome/awesome-wm-widgets/pomodoroarc-widget/README.md
new file mode 100755
index 0000000..49b1b2c
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/pomodoroarc-widget/README.md
@@ -0,0 +1,16 @@
+# Pomodoro Widget
+
+:construction: This widget is under construction :construction_worker:
+
+## Installation
+
+This widget is based on [@jsspencer](https://github.com/jsspencer)' [pomo](https://github.com/jsspencer/pomo) - a simple pomodoro timer.
+So first install/clone it anywhere you like, then either
+ - in widget's code provide path to the pomo.sh, or
+ - add pomo.sh to the PATH, or
+ - make a soft link in /usr/local/bin/ to it:
+ ```bash
+ sudo ln -sf /opt/pomodoro/pomo.sh /usr/local/bin/pomo
+ ```
+
+Note that by default widget's code expects third way and calls script by `pomo`.
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/pomodoroarc-widget/pomodoroarc.lua b/.config/awesome/awesome-wm-widgets/pomodoroarc-widget/pomodoroarc.lua
new file mode 100755
index 0000000..497a208
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/pomodoroarc-widget/pomodoroarc.lua
@@ -0,0 +1,135 @@
+-------------------------------------------------
+-- Pomodoro Arc Widget for Awesome Window Manager
+-- Modelled after Pavel Makhov's work
+
+-- @author Raphaël Fournier-S'niehotta
+-- @copyright 2018 Raphaël Fournier-S'niehotta
+-------------------------------------------------
+
+local awful = require("awful")
+local beautiful = require("beautiful")
+local spawn = require("awful.spawn")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+local naughty = require("naughty")
+
+local GET_pomodoro_CMD = "pomo clock"
+local PAUSE_pomodoro_CMD = "pomo pause"
+local START_pomodoro_CMD = "pomo start"
+local STOP_pomodoro_CMD = "pomo stop"
+
+local text = wibox.widget {
+ id = "txt",
+ --font = "Play 12",
+font = "Inconsolata Medium 13",
+ widget = wibox.widget.textbox
+}
+-- mirror the text, because the whole widget will be mirrored after
+local mirrored_text = wibox.container.margin(wibox.container.mirror(text, { horizontal = true }))
+mirrored_text.right = 5 -- pour centrer le texte dans le rond
+--
+--local mirrored_text = wibox.container.mirror(text, { horizontal = true })
+
+-- mirrored text with background
+local mirrored_text_with_background = wibox.container.background(mirrored_text)
+
+local pomodoroarc = wibox.widget {
+ mirrored_text_with_background,
+ max_value = 1,
+ thickness = 2,
+ start_angle = 4.71238898, -- 2pi*3/4
+ forced_height = 32,
+ forced_width = 32,
+ rounded_edge = true,
+ bg = "#ffffff11",
+ paddings = 0,
+ widget = wibox.container.arcchart
+}
+
+local pomodoroarc_widget = wibox.container.mirror(pomodoroarc, { horizontal = true })
+
+local update_graphic = function(widget, stdout, _, _, _)
+ local pomostatus = string.match(stdout, " (%D?%D?):%D?%D?")
+ if pomostatus == "--" then
+text.font = "Inconsolata Medium 13"
+ widget.colors = { beautiful.widget_main_color }
+ text.text = "25"
+ widget.value = 1
+ else
+text.font = "Inconsolata Medium 13"
+ local pomomin = string.match(stdout, "[ P]?[BW](%d?%d?):%d?%d?")
+ local pomosec = string.match(stdout, "[ P]?[BW]%d?%d?:(%d?%d?)")
+ local pomodoro = pomomin * 60 + pomosec
+
+ local status = string.match(stdout, "([ P]?)[BW]%d?%d?:%d?%d?")
+ local workbreak = string.match(stdout, "[ P]?([BW])%d?%d?:%d?%d?")
+ text.text = pomomin
+
+-- Helps debugging
+ --naughty.notify {
+ --text = pomomin,
+ --title = "pomodoro debug",
+ --timeout = 5,
+ --hover_timeout = 0.5,
+ --width = 200,
+ --}
+
+ if status == " " then -- clock ticking
+ if workbreak == "W" then
+ widget.value = tonumber(pomodoro/(25*60))
+ if tonumber(pomomin) < 5 then -- last 5 min of pomo
+ widget.colors = { beautiful.widget_red }
+ else
+ widget.colors = { beautiful.widget_blue }
+ end
+ elseif workbreak == "B" then -- color during pause
+ widget.colors = { beautiful.widget_green }
+ widget.value = tonumber(pomodoro/(5*60))
+ end
+ elseif status == "P" then -- paused
+ if workbreak == "W" then
+ widget.colors = { beautiful.widget_yellow }
+ widget.value = tonumber(pomodoro/(25*60))
+text.font = "Inconsolata Medium 13"
+ text.text = "PW"
+ elseif workbreak == "B" then
+ widget.colors = { beautiful.widget_yellow }
+ widget.value = tonumber(pomodoro/(5*60))
+text.font = "Inconsolata Medium 13"
+ text.text = "PB"
+ end
+ end
+ end
+end
+
+pomodoroarc:connect_signal("button::press", function(_, _, _, button)
+ if (button == 2) then awful.spawn(PAUSE_pomodoro_CMD, false)
+ elseif (button == 1) then awful.spawn(START_pomodoro_CMD, false)
+ elseif (button == 3) then awful.spawn(STOP_pomodoro_CMD, false)
+ end
+
+ spawn.easy_async(GET_pomodoro_CMD, function(stdout, stderr, exitreason, exitcode)
+ update_graphic(pomodoroarc, stdout, stderr, exitreason, exitcode)
+ end)
+end)
+
+local notification
+local function show_pomodoro_status()
+ spawn.easy_async(GET_pomodoro_CMD,
+ function(stdout, _, _, _)
+ notification = naughty.notify {
+ text = stdout,
+ title = "pomodoro status",
+ timeout = 5,
+ hover_timeout = 0.5,
+ width = 200,
+ }
+ end)
+end
+
+pomodoroarc:connect_signal("mouse::enter", function() show_pomodoro_status() end)
+pomodoroarc:connect_signal("mouse::leave", function() naughty.destroy(notification) end)
+
+watch(GET_pomodoro_CMD, 1, update_graphic, pomodoroarc)
+
+return pomodoroarc_widget
diff --git a/.config/awesome/awesome-wm-widgets/ram-widget/README.md b/.config/awesome/awesome-wm-widgets/ram-widget/README.md
new file mode 100755
index 0000000..568245b
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/ram-widget/README.md
@@ -0,0 +1,41 @@
+# Ram widget
+
+This widget shows the RAM usage. When clicked another widget appears with more detailed information:
+
+
+
+Note: this widget is compatible with Awesome v4.3+, as it is using [awful.popup](https://awesomewm.org/doc/api/classes/awful.popup.html)
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `color_used` | `beautiful.bg_urgent` | Color for used RAM |
+| `color_free` | `beautiful.fg_normal` | Color for free RAM |
+| `color_buf` | `beautiful.border_color_active` | Color for buffers/cache |
+| `widget_height` | `25` | Height of the widget |
+| `widget_width` | `25` | Width of the widget |
+| `widget_show_buf` | `false` | Whether to display buffers/cache separately in the tray widget. If `false`, buffers/cache are considered free RAM. |
+| `timeout` | 1 | How often (in seconds) the widget refreshes |
+
+## Installation
+
+Please refer to the [installation](https://github.com/streetturtle/awesome-wm-widgets#installation) section of the repo.
+
+Clone repo, include widget and use it in **rc.lua**:
+
+```lua
+local ram_widget = require("awesome-wm-widgets.ram-widget.ram-widget")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ ram_widget(),
+ ...
+ }
+ ...
+```
+
diff --git a/.config/awesome/awesome-wm-widgets/ram-widget/out.gif b/.config/awesome/awesome-wm-widgets/ram-widget/out.gif
new file mode 100755
index 0000000..736f894
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/ram-widget/out.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/ram-widget/ram-widget.lua b/.config/awesome/awesome-wm-widgets/ram-widget/ram-widget.lua
new file mode 100755
index 0000000..867d28e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/ram-widget/ram-widget.lua
@@ -0,0 +1,108 @@
+local awful = require("awful")
+local beautiful = require("beautiful")
+local gears = require("gears")
+local watch = require("awful.widget.watch")
+local wibox = require("wibox")
+
+
+local ramgraph_widget = {}
+
+
+local function worker(user_args)
+ local args = user_args or {}
+ local timeout = args.timeout or 1
+ local color_used = args.color_used or beautiful.bg_urgent
+ local color_free = args.color_free or beautiful.fg_normal
+ local color_buf = args.color_buf or beautiful.border_color_active
+ local widget_show_buf = args.widget_show_buf or false
+ local widget_height = args.widget_height or 25
+ local widget_width = args.widget_width or 25
+
+ --- Main ram widget shown on wibar
+ ramgraph_widget = wibox.widget {
+ border_width = 0,
+ colors = {
+ color_used,
+ color_free,
+ color_buf,
+ },
+ display_labels = false,
+ forced_height = widget_height,
+ forced_width = widget_width,
+ widget = wibox.widget.piechart
+ }
+
+ --- Widget which is shown when user clicks on the ram widget
+ local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ widget = {
+ widget = wibox.widget.piechart,
+ forced_height = 200,
+ forced_width = 400,
+ colors = {
+ color_used,
+ color_free,
+ color_buf, -- buf_cache
+ },
+ },
+ shape = gears.shape.rounded_rect,
+ border_color = beautiful.border_color_active,
+ border_width = 1,
+ offset = { y = 5 },
+ }
+
+ --luacheck:ignore 231
+ local total, used, free, shared, buff_cache, available, total_swap, used_swap, free_swap
+
+ local function getPercentage(value)
+ return math.floor(value / (total+total_swap) * 100 + 0.5) .. '%'
+ end
+
+ watch('bash -c "LANGUAGE=en_US.UTF-8 free | grep -z Mem.*Swap.*"', timeout,
+ function(widget, stdout)
+ total, used, free, shared, buff_cache, available, total_swap, used_swap, free_swap =
+ stdout:match('(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*Swap:%s*(%d+)%s*(%d+)%s*(%d+)')
+
+ if widget_show_buf then
+ widget.data = { used, free, buff_cache }
+ else
+ widget.data = { used, total-used }
+ end
+
+ if popup.visible then
+ popup:get_widget().data_list = {
+ {'used ' .. getPercentage(used + used_swap), used + used_swap},
+ {'free ' .. getPercentage(free + free_swap), free + free_swap},
+ {'buff_cache ' .. getPercentage(buff_cache), buff_cache}
+ }
+ end
+ end,
+ ramgraph_widget
+ )
+
+ ramgraph_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ popup:get_widget().data_list = {
+ {'used ' .. getPercentage(used + used_swap), used + used_swap},
+ {'free ' .. getPercentage(free + free_swap), free + free_swap},
+ {'buff_cache ' .. getPercentage(buff_cache), buff_cache}
+ }
+
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ return ramgraph_widget
+end
+
+
+return setmetatable(ramgraph_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/run-shell-2/run-shell-2.lua b/.config/awesome/awesome-wm-widgets/run-shell-2/run-shell-2.lua
new file mode 100755
index 0000000..89491de
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/run-shell-2/run-shell-2.lua
@@ -0,0 +1,102 @@
+-------------------------------------------------
+-- Spotify Shell for Awesome Window Manager
+-- Simplifies interaction with Spotify for Linux
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/spotify-shell
+
+-- @author Pavel Makhov
+-- @copyright 2018 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local gfs = require("gears.filesystem")
+local wibox = require("wibox")
+local gears = require("gears")
+local completion = require("awful.completion")
+
+local run = require("awesome-wm-widgets.run-shell-2.run")
+
+local run_shell = awful.widget.prompt()
+
+local w = wibox {
+ bg = '#2e3440',
+ border_width = 1,
+ border_color = '#3b4252',
+ max_widget_size = 500,
+ ontop = true,
+ height = 50,
+ width = 250,
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 3)
+ -- ` gears.shape.infobubble(cr, width, height)
+ end
+}
+
+local g = {
+ {
+ layout = wibox.container.margin,
+ left = 10,
+ run_shell,
+ },
+ id = 'left',
+ layout = wibox.layout.fixed.horizontal
+}
+
+
+local function launch(type)
+
+ if type == 'run' then
+ table.insert(g, 1, run.icon)
+ w:setup(g)
+ awful.placement.top(w, { margins = { top = 40 }, parent = awful.screen.focused() })
+ w.visible = true
+ awful.prompt.run {
+ prompt = run.text,
+ bg_cursor = run.cursor_color,
+ textbox = run_shell.widget,
+ completion_callback = completion.shell,
+ exe_callback = function(...)
+ run_shell:spawn_and_handle_error(...)
+ end,
+ history_path = gfs.get_cache_dir() .. run.history,
+ done_callback = function()
+ w.visible = false
+ table.remove(g, 1)
+ end
+ }
+ elseif type == 'spotify' then
+ table.insert(g, 1, {
+ {
+ image = '/usr/share/icons/Papirus-Light/32x32/apps/spotify-linux-48x48.svg',
+ widget = wibox.widget.imagebox,
+ resize = false
+ },
+ id = 'icon',
+ top = 9,
+ left = 10,
+ layout = wibox.container.margin
+ })
+ w:setup(g)
+ awful.placement.top(w, { margins = { top = 40 }, parent = awful.screen.focused() })
+ w.visible = true
+
+ awful.prompt.run {
+ prompt = "Spotify Shell: ",
+ bg_cursor = '#84bd00',
+ textbox = run_shell.widget,
+ history_path = gfs.get_dir('cache') .. '/spotify_history',
+ exe_callback = function(input_text)
+ if not input_text or #input_text == 0 then return end
+ awful.spawn("sp " .. input_text)
+ end,
+ done_callback = function()
+ w.visible = false
+ table.remove(g, 1)
+ end
+ }
+ end
+end
+
+return {
+ launch = launch
+}
diff --git a/.config/awesome/awesome-wm-widgets/run-shell-2/run.lua b/.config/awesome/awesome-wm-widgets/run-shell-2/run.lua
new file mode 100755
index 0000000..12644ba
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/run-shell-2/run.lua
@@ -0,0 +1,21 @@
+local wibox = require("wibox")
+
+local icon = {
+ {
+ markup = 'a',
+ widget = wibox.widget.textbox,
+ },
+ id = 'icon',
+ top = 2,
+ left = 10,
+ layout = wibox.container.margin
+}
+
+local text = 'Run: '
+
+return {
+ icon = icon,
+ text = text,
+ cursor_color = '#74aeab',
+ history = '/history'
+}
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/run-shell-3/README.md b/.config/awesome/awesome-wm-widgets/run-shell-3/README.md
new file mode 100755
index 0000000..95749d1
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/run-shell-3/README.md
@@ -0,0 +1,34 @@
+# Run Shell
+
+Blurs / pixelates background and shows widget with run prompt:
+
+
+
+
+
+## Installation
+
+1. To blur / pixelate the background this widget used [ffmpeg](https://www.ffmpeg.org/) and [frei0r](https://frei0r.dyne.org/) plugins (if you want to pixelate the background), which you need to install. Installation of those depends on your distribution, for ffmpeg just follow the installation section of the site, for frei0r I was able to install it by simply running
+
+ ```
+ sudo apt-get install frei0r-plugins
+ ```
+
+1. Clone this repo under **~/.config/awesome/**:
+
+ ```bash
+ git clone https://github.com/streetturtle/awesome-wm-widgets.git ~/.config/awesome/awesome-wm-widgets
+ ```
+
+1. Require widget at the beginning of **rc.lua**:
+
+ ```lua
+ local run_shell = require("awesome-wm-widgets.run-shell-3.run-shell")
+ ```
+
+1. Use it (don't forget to comment out the default prompt):
+
+ ```lua
+ awful.key({modkey}, "r", function () run_shell.launch() end),
+ ```
+:warning: I am not 100% sure but it may (memory) leak. If awesome uses lots of RAM just reload config (Ctrl + Mod4 + r).
diff --git a/.config/awesome/awesome-wm-widgets/run-shell-3/blur.png b/.config/awesome/awesome-wm-widgets/run-shell-3/blur.png
new file mode 100755
index 0000000..4e8b54c
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/run-shell-3/blur.png differ
diff --git a/.config/awesome/awesome-wm-widgets/run-shell-3/pixelate.png b/.config/awesome/awesome-wm-widgets/run-shell-3/pixelate.png
new file mode 100755
index 0000000..fedf320
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/run-shell-3/pixelate.png differ
diff --git a/.config/awesome/awesome-wm-widgets/run-shell-3/run-shell.lua b/.config/awesome/awesome-wm-widgets/run-shell-3/run-shell.lua
new file mode 100755
index 0000000..0015232
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/run-shell-3/run-shell.lua
@@ -0,0 +1,129 @@
+-------------------------------------------------
+-- Run Shell for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/run-shell
+
+-- @author Pavel Makhov
+-- @copyright 2018 Pavel Makhov
+-- @copyright 2019 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local gfs = require("gears.filesystem")
+local wibox = require("wibox")
+local gears = require("gears")
+local naughty = require("naughty")
+local completion = require("awful.completion")
+
+local run_shell = awful.widget.prompt()
+
+local widget = {}
+
+function widget.new()
+ local widget_instance = {
+ _cached_wiboxes = {},
+ _cmd_pixelate = [[sh -c 'ffmpeg -loglevel panic -f x11grab -video_size 1920x1060 -y -i :0.0+%s,20 -vf ]]
+ .. [[frei0r=pixeliz0r -vframes 1 /tmp/i3lock-%s.png ; echo done']],
+ _cmd_blur = [[sh -c 'ffmpeg -loglevel panic -f x11grab -video_size 1920x1060 -y -i :0.0+%s,20 ]]
+ .. [[-filter_complex "boxblur=9" -vframes 1 /tmp/i3lock-%s.png ; echo done']]
+ }
+
+ function widget_instance:_create_wibox()
+ local w = wibox {
+ visible = false,
+ ontop = true,
+ height = mouse.screen.geometry.height,
+ width = mouse.screen.geometry.width,
+ }
+
+ w:setup {
+ {
+ {
+ {
+ {
+ markup = 'a',
+ widget = wibox.widget.textbox,
+ },
+ id = 'icon',
+ left = 10,
+ layout = wibox.container.margin
+ },
+ {
+ run_shell,
+ left = 10,
+ layout = wibox.container.margin,
+ },
+ id = 'left',
+ layout = wibox.layout.fixed.horizontal
+ },
+ widget = wibox.container.background,
+ bg = '#333333',
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 3)
+ end,
+ shape_border_color = '#74aeab',
+ shape_border_width = 1,
+ forced_width = 200,
+ forced_height = 50
+ },
+ layout = wibox.container.place
+ }
+
+ return w
+ end
+
+ function widget_instance:launch()
+ local s = mouse.screen
+ if not self._cached_wiboxes[s] then
+ self._cached_wiboxes[s] = {}
+ end
+ if not self._cached_wiboxes[s][1] then
+ self._cached_wiboxes[s][1] = self:_create_wibox()
+ end
+ local w = self._cached_wiboxes[s][1]
+ local rnd = math.random()
+ awful.spawn.with_line_callback(
+ string.format(self._cmd_blur, tostring(awful.screen.focused().geometry.x), rnd), {
+ stdout = function()
+ w.visible = true
+ w.bgimage = '/tmp/i3lock-' .. rnd ..'.png'
+ awful.placement.top(w, { margins = { top = 20 }, parent = awful.screen.focused() })
+ awful.prompt.run {
+ prompt = 'Run: ',
+ bg_cursor = '#74aeab',
+ textbox = run_shell.widget,
+ completion_callback = completion.shell,
+ exe_callback = function(...)
+ run_shell:spawn_and_handle_error(...)
+ end,
+ history_path = gfs.get_cache_dir() .. "/history",
+ done_callback = function()
+ w.visible = false
+ w.bgimage = ''
+ awful.spawn([[bash -c 'rm -f /tmp/i3lock*']])
+ end
+ }
+ end,
+ stderr = function(line)
+ naughty.notify { text = "ERR:" .. line }
+ end,
+ })
+
+ end
+
+ return widget_instance
+end
+
+local function get_default_widget()
+ if not widget.default_widget then
+ widget.default_widget = widget.new()
+ end
+ return widget.default_widget
+end
+
+function widget.launch(...)
+ return get_default_widget():launch(...)
+end
+
+return widget
+
diff --git a/.config/awesome/awesome-wm-widgets/run-shell/README.md b/.config/awesome/awesome-wm-widgets/run-shell/README.md
new file mode 100755
index 0000000..9ae7f5d
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/run-shell/README.md
@@ -0,0 +1,25 @@
+# Run Shell
+
+Run prompt which is put inside a widget:
+
+[Demo](https://imgur.com/ohjAuCQ.mp4)
+
+## Installation
+
+1. Clone this repo under **~/.config/awesome/**:
+
+ ```bash
+ git clone https://github.com/streetturtle/awesome-wm-widgets.git ~/.config/awesome/awesome-wm-widgets
+ ```
+
+1. Require widget at the beginning of **rc.lua**:
+
+ ```lua
+ local run_shell = require("awesome-wm-widgets.run-shell.run-shell")
+ ```
+
+1. Use it (don't forget to comment out the default prompt):
+
+ ```lua
+ awful.key({modkey}, "r", function () run_shell.launch() end),
+
diff --git a/.config/awesome/awesome-wm-widgets/run-shell/out.mp4 b/.config/awesome/awesome-wm-widgets/run-shell/out.mp4
new file mode 100755
index 0000000..5db8172
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/run-shell/out.mp4 differ
diff --git a/.config/awesome/awesome-wm-widgets/run-shell/run-shell.lua b/.config/awesome/awesome-wm-widgets/run-shell/run-shell.lua
new file mode 100755
index 0000000..a43c4d5
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/run-shell/run-shell.lua
@@ -0,0 +1,169 @@
+-------------------------------------------------
+-- Run Shell for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/run-shell
+
+-- @author Pavel Makhov
+-- @copyright 2019 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local gfs = require("gears.filesystem")
+local wibox = require("wibox")
+local gears = require("gears")
+local completion = require("awful.completion")
+local naughty = require("naughty")
+
+local HOME = os.getenv("HOME")
+
+local run_shell = awful.widget.prompt()
+
+local widget = {}
+
+function widget.new()
+
+ local widget_instance = {
+ _cached_wiboxes = {}
+ }
+
+ function widget_instance:_create_wibox()
+ local w = wibox {
+ visible = false,
+ ontop = true,
+ height = mouse.screen.geometry.height,
+ width = mouse.screen.geometry.width,
+ opacity = 0.9,
+ bg = 'radial:'.. mouse.screen.geometry.width/2 .. ','
+ .. mouse.screen.geometry.height/2 .. ',20:'
+ .. mouse.screen.geometry.width/2 .. ','
+ .. mouse.screen.geometry.height/2
+ .. ',700:0,#2E344022:0.2,#4C566A88:1,#2E3440ff'
+ }
+
+ local suspend_button = wibox.widget {
+ image = '/usr/share/icons/Arc/actions/symbolic/system-shutdown-symbolic.svg',
+ widget = wibox.widget.imagebox,
+ resize = false,
+ opacity = 0.2,
+ --luacheck:ignore 432
+ set_hover = function(self, opacity)
+ self.opacity = opacity
+ self.image = '/usr/share/icons/Arc/actions/symbolic/system-shutdown-symbolic.svg'
+ end
+ }
+
+ local turnoff_notification
+
+ suspend_button:connect_signal("mouse::enter", function()
+ turnoff_notification = naughty.notify{
+ icon = HOME .. "/.config/awesome/nichosi.png",
+ icon_size=100,
+ title = "Huston, we have a problem",
+ text = "You're about to turn off your computer",
+ timeout = 5, hover_timeout = 0.5,
+ position = "bottom_right",
+ bg = "#F06060",
+ fg = "#EEE9EF",
+ width = 300,
+ }
+ suspend_button:set_hover(1)
+ end)
+
+ suspend_button:connect_signal("mouse::leave", function()
+ naughty.destroy(turnoff_notification)
+ suspend_button:set_hover(0.2)
+ end)
+
+ suspend_button:connect_signal("button::press", function(_,_,_,button)
+ if (button == 1) then
+ awful.spawn("shutdown now")
+ end
+ end)
+
+ w:setup {
+ {
+ {
+ {
+ {
+ {
+ markup = 'a',
+ widget = wibox.widget.textbox,
+ },
+ id = 'icon',
+ left = 10,
+ layout = wibox.container.margin
+ },
+ {
+ run_shell,
+ left = 10,
+ layout = wibox.container.margin,
+ },
+ id = 'left',
+ layout = wibox.layout.fixed.horizontal
+ },
+ bg = '#333333',
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 3)
+ end,
+ shape_border_color = '#74aeab',
+ shape_border_width = 1,
+ forced_width = 200,
+ forced_height = 50,
+ widget = wibox.container.background
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ {
+ suspend_button,
+ layout = wibox.layout.fixed.horizontal
+ },
+ valign = 'bottom',
+ layout = wibox.container.place,
+ },
+ layout = wibox.layout.stack
+ }
+
+ return w
+ end
+
+ function widget_instance:launch()
+ local s = mouse.screen
+ if not self._cached_wiboxes[s] then
+ self._cached_wiboxes[s] = {}
+ end
+ if not self._cached_wiboxes[s][1] then
+ self._cached_wiboxes[s][1] = self:_create_wibox()
+ end
+ local w = self._cached_wiboxes[s][1]
+ w.visible = true
+ awful.placement.top(w, { margins = { top = 20 }, parent = awful.screen.focused() })
+ awful.prompt.run {
+ prompt = 'Run: ',
+ bg_cursor = '#74aeab',
+ textbox = run_shell.widget,
+ completion_callback = completion.shell,
+ exe_callback = function(...)
+ run_shell:spawn_and_handle_error(...)
+ end,
+ history_path = gfs.get_cache_dir() .. "/history",
+ done_callback = function() w.visible = false end
+ }
+ end
+
+ return widget_instance
+end
+
+local function get_default_widget()
+ if not widget.default_widget then
+ widget.default_widget = widget.new()
+ end
+ return widget.default_widget
+end
+
+function widget.launch(...)
+ return get_default_widget():launch(...)
+end
+
+return widget
diff --git a/.config/awesome/awesome-wm-widgets/screenshot.png b/.config/awesome/awesome-wm-widgets/screenshot.png
new file mode 100755
index 0000000..9406ebf
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/screenshot_with_sprtrs.png b/.config/awesome/awesome-wm-widgets/screenshot_with_sprtrs.png
new file mode 100755
index 0000000..361d5a2
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/screenshot_with_sprtrs.png differ
diff --git a/.config/awesome/awesome-wm-widgets/scripts/update_site.sh b/.config/awesome/awesome-wm-widgets/scripts/update_site.sh
new file mode 100755
index 0000000..35fc971
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/scripts/update_site.sh
@@ -0,0 +1,14 @@
+mkdir ./_widgets
+for D in *; do
+ if [[ -d "${D}" ]] && [[ ${D} == *"-widget"* ]]; then
+ echo "${D}"
+ cp ${D}/README.md ./_widgets/${D}.md
+ sed -i '1s/^/---\nlayout: page\n---\n/' ./_widgets/${D}.md
+
+ mkdir -p ./assets/img/widgets/screenshots/${D}
+
+ find ${D}/ \( -name '*.jpg' -o -name '*.png' -o -name '*.gif' \) -exec cp '{}' ./assets/img/widgets/screenshots/${D} \;
+
+ sed -i "s/](\.\(\/screenshots\)\{0,1\}/](..\/awesome-wm-widgets\/assets\/img\/widgets\/screenshots\/$D/g" ./_widgets/${D}.md
+ fi
+done
diff --git a/.config/awesome/awesome-wm-widgets/spotify-shell/README.md b/.config/awesome/awesome-wm-widgets/spotify-shell/README.md
new file mode 100755
index 0000000..0f4981d
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/spotify-shell/README.md
@@ -0,0 +1,73 @@
+# Spotify Shell
+
+
+
+
+## Features
+
+1. Supports following commands (same as `sp` client):
+ - `play`/`pause`/`next`;
+ - any other string will start a search and play the first result for a given search query;
+ - feh - shows the current artwork with `feh`;
+
+1. Stores history and allows navigating through it;
+
+1. Highly customizable
+
+## Controls
+
+Keyboard navigation (copied from [`awful.prompt`](https://awesomewm.org/doc/api/libraries/awful.prompt.html) API documentation page):
+
+| Name | Usage |
+|---|---|
+| CTRL+A | beginning-of-line |
+| CTRL+B | backward-char |
+| CTRL+C | cancel |
+| CTRL+D | delete-char |
+| CTRL+E | end-of-line |
+| CTRL+J | accept-line |
+| CTRL+M | accept-line |
+| CTRL+F | move-cursor-right |
+| CTRL+H | backward-delete-char |
+| CTRL+K | kill-line |
+| CTRL+U | unix-line-discard |
+| CTRL+W | unix-word-rubout |
+| CTRL+BACKSPACE | unix-word-rubout |
+| SHIFT+INSERT | paste |
+| HOME | beginning-of-line |
+| END | end-of-line |
+| CTRL+R | reverse history search, matches any history entry containing search term. |
+| CTRL+S | forward history search, matches any history entry containing search term. |
+| CTRL+UP | ZSH up line or search, matches any history entry starting with search term. |
+| CTRL+DOWN | ZSH down line or search, matches any history entry starting with search term. |
+| CTRL+DELETE | delete the currently visible history entry from history file. This does not delete new commands or history entries under user editing. |
+
+
+## Installation
+
+1. Install [sp](https://gist.github.com/streetturtle/fa6258f3ff7b17747ee3) - CLI client for [Spotify for Linux](https://www.spotify.com/ca-en/download/linux/):
+
+ ```bash
+ $ sudo git clone https://gist.github.com/fa6258f3ff7b17747ee3.git ~/dev/
+ $ sudo ln -s ~/dev/sp /usr/local/bin/
+ ```
+
+ Check if it works by running `sp help`.
+
+1. Get an 'id' and 'secret' from [developer.spotify.com](https://beta.developer.spotify.com/documentation/general/guides/app-settings/) and paste it in the header of the `sp` (`SP_ID` and `SP_SECRET`) - this enables search feature.
+
+1. Clone this repo under **~/.config/awesome/**
+
+1. Require spotify-shell at the beginning of **rc.lua**:
+
+ ```lua
+ local spotify_shell = require("awesome-wm-widgets.spotify-shell.spotify-shell")
+ ```
+
+1. Add a shortcut which will show Spotify Shell widget:
+
+ ```lua
+ awful.key({ modkey, }, "d", function () spotify_shell.launch() end, {description = "spotify shell", group = "music"}),
+ ```
+
+1. It uses icon from [Papirus Icon Theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme). So you should either install this icon theme, or download an icon you want to use and provide path to it in **spotify-shell.lua**.
diff --git a/.config/awesome/awesome-wm-widgets/spotify-shell/demo.gif b/.config/awesome/awesome-wm-widgets/spotify-shell/demo.gif
new file mode 100755
index 0000000..6696b3a
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/spotify-shell/demo.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/spotify-shell/spotify-shell.lua b/.config/awesome/awesome-wm-widgets/spotify-shell/spotify-shell.lua
new file mode 100755
index 0000000..0611e66
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/spotify-shell/spotify-shell.lua
@@ -0,0 +1,75 @@
+-------------------------------------------------
+-- Spotify Shell for Awesome Window Manager
+-- Simplifies interaction with Spotify for Linux
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/spotify-shell
+
+-- @author Pavel Makhov
+-- @copyright 2018 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local gfs = require("gears.filesystem")
+local wibox = require("wibox")
+local gears = require("gears")
+
+local ICON = '/usr/share/icons/Papirus-Light/32x32/apps/spotify-linux-48x48.svg'
+
+local spotify_shell = awful.widget.prompt()
+
+local w = wibox {
+ bg = '#1e252c',
+ border_width = 1,
+ border_color = '#84bd00',
+ max_widget_size = 500,
+ ontop = true,
+ height = 50,
+ width = 250,
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 3)
+ end
+}
+
+w:setup {
+ {
+ {
+ image = ICON,
+ widget = wibox.widget.imagebox,
+ resize = false
+ },
+ id = 'icon',
+ top = 9,
+ left = 10,
+ layout = wibox.container.margin
+ },
+ {
+ layout = wibox.container.margin,
+ left = 10,
+ spotify_shell,
+ },
+ id = 'left',
+ layout = wibox.layout.fixed.horizontal
+}
+
+local function launch()
+ w.visible = true
+
+ awful.placement.top(w, { margins = {top = 40}, parent = awful.screen.focused()})
+ awful.prompt.run{
+ prompt = "Spotify Shell: ",
+ bg_cursor = '#84bd00',
+ textbox = spotify_shell.widget,
+ history_path = gfs.get_dir('cache') .. '/spotify_history',
+ exe_callback = function(input_text)
+ if not input_text or #input_text == 0 then return end
+ awful.spawn("sp " .. input_text)
+ end,
+ done_callback = function()
+ w.visible = false
+ end
+ }
+end
+
+return {
+ launch = launch
+}
diff --git a/.config/awesome/awesome-wm-widgets/spotify-widget/README.md b/.config/awesome/awesome-wm-widgets/spotify-widget/README.md
new file mode 100755
index 0000000..3a7b8d7
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/spotify-widget/README.md
@@ -0,0 +1,95 @@
+# Spotify widget
+
+This widget displays currently playing song on [Spotify for Linux](https://www.spotify.com/download/linux/) client: 
+
+Some features:
+
+ - status icon which shows if music is currently playing
+ - artist and name of the current song
+ - dim widget if spotify is paused
+ - trim long artist/song names
+ - tooltip with more info about the song
+
+## Controls
+
+ - left click - play/pause
+ - scroll up - play next song
+ - scroll down - play previous song
+
+## Dependencies
+
+Note that widget uses the Arc icon theme, so it should be [installed](https://github.com/horst3180/arc-icon-theme#installation) first under **/usr/share/icons/Arc/** folder.
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `play_icon` | `/usr/share/icons/Arc/actions/24/player_play.png` | Play icon |
+| `pause_icon` | `/usr/share/icons/Arc/actions/24/player_pause.png` | Pause icon |
+| `font` | `Play 9`| Font |
+| `dim_when_paused` | `false` | Decrease the widget opacity if spotify is paused |
+| `dim_opacity` | `0.2` | Widget's opacity when dimmed, `dim_when_paused` should be set to `true` |
+| `max_length` | `15` | Maximum lentgh of artist and title names. Text will be ellipsized if longer. |
+| `show_tooltip` | `true` | Show tooltip on hover with information about the playing song |
+| `timeout` | 1 | How often in seconds the widget refreshes |
+| `sp_bin` | `sp` | Path to the `sp` binary. Required if `sp` is not in environment PATH. |
+
+
+### Example:
+
+```lua
+spotify_widget({
+ font = 'Ubuntu Mono 9',
+ play_icon = '/usr/share/icons/Papirus-Light/24x24/categories/spotify.svg',
+ pause_icon = '/usr/share/icons/Papirus-Dark/24x24/panel/spotify-indicator.svg',
+ dim_when_paused = true,
+ dim_opacity = 0.5,
+ max_length = -1,
+ show_tooltip = false,
+ sp_bin = gears.filesystem.get_configuration_dir() .. 'scripts/sp'
+})
+```
+
+Gives following widget
+
+Playing:
+
+
+Paused:
+
+
+## Installation
+
+First you need to have spotify CLI installed, it uses dbus to communicate with spotify-client:
+
+```bash
+git clone https://gist.github.com/fa6258f3ff7b17747ee3.git
+cd ./fa6258f3ff7b17747ee3
+chmod +x sp
+# This widget will work by default if the binary is in the system PATH
+sudo cp ./sp /usr/local/bin/
+# Alternatively, you may save the binary anywhere and supply the path via this widget's sp_bin argument:
+# cp ./sp ~/.config/awesome/scripts/
+```
+
+Then clone repo under **~/.config/awesome/** and add widget in **rc.lua**:
+
+```lua
+local spotify_widget = require("awesome-wm-widgets.spotify-widget.spotify")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ spotify_widget(),
+ -- customized
+ spotify_widget({
+ font = 'Ubuntu Mono 9',
+ play_icon = '/usr/share/icons/Papirus-Light/24x24/categories/spotify.svg',
+ pause_icon = '/usr/share/icons/Papirus-Dark/24x24/panel/spotify-indicator.svg'
+ }),
+ ...
+```
diff --git a/.config/awesome/awesome-wm-widgets/spotify-widget/spo-wid-1.png b/.config/awesome/awesome-wm-widgets/spotify-widget/spo-wid-1.png
new file mode 100755
index 0000000..5c7e403
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/spotify-widget/spo-wid-1.png differ
diff --git a/.config/awesome/awesome-wm-widgets/spotify-widget/spotify-widget-custom-paused.png b/.config/awesome/awesome-wm-widgets/spotify-widget/spotify-widget-custom-paused.png
new file mode 100755
index 0000000..9ac9c4a
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/spotify-widget/spotify-widget-custom-paused.png differ
diff --git a/.config/awesome/awesome-wm-widgets/spotify-widget/spotify-widget-custom-playing.png b/.config/awesome/awesome-wm-widgets/spotify-widget/spotify-widget-custom-playing.png
new file mode 100755
index 0000000..f9628f9
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/spotify-widget/spotify-widget-custom-playing.png differ
diff --git a/.config/awesome/awesome-wm-widgets/spotify-widget/spotify.lua b/.config/awesome/awesome-wm-widgets/spotify-widget/spotify.lua
new file mode 100755
index 0000000..2c30685
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/spotify-widget/spotify.lua
@@ -0,0 +1,171 @@
+-------------------------------------------------
+-- Spotify Widget for Awesome Window Manager
+-- Shows currently playing song on Spotify for Linux client
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/spotify-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+
+local function ellipsize(text, length)
+ -- utf8 only available in Lua 5.3+
+ if utf8 == nil then
+ return text:sub(0, length)
+ end
+ return (utf8.len(text) > length and length > 0)
+ and text:sub(0, utf8.offset(text, length - 2) - 1) .. '...'
+ or text
+end
+
+local spotify_widget = {}
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local play_icon = args.play_icon or '/usr/share/icons/Arc/actions/24/player_play.png'
+ local pause_icon = args.pause_icon or '/usr/share/icons/Arc/actions/24/player_pause.png'
+ local font = args.font or 'Play 9'
+ local dim_when_paused = args.dim_when_paused == nil and false or args.dim_when_paused
+ local dim_opacity = args.dim_opacity or 0.2
+ local max_length = args.max_length or 15
+ local show_tooltip = args.show_tooltip == nil and true or args.show_tooltip
+ local timeout = args.timeout or 1
+ local sp_bin = args.sp_bin or 'sp'
+
+ local GET_SPOTIFY_STATUS_CMD = sp_bin .. ' status'
+ local GET_CURRENT_SONG_CMD = sp_bin .. ' current'
+
+ local cur_artist = ''
+ local cur_title = ''
+ local cur_album = ''
+
+ spotify_widget = wibox.widget {
+ {
+ id = 'artistw',
+ font = font,
+ widget = wibox.widget.textbox,
+ },
+ {
+ layout = wibox.layout.stack,
+ {
+ id = "icon",
+ widget = wibox.widget.imagebox,
+ },
+ {
+ widget = wibox.widget.textbox,
+ font = font,
+ text = ' ',
+ forced_height = 1
+ }
+ },
+ {
+ layout = wibox.container.scroll.horizontal,
+ max_size = 100,
+ step_function = wibox.container.scroll.step_functions.waiting_nonlinear_back_and_forth,
+ speed = 40,
+ {
+ id = 'titlew',
+ font = font,
+ widget = wibox.widget.textbox
+ }
+ },
+ layout = wibox.layout.align.horizontal,
+ set_status = function(self, is_playing)
+ self:get_children_by_id('icon')[1]:set_image(is_playing and play_icon or pause_icon)
+ if dim_when_paused then
+ self:get_children_by_id('icon')[1]:set_opacity(is_playing and 1 or dim_opacity)
+
+ self:get_children_by_id('titlew')[1]:set_opacity(is_playing and 1 or dim_opacity)
+ self:get_children_by_id('titlew')[1]:emit_signal('widget::redraw_needed')
+
+ self:get_children_by_id('artistw')[1]:set_opacity(is_playing and 1 or dim_opacity)
+ self:get_children_by_id('artistw')[1]:emit_signal('widget::redraw_needed')
+ end
+ end,
+ set_text = function(self, artist, song)
+ local artist_to_display = ellipsize(artist, max_length)
+ if self:get_children_by_id('artistw')[1]:get_markup() ~= artist_to_display then
+ self:get_children_by_id('artistw')[1]:set_markup(artist_to_display)
+ end
+ local title_to_display = ellipsize(song, max_length)
+ if self:get_children_by_id('titlew')[1]:get_markup() ~= title_to_display then
+ self:get_children_by_id('titlew')[1]:set_markup(title_to_display)
+ end
+ end
+ }
+
+ local update_widget_icon = function(widget, stdout, _, _, _)
+ stdout = string.gsub(stdout, "\n", "")
+ widget:set_status(stdout == 'Playing' and true or false)
+ end
+
+ local update_widget_text = function(widget, stdout, _, _, _)
+ if string.find(stdout, 'Error: Spotify is not running.') ~= nil then
+ widget:set_text('','')
+ widget:set_visible(false)
+ return
+ end
+
+ local escaped = string.gsub(stdout, "&", '&')
+ local album, _, artist, title =
+ string.match(escaped, 'Album%s*(.*)\nAlbumArtist%s*(.*)\nArtist%s*(.*)\nTitle%s*(.*)\n')
+
+ if album ~= nil and title ~=nil and artist ~= nil then
+ cur_artist = artist
+ cur_title = title
+ cur_album = album
+
+ widget:set_text(artist, title)
+ widget:set_visible(true)
+ end
+ end
+
+ watch(GET_SPOTIFY_STATUS_CMD, timeout, update_widget_icon, spotify_widget)
+ watch(GET_CURRENT_SONG_CMD, timeout, update_widget_text, spotify_widget)
+
+ --- Adds mouse controls to the widget:
+ -- - left click - play/pause
+ -- - scroll up - play next song
+ -- - scroll down - play previous song
+ spotify_widget:connect_signal("button::press", function(_, _, _, button)
+ if (button == 1) then
+ awful.spawn("sp play", false) -- left click
+ elseif (button == 4) then
+ awful.spawn("sp next", false) -- scroll up
+ elseif (button == 5) then
+ awful.spawn("sp prev", false) -- scroll down
+ end
+ awful.spawn.easy_async(GET_SPOTIFY_STATUS_CMD, function(stdout, stderr, exitreason, exitcode)
+ update_widget_icon(spotify_widget, stdout, stderr, exitreason, exitcode)
+ end)
+ end)
+
+
+ if show_tooltip then
+ local spotify_tooltip = awful.tooltip {
+ mode = 'outside',
+ preferred_positions = {'bottom'},
+ }
+
+ spotify_tooltip:add_to_object(spotify_widget)
+
+ spotify_widget:connect_signal('mouse::enter', function()
+ spotify_tooltip.markup = 'Album: ' .. cur_album
+ .. '\nArtist: ' .. cur_artist
+ .. '\nSong: ' .. cur_title
+ end)
+ end
+
+ return spotify_widget
+
+end
+
+return setmetatable(spotify_widget, { __call = function(_, ...)
+ return worker(...)
+end })
diff --git a/.config/awesome/awesome-wm-widgets/stackoverflow-widget/README.md b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/README.md
new file mode 100755
index 0000000..0098062
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/README.md
@@ -0,0 +1,47 @@
+# Stackoverflow widget
+
+When clicked, widget shows latest questions from stackoverflow.com with a given tag(s).
+
+
+
+## Customization
+
+It is possible to customize widget by providing a table with all or some of the following config parameters:
+
+| Name | Default | Description |
+|---|---|---|
+| `icon`| `/.config/awesome/awesome-wm-widgets/stackoverflow-widget/so-icon.svg` | Path to the icon |
+| `limit` | 5 | Number of items to show in the widget |
+| `tagged` | awesome-wm | Tag, or comma-separated tags |
+| `timeout` | 300 | How often in seconds the widget refreshes |
+
+## Installation
+
+1. Clone this repo (if not cloned yet) under **~/.config/awesome/**:
+
+ ```bash
+ git clone https://github.com/streetturtle/awesome-wm-widgets.git ~/.config/awesome/
+ ```
+
+1. Require widget at the top of the **rc.lua**:
+
+ ```lua
+ local stackoverflow_widget = require("awesome-wm-widgets.stackoverflow-widget.stackoverflow")
+ ```
+
+1. Add widget to the tasklist:
+
+ ```lua
+ s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ --default
+ stackoverflow_widget(),
+ --customized
+ stackoverflow_widget({
+ limit = 10
+ })
+ ...
+ ```
+
diff --git a/.config/awesome/awesome-wm-widgets/stackoverflow-widget/screenshot.png b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/screenshot.png
new file mode 100755
index 0000000..b0058ab
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/screenshot.png differ
diff --git a/.config/awesome/awesome-wm-widgets/stackoverflow-widget/so-icon.svg b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/so-icon.svg
new file mode 100755
index 0000000..5298d4c
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/so-icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/stackoverflow-widget/stackoverflow.lua b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/stackoverflow.lua
new file mode 100755
index 0000000..15d2837
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/stackoverflow-widget/stackoverflow.lua
@@ -0,0 +1,125 @@
+-------------------------------------------------
+-- Stackoverflow Widget for Awesome Window Manager
+-- Shows new questions by a given tag
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/stackoverflow-widget
+
+-- @author Pavel Makhov
+-- @copyright 2019 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local watch = require("awful.widget.watch")
+local json = require("json")
+local spawn = require("awful.spawn")
+local gears = require("gears")
+local beautiful = require("beautiful")
+
+local HOME_DIR = os.getenv("HOME")
+
+local GET_QUESTIONS_CMD = [[bash -c "curl --compressed -s -X GET]]
+ .. [[ 'http://api.stackexchange.com/2.2/questions/no-answers]]
+ .. [[?page=1&pagesize=%s&order=desc&sort=activity&tagged=%s&site=stackoverflow'"]]
+
+local stackoverflow_widget = {}
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or HOME_DIR .. '/.config/awesome/awesome-wm-widgets/stackoverflow-widget/so-icon.svg'
+ local limit = args.limit or 5
+ local tagged = args.tagged or 'awesome-wm'
+ local timeout = args.timeout or 300
+
+ local rows = {
+ { widget = wibox.widget.textbox },
+ layout = wibox.layout.fixed.vertical,
+ }
+
+ local popup = awful.popup{
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ preferred_positions = 'top',
+ offset = { y = 5 },
+ widget = {}
+ }
+
+ stackoverflow_widget = wibox.widget {
+ {
+ image = icon,
+ widget = wibox.widget.imagebox
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_text = function(self, new_value)
+ self.txt.text = new_value
+ end,
+ }
+
+ local update_widget = function(_, stdout, _, _, _)
+
+ local result = json.decode(stdout)
+
+ for i = 0, #rows do rows[i]=nil end
+ for _, item in ipairs(result.items) do
+ local tags = ''
+ for i = 1, #item.tags do tags = tags .. item.tags[i] .. ' ' end
+ local row = wibox.widget {
+ {
+ {
+ {
+ text = item.title,
+ widget = wibox.widget.textbox
+ },
+ {
+ text = tags,
+ align = 'right',
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.align.vertical
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("button::release", function()
+ spawn.with_shell("xdg-open " .. item.link)
+ popup.visible = false
+ end)
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ table.insert(rows, row)
+ end
+
+ popup:setup(rows)
+ end
+
+ stackoverflow_widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+ watch(string.format(GET_QUESTIONS_CMD, limit, tagged), timeout, update_widget, stackoverflow_widget)
+ return stackoverflow_widget
+end
+
+return setmetatable(stackoverflow_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/README.md b/.config/awesome/awesome-wm-widgets/todo-widget/README.md
new file mode 100755
index 0000000..c97d845
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/README.md
@@ -0,0 +1,28 @@
+# ToDo Widget
+
+This widget displays a list of todo items and allows marking item as done/undone, delete an item and create new ones:
+
+
+
+# Installation
+
+Widget persists todo items as a JSON, so in order to simplify JSON serialisation/deserialisation download a **json.lua** from this repository: https://github.com/rxi/json.lua under `~/.config/awesone` folder. And don't forget to star a repo :)
+
+Then clone this repository under **~/.config/awesome/** and add the widget in **rc.lua**:
+
+```lua
+local todo_widget = require("awesome-wm-widgets.todo-widget.todo")
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ todo_widget(),
+ ...
+```
+Also note that widget uses [Arc Icons](https://github.com/horst3180/arc-icon-theme) and expects them to be installed under `/usr/share/icons/Arc/`.
+
+# Theming
+
+Widget uses your theme's colors. In case you want to have different colors, without changing your theme, please create an issue for it. I'll extract them as widget parameters.
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/checkbox-checked-symbolic.svg b/.config/awesome/awesome-wm-widgets/todo-widget/checkbox-checked-symbolic.svg
new file mode 100755
index 0000000..afeca62
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/checkbox-checked-symbolic.svg
@@ -0,0 +1,148 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/chevron-down.svg b/.config/awesome/awesome-wm-widgets/todo-widget/chevron-down.svg
new file mode 100755
index 0000000..dceeb0f
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/chevron-down.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/chevron-up.svg b/.config/awesome/awesome-wm-widgets/todo-widget/chevron-up.svg
new file mode 100755
index 0000000..88474ce
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/chevron-up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/list-add-symbolic.svg b/.config/awesome/awesome-wm-widgets/todo-widget/list-add-symbolic.svg
new file mode 100755
index 0000000..9cc2d3a
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/list-add-symbolic.svg
@@ -0,0 +1,157 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/todo.gif b/.config/awesome/awesome-wm-widgets/todo-widget/todo.gif
new file mode 100755
index 0000000..7160e21
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/todo-widget/todo.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/todo.lua b/.config/awesome/awesome-wm-widgets/todo-widget/todo.lua
new file mode 100755
index 0000000..78ca262
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/todo.lua
@@ -0,0 +1,340 @@
+-------------------------------------------------
+-- ToDo Widget for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/todo-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local json = require("json")
+local spawn = require("awful.spawn")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local gfs = require("gears.filesystem")
+
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/todo-widget'
+local STORAGE = HOME_DIR .. '/.cache/awmw/todo-widget/todos.json'
+
+local GET_TODO_ITEMS = 'bash -c "cat ' .. STORAGE .. '"'
+
+local rows = { layout = wibox.layout.fixed.vertical }
+local todo_widget = {}
+local update_widget
+todo_widget.widget = wibox.widget {
+ {
+ {
+ {
+ {
+ id = "icon",
+ forced_height = 16,
+ forced_width = 16,
+ widget = wibox.widget.imagebox
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ id = "txt",
+ widget = wibox.widget.textbox
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 4)
+ end,
+ widget = wibox.container.background,
+ set_text = function(self, new_value)
+ self:get_children_by_id("txt")[1].text = new_value
+ end,
+ set_icon = function(self, new_value)
+ self:get_children_by_id("icon")[1].image = new_value
+ end
+}
+
+function todo_widget:update_counter(todos)
+ local todo_count = 0
+ for _,p in ipairs(todos) do
+ if not p.status then
+ todo_count = todo_count + 1
+ end
+ end
+
+ todo_widget.widget:set_text(todo_count)
+end
+
+local popup = awful.popup{
+ bg = beautiful.bg_normal,
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local add_button = wibox.widget {
+ {
+ {
+ image = WIDGET_DIR .. '/list-add-symbolic.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ top = 11,
+ left = 8,
+ right = 8,
+ layout = wibox.container.margin
+ },
+ shape = function(cr, width, height)
+ gears.shape.circle(cr, width, height, 12)
+ end,
+ widget = wibox.container.background
+}
+
+add_button:connect_signal("button::press", function()
+ local pr = awful.widget.prompt()
+
+ table.insert(rows, wibox.widget {
+ {
+ {
+ pr.widget,
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+ awful.prompt.run{
+ prompt = "New item: ",
+ bg = beautiful.bg_normal,
+ bg_cursor = beautiful.fg_urgent,
+ textbox = pr.widget,
+ exe_callback = function(input_text)
+ if not input_text or #input_text == 0 then return end
+ spawn.easy_async(GET_TODO_ITEMS, function(stdout)
+ local res = json.decode(stdout)
+ table.insert(res.todo_items, {todo_item = input_text, status = false})
+ spawn.easy_async_with_shell("echo '" .. json.encode(res) .. "' > " .. STORAGE, function()
+ spawn.easy_async(GET_TODO_ITEMS, function(items) update_widget(items) end)
+ end)
+ end)
+ end
+ }
+ popup:setup(rows)
+end)
+add_button:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+add_button:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local icon = args.icon or WIDGET_DIR .. '/checkbox-checked-symbolic.svg'
+
+ todo_widget.widget:set_icon(icon)
+
+ function update_widget(stdout)
+ local result = json.decode(stdout)
+ if result == nil or result == '' then result = {} end
+ todo_widget:update_counter(result.todo_items)
+
+ for i = 0, #rows do rows[i]=nil end
+
+ local first_row = wibox.widget {
+ {
+ {widget = wibox.widget.textbox},
+ {
+ markup = 'ToDo',
+ align = 'center',
+ forced_width = 350, -- for horizontal alignment
+ forced_height = 40,
+ widget = wibox.widget.textbox
+ },
+ add_button,
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ table.insert(rows, first_row)
+
+ for i, todo_item in ipairs(result.todo_items) do
+
+ local checkbox = wibox.widget {
+ checked = todo_item.status,
+ color = beautiful.bg_normal,
+ paddings = 2,
+ shape = gears.shape.circle,
+ forced_width = 20,
+ forced_height = 20,
+ check_color = beautiful.fg_urgent,
+ widget = wibox.widget.checkbox
+ }
+
+ checkbox:connect_signal("button::press", function(c)
+ c:set_checked(not c.checked)
+ todo_item.status = not todo_item.status
+ result.todo_items[i] = todo_item
+ spawn.easy_async_with_shell("echo '" .. json.encode(result) .. "' > " .. STORAGE, function ()
+ todo_widget:update_counter(result.todo_items)
+ end)
+ end)
+
+
+ local trash_button = wibox.widget {
+ {
+ { image = WIDGET_DIR .. '/window-close-symbolic.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ },
+ margins = 5,
+ layout = wibox.container.margin
+ },
+ border_width = 1,
+ shape = function(cr, width, height)
+ gears.shape.circle(cr, width, height, 10)
+ end,
+ widget = wibox.container.background
+ }
+
+ trash_button:connect_signal("button::press", function()
+ table.remove(result.todo_items, i)
+ spawn.easy_async_with_shell("printf '" .. json.encode(result) .. "' > " .. STORAGE, function ()
+ spawn.easy_async(GET_TODO_ITEMS, function(items) update_widget(items) end)
+ end)
+ end)
+
+
+ local move_up = wibox.widget {
+ image = WIDGET_DIR .. '/chevron-up.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ }
+
+ move_up:connect_signal("button::press", function()
+ local temp = result.todo_items[i]
+ result.todo_items[i] = result.todo_items[i-1]
+ result.todo_items[i-1] = temp
+ spawn.easy_async_with_shell("printf '" .. json.encode(result) .. "' > " .. STORAGE, function ()
+ spawn.easy_async(GET_TODO_ITEMS, function(items) update_widget(items) end)
+ end)
+ end)
+
+ local move_down = wibox.widget {
+ image = WIDGET_DIR .. '/chevron-down.svg',
+ resize = false,
+ widget = wibox.widget.imagebox
+ }
+
+ move_down:connect_signal("button::press", function()
+ local temp = result.todo_items[i]
+ result.todo_items[i] = result.todo_items[i+1]
+ result.todo_items[i+1] = temp
+ spawn.easy_async_with_shell("printf '" .. json.encode(result) .. "' > " .. STORAGE, function ()
+ spawn.easy_async(GET_TODO_ITEMS, function(items) update_widget(items) end)
+ end)
+ end)
+
+
+ local move_buttons = {
+ layout = wibox.layout.fixed.vertical
+ }
+
+ if i == 1 and #result.todo_items > 1 then
+ table.insert(move_buttons, move_down)
+ elseif i == #result.todo_items and #result.todo_items > 1 then
+ table.insert(move_buttons, move_up)
+ elseif #result.todo_items > 1 then
+ table.insert(move_buttons, move_up)
+ table.insert(move_buttons, move_down)
+ end
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ checkbox,
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ {
+ text = todo_item.todo_item,
+ align = 'left',
+ widget = wibox.widget.textbox
+ },
+ left = 10,
+ layout = wibox.container.margin
+ },
+ {
+ {
+ move_buttons,
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ trash_button,
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 8,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ table.insert(rows, row)
+ end
+
+ popup:setup(rows)
+ end
+
+ todo_widget.widget:buttons(
+ gears.table.join(
+ awful.button({}, 1, function()
+ if popup.visible then
+ todo_widget.widget:set_bg('#00000000')
+ popup.visible = not popup.visible
+ else
+ todo_widget.widget:set_bg(beautiful.bg_focus)
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)
+ )
+ )
+
+ spawn.easy_async(GET_TODO_ITEMS, function(stdout) update_widget(stdout) end)
+
+ return todo_widget.widget
+end
+
+if not gfs.file_readable(STORAGE) then
+ spawn.easy_async(string.format([[bash -c "dirname %s | xargs mkdir -p && echo '{\"todo_items\":{}}' > %s"]],
+ STORAGE, STORAGE))
+end
+
+return setmetatable(todo_widget, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/todo-widget/window-close-symbolic.svg b/.config/awesome/awesome-wm-widgets/todo-widget/window-close-symbolic.svg
new file mode 100755
index 0000000..46ff888
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/todo-widget/window-close-symbolic.svg
@@ -0,0 +1,95 @@
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/translate-widget/README.MD b/.config/awesome/awesome-wm-widgets/translate-widget/README.MD
new file mode 100755
index 0000000..a82fda7
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/translate-widget/README.MD
@@ -0,0 +1,38 @@
+# Translate Widget
+
+This widget allows quickly translate words or phrases without opening a browser - just using Awesome. To provide direction of the translation add the 2 letters code of the source and target languages at the end of the phrase, for example _hello enfr_ will translate _hello_ from English to French. This widget is based on [Watson Language Translator](https://www.ibm.com/watson/services/language-translator/) from IBM.
+
+
+
+## Controls
+
+ - Mod4 + c - opens a translate prompt;
+ - left click on the popup widget - copies the translation to the clipboard and closes widget;
+ - right click on the popup widget - copies text to translate to the clipboard and closes widget.
+
+## Installation
+
+1. Clone repo under **~/.config/awesome/**
+1. Create an IBM Cloud API key at [cloud.ibm.com/iam/apikeys](https://cloud.ibm.com/iam/apikeys)
+1. Copy a service URL by going to [resource list](https://cloud.ibm.com/resources), then under "Services" select "Language Translator" option, and then copy URL from the "Credentials" section
+1. Require widget in **rc.lua**:
+
+ ```lua
+ local translate = require("awesome-wm-widgets.translate-widget.translate")
+ ```
+
+1. Add a shortcut to run translate prompt:
+
+ ```lua
+ awful.key({ modkey }, "c", function()
+ translate.launch{api_key = '', url = 'url'}
+ end, { description = "run translate prompt", group = "launcher" })
+ ```
+
+
+
+
+
+
+
+
diff --git a/.config/awesome/awesome-wm-widgets/translate-widget/demo.gif b/.config/awesome/awesome-wm-widgets/translate-widget/demo.gif
new file mode 100755
index 0000000..3645d47
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/translate-widget/demo.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/translate-widget/demo1.gif b/.config/awesome/awesome-wm-widgets/translate-widget/demo1.gif
new file mode 100755
index 0000000..62cc9f4
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/translate-widget/demo1.gif differ
diff --git a/.config/awesome/awesome-wm-widgets/translate-widget/gnome-translate.svg b/.config/awesome/awesome-wm-widgets/translate-widget/gnome-translate.svg
new file mode 100755
index 0000000..ca02b1a
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/translate-widget/gnome-translate.svg
@@ -0,0 +1,12 @@
+
diff --git a/.config/awesome/awesome-wm-widgets/translate-widget/translate.lua b/.config/awesome/awesome-wm-widgets/translate-widget/translate.lua
new file mode 100755
index 0000000..d680c4e
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/translate-widget/translate.lua
@@ -0,0 +1,201 @@
+-------------------------------------------------
+-- Translate Widget based on the Yandex.Translate API
+-- https://tech.yandex.com/translate/
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local spawn = require("awful.spawn")
+local capi = {keygrabber = keygrabber }
+local beautiful = require("beautiful")
+local json = require("json")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local gears = require("gears")
+local gfs = require("gears.filesystem")
+
+local TRANSLATE_CMD = [[bash -c 'curl -s -u "apikey:%s" -H "Content-Type: application/json"]]
+ ..[[ -d '\''{"text": ["%s"], "model_id":"%s"}'\'' "%s/v3/translate?version=2018-05-01"']]
+local ICON = os.getenv("HOME") .. '/.config/awesome/awesome-wm-widgets/translate-widget/gnome-translate.svg'
+
+--- Returns two values - string to translate and direction:
+-- 'dog enfr' -> 'dog', 'en-fr'
+-- @param input_string user's input which consists of
+-- text to translate and direction, 'dog enfr'
+local function extract(input_string)
+ local word, lang = input_string:match('^(.+)%s(%a%a%a%a)$')
+
+ if word ~= nil and lang ~= nil then
+ lang = lang:sub(1, 2) .. '-' .. lang:sub(3)
+ end
+ return word, lang
+end
+
+local function show_warning(message)
+ naughty.notify{
+ preset = naughty.config.presets.critical,
+ title = 'Translate Shell',
+ text = message}
+end
+
+local w = awful.popup {
+ widget = {},
+ visible = false,
+ border_width = 1,
+ maximum_width = 400,
+ width = 400,
+ border_color = '#66ccff',
+ ontop = true,
+ bg = beautiful.bg_normal,
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 3)
+ end,
+}
+awful.placement.top(w, { margins = {top = 40}})
+
+
+--- Main function - takes the user input and shows the widget with translation
+-- @param request_string - user input (dog enfr)
+local function translate(to_translate, lang, api_key, url)
+
+ local cmd = string.format(TRANSLATE_CMD, api_key, to_translate, lang, url)
+ spawn.easy_async(cmd, function (stdout, stderr)
+ if stderr ~= '' then
+ show_warning(stderr)
+ end
+
+ local resp = json.decode(stdout)
+
+ w:setup {
+ {
+ {
+ {
+ {
+ image = ICON,
+ widget = wibox.widget.imagebox,
+ resize = false
+ },
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ {
+ id = 'src',
+ markup = '' .. lang:sub(1,2) .. ': '
+ .. to_translate .. '',
+ widget = wibox.widget.textbox
+ },
+ {
+ id = 'res',
+ markup = '' .. lang:sub(4) .. ': '
+ .. resp.translations[1].translation .. '',
+ widget = wibox.widget.textbox
+ },
+ id = 'text',
+ layout = wibox.layout.fixed.vertical,
+ },
+ id = 'left',
+ spacing = 8,
+ layout = wibox.layout.fixed.horizontal
+ },
+ bg = beautiful.bg_normal,
+ forced_width = 400,
+ widget = wibox.container.background
+ },
+ color = beautiful.bg_normal,
+ margins = 8,
+ widget = wibox.container.margin
+ }
+
+ w.visible = true
+ w:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function()
+ spawn.with_shell("echo '" .. resp.translations[1].translation .. "' | xclip -selection clipboard")
+ w.visible = false
+ end),
+ awful.button({}, 3, function()
+ spawn.with_shell("echo '" .. to_translate .."' | xclip -selection clipboard")
+ w.visible = false
+ end)
+ )
+ )
+
+ capi.keygrabber.run(function(_, key, event)
+ if event == "release" then return end
+ if key then
+ capi.keygrabber.stop()
+ w.visible = false
+ end
+ end)
+ end)
+end
+
+local prompt = awful.widget.prompt()
+local input_widget = wibox {
+ visible = false,
+ width = 300,
+ height = 100,
+ maxmimum_width = 300,
+ maxmimum_height = 900,
+ ontop = true,
+ screen = mouse.screen,
+ expand = true,
+ bg = beautiful.bg_normal,
+ max_widget_size = 500,
+ border_width = 1,
+ border_color = '#66ccff',
+ shape = function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, 3)
+ end,
+}
+
+input_widget:setup{
+ {
+ prompt,
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ },
+ margins = 8,
+ widget = wibox.container.margin
+}
+
+local function launch(user_args)
+
+ local args = user_args or {}
+
+ local api_key = args.api_key
+ local url = args.url
+
+ awful.placement.top(input_widget, { margins = {top = 40}, parent = awful.screen.focused()})
+ input_widget.visible = true
+
+ awful.prompt.run {
+ prompt = "Translate: ",
+ textbox = prompt.widget,
+ history_path = gfs.get_dir('cache') .. '/translate_history',
+ bg_cursor = '#66ccff',
+ exe_callback = function(text)
+ if not text or #text == 0 then return end
+ local to_translate, lang = extract(text)
+ if not to_translate or #to_translate==0 or not lang or #lang == 0 then
+ naughty.notify({
+ preset = naughty.config.presets.critical,
+ title = 'Translate Widget Error',
+ text = 'Language is not provided',
+ })
+ return
+ end
+ translate(to_translate, lang, api_key, url)
+ end,
+ done_callback = function()
+ input_widget.visible = false
+ end
+ }
+end
+
+return {
+ launch = launch
+}
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/README.md b/.config/awesome/awesome-wm-widgets/volume-widget/README.md
new file mode 100755
index 0000000..4fc7f55
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/README.md
@@ -0,0 +1,119 @@
+# Volume widget
+
+Volume widget based on [amixer](https://linux.die.net/man/1/amixer) (is used for controlling the audio volume) and [pacmd](https://linux.die.net/man/1/pacmd) (is used for selecting a sink/source). Also, the widget provides an easy way to customize how it looks, following types are supported out-of-the-box:
+
+
+
+From left to right: `horizontal_bar`, `vertical_bar`, `icon`, `icon_and_text`, `arc`
+
+A right-click on the widget opens a popup where you can choose a sink/source:
+
+
+Left click toggles mute and middle click opens a mixer ([pavucontrol](https://freedesktop.org/software/pulseaudio/pavucontrol/) by default).
+
+### Features
+
+ - switch between sinks/sources by right click on the widget;
+ - more responsive than previous versions of volume widget, which were refreshed once a second;
+ - 5 predefined customizable looks;
+
+## Installation
+
+Clone the repo under **~/.config/awesome/** and add widget in **rc.lua**:
+
+```lua
+local volume_widget = require('awesome-wm-widgets.volume-widget.volume')
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ volume_widget(),
+ -- customized
+ volume_widget{
+ widget_type = 'arc'
+ },
+```
+
+Note that widget uses following command the get the current volume: `amixer -D pulse sget Master`, so please make sure that it works for you, otherwise you need to set parameter `device = 'default'`.
+
+### Shortcuts
+
+To improve responsiveness of the widget when volume level is changed by a shortcut use corresponding methods of the widget:
+
+```lua
+awful.key({ modkey }, "]", function() volume_widget:inc(5) end),
+awful.key({ modkey }, "[", function() volume_widget:dec(5) end),
+awful.key({ modkey }, "\\", function() volume_widget:toggle() end),
+```
+
+## Customization
+
+It is possible to customize the widget by providing a table with all or some of the following config parameters:
+
+### Generic parameter
+
+| Name | Default | Description |
+|---|---|---|
+| `mixer_cmd` | `pavucontrol` | command to run on middle click (e.g. a mixer program) |
+| `step` | `5` | How much the volume is raised or lowered at once (in %) |
+| `widget_type`| `icon_and_text`| Widget type, one of `horizontal_bar`, `vertical_bar`, `icon`, `icon_and_text`, `arc` |
+| `device` | `pulse` | Select the device name to control |
+
+Depends on the chosen widget type add parameters from the corresponding section below:
+
+#### `icon` parameters
+
+| Name | Default | Description |
+|---|---|---|
+| `icon_dir`| `./icons`| Path to the folder with icons |
+
+_Note:_ if you are changing icons, the folder should contain following .svg images:
+ - audio-volume-high-symbolic
+ - audio-volume-medium-symbolic
+ - audio-volume-low-symbolic
+ - audio-volume-muted-symbolic
+
+#### `icon_and_text` parameters
+
+| Name | Default | Description |
+|---|---|---|
+| `icon_dir`| `./icons`| Path to the folder with icons |
+| `font` | `beautiful.font` | Font name and size, like `Play 12` |
+
+#### `arc` parameters
+
+| Name | Default | Description |
+|---|---|---|
+| `thickness` | 2 | Thickness of the arc |
+| `main_color` | `beautiful.fg_color` | Color of the arc |
+| `bg_color` | `#ffffff11` | Color of the arc's background |
+| `mute_color` | `beautiful.fg_urgent` | Color of the arc when mute |
+| `size` | 18 | Size of the widget |
+
+#### `horizontal_bar` parameters
+
+| Name | Default | Description |
+|---|---|---|
+| `main_color` | `beautiful.fg_normal` | Color of the bar |
+| `mute_color` | `beautiful.fg_urgent` | Color of the bar when mute |
+| `bg_color` | `'#ffffff11'` | Color of the bar's background |
+| `width` | `50` | The bar width |
+| `margins` | `10` | Top and bottom margins (if your wibar is 22 px high, bar will be 2 px = 22 - 2*10) |
+| `shape` | `'bar'` | [gears.shape](https://awesomewm.org/doc/api/libraries/gears.shape.html), could be `octogon`, `hexagon`, `powerline`, etc |
+| `with_icon` | `true` | Show volume icon|
+
+_Note:_ I didn't figure out how does the `forced_height` property of progressbar widget work (maybe it doesn't work at all), thus there is a workaround with margins.
+
+#### `vertical_bar` parameters
+
+| Name | Default | Description |
+|---|---|---|
+| `main_color` | `beautiful.fg_normal` | Color of the bar |
+| `mute_color` | `beautiful.fg_urgent` | Color of the bar when mute |
+| `bg_color` | `'#ffffff11'` | Color of the bar's background |
+| `width` | `10` | The bar width |
+| `margins` | `20` | Top and bottom margins (if your wibar is 22 px high, bar will be 2 px = 22 - 2*10) |
+| `shape` | `'bar'` | [gears.shape](https://awesomewm.org/doc/api/libraries/gears.shape.html), could be `octogon`, `hexagon`, `powerline`, etc |
+| `with_icon` | `true` | Show volume icon|
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-high-symbolic.svg b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-high-symbolic.svg
new file mode 100755
index 0000000..985c107
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-high-symbolic.svg
@@ -0,0 +1,88 @@
+
+
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-low-symbolic.svg b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-low-symbolic.svg
new file mode 100755
index 0000000..7eb4531
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-low-symbolic.svg
@@ -0,0 +1,88 @@
+
+
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-medium-symbolic.svg b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-medium-symbolic.svg
new file mode 100755
index 0000000..11e44fe
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-medium-symbolic.svg
@@ -0,0 +1,88 @@
+
+
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-muted-symbolic.svg b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-muted-symbolic.svg
new file mode 100755
index 0000000..e577d05
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/icons/audio-volume-muted-symbolic.svg
@@ -0,0 +1,88 @@
+
+
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/screenshots/variations.png b/.config/awesome/awesome-wm-widgets/volume-widget/screenshots/variations.png
new file mode 100755
index 0000000..21d7ead
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/volume-widget/screenshots/variations.png differ
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/screenshots/volume-sink-sources.png b/.config/awesome/awesome-wm-widgets/volume-widget/screenshots/volume-sink-sources.png
new file mode 100755
index 0000000..7d010bc
Binary files /dev/null and b/.config/awesome/awesome-wm-widgets/volume-widget/screenshots/volume-sink-sources.png differ
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/utils.lua b/.config/awesome/awesome-wm-widgets/volume-widget/utils.lua
new file mode 100755
index 0000000..417a666
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/utils.lua
@@ -0,0 +1,105 @@
+
+
+local utils = {}
+
+local function split(string_to_split, separator)
+ if separator == nil then separator = "%s" end
+ local t = {}
+
+ for str in string.gmatch(string_to_split, "([^".. separator .."]+)") do
+ table.insert(t, str)
+ end
+
+ return t
+end
+
+function utils.extract_sinks_and_sources(pacmd_output)
+ local sinks = {}
+ local sources = {}
+ local device
+ local properties
+ local ports
+ local in_sink = false
+ local in_source = false
+ local in_device = false
+ local in_properties = false
+ local in_ports = false
+ for line in pacmd_output:gmatch("[^\r\n]+") do
+ if string.match(line, 'source%(s%) available.') then
+ in_sink = false
+ in_source = true
+ end
+ if string.match(line, 'sink%(s%) available.') then
+ in_sink = true
+ in_source = false
+ end
+
+ if string.match(line, 'index:') then
+ in_device = true
+ in_properties = false
+ device = {
+ id = line:match(': (%d+)'),
+ is_default = string.match(line, '*') ~= nil
+ }
+ if in_sink then
+ table.insert(sinks, device)
+ elseif in_source then
+ table.insert(sources, device)
+ end
+ end
+
+ if string.match(line, '^\tproperties:') then
+ in_device = false
+ in_properties = true
+ properties = {}
+ device['properties'] = properties
+ end
+
+ if string.match(line, 'ports:') then
+ in_device = false
+ in_properties = false
+ in_ports = true
+ ports = {}
+ device['ports'] = ports
+ end
+
+ if string.match(line, 'active port:') then
+ in_device = false
+ in_properties = false
+ in_ports = false
+ device['active_port'] = line:match(': (.+)'):gsub('<',''):gsub('>','')
+ end
+
+ if in_device then
+ local t = split(line, ': ')
+ local key = t[1]:gsub('\t+', ''):lower()
+ local value = t[2]:gsub('^<', ''):gsub('>$', '')
+ device[key] = value
+ end
+
+ if in_properties then
+ local t = split(line, '=')
+ local key = t[1]:gsub('\t+', ''):gsub('%.', '_'):gsub('-', '_'):gsub(':', ''):gsub("%s+$", "")
+ local value
+ if t[2] == nil then
+ value = t[2]
+ else
+ value = t[2]:gsub('"', ''):gsub("^%s+", ""):gsub(' Analog Stereo', '')
+ end
+ properties[key] = value
+ end
+
+ if in_ports then
+ local t = split(line, ': ')
+ local key = t[1]
+ if key ~= nil then
+ key = key:gsub('\t+', '')
+ end
+ ports[key] = t[2]
+ end
+ end
+
+ return sinks, sources
+end
+
+return utils
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/volume-2.svg b/.config/awesome/awesome-wm-widgets/volume-widget/volume-2.svg
new file mode 100755
index 0000000..10f1c67
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/volume-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/volume.lua b/.config/awesome/awesome-wm-widgets/volume-widget/volume.lua
new file mode 100755
index 0000000..4c44042
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/volume.lua
@@ -0,0 +1,228 @@
+-------------------------------------------------
+-- The Ultimate Volume Widget for Awesome Window Manager
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/volume-widget
+
+-- @author Pavel Makhov
+-- @copyright 2020 Pavel Makhov
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local spawn = require("awful.spawn")
+local gears = require("gears")
+local beautiful = require("beautiful")
+local watch = require("awful.widget.watch")
+local utils = require("awesome-wm-widgets.volume-widget.utils")
+
+
+local LIST_DEVICES_CMD = [[sh -c "pacmd list-sinks; pacmd list-sources"]]
+local function GET_VOLUME_CMD(device) return 'amixer -D ' .. device .. ' sget Master' end
+local function INC_VOLUME_CMD(device, step) return 'amixer -D ' .. device .. ' sset Master ' .. step .. '%+' end
+local function DEC_VOLUME_CMD(device, step) return 'amixer -D ' .. device .. ' sset Master ' .. step .. '%-' end
+local function TOG_VOLUME_CMD(device) return 'amixer -D ' .. device .. ' sset Master toggle' end
+
+
+local widget_types = {
+ icon_and_text = require("awesome-wm-widgets.volume-widget.widgets.icon-and-text-widget"),
+ icon = require("awesome-wm-widgets.volume-widget.widgets.icon-widget"),
+ arc = require("awesome-wm-widgets.volume-widget.widgets.arc-widget"),
+ horizontal_bar = require("awesome-wm-widgets.volume-widget.widgets.horizontal-bar-widget"),
+ vertical_bar = require("awesome-wm-widgets.volume-widget.widgets.vertical-bar-widget")
+}
+local volume = {}
+
+local rows = { layout = wibox.layout.fixed.vertical }
+
+local popup = awful.popup{
+ bg = beautiful.bg_normal,
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local function build_main_line(device)
+ if device.active_port ~= nil and device.ports[device.active_port] ~= nil then
+ return device.properties.device_description .. ' · ' .. device.ports[device.active_port]
+ else
+ return device.properties.device_description
+ end
+end
+
+local function build_rows(devices, on_checkbox_click, device_type)
+ local device_rows = { layout = wibox.layout.fixed.vertical }
+ for _, device in pairs(devices) do
+
+ local checkbox = wibox.widget {
+ checked = device.is_default,
+ color = beautiful.bg_normal,
+ paddings = 2,
+ shape = gears.shape.circle,
+ forced_width = 20,
+ forced_height = 20,
+ check_color = beautiful.fg_urgent,
+ widget = wibox.widget.checkbox
+ }
+
+ checkbox:connect_signal("button::press", function()
+ spawn.easy_async(string.format([[sh -c 'pacmd set-default-%s "%s"']], device_type, device.name), function()
+ on_checkbox_click()
+ end)
+ end)
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ checkbox,
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ {
+ text = build_main_line(device),
+ align = 'left',
+ widget = wibox.widget.textbox
+ },
+ left = 10,
+ layout = wibox.container.margin
+ },
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ local old_cursor, old_wibox
+ row:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:connect_signal("button::press", function()
+ spawn.easy_async(string.format([[sh -c 'pacmd set-default-%s "%s"']], device_type, device.name), function()
+ on_checkbox_click()
+ end)
+ end)
+
+ table.insert(device_rows, row)
+ end
+
+ return device_rows
+end
+
+local function build_header_row(text)
+ return wibox.widget{
+ {
+ markup = "" .. text .. "",
+ align = 'center',
+ widget = wibox.widget.textbox
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+end
+
+local function rebuild_popup()
+ spawn.easy_async(LIST_DEVICES_CMD, function(stdout)
+
+ local sinks, sources = utils.extract_sinks_and_sources(stdout)
+
+ for i = 0, #rows do rows[i]=nil end
+
+ table.insert(rows, build_header_row("SINKS"))
+ table.insert(rows, build_rows(sinks, function() rebuild_popup() end, "sink"))
+ table.insert(rows, build_header_row("SOURCES"))
+ table.insert(rows, build_rows(sources, function() rebuild_popup() end, "source"))
+
+ popup:setup(rows)
+ end)
+end
+
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local mixer_cmd = args.mixer_cmd or 'pavucontrol'
+ local widget_type = args.widget_type
+ local refresh_rate = args.refresh_rate or 1
+ local step = args.step or 5
+ local device = args.device or 'pulse'
+
+ if widget_types[widget_type] == nil then
+ volume.widget = widget_types['icon_and_text'].get_widget(args.icon_and_text_args)
+ else
+ volume.widget = widget_types[widget_type].get_widget(args)
+ end
+
+ local function update_graphic(widget, stdout)
+ local mute = string.match(stdout, "%[(o%D%D?)%]") -- \[(o\D\D?)\] - [on] or [off]
+ if mute == 'off' then widget:mute()
+ elseif mute == 'on' then widget:unmute()
+ end
+ local volume_level = string.match(stdout, "(%d?%d?%d)%%") -- (\d?\d?\d)\%)
+ volume_level = string.format("% 3d", volume_level)
+ widget:set_volume_level(volume_level)
+ end
+
+ function volume:inc(s)
+ spawn.easy_async(INC_VOLUME_CMD(device, s or step), function(stdout) update_graphic(volume.widget, stdout) end)
+ end
+
+ function volume:dec(s)
+ spawn.easy_async(DEC_VOLUME_CMD(device, s or step), function(stdout) update_graphic(volume.widget, stdout) end)
+ end
+
+ function volume:toggle()
+ spawn.easy_async(TOG_VOLUME_CMD(device), function(stdout) update_graphic(volume.widget, stdout) end)
+ end
+
+ function volume:mixer()
+ if mixer_cmd then
+ spawn.easy_async(mixer_cmd)
+ end
+ end
+
+ volume.widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 3, function()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ rebuild_popup()
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end),
+ awful.button({}, 4, function() volume:inc() end),
+ awful.button({}, 5, function() volume:dec() end),
+ awful.button({}, 2, function() volume:mixer() end),
+ awful.button({}, 1, function() volume:toggle() end)
+ )
+ )
+
+ watch(GET_VOLUME_CMD(device), refresh_rate, update_graphic, volume.widget)
+
+ return volume.widget
+end
+
+return setmetatable(volume, { __call = function(_, ...) return worker(...) end })
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/widgets/arc-widget.lua b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/arc-widget.lua
new file mode 100755
index 0000000..b512f12
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/arc-widget.lua
@@ -0,0 +1,46 @@
+local wibox = require("wibox")
+local beautiful = require('beautiful')
+
+local ICON_DIR = os.getenv("HOME") .. '/.config/awesome/awesome-wm-widgets/volume-widget/icons/'
+
+local widget = {}
+
+function widget.get_widget(widgets_args)
+ local args = widgets_args or {}
+
+ local thickness = args.thickness or 2
+ local main_color = args.main_color or beautiful.fg_color
+ local bg_color = args.bg_color or '#ffffff11'
+ local mute_color = args.mute_color or beautiful.fg_urgent
+ local size = args.size or 18
+
+ return wibox.widget {
+ {
+ id = "icon",
+ image = ICON_DIR .. 'audio-volume-high-symbolic.svg',
+ resize = true,
+ widget = wibox.widget.imagebox,
+ },
+ max_value = 100,
+ thickness = thickness,
+ start_angle = 4.71238898, -- 2pi*3/4
+ forced_height = size,
+ forced_width = size,
+ bg = bg_color,
+ paddings = 2,
+ widget = wibox.container.arcchart,
+ set_volume_level = function(self, new_value)
+ self.value = new_value
+ end,
+ mute = function(self)
+ self.colors = { mute_color }
+ end,
+ unmute = function(self)
+ self.colors = { main_color }
+ end
+ }
+
+end
+
+
+return widget
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/widgets/horizontal-bar-widget.lua b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/horizontal-bar-widget.lua
new file mode 100755
index 0000000..be1f38d
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/horizontal-bar-widget.lua
@@ -0,0 +1,58 @@
+local wibox = require("wibox")
+local beautiful = require('beautiful')
+local gears = require("gears")
+
+local ICON_DIR = os.getenv("HOME") .. '/.config/awesome/awesome-wm-widgets/volume-widget/icons/'
+
+local widget = {}
+
+function widget.get_widget(widgets_args)
+ local args = widgets_args or {}
+
+ local main_color = args.main_color or beautiful.fg_normal
+ local mute_color = args.mute_color or beautiful.fg_urgent
+ local bg_color = args.bg_color or '#ffffff11'
+ local width = args.width or 50
+ local margins = args.margins or 10
+ local shape = args.shape or 'bar'
+ local with_icon = args.with_icon == true and true or false
+
+ local bar = wibox.widget {
+ {
+ {
+ id = "icon",
+ image = ICON_DIR .. 'audio-volume-high-symbolic.svg',
+ resize = false,
+ widget = wibox.widget.imagebox,
+ },
+ valign = 'center',
+ visible = with_icon,
+ layout = wibox.container.place,
+ },
+ {
+ id = 'bar',
+ max_value = 100,
+ forced_width = width,
+ color = main_color,
+ margins = { top = margins, bottom = margins },
+ background_color = bg_color,
+ shape = gears.shape[shape],
+ widget = wibox.widget.progressbar,
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ set_volume_level = function(self, new_value)
+ self:get_children_by_id('bar')[1]:set_value(tonumber(new_value))
+ end,
+ mute = function(self)
+ self:get_children_by_id('bar')[1]:set_color(mute_color)
+ end,
+ unmute = function(self)
+ self:get_children_by_id('bar')[1]:set_color(main_color)
+ end
+ }
+
+ return bar
+end
+
+return widget
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/widgets/icon-and-text-widget.lua b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/icon-and-text-widget.lua
new file mode 100755
index 0000000..b1a2793
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/icon-and-text-widget.lua
@@ -0,0 +1,59 @@
+local wibox = require("wibox")
+local beautiful = require('beautiful')
+
+local widget = {}
+
+local ICON_DIR = os.getenv("HOME") .. '/.config/awesome/awesome-wm-widgets/volume-widget/icons/'
+
+function widget.get_widget(widgets_args)
+ local args = widgets_args or {}
+
+ local font = args.font or beautiful.font
+ local icon_dir = args.icon_dir or ICON_DIR
+
+ return wibox.widget {
+ {
+ {
+ id = "icon",
+ resize = false,
+ widget = wibox.widget.imagebox,
+ },
+ valign = 'center',
+ layout = wibox.container.place
+ },
+ {
+ id = 'txt',
+ font = font,
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.horizontal,
+ set_volume_level = function(self, new_value)
+ self:get_children_by_id('txt')[1]:set_text(new_value)
+ local volume_icon_name
+ if self.is_muted then
+ volume_icon_name = 'audio-volume-muted-symbolic'
+ else
+ local new_value_num = tonumber(new_value)
+ if (new_value_num >= 0 and new_value_num < 33) then
+ volume_icon_name="audio-volume-low-symbolic"
+ elseif (new_value_num < 66) then
+ volume_icon_name="audio-volume-medium-symbolic"
+ else
+ volume_icon_name="audio-volume-high-symbolic"
+ end
+ end
+ self:get_children_by_id('icon')[1]:set_image(icon_dir .. volume_icon_name .. '.svg')
+ end,
+ mute = function(self)
+ self.is_muted = true
+ self:get_children_by_id('icon')[1]:set_image(icon_dir .. 'audio-volume-muted-symbolic.svg')
+ end,
+ unmute = function(self)
+ self.is_muted = false
+ end
+ }
+
+end
+
+
+return widget
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/widgets/icon-widget.lua b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/icon-widget.lua
new file mode 100755
index 0000000..cc39a3d
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/icon-widget.lua
@@ -0,0 +1,46 @@
+local wibox = require("wibox")
+
+local widget = {}
+
+local ICON_DIR = os.getenv("HOME") .. '/.config/awesome/awesome-wm-widgets/volume-widget/icons/'
+
+function widget.get_widget(widgets_args)
+ local args = widgets_args or {}
+
+ local icon_dir = args.icon_dir or ICON_DIR
+
+ return wibox.widget {
+ {
+ id = "icon",
+ resize = false,
+ widget = wibox.widget.imagebox,
+ },
+ valign = 'center',
+ layout = wibox.container.place,
+ set_volume_level = function(self, new_value)
+ local volume_icon_name
+ if self.is_muted then
+ volume_icon_name = 'audio-volume-muted-symbolic'
+ else
+ local new_value_num = tonumber(new_value)
+ if (new_value_num >= 0 and new_value_num < 33) then
+ volume_icon_name="audio-volume-low-symbolic"
+ elseif (new_value_num < 66) then
+ volume_icon_name="audio-volume-medium-symbolic"
+ else
+ volume_icon_name="audio-volume-high-symbolic"
+ end
+ end
+ self:get_children_by_id('icon')[1]:set_image(icon_dir .. volume_icon_name .. '.svg')
+ end,
+ mute = function(self)
+ self.is_muted = true
+ self:get_children_by_id('icon')[1]:set_image(icon_dir .. 'audio-volume-muted-symbolic.svg')
+ end,
+ unmute = function(self)
+ self.is_muted = false
+ end
+ }
+end
+
+return widget
\ No newline at end of file
diff --git a/.config/awesome/awesome-wm-widgets/volume-widget/widgets/vertical-bar-widget.lua b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/vertical-bar-widget.lua
new file mode 100755
index 0000000..6f32b50
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/volume-widget/widgets/vertical-bar-widget.lua
@@ -0,0 +1,64 @@
+local wibox = require("wibox")
+local beautiful = require('beautiful')
+local gears = require("gears")
+
+local ICON_DIR = os.getenv("HOME") .. '/.config/awesome/awesome-wm-widgets/volume-widget/icons/'
+
+local widget = {}
+
+function widget.get_widget(widgets_args)
+ local args = widgets_args or {}
+
+ local main_color = args.main_color or beautiful.fg_normal
+ local mute_color = args.mute_color or beautiful.fg_urgent
+ local bg_color = args.bg_color or '#ffffff11'
+ local width = args.width or 10
+ local margins = args.height or 2
+ local shape = args.shape or 'bar'
+ local with_icon = args.with_icon == true and true or false
+
+ local bar = wibox.widget {
+ {
+ {
+ id = "icon",
+ image = ICON_DIR .. 'audio-volume-high-symbolic.svg',
+ resize = false,
+ widget = wibox.widget.imagebox,
+ },
+ valign = 'center',
+ visible = with_icon,
+ layout = wibox.container.place,
+ },
+ {
+ {
+ id = 'bar',
+ max_value = 100,
+ forced_width = width,
+ forced_height = 5,
+ margins = { top = margins, bottom = margins },
+ color = main_color,
+ background_color = bg_color,
+ shape = gears.shape[shape],
+ widget = wibox.widget.progressbar,
+ },
+ forced_width = width,
+ direction = 'east',
+ layout = wibox.container.rotate,
+ },
+ spacing = 4,
+ layout = wibox.layout.fixed.horizontal,
+ set_volume_level = function(self, new_value)
+ self:get_children_by_id('bar')[1]:set_value(tonumber(new_value))
+ end,
+ mute = function(self)
+ self:get_children_by_id('bar')[1]:set_color(mute_color)
+ end,
+ unmute = function(self)
+ self:get_children_by_id('bar')[1]:set_color(main_color)
+ end
+ }
+
+ return bar
+end
+
+return widget
diff --git a/.config/awesome/awesome-wm-widgets/weather-widget/README.md b/.config/awesome/awesome-wm-widgets/weather-widget/README.md
new file mode 100755
index 0000000..3bf6228
--- /dev/null
+++ b/.config/awesome/awesome-wm-widgets/weather-widget/README.md
@@ -0,0 +1,144 @@
+# Weather widget
+
+
+
+## Why
+
+[AwesomeWM](https://awesomewm.org/) is literally what it stands for, an awesome window manager.
+
+Its unique selling point has always been the widget system, which allows for fancy buttons, sliders, bars, dashboards and anything you can imagine. But that feature can be a curse. Most modules focus on the widget side of things which leave the actual window managing part of AwesomeWM underdeveloped compared to, for example, [xmonad](https://xmonad.org/) even though it's probably just as powerfull in that area.
+
+This project focuses on that problem - adding new layouts and modules that make use of the widget system, but primarily focus on the new window managing features.
+
+## Installation
+- clone this repo into your `~/.config/awesome` folder
+ - `git clone https://github.com/BlingCorp/bling.git ~/.config/awesome/bling`
+- require the module in your `rc.lua`, and make sure it's under the beautiful module initialization
+
+```lua
+-- other imports
+
+local beautiful = require("beautiful")
+
+-- other configuration stuff here
+
+beautiful.init("some_theme.lua")
+local bling = require("bling")
+```
+
+## Contributors
+A special thanks to all our contributors...
+
+
+
+
+
+Made with [contributors-img](https://contrib.rocks).
diff --git a/.config/awesome/bling/docs/index.html b/.config/awesome/bling/docs/index.html
new file mode 100755
index 0000000..ea8d732
--- /dev/null
+++ b/.config/awesome/bling/docs/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+ Bling Docs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.config/awesome/bling/docs/javacafe.css b/.config/awesome/bling/docs/javacafe.css
new file mode 100755
index 0000000..18962da
--- /dev/null
+++ b/.config/awesome/bling/docs/javacafe.css
@@ -0,0 +1,1069 @@
+@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");
+
+* {
+ -webkit-font-smoothing: antialiased;
+ -webkit-overflow-scrolling: touch;
+ -webkit-tap-highlight-color: #ffffff;
+ -webkit-text-size-adjust: none;
+ -webkit-touch-callout: none;
+ box-sizing: border-box;
+}
+
+body:not(.ready) {
+ overflow: hidden;
+}
+
+body:not(.ready) [data-cloak],
+body:not(.ready) .app-nav,
+body:not(.ready) > nav {
+ display: none;
+}
+
+::-webkit-scrollbar,
+::-webkit-scrollbar-track,
+::-webkit-scrollbar-track-piece {
+ background: transparent !important;
+ -webkit-box-shadow: none !important;
+ -moz-box-shadow: none !important;
+ box-shadow: none !important;
+ border: none !important;
+ width: 8px !important;
+}
+::-webkit-scrollbar-thumb {
+ background:#29343d !important;
+ width:8px !important;
+ border: none !important;
+ border-radius: 2px !important;
+}
+
+
+div#app {
+ font-size: 30px;
+ font-weight: lighter;
+ margin: 40vh auto;
+ text-align: center;
+}
+
+div#app:empty::before {
+ content: 'Loading...';
+}
+
+.emoji {
+ height: 1.2rem;
+ vertical-align: middle;
+}
+
+.progress {
+ background-color: var(--theme-color, #7ed491);
+ height: 2px;
+ left: 0px;
+ position: fixed;
+ right: 0px;
+ top: 0px;
+ transition: width 0.2s, opacity 0.4s;
+ width: 0%;
+ z-index: 999999;
+}
+
+.search a:hover {
+ color: var(--theme-color, #7ed491);
+}
+
+.search .search-keyword {
+ color: var(--theme-color, #7ed491);
+ font-style: normal;
+ font-weight: bold;
+}
+
+html,
+body {
+ height: 100%;
+}
+
+body {
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ color: #34495e;
+ font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 15px;
+ letter-spacing: 0;
+ margin: 0;
+ overflow-x: hidden;
+}
+
+img {
+ max-width: 65%;
+}
+
+a[disabled] {
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+kbd {
+ border: solid 1px #ccc;
+ border-radius: 3px;
+ display: inline-block;
+ font-size: 12px !important;
+ line-height: 12px;
+ margin-bottom: 3px;
+ padding: 3px 5px;
+ vertical-align: middle;
+}
+
+li input[type='checkbox'] {
+ margin: 0 0.2em 0.25em 0;
+ vertical-align: middle;
+}
+
+.app-nav {
+ margin: 25px 60px 0 0;
+ position: absolute;
+ right: 0;
+ text-align: right;
+ z-index: 10;
+/* navbar dropdown */
+}
+
+.app-nav.no-badge {
+ margin-right: 25px;
+}
+
+.app-nav p {
+ margin: 0;
+}
+
+.app-nav > a {
+ margin: 0 1rem;
+ padding: 5px 0;
+}
+
+.app-nav ul,
+.app-nav li {
+ display: inline-block;
+ list-style: none;
+ margin: 0;
+}
+
+.app-nav a {
+ color: inherit;
+ font-size: 16px;
+ text-decoration: none;
+ transition: color 0.3s;
+}
+
+.app-nav a:hover {
+ color: var(--theme-color, #7ed491);
+}
+
+.app-nav a.active {
+ border-bottom: 2px solid var(--theme-color, #7ed491);
+ color: var(--theme-color, #7ed491);
+}
+
+.app-nav li {
+ display: inline-block;
+ margin: 0 1rem;
+ padding: 5px 0;
+ position: relative;
+ cursor: pointer;
+}
+
+.app-nav li ul {
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-bottom-color: #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+ display: none;
+ max-height: calc(100vh - 61px);
+ overflow-y: auto;
+ padding: 10px 0;
+ position: absolute;
+ right: -15px;
+ text-align: left;
+ top: 100%;
+ white-space: nowrap;
+}
+
+.app-nav li ul li {
+ display: block;
+ font-size: 14px;
+ line-height: 1rem;
+ margin: 0;
+ margin: 8px 14px;
+ white-space: nowrap;
+}
+
+.app-nav li ul a {
+ display: block;
+ font-size: inherit;
+ margin: 0;
+ padding: 0;
+}
+
+.app-nav li ul a.active {
+ border-bottom: 0;
+}
+
+.app-nav li:hover ul {
+ display: block;
+}
+
+.github-corner {
+ border-bottom: 0;
+ position: fixed;
+ right: 0;
+ text-decoration: none;
+ top: 0;
+ z-index: 1;
+}
+
+.github-corner:hover .octo-arm {
+ -webkit-animation: octocat-wave 560ms ease-in-out;
+ animation: octocat-wave 560ms ease-in-out;
+}
+
+.github-corner svg {
+ color: #fff;
+ fill: var(--theme-color, #7ed491);
+ height: 80px;
+ width: 80px;
+}
+
+main {
+ display: block;
+ position: relative;
+ width: 100vw;
+ height: 100%;
+ z-index: 0;
+}
+
+main.hidden {
+ display: none;
+}
+
+.anchor {
+ display: inline-block;
+ text-decoration: none;
+ transition: all 0.3s;
+}
+
+.anchor span {
+ color: #FFFFFF;
+}
+
+.anchor:hover {
+ text-decoration: underline;
+}
+
+.sidebar {
+ border-right: 1px solid rgba(0,0,0,0.07);
+ overflow-y: auto;
+ padding: 40px 0 0;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ transition: transform 250ms ease-out;
+ width: 300px;
+ z-index: 20;
+}
+
+.sidebar > h1 {
+ margin: 0 auto 1rem;
+ font-size: 1.5rem;
+ font-weight: 300;
+ text-align: center;
+}
+
+.sidebar > h1 a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.sidebar > h1 .app-nav {
+ display: block;
+ position: static;
+}
+
+.sidebar .sidebar-nav {
+ line-height: 2em;
+ padding-bottom: 40px;
+}
+
+.sidebar li.collapse .app-sub-sidebar {
+ display: none;
+}
+
+.sidebar ul {
+ margin: 0 0 0 15px;
+ padding: 0;
+}
+
+.sidebar li > p {
+ font-weight: 700;
+ margin: 0;
+}
+
+.sidebar ul,
+.sidebar ul li {
+ list-style: none;
+}
+
+.sidebar ul li a {
+ border-bottom: none;
+ display: block;
+}
+
+.sidebar ul li ul {
+ padding-left: 20px;
+}
+
+.sidebar::-webkit-scrollbar {
+ width: 4px;
+}
+
+.sidebar::-webkit-scrollbar-thumb {
+ background: transparent;
+ border-radius: 4px;
+}
+
+.sidebar:hover::-webkit-scrollbar-thumb {
+ background: rgba(136,136,136,0.4);
+}
+
+.sidebar:hover::-webkit-scrollbar-track {
+ background: rgba(136,136,136,0.1);
+}
+
+.sidebar-toggle {
+ background-color: transparent;
+ background-color: #1a2026;
+ border: 0;
+ outline: none;
+ padding: 10px;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ text-align: center;
+ transition: opacity 0.3s;
+ width: 284px;
+ z-index: 30;
+ cursor: pointer;
+}
+
+.sidebar-toggle:hover .sidebar-toggle-button {
+ opacity: 0.4;
+}
+
+.sidebar-toggle span {
+ background-color: var(--theme-color, #7ed491);
+ display: block;
+ margin-bottom: 4px;
+ width: 16px;
+ height: 2px;
+}
+
+body.sticky .sidebar,
+body.sticky .sidebar-toggle {
+ position: fixed;
+}
+
+.content {
+ padding-top: 60px;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 300px;
+ transition: left 250ms ease;
+}
+
+.markdown-section {
+ margin: 0 auto;
+ max-width: 80%;
+ padding: 30px 15px 40px 15px;
+ position: relative;
+}
+
+.markdown-section > * {
+ box-sizing: border-box;
+ font-size: inherit;
+}
+
+.markdown-section > :first-child {
+ margin-top: 0 !important;
+}
+
+.markdown-section hr {
+ border: none;
+ border-bottom: 1px solid #eee;
+ margin: 2em 0;
+}
+
+.markdown-section iframe {
+ border: 1px solid #eee;
+/* fix horizontal overflow on iOS Safari */
+ width: 1px;
+ min-width: 100%;
+}
+
+.markdown-section table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ display: block;
+ margin-bottom: 1rem;
+ overflow: auto;
+ width: 100%;
+}
+
+.markdown-section th {
+ border: 1px solid #29343d;
+ color: #9fb4c4;
+ background-color: #13181c;
+ font-weight: bold;
+ padding: 6px 13px;
+}
+
+.markdown-section td {
+ border: 1px solid #29343d;
+ color: #9fb4c4;
+ background-color: #13181c;
+ padding: 6px 13px;
+}
+
+.markdown-section tr {
+ border-top: 1px solid #303436;
+}
+
+.markdown-section tr:nth-child(2n) {
+ background-color: #FFFFFF;
+}
+
+.markdown-section p.tip {
+ background-color: #f8f8f8;
+ border-bottom-right-radius: 2px;
+ border-left: 4px solid #f66;
+ border-top-right-radius: 2px;
+ margin: 2em 0;
+ padding: 12px 24px 12px 30px;
+ position: relative;
+}
+
+.markdown-section p.tip:before {
+ background-color: #f66;
+ border-radius: 100%;
+ color: #fff;
+ content: '!';
+ font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 14px;
+ font-weight: bold;
+ left: -12px;
+ line-height: 20px;
+ position: absolute;
+ height: 20px;
+ width: 20px;
+ text-align: center;
+ top: 14px;
+}
+
+.markdown-section p.tip code {
+ background-color: #eaeaea;
+}
+
+.markdown-section p.tip em {
+ color: #eaeaea;
+}
+
+.markdown-section p.warn {
+ background: #eaeaea;
+ border-radius: 2px;
+ padding: 1rem;
+}
+
+.markdown-section ul.task-list > li {
+ list-style-type: none;
+}
+
+body.close .sidebar {
+ transform: translateX(-300px);
+}
+
+body.close .sidebar-toggle {
+ width: auto;
+}
+
+body.close .content {
+ left: 0;
+}
+
+@media print {
+ .github-corner,
+ .sidebar-toggle,
+ .sidebar,
+ .app-nav {
+ display: none;
+ }
+}
+
+@media screen and (max-width: 768px) {
+ .github-corner,
+ .sidebar-toggle,
+ .sidebar {
+ position: fixed;
+ }
+
+ .app-nav {
+ margin-top: 16px;
+ }
+
+ .app-nav li ul {
+ top: 30px;
+ }
+
+ main {
+ height: auto;
+ overflow-x: hidden;
+ }
+
+ .sidebar {
+ left: -300px;
+ transition: transform 250ms ease-out;
+ }
+
+ .content {
+ left: 0;
+ max-width: 100vw;
+ position: static;
+ padding-top: 20px;
+ transition: transform 250ms ease;
+ }
+
+ .app-nav,
+ .github-corner {
+ transition: transform 250ms ease-out;
+ }
+
+ .sidebar-toggle {
+ background-color: transparent;
+ width: auto;
+ padding: 30px 30px 10px 10px;
+ }
+
+ body.close .sidebar {
+ transform: translateX(300px);
+ }
+
+ body.close .sidebar-toggle {
+ background-color: rgba(255,255,255,0.8);
+ transition: 1s background-color;
+ width: 284px;
+ padding: 10px;
+ }
+
+ body.close .content {
+ transform: translateX(300px);
+ }
+
+ body.close .app-nav,
+ body.close .github-corner {
+ display: none;
+ }
+
+ .github-corner:hover .octo-arm {
+ -webkit-animation: none;
+ animation: none;
+ }
+
+ .github-corner .octo-arm {
+ -webkit-animation: octocat-wave 560ms ease-in-out;
+ animation: octocat-wave 560ms ease-in-out;
+ }
+}
+
+@-webkit-keyframes octocat-wave {
+ 0%, 100% {
+ transform: rotate(0);
+ }
+ 20%, 60% {
+ transform: rotate(-25deg);
+ }
+ 40%, 80% {
+ transform: rotate(10deg);
+ }
+}
+
+@keyframes octocat-wave {
+ 0%, 100% {
+ transform: rotate(0);
+ }
+ 20%, 60% {
+ transform: rotate(-25deg);
+ }
+ 40%, 80% {
+ transform: rotate(10deg);
+ }
+}
+
+section.cover {
+ align-items: center;
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ height: 100vh;
+ width: 100vw;
+ display: none;
+}
+
+section.cover.show {
+ display: flex;
+}
+
+section.cover.has-mask .mask {
+ background-color: #fff;
+ opacity: 0.8;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ width: 100%;
+}
+
+section.cover .cover-main {
+ flex: 1;
+ margin: -20px 16px 0;
+ text-align: center;
+ position: relative;
+}
+
+section.cover a {
+ color: inherit;
+ text-decoration: none;
+}
+
+section.cover a:hover {
+ text-decoration: none;
+}
+
+section.cover p {
+ line-height: 1.5rem;
+ margin: 1em 0;
+}
+
+section.cover h1 {
+ color: inherit;
+ font-size: 2.5rem;
+ font-weight: 300;
+ margin: 0.625rem 0 2.5rem;
+ position: relative;
+ text-align: center;
+}
+
+section.cover h1 a {
+ display: block;
+}
+
+section.cover h1 small {
+ bottom: -0.4375rem;
+ font-size: 1rem;
+ position: absolute;
+}
+
+section.cover blockquote {
+ font-size: 1.5rem;
+ text-align: center;
+}
+
+section.cover ul {
+ line-height: 1.8;
+ list-style-type: none;
+ margin: 1em auto;
+ max-width: 500px;
+ padding: 0;
+}
+
+section.cover .cover-main > p:last-child a {
+ border-color: var(--theme-color, #7ed491);
+ border-radius: 2rem;
+ border-style: solid;
+ border-width: 1px;
+ box-sizing: border-box;
+ color: var(--theme-color, #7ed491);
+ display: inline-block;
+ font-size: 1.05rem;
+ letter-spacing: 0.1rem;
+ margin: 0.5rem 1rem;
+ padding: 0.75em 2rem;
+ text-decoration: none;
+ transition: all 0.15s ease;
+}
+
+section.cover .cover-main > p:last-child a:last-child {
+ background-color: var(--theme-color, #7ed491);
+ color: #fff;
+}
+
+section.cover .cover-main > p:last-child a:last-child:hover {
+ color: inherit;
+ opacity: 0.8;
+}
+
+section.cover .cover-main > p:last-child a:hover {
+ color: inherit;
+}
+
+section.cover blockquote > p > a {
+ border-bottom: 2px solid var(--theme-color, #7ed491);
+ transition: color 0.3s;
+}
+
+section.cover blockquote > p > a:hover {
+ color: var(--theme-color, #7ed491);
+}
+
+body {
+ background-color: #181e24;
+}
+
+/* sidebar */
+.sidebar {
+ background-color: #1a2026;
+ color: #fff;
+}
+
+.sidebar > h1 a {
+ font-weight: bold;
+}
+
+.sidebar li {
+ margin: 6px 0 6px 0;
+}
+
+.sidebar ul li a {
+ color: #eaeaea;
+ font-size: 14px;
+ font-weight: normal;
+ overflow: hidden;
+ text-decoration: none;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.sidebar ul li a:hover {
+ text-decoration: underline;
+}
+
+.sidebar ul li ul {
+ padding: 0;
+}
+
+.sidebar ul li.active > a {
+ border-right: 2px solid;
+ color: var(--theme-color, #7ed491);
+ font-weight: 600;
+}
+
+.app-sub-sidebar li::before {
+ content: '-';
+ padding-right: 4px;
+ float: left;
+}
+
+/* markdown content found on pages */
+.markdown-section h1,
+.markdown-section h2,
+.markdown-section h3,
+.markdown-section h4,
+.markdown-section strong {
+ color: #eaeaea;
+ font-weight: 600;
+}
+
+.markdown-section a {
+ color: var(--theme-color, #7ed491);
+ font-weight: 600;
+}
+
+.markdown-section h1 {
+ font-size: 2rem;
+ margin: 0 0 1rem;
+}
+
+.markdown-section h2 {
+ font-size: 1.75rem;
+ margin: 45px 0 0.8rem;
+}
+
+.markdown-section h3 {
+ font-size: 1.5rem;
+ margin: 40px 0 0.6rem;
+}
+
+.markdown-section h4 {
+ font-size: 1.25rem;
+}
+
+.markdown-section h5 {
+ font-size: 1rem;
+}
+
+.markdown-section h6 {
+ color: #777;
+ font-size: 1rem;
+}
+
+.markdown-section figure,
+.markdown-section p {
+ margin: 1.2em 0;
+}
+
+.markdown-section p,
+.markdown-section ul,
+.markdown-section ol {
+ line-height: 1.6rem;
+ word-spacing: 0.05rem;
+}
+
+.markdown-section ul,
+.markdown-section ol {
+ padding-left: 1.5rem;
+}
+
+.markdown-section blockquote {
+ border-left: 4px solid var(--theme-color, #7ed491);
+ color: #858585;
+ margin: 2em 0;
+ padding-left: 20px;
+}
+
+.markdown-section blockquote p {
+ font-weight: 600;
+ margin-left: 0;
+}
+
+.markdown-section iframe {
+ margin: 1em 0;
+}
+
+.markdown-section em {
+ color: #7f8c8d;
+}
+
+.markdown-section code,
+.markdown-section pre,
+.markdown-section output::after {
+ font-family: 'Roboto Mono', Monaco, courier, monospace;
+}
+
+.markdown-section code,
+.markdown-section pre {
+ color: #575D74;
+ background-color: #13181c
+}
+
+.markdown-section pre,
+.markdown-section output {
+ margin: 1.2em 0;
+ position: relative;
+}
+
+.markdown-section pre > code,
+.markdown-section output {
+ border-radius: 2px;
+ display: block;
+}
+
+.markdown-section pre > code,
+.markdown-section output::after {
+ -moz-osx-font-smoothing: initial;
+ -webkit-font-smoothing: initial;
+}
+
+.markdown-section code {
+ border-radius: 2px;
+ color: #3b4b58
+ margin: 0 2px;
+ padding: 3px 5px;
+ white-space: pre-wrap;
+}
+
+.markdown-section > :not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) code {
+ font-size: 0.8rem;
+}
+
+.markdown-section pre {
+ padding: 0 1.4rem;
+ line-height: 1.5rem;
+ overflow: auto;
+ word-wrap: normal;
+}
+
+.markdown-section pre > code {
+ color: #eaeaea;
+ font-size: 0.8rem;
+ padding: 2.2em 5px;
+ line-height: inherit;
+ margin: 0 2px;
+ max-width: inherit;
+ overflow: inherit;
+ white-space: inherit;
+}
+
+.markdown-section output {
+ padding: 1.7rem 1.4rem;
+ border: 1px dotted #ccc;
+}
+
+.markdown-section output > :first-child {
+ margin-top: 0;
+}
+
+.markdown-section output > :last-child {
+ margin-bottom: 0;
+}
+
+.markdown-section code::after,
+.markdown-section code::before,
+.markdown-section output::after,
+.markdown-section output::before {
+ letter-spacing: 0.05rem;
+}
+
+.markdown-section pre::after,
+.markdown-section output::after {
+ color: #ccc;
+ font-size: 0.6rem;
+ font-weight: 600;
+ height: 15px;
+ line-height: 15px;
+ padding: 5px 10px 0;
+ position: absolute;
+ right: 0;
+ text-align: right;
+ top: 0;
+}
+
+.markdown-section pre::after,
+.markdown-section output::after {
+ content: attr(data-lang);
+}
+
+.markdown-section p, .markdown-section ul, .markdown-section ol {
+ color: #eaeaea;
+}
+
+/* code highlight */
+.token.comment {
+ color: #575D74;
+}
+
+.token.prolog,
+.token.doctype,
+.token.cdata {
+ color: #8e908c;
+}
+
+.token.namespace {
+ opacity: 0.7;
+}
+
+.token.boolean {
+ color: #FCC5A3
+}
+
+.token.number {
+ color: #FCC5A3;
+}
+
+.token.punctuation {
+ color: #fca3ab;
+}
+
+.token.property {
+ color: #d7c1ed;
+}
+
+.token.tag {
+ color: #2973b7;
+}
+
+.token.string {
+ color: var(--theme-color, #93cfab);
+}
+
+.token.selector {
+ color: #6679cc;
+}
+
+.token.attr-name {
+ color: #2973b7;
+}
+
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+ color: #22a2c9;
+}
+
+.token.attr-value,
+.token.control,
+.token.directive,
+.token.unit {
+ color: var(--theme-color, #7ed491);
+}
+
+.token.keyword {
+ color: #d7c1ed
+}
+
+.token.function {
+ color: #bac8ef;
+}
+
+.token.statement {
+ color: #fca2aa;
+}
+
+.token.regex,
+.token.atrule {
+ color: #22a2c9;
+}
+
+.token.placeholder,
+.token.variable {
+ color: #3d8fd1;
+}
+
+.token.deleted {
+ text-decoration: line-through;
+}
+
+.token.inserted {
+ border-bottom: 1px dotted #202746;
+ text-decoration: none;
+}
+
+.token.italic {
+ font-style: italic;
+}
+
+.token.bold {
+ font-weight: bold;
+}
+
+.token.important {
+ color: #ccaced;
+}
+
+.token.entity {
+ cursor: help;
+}
+
+code .token {
+ -moz-osx-font-smoothing: initial;
+ -webkit-font-smoothing: initial;
+ min-height: 1.5rem;
+ position: relative;
+ left: auto;
+}
\ No newline at end of file
diff --git a/.config/awesome/bling/docs/layouts/layout.md b/.config/awesome/bling/docs/layouts/layout.md
new file mode 100755
index 0000000..85fc547
--- /dev/null
+++ b/.config/awesome/bling/docs/layouts/layout.md
@@ -0,0 +1,65 @@
+## 📎 Layouts
+
+Choose layouts from the list below and add them to to your `awful.layouts` list in your `rc.lua`.
+
+Everyone of them supports multiple master clients and master width factor making them easy to use.
+
+The mstab layout uses the tab theme from the tabbed module.
+
+```lua
+bling.layout.mstab
+bling.layout.centered
+bling.layout.vertical
+bling.layout.horizontal
+bling.layout.equalarea
+bling.layout.deck
+```
+
+### Theme Variables
+
+```lua
+-- mstab
+theme.mstab_bar_disable = false -- disable the tabbar
+theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients
+theme.mstab_dont_resize_slaves = false -- whether the tabbed stack windows should be smaller than the
+ -- currently focused stack window (set it to true if you use
+ -- transparent terminals. False if you use shadows on solid ones
+theme.mstab_bar_padding = "default" -- how much padding there should be between clients and your tabbar
+ -- by default it will adjust based on your useless gaps.
+ -- If you want a custom value. Set it to the number of pixels (int)
+theme.mstab_border_radius = 0 -- border radius of the tabbar
+theme.mstab_bar_height = 40 -- height of the tabbar
+theme.mstab_tabbar_position = "top" -- position of the tabbar (mstab currently does not support left,right)
+theme.mstab_tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
+ -- defaults to the tabbar_style so only change if you want a
+ -- different style for mstab and tabbed
+```
+
+### Previews
+
+#### Mstab (dynamic tabbing layout)
+
+
+
+*screenshot by [JavaCafe01](https://github.com/JavaCafe01)*
+
+#### Centered
+
+
+
+*screenshot by [HeavyRain266](https://github.com/HeavyRain266)*
+
+#### Equal area
+
+
+
+*screenshot by [bysmutheye](https://github.com/bysmutheye)*
+
+#### Deck
+
+The left area shows the deck layout in action. In this screenshot it is used together with [layout machi](https://github.com/xinhaoyuan/layout-machi) and its sublayout support.
+
+
+
+*screenshot by [JavaCafe01](https://github.com/JavaCafe01)*
+
diff --git a/.config/awesome/bling/docs/module/flash.md b/.config/awesome/bling/docs/module/flash.md
new file mode 100755
index 0000000..a12b78d
--- /dev/null
+++ b/.config/awesome/bling/docs/module/flash.md
@@ -0,0 +1,33 @@
+## 🔦 Flash Focus
+
+Flash focus does an opacity animation effect on a client when it is focused.
+
+
+### Usage
+
+There are two ways in which you can use this module. You can enable it by calling the `enable()` function:
+```lua
+bling.module.flash_focus.enable()
+```
+This connects to the focus signal of a client, which means that the flash focus will activate however you focus the client.
+
+The other way is to call the function itself like this: `bling.module.flash_focus.flashfocus(someclient)`. This allows you to activate on certain keybinds like so:
+```lua
+awful.key({modkey}, "Up",
+ function()
+ awful.client.focus.bydirection("up")
+ bling.module.flash_focus.flashfocus(client.focus)
+ end, {description = "focus up", group = "client"})
+```
+
+### Theme Variables
+```lua
+theme.flash_focus_start_opacity = 0.6 -- the starting opacity
+theme.flash_focus_step = 0.01 -- the step of animation
+```
+
+### Preview
+
+
+
+*gif by [JavaCafe01](https://github.com/JavaCafe01)*
diff --git a/.config/awesome/bling/docs/module/scratch.md b/.config/awesome/bling/docs/module/scratch.md
new file mode 100755
index 0000000..54f8ddd
--- /dev/null
+++ b/.config/awesome/bling/docs/module/scratch.md
@@ -0,0 +1,75 @@
+## 🍃 Scratchpad
+
+An easy way to create multiple scratchpads.
+
+### A... what?
+
+You can think about a scratchpad as a window whose visibility can be toggled, but still runs in the background without being visible (or minimized) most of the time. Many people use it to have one terminal in which to perform minor tasks, but it is the most useful for windows which only need a couple seconds in between your actual activity, such as music players or chat applications.
+
+### Rubato Animation Support
+
+#### Awestore is now deprecated from Bling, we are switching to Rubato.
+
+Please go over to the [rubato](https://github.com/andOrlando/rubato) repository for installation instructions. Give it a star as well! The animations are completely optional, and if you choose not to use it, you do not need rubato installed.
+
+### Usage
+
+To initalize a scratchpad you can do something like the following:
+
+```lua
+local bling = require("bling")
+local rubato = require("rubato") -- Totally optional, only required if you are using animations.
+
+-- These are example rubato tables. You can use one for just y, just x, or both.
+-- The duration and easing is up to you. Please check out the rubato docs to learn more.
+local anim_y = rubato.timed {
+ pos = 1090,
+ rate = 60,
+ easing = rubato.quadratic,
+ intro = 0.1,
+ duration = 0.3,
+ awestore_compat = true -- This option must be set to true.
+}
+
+local anim_x = rubato.timed {
+ pos = -970,
+ rate = 60,
+ easing = rubato.quadratic,
+ intro = 0.1,
+ duration = 0.3,
+ awestore_compat = true -- This option must be set to true.
+}
+
+local term_scratch = bling.module.scratchpad {
+ command = "wezterm start --class spad", -- How to spawn the scratchpad
+ rule = { instance = "spad" }, -- The rule that the scratchpad will be searched by
+ sticky = true, -- Whether the scratchpad should be sticky
+ autoclose = true, -- Whether it should hide itself when losing focus
+ floating = true, -- Whether it should be floating (MUST BE TRUE FOR ANIMATIONS)
+ geometry = {x=360, y=90, height=900, width=1200}, -- The geometry in a floating state
+ reapply = true, -- Whether all those properties should be reapplied on every new opening of the scratchpad (MUST BE TRUE FOR ANIMATIONS)
+ dont_focus_before_close = false, -- When set to true, the scratchpad will be closed by the toggle function regardless of whether its focused or not. When set to false, the toggle function will first bring the scratchpad into focus and only close it on a second call
+ rubato = {x = anim_x, y = anim_y} -- Optional. This is how you can pass in the rubato tables for animations. If you don't want animations, you can ignore this option.
+}
+```
+
+Once initalized, you can use the object (which in this case is named `term_scratch`) like this:
+
+```lua
+term_scratch:toggle() -- toggles the scratchpads visibility
+term_scratch:turn_on() -- turns the scratchpads visibility on
+term_scratch:turn_off() -- turns the scratchpads visibility off
+```
+
+You can also connect to signals as you are used to for further customization. For example like that:
+
+```lua
+term_scratch:connect_signal("turn_on", function(c) naughty.notify({title = "Turned on!"}) end)
+```
+
+The following signals are currently available. `turn_on`, `turn_off` and `inital_apply` pass the client on which they operated as an argument:
+
+- `turn_on` fires when the scratchpad is turned on on a tag that it wasn't present on before
+- `turn_off` fires when the scratchpad is turned off on a tag
+- `spawn` fires when the scratchpad is launched with the given command
+- `inital_apply` fires after `spawn`, when a corresponding client has been found and the properties have been applied
diff --git a/.config/awesome/bling/docs/module/swal.md b/.config/awesome/bling/docs/module/swal.md
new file mode 100755
index 0000000..0b3fed6
--- /dev/null
+++ b/.config/awesome/bling/docs/module/swal.md
@@ -0,0 +1,25 @@
+## 😋 Window Swallowing
+
+Can your window manager swallow? It probably can...
+
+### Usage
+
+To activate and deactivate window swallowing here are the following functions. If you want to activate it, just call the `start` function once in your `rc.lua`.
+```lua
+bling.module.window_swallowing.start() -- activates window swallowing
+bling.module.window_swallowing.stop() -- deactivates window swallowing
+bling.module.window_swallowing.toggle() -- toggles window swallowing
+```
+
+### Theme Variables
+```lua
+theme.parent_filter_list = {"firefox", "Gimp"} -- class names list of parents that should not be swallowed
+theme.child_filter_list = { "Dragon" } -- class names list that should not swallow their parents
+theme.swallowing_filter = true -- whether the filters above should be active
+```
+
+### Preview
+
+
+
+*gif by [Nooo37](https://github.com/Nooo37)*
diff --git a/.config/awesome/bling/docs/module/tabbed.md b/.config/awesome/bling/docs/module/tabbed.md
new file mode 100755
index 0000000..e705357
--- /dev/null
+++ b/.config/awesome/bling/docs/module/tabbed.md
@@ -0,0 +1,66 @@
+## 📑 Tabbed
+
+Tabbed implements a tab container. There are also different themes for the tabs.
+
+### Usage
+
+You should bind these functions to keys in order to use the tabbed module effectively:
+```lua
+bling.module.tabbed.pick() -- picks a client with your cursor to add to the tabbing group
+bling.module.tabbed.pop() -- removes the focused client from the tabbing group
+bling.module.tabbed.iter() -- iterates through the currently focused tabbing group
+bling.module.tabbed.pick_with_dmenu() -- picks a client with a dmenu application (defaults to rofi, other options can be set with a string parameter like "dmenu")
+bling.module.tabbed.pick_by_direction(dir) -- picks a client based on direction ("up", "down", "left" or "right")
+```
+
+### Theme Variables
+
+```lua
+-- For tabbed only
+theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the focused tabbing container
+
+-- For tabbar in general
+theme.tabbar_ontop = false
+theme.tabbar_radius = 0 -- border radius of the tabbar
+theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
+theme.tabbar_font = "Sans 11" -- font of the tabbar
+theme.tabbar_size = 40 -- size of the tabbar
+theme.tabbar_position = "top" -- position of the tabbar
+theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar
+theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar
+theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar
+theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar
+theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive
+theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive
+theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive
+theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive
+theme.tabbar_disable = false -- disable the tab bar entirely
+
+-- the following variables are currently only for the "modern" tabbar style
+theme.tabbar_color_close = "#f9929b" -- chnges the color of the close button
+theme.tabbar_color_min = "#fbdf90" -- chnges the color of the minimize button
+theme.tabbar_color_float = "#ccaced" -- chnges the color of the float button
+```
+
+### Preview
+
+Modern theme:
+
+
+
+*screenshot by [JavaCafe01](https://github.com/JavaCafe01)*
+
+### Signals
+The tabbed module emits a few signals for the purpose of integration,
+```lua
+-- bling::tabbed::update -- triggered whenever a tabbed object is updated
+-- tabobj -- the object that caused the update
+-- bling::tabbed::client_added -- triggered whenever a new client is added to a tab group
+-- tabobj -- the object that the client was added to
+-- client -- the client that added
+-- bling::tabbed::client_removed -- triggered whenever a client is removed from a tab group
+-- tabobj -- the object that the client was removed from
+-- client -- the client that was removed
+-- bling::tabbed::changed_focus -- triggered whenever a tab group's focus is changed
+-- tabobj -- the modified tab group
+```
diff --git a/.config/awesome/bling/docs/module/twall.md b/.config/awesome/bling/docs/module/twall.md
new file mode 100755
index 0000000..69c09c3
--- /dev/null
+++ b/.config/awesome/bling/docs/module/twall.md
@@ -0,0 +1,26 @@
+## 🏬 Tiled Wallpaper
+
+### Usage
+
+The function to set an automatically created tiled wallpaper can be called the following way (you don't need to set every option in the table):
+```lua
+awful.screen.connect_for_each_screen(function(s) -- that way the wallpaper is applied to every screen
+ bling.module.tiled_wallpaper("x", s, { -- call the actual function ("x" is the string that will be tiled)
+ fg = "#ff0000", -- define the foreground color
+ bg = "#00ffff", -- define the background color
+ offset_y = 25, -- set a y offset
+ offset_x = 25, -- set a x offset
+ font = "Hack", -- set the font (without the size)
+ font_size = 14, -- set the font size
+ padding = 100, -- set padding (default is 100)
+ zickzack = true -- rectangular pattern or criss cross
+ })
+end)
+```
+
+### Preview
+
+
+
+*screenshots by [Nooo37](https://github.com/Nooo37)*
+
diff --git a/.config/awesome/bling/docs/module/wall.md b/.config/awesome/bling/docs/module/wall.md
new file mode 100755
index 0000000..cdab6cc
--- /dev/null
+++ b/.config/awesome/bling/docs/module/wall.md
@@ -0,0 +1,142 @@
+## 🎇 Wallpaper Easy Setup
+
+This is a simple-to-use, extensible, declarative wallpaper manager.
+
+### Practical Examples
+
+```lua
+-- A default Awesome wallpaper
+bling.module.wallpaper.setup()
+
+-- A slideshow with pictures from different sources changing every 30 minutes
+bling.module.wallpaper.setup {
+ wallpaper = {"/images/my_dog.jpg", "/images/my_cat.jpg"},
+ change_timer = 1800
+}
+
+-- A random wallpaper with images from multiple folders
+bling.module.wallpaper.setup {
+ set_function = bling.module.wallpaper.setters.random,
+ wallpaper = {"/path/to/a/folder", "/path/to/another/folder"},
+ change_timer = 631, -- prime numbers are better for timers
+ position = "fit",
+ background = "#424242"
+}
+
+-- wallpapers based on a schedule, like awesome-glorious-widgets dynamic wallpaper
+-- https://github.com/manilarome/awesome-glorious-widgets/tree/master/dynamic-wallpaper
+bling.module.wallpaper.setup {
+ set_function = bling.module.wallpaper.setters.simple_schedule,
+ wallpaper = {
+ ["06:22:00"] = "morning-wallpaper.jpg",
+ ["12:00:00"] = "noon-wallpaper.jpg",
+ ["17:58:00"] = "night-wallpaper.jpg",
+ ["24:00:00"] = "midnight-wallpaper.jpg",
+ },
+ position = "maximized",
+}
+
+-- random wallpapers, from different folder depending on time of the day
+bling.module.wallpaper.setup {
+ set_function = bling.module.wallpaper.setters.simple_schedule,
+ wallpaper = {
+ ["09:00:00"] = "~/Pictures/safe_for_work",
+ ["18:00:00"] = "~/Pictures/personal",
+ },
+ schedule_set_function = bling.module.wallpaper.setters.random
+ position = "maximized",
+ recursive = false,
+ change_timer = 600
+}
+
+-- setup for multiple screens at once
+-- the 'screen' argument can be a table of screen objects
+bling.module.wallpaper.setup {
+ set_function = bling.module.wallpaper.setters.random,
+ screen = screen, -- The awesome 'screen' variable is an array of all screen objects
+ wallpaper = {"/path/to/a/folder", "/path/to/another/folder"},
+ change_timer = 631
+}
+```
+### Details
+
+The setup function will do 2 things: call the set-function when awesome requests a wallpaper, and manage a timer to call `set_function` periodically.
+
+Its argument is a args table that is passed to ohter functions (setters and wallpaper functions), so you define everything with setup.
+
+The `set_function` is a function called every times a wallpaper is needed.
+
+The module provides some setters:
+
+* `bling.module.wallpaper.setters.awesome_wallpaper`: beautiful.theme_assets.wallpaper with defaults from beautiful.
+* `bling.module.wallpaper.setters.simple`: slideshow from the `wallpaper` argument.
+* `bling.module.wallpaper.setters.random`: same as simple but in a random way.
+* `bling.module.wallpaper.setters.simple_schedule`: takes a table of `["HH:MM:SS"] = wallpaper` arguments, where wallpaper is the `wallpaper` argument used by `schedule_set_function`.
+
+A wallpaper is one of the following elements:
+
+* a color
+* an image
+* a folder containing images
+* a function that sets a wallpaper
+* everything gears.wallpaper functions can manage (cairo surface, cairo pattern string)
+* a list containing any of the elements above
+
+To set up for multiple screens, two possible methods are:
+* Call the `setup` function for each screen, passing the appropriate configuration and `screen` arg
+* Call the `setup` function once, passing a table of screens as the `screen` arg. This applies the same configuration to all screens in the table
+_Note_: Multiple screen setup only works for the `simple` and `random` setters
+
+```lua
+-- This is a valid wallpaper definition
+bling.module.wallpaper.setup {
+ wallpaper = { -- a list
+ "black", "#112233", -- colors
+ "wall1.jpg", "wall2.png", -- files
+ "/path/to/wallpapers", -- folders
+ -- cairo patterns
+ "radial:600,50,100:105,550,900:0,#2200ff:0.5,#00ff00:1,#101010",
+ -- or functions that set a wallpaper
+ function(args) bling.module.tiled_wallpaper("\\o/", args.screen) end,
+ bling.module.wallpaper.setters.awesome_wallpaper,
+ },
+ change_timer = 10,
+}
+```
+The provided setters `simple` and `random` will use 2 internal functions that you can use to write your own setter:
+
+* `bling.module.wallpaper.prepare_list`: return a list of wallpapers directly usable by `apply` (for now, it just explores folders)
+* `bling.module.wallpaper.apply`: a wrapper for gears.wallpaper functions, using the args table of setup
+
+Here are the defaults:
+
+```lua
+-- Default parameters
+bling.module.wallpaper.setup {
+ screen = nil, -- the screen to apply the wallpaper, as seen in gears.wallpaper functions
+ screens = nil, -- an array of screens to apply the wallpaper on. If 'screen' is also provided, this is overridden
+ change_timer = nil, -- the timer in seconds. If set, call the set_function every change_timer seconds
+ set_function = nil, -- the setter function
+
+ -- parameters used by bling.module.wallpaper.prepare_list
+ wallpaper = nil, -- the wallpaper object, see simple or simple_schedule documentation
+ image_formats = {"jpg", "jpeg", "png", "bmp"}, -- when searching in folder, consider these files only
+ recursive = true, -- when searching in folder, search also in subfolders
+
+ -- parameters used by bling.module.wallpaper.apply
+ position = nil, -- use a function of gears.wallpaper when applicable ("centered", "fit", "maximized", "tiled")
+ background = beautiful.bg_normal or "black", -- see gears.wallpaper functions
+ ignore_aspect = false, -- see gears.wallpaper.maximized
+ offset = {x = 0, y = 0}, -- see gears.wallpaper functions
+ scale = 1, -- see gears.wallpaper.centered
+
+ -- parameters that only apply to bling.module.wallpaper.setter.awesome (as a setter or as a wallpaper function)
+ colors = { -- see beautiful.theme_assets.wallpaper
+ bg = beautiful.bg_color, -- the actual default is this color but darkened or lightned
+ fg = beautiful.fg_color,
+ alt_fg = beautiful.fg_focus
+ }
+}
+```
+
+Check documentation in [module/wallpaper.lua](module/wallpaper.lua) for more details.
diff --git a/.config/awesome/bling/docs/signals/pctl.md b/.config/awesome/bling/docs/signals/pctl.md
new file mode 100755
index 0000000..89a0709
--- /dev/null
+++ b/.config/awesome/bling/docs/signals/pctl.md
@@ -0,0 +1,240 @@
+## 🎵 Playerctl
+
+This is a signal module in which you can connect to certain bling signals to grab playerctl info. Currently, this is what it supports:
+
+- Song title and artist
+- Album art (the path this module downloaded the art to)
+- If playing or not
+- Position
+- Song length
+- If there are no players on
+
+This module relies on `playerctl` and `curl`. If you have this module disabled, you won't need those programs. With this module, you can create a widget like below without worrying about the backend.
+
+
+
+*screenshot by [javacafe](https://github.com/JavaCafe01)*
+
+### Usage
+
+To enable: `playerctl = bling.signal.playerctl.lib/cli()`
+
+To disable: `playerctl:disable()`
+
+#### Playerctl_lib Signals
+
+**Note**: When connecting to signals with the new `playerctl` module, the object itself is always given to you as the first parameter.
+
+```lua
+-- metadata
+-- title (string)
+-- artist (string)
+-- album_path (string)
+-- album (string)
+-- new (bool)
+-- player_name (string)
+-- position
+-- interval_sec (number)
+-- length_sec (number)
+-- player_name (string)
+-- playback_status
+-- playing (boolean)
+-- player_name (string)
+-- seeked
+-- position (number)
+-- player_name (string)
+-- volume
+-- volume (number)
+-- player_name (string)
+-- loop_status
+-- loop_status (string)
+-- player_name (string)
+-- shuffle
+-- shuffle (boolean)
+-- player_name (string)
+-- exit
+-- player_name (string)
+-- no_players
+-- (No parameters)
+```
+
+#### Playerctl_cli Signals
+
+```lua
+-- metadata
+-- title (string)
+-- artist (string)
+-- album_path (string)
+-- album (string)
+-- player_name (string)
+-- position
+-- interval_sec (number)
+-- length_sec (number)
+-- playback_status
+-- playing (boolean)
+-- volume
+-- volume (number)
+-- loop_status
+-- loop_status (string)
+-- shuffle
+-- shuffle (bool)
+-- no_players
+-- (No parameters)
+```
+
+#### Playerctl Functions
+
+With this library we also give the user a way to interact with Playerctl, such as playing, pausing, seeking, etc.
+
+Here are the functions provided:
+
+```lua
+-- disable()
+-- pause(player)
+-- play(player)
+-- stop(player)
+-- play_pause(player)
+-- previous(player)
+-- next(player)
+-- set_loop_status(loop_status, player)
+-- cycle_loop_status(player)
+-- set_position(position, player)
+-- set_shuffle(shuffle, player)
+-- cycle_shuffle(player)
+-- set_volume(volume, player)
+-- get_manager()
+-- get_active_player()
+-- get_player_of_name(name)
+```
+
+### Example Implementation
+
+Lets say we have an imagebox. If I wanted to set the imagebox to show the album art, all I have to do is this:
+
+```lua
+local art = wibox.widget {
+ image = "default_image.png",
+ resize = true,
+ forced_height = dpi(80),
+ forced_width = dpi(80),
+ widget = wibox.widget.imagebox
+}
+
+local name_widget = wibox.widget {
+ markup = 'No players',
+ align = 'center',
+ valign = 'center',
+ widget = wibox.widget.textbox
+}
+
+local title_widget = wibox.widget {
+ markup = 'Nothing Playing',
+ align = 'center',
+ valign = 'center',
+ widget = wibox.widget.textbox
+}
+
+local artist_widget = wibox.widget {
+ markup = 'Nothing Playing',
+ align = 'center',
+ valign = 'center',
+ widget = wibox.widget.textbox
+}
+
+-- Get Song Info
+local playerctl = bling.signal.playerctl.lib()
+playerctl:connect_signal("metadata",
+ function(_, title, artist, album_path, album, new, player_name)
+ -- Set art widget
+ art:set_image(gears.surface.load_uncached(album_path))
+
+ -- Set player name, title and artist widgets
+ name_widget:set_markup_silently(player_name)
+ title_widget:set_markup_silently(title)
+ artist_widget:set_markup_silently(artist)
+end)
+```
+
+Thats all! You don't even have to worry about updating the widgets, the signals will handle that for you.
+
+Here's another example in which you get a notification with the album art, title, and artist whenever the song changes.
+
+```lua
+local naughty = require("naughty")
+local playerctl = bling.signal.playerctl.lib()
+
+playerctl:connect_signal("metadata",
+ function(_, title, artist, album_path, album, new, player_name)
+ if new == true then
+ naughty.notify({title = title, text = artist, image = album_path})
+ end
+end)
+```
+
+We can also link a playerctl function to a button click!
+
+```lua
+local playerctl = bling.signal.playerctl.lib()
+button:buttons(gears.table.join(
+ awful.button({}, 1, function()
+ playerctl:play_pause()
+ end)
+))
+```
+
+### Theme Variables and Configuration
+
+By default, this module will output signals from the most recently active player. If you wish to customize the behavior furthur, the following configuration options are available depending on the selected backend. Here is a summary of the two backends and which configuration options they support.
+
+| Option | playerctl_cli | playerctl_lib |
+| ------------------- | ------------------ | ------------------ |
+| ignore | :heavy_check_mark: | :heavy_check_mark: |
+| player | :heavy_check_mark: | :heavy_check_mark: |
+| update_on_activity | | :heavy_check_mark: |
+| interval | :heavy_check_mark: | :heavy_check_mark: |
+| debounce_delay | :heavy_check_mark: | :heavy_check_mark: |
+
+- `ignore`: This option is either a string with a single name or a table of strings containing names of players that will be ignored by this module. It is empty by default.
+
+- `player`: This option is either a string with a single name or a table of strings containing names of players this module will emit signals for. It also acts as a way to prioritize certain players over others with players listed earlier in the table being preferred over players listed later. The special name `%any` can be used once to match any player not found in the list. It is empty by default.
+
+- `update_on_activity`: This option is a boolean that, when true, will cause the module to output signals from the most recently active player while still adhering to the player priority specified with the `player` option. If `false`, the module will output signals from the player that started first, again, while still adhering to the player priority. It is `true` by default.
+
+- `interval`: This option is a number specifying the update interval for fetching the player position. It is 1 by default.
+
+- `debounce_delay`: This option is a number specifying the debounce timer interval. If a new metadata signal gets emitted before debounce_delay has passed, the last signal will be dropped.
+This is to help with some players sending multiple signals. It is `0.35` by default.
+
+These options can be set through a call to `bling.signal.playerctl.lib/cli()` or these theme variables:
+
+```lua
+theme.playerctl_ignore = {}
+theme.playerctl_player = {}
+theme.playerctl_update_on_activity = true
+theme.playerctl_position_update_interval = 1
+```
+
+#### Example Configurations
+
+```lua
+-- Prioritize ncspot over all other players and ignore firefox players (e.g. YouTube and Twitch tabs) completely
+playerctl = bling.signal.playerctl.lib {
+ ignore = "firefox",
+ player = {"ncspot", "%any"}
+}
+
+-- OR in your theme file:
+-- Same config as above but with theme variables
+theme.playerctl_ignore = "firefox"
+theme.playerctl_player = {"ncspot", "%any"}
+
+-- Prioritize vlc over all other players and deprioritize spotify
+theme.playerctl_backend = "playerctl_lib"
+theme.playerctl_player = {"vlc", "%any", "spotify"}
+
+-- Disable priority of most recently active players
+theme.playerctl_update_on_activity = false
+
+-- Only emit the position signal every 2 seconds
+theme.playerctl_position_update_interval = 2
+```
diff --git a/.config/awesome/bling/docs/theme.md b/.config/awesome/bling/docs/theme.md
new file mode 100755
index 0000000..0d1d0b3
--- /dev/null
+++ b/.config/awesome/bling/docs/theme.md
@@ -0,0 +1,117 @@
+```lua
+--[[ Bling theme variables template
+This file has all theme variables of the bling module.
+Every variable has a small comment on what it does.
+You might just want to copy that whole part into your theme.lua and start adjusting from there.
+--]]
+
+
+-- window swallowing
+theme.dont_swallow_classname_list = {"firefox", "Gimp"} -- list of class names that should not be swallowed
+theme.dont_swallow_filter_activated = true -- whether the filter above should be active
+
+-- flash focus
+theme.flash_focus_start_opacity = 0.6 -- the starting opacity
+theme.flash_focus_step = 0.01 -- the step of animation
+
+-- playerctl signal
+theme.playerctl_backend = "playerctl_cli" -- backend to use
+theme.playerctl_ignore = {} -- list of players to be ignored
+theme.playerctl_player = {} -- list of players to be used in priority order
+theme.playerctl_update_on_activity = true -- whether to prioritize the most recently active players or not
+theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl
+
+-- tabbed
+theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the focused tabbing container
+
+-- tabbar general
+theme.tabbar_ontop = false
+theme.tabbar_radius = 0 -- border radius of the tabbar
+theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
+theme.tabbar_font = "Sans 11" -- font of the tabbar
+theme.tabbar_size = 40 -- size of the tabbar
+theme.tabbar_position = "top" -- position of the tabbar
+theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar
+theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar
+theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar
+theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar
+theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive
+theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive
+theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive
+theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive
+theme.tabbar_disable = false -- disable the tab bar entirely
+
+-- mstab
+theme.mstab_bar_disable = false -- disable the tabbar
+theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients
+theme.mstab_dont_resize_slaves = false -- whether the tabbed stack windows should be smaller than the
+ -- currently focused stack window (set it to true if you use
+ -- transparent terminals. False if you use shadows on solid ones
+theme.mstab_bar_padding = "default" -- how much padding there should be between clients and your tabbar
+ -- by default it will adjust based on your useless gaps.
+ -- If you want a custom value. Set it to the number of pixels (int)
+theme.mstab_border_radius = 0 -- border radius of the tabbar
+theme.mstab_bar_height = 40 -- height of the tabbar
+theme.mstab_tabbar_position = "top" -- position of the tabbar (mstab currently does not support left,right)
+theme.mstab_tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
+ -- defaults to the tabbar_style so only change if you want a
+ -- different style for mstab and tabbed
+
+-- the following variables are currently only for the "modern" tabbar style
+theme.tabbar_color_close = "#f9929b" -- chnges the color of the close button
+theme.tabbar_color_min = "#fbdf90" -- chnges the color of the minimize button
+theme.tabbar_color_float = "#ccaced" -- chnges the color of the float button
+
+-- tag preview widget
+theme.tag_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
+theme.tag_preview_client_border_radius = 0 -- Border radius of each client in the widget (With AA)
+theme.tag_preview_client_opacity = 0.5 -- Opacity of each client
+theme.tag_preview_client_bg = "#000000" -- The bg color of each client
+theme.tag_preview_client_border_color = "#ffffff" -- The border color of each client
+theme.tag_preview_client_border_width = 3 -- The border width of each client
+theme.tag_preview_widget_bg = "#000000" -- The bg color of the widget
+theme.tag_preview_widget_border_color = "#ffffff" -- The border color of the widget
+theme.tag_preview_widget_border_width = 3 -- The border width of the widget
+theme.tag_preview_widget_margin = 0 -- The margin of the widget
+
+-- task preview widget
+theme.task_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
+theme.task_preview_widget_bg = "#000000" -- The bg color of the widget
+theme.task_preview_widget_border_color = "#ffffff" -- The border color of the widget
+theme.task_preview_widget_border_width = 3 -- The border width of the widget
+theme.task_preview_widget_margin = 0 -- The margin of the widget
+
+-- tabbed misc widget(s)
+theme.bling_tabbed_misc_titlebar_indicator = {
+ layout_spacing = dpi(4),
+ icon_size = dpi(20),
+ icon_margin = dpi(4),
+ bg_color_focus = "#ff0000",
+ bg_color = "#00000000",
+ icon_shape = function(cr, w, h)
+ gears.shape.rounded_rect(cr, w, h, 0)
+ end,
+ layout = wibox.layout.fixed.horizontal
+}
+
+-- window switcher widget
+theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget
+theme.window_switcher_widget_border_width = 3 -- The border width of the widget
+theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget
+theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget
+theme.window_switcher_clients_spacing = 20 -- The space between each client item
+theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text
+theme.window_switcher_client_width = 150 -- The width of one client widget
+theme.window_switcher_client_height = 250 -- The height of one client widget
+theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget
+theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget
+theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto"
+theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget
+theme.window_switcher_name_valign = "center" -- How to vertically align one clients title
+theme.window_switcher_name_forced_width = 200 -- The width of one title
+theme.window_switcher_name_font = "sans 11" -- The font of all titles
+theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused
+theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused
+theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon
+theme.window_switcher_icon_width = 40 -- The width of one icon
+```
diff --git a/.config/awesome/bling/docs/widgets/tabbed_misc.md b/.config/awesome/bling/docs/widgets/tabbed_misc.md
new file mode 100755
index 0000000..89c4557
--- /dev/null
+++ b/.config/awesome/bling/docs/widgets/tabbed_misc.md
@@ -0,0 +1,117 @@
+## 🧱 Tabbed Miscellaneous
+
+This comprises a few widgets to better represent tabbed groups (from the tabbed module) in your desktop.
+The widgets currently included are:
+- Titlebar Indicator
+- Tasklist
+
+
+
+## Titlebar Indicator
+
+### Usage
+
+To use the task list indicator:
+**NOTE:** Options can be set as theme vars under the table `theme.bling_tabbed_misc_titlebar_indicator`
+
+```lua
+bling.widget.tabbed_misc.titlebar_indicator(client, {
+ layout = wibox.layout.fixed.vertical,
+ layout_spacing = dpi(5), -- Set spacing in between items
+ icon_size = dpi(24), -- Set icon size
+ icon_margin = 0, -- Set icon margin
+ fg_color = "#cccccc", -- Normal color for text
+ fg_color_focus = "#ffffff", -- Color for focused text
+ bg_color_focus = "#282828", -- Color for the focused items
+ bg_color = "#1d2021", -- Color for normal / unfocused items
+ icon_shape = gears.shape.circle -- Set icon shape,
+})
+```
+
+a widget_template option is also available:
+```lua
+bling.widget.tabbed_misc.titlebar_indicator(client, {
+ widget_template = {
+ {
+ widget = awful.widget.clienticon,
+ id = 'icon_role'
+ },
+ widget = wibox.container.margin,
+ margins = 2,
+ id = 'bg_role',
+ update_callback = function(self, client, group)
+ if client == group.clients[group.focused_idx] then
+ self.margins = 5
+ end
+ end
+ }
+})
+```
+
+### Example Implementation
+
+You normally embed the widget in your titlebar...
+```lua
+awful.titlebar(c).widget = {
+ { -- Left
+ bling.widget.tabbed_misc.titlebar_indicator(c),
+ layout = wibox.layout.fixed.horizontal
+ },
+ { -- Middle
+ { -- Title
+ align = "center",
+ widget = awful.titlebar.widget.titlewidget(c)
+ },
+ buttons = buttons,
+ layout = wibox.layout.flex.horizontal
+ },
+ { -- Right
+ awful.titlebar.widget.maximizedbutton(c),
+ awful.titlebar.widget.closebutton (c),
+ layout = wibox.layout.fixed.horizontal
+ },
+ layout = wibox.layout.align.horizontal
+ }
+```
+
+## Tasklist
+The module exports a function that can be added to your tasklist as a `update_callback`
+
+### Usage
+```lua
+awful.widget.tasklist({
+ screen = s,
+ filter = awful.widget.tasklist.filter.currenttags,
+ layout = {
+ spacing = dpi(10),
+ layout = wibox.layout.fixed.vertical,
+ },
+ style = {
+ bg_normal = "#00000000",
+ },
+ widget_template = {
+ {
+ {
+ widget = wibox.widget.imagebox,
+ id = "icon_role",
+ align = "center",
+ valign = "center",
+ },
+ width = dpi(24),
+ height = dpi(24),
+ widget = wibox.container.constraint,
+ },
+ widget = wibox.container.background, -- IT MUST BE A CONTAINER WIDGET AS THAT IS WHAT THE FUNCTION EXPECTS
+ update_callback = require("bling.widget.tabbed_misc").custom_tasklist,
+ id = "background_role",
+ },
+})
+```
+
+If you need to do something else, it can be used like so
+```lua
+update_callback = function(self, client, index, clients)
+ require("bling.widget.tabbed_misc").custom_tasklist(self, client, index, clients)
+ require("naughty").notify({ text = "Tasklist was updated" })
+end
+```
diff --git a/.config/awesome/bling/docs/widgets/tag_preview.md b/.config/awesome/bling/docs/widgets/tag_preview.md
new file mode 100755
index 0000000..bdf033d
--- /dev/null
+++ b/.config/awesome/bling/docs/widgets/tag_preview.md
@@ -0,0 +1,155 @@
+## 🔍 Tag Preview
+
+This is a popup widget that will show a preview of a specified tag that illustrates the position, size, content, and icon of all clients.
+
+
+
+*gif by [javacafe](https://github.com/JavaCafe01)*
+
+### Usage
+
+To enable:
+
+```lua
+bling.widget.tag_preview.enable {
+ show_client_content = false, -- Whether or not to show the client content
+ x = 10, -- The x-coord of the popup
+ y = 10, -- The y-coord of the popup
+ scale = 0.25, -- The scale of the previews compared to the screen
+ honor_padding = false, -- Honor padding when creating widget size
+ honor_workarea = false, -- Honor work area when creating widget size
+ placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y)
+ awful.placement.top_left(c, {
+ margins = {
+ top = 30,
+ left = 30
+ }
+ })
+ end,
+ background_widget = wibox.widget { -- Set a background image (like a wallpaper) for the widget
+ image = beautiful.wallpaper,
+ horizontal_fit_policy = "fit",
+ vertical_fit_policy = "fit",
+ widget = wibox.widget.imagebox
+ }
+}
+```
+
+Here are the signals available:
+
+```lua
+-- bling::tag_preview::update -- first line is the signal
+-- t (tag) -- indented lines are function parameters
+-- bling::tag_preview::visibility
+-- s (screen)
+-- v (boolean)
+```
+
+By default, the widget is not visible. You must implement when it will update and when it will show.
+
+### Example Implementation
+
+We can trigger the widget to show the specific tag when hovering over it in the taglist. The code shown below is the example taglist from the [AwesomeWM docs](https://awesomewm.org/doc/api/classes/awful.widget.taglist.html). Basically, we are going to update the widget and toggle it through the taglist's `create_callback`. (The bling addons are commented)
+```lua
+s.mytaglist = awful.widget.taglist {
+ screen = s,
+ filter = awful.widget.taglist.filter.all,
+ style = {
+ shape = gears.shape.powerline
+ },
+ layout = {
+ spacing = -12,
+ spacing_widget = {
+ color = '#dddddd',
+ shape = gears.shape.powerline,
+ widget = wibox.widget.separator,
+ },
+ layout = wibox.layout.fixed.horizontal
+ },
+ widget_template = {
+ {
+ {
+ {
+ {
+ {
+ id = 'index_role',
+ widget = wibox.widget.textbox,
+ },
+ margins = 4,
+ widget = wibox.container.margin,
+ },
+ bg = '#dddddd',
+ shape = gears.shape.circle,
+ widget = wibox.container.background,
+ },
+ {
+ {
+ id = 'icon_role',
+ widget = wibox.widget.imagebox,
+ },
+ margins = 2,
+ widget = wibox.container.margin,
+ },
+ {
+ id = 'text_role',
+ widget = wibox.widget.textbox,
+ },
+ layout = wibox.layout.fixed.horizontal,
+ },
+ left = 18,
+ right = 18,
+ widget = wibox.container.margin
+ },
+ id = 'background_role',
+ widget = wibox.container.background,
+ -- Add support for hover colors and an index label
+ create_callback = function(self, c3, index, objects) --luacheck: no unused args
+ self:get_children_by_id('index_role')[1].markup = ' '..index..' '
+ self:connect_signal('mouse::enter', function()
+
+ -- BLING: Only show widget when there are clients in the tag
+ if #c3:clients() > 0 then
+ -- BLING: Update the widget with the new tag
+ awesome.emit_signal("bling::tag_preview::update", c3)
+ -- BLING: Show the widget
+ awesome.emit_signal("bling::tag_preview::visibility", s, true)
+ end
+
+ if self.bg ~= '#ff0000' then
+ self.backup = self.bg
+ self.has_backup = true
+ end
+ self.bg = '#ff0000'
+ end)
+ self:connect_signal('mouse::leave', function()
+
+ -- BLING: Turn the widget off
+ awesome.emit_signal("bling::tag_preview::visibility", s, false)
+
+ if self.has_backup then self.bg = self.backup end
+ end)
+ end,
+ update_callback = function(self, c3, index, objects) --luacheck: no unused args
+ self:get_children_by_id('index_role')[1].markup = ' '..index..' '
+ end,
+ },
+ buttons = taglist_buttons
+}
+```
+
+### Theme Variables
+
+```lua
+theme.tag_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
+theme.tag_preview_client_border_radius = 0 -- Border radius of each client in the widget (With AA)
+theme.tag_preview_client_opacity = 0.5 -- Opacity of each client
+theme.tag_preview_client_bg = "#000000" -- The bg color of each client
+theme.tag_preview_client_border_color = "#ffffff" -- The border color of each client
+theme.tag_preview_client_border_width = 3 -- The border width of each client
+theme.tag_preview_widget_bg = "#000000" -- The bg color of the widget
+theme.tag_preview_widget_border_color = "#ffffff" -- The border color of the widget
+theme.tag_preview_widget_border_width = 3 -- The border width of the widget
+theme.tag_preview_widget_margin = 0 -- The margin of the widget
+```
+
+NOTE: I recommend to only use the widget border radius theme variable when not using shadows with a compositor, as anti-aliased rounding with the outer widgets made with AwesomeWM rely on the actual bg being transparent. If you want rounding with shadows on the widget, use a compositor like [jonaburg's fork](https://github.com/jonaburg/picom).
diff --git a/.config/awesome/bling/docs/widgets/task_preview.md b/.config/awesome/bling/docs/widgets/task_preview.md
new file mode 100755
index 0000000..30a85f8
--- /dev/null
+++ b/.config/awesome/bling/docs/widgets/task_preview.md
@@ -0,0 +1,152 @@
+## 🔍 Task Preview
+
+This is a popup widget that will show a preview of the specified client. It is supposed to mimic the small popup that Windows has when hovering over the application icon.
+
+
+
+*image by [javacafe](https://github.com/JavaCafe01)*
+
+### Usage
+
+To enable:
+
+```lua
+bling.widget.task_preview.enable {
+ x = 20, -- The x-coord of the popup
+ y = 20, -- The y-coord of the popup
+ height = 200, -- The height of the popup
+ width = 200, -- The width of the popup
+ placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y)
+ awful.placement.bottom(c, {
+ margins = {
+ bottom = 30
+ }
+ })
+ end
+}
+```
+
+To allow for more customization, there is also a `widget_structure` property (as seen in some default awesome widgets) which is optional. An example is as follows -
+```lua
+bling.widget.task_preview.enable {
+ x = 20, -- The x-coord of the popup
+ y = 20, -- The y-coord of the popup
+ height = 200, -- The height of the popup
+ width = 200, -- The width of the popup
+ placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y)
+ awful.placement.bottom(c, {
+ margins = {
+ bottom = 30
+ }
+ })
+ end,
+ -- Your widget will automatically conform to the given size due to a constraint container.
+ widget_structure = {
+ {
+ {
+ {
+ id = 'icon_role',
+ widget = awful.widget.clienticon, -- The client icon
+ },
+ {
+ id = 'name_role', -- The client name / title
+ widget = wibox.widget.textbox,
+ },
+ layout = wibox.layout.flex.horizontal
+ },
+ widget = wibox.container.margin,
+ margins = 5
+ },
+ {
+ id = 'image_role', -- The client preview
+ resize = true,
+ valign = 'center',
+ halign = 'center',
+ widget = wibox.widget.imagebox,
+ },
+ layout = wibox.layout.fixed.vertical
+ }
+}
+```
+
+Here are the signals available:
+
+```lua
+-- bling::task_preview::visibility -- first line is the signal
+-- s (screen) -- indented lines are function parameters
+-- v (boolean)
+-- c (client)
+```
+
+By default, the widget is not visible. You must implement when it will update and when it will show.
+
+### Example Implementation
+
+We can trigger the widget to show the specific client when hovering over it in the tasklist. The code shown below is the example icon only tasklist from the [AwesomeWM docs](https://awesomewm.org/doc/api/classes/awful.widget.tasklist.html). Basically, we are going to toggle the widget through the tasklist's `create_callback`. (The bling addons are commented)
+```lua
+s.mytasklist = awful.widget.tasklist {
+ screen = s,
+ filter = awful.widget.tasklist.filter.currenttags,
+ buttons = tasklist_buttons,
+ layout = {
+ spacing_widget = {
+ {
+ forced_width = 5,
+ forced_height = 24,
+ thickness = 1,
+ color = '#777777',
+ widget = wibox.widget.separator
+ },
+ valign = 'center',
+ halign = 'center',
+ widget = wibox.container.place,
+ },
+ spacing = 1,
+ layout = wibox.layout.fixed.horizontal
+ },
+ -- Notice that there is *NO* wibox.wibox prefix, it is a template,
+ -- not a widget instance.
+ widget_template = {
+ {
+ wibox.widget.base.make_widget(),
+ forced_height = 5,
+ id = 'background_role',
+ widget = wibox.container.background,
+ },
+ {
+ {
+ id = 'clienticon',
+ widget = awful.widget.clienticon,
+ },
+ margins = 5,
+ widget = wibox.container.margin
+ },
+ nil,
+ create_callback = function(self, c, index, objects) --luacheck: no unused args
+ self:get_children_by_id('clienticon')[1].client = c
+
+ -- BLING: Toggle the popup on hover and disable it off hover
+ self:connect_signal('mouse::enter', function()
+ awesome.emit_signal("bling::task_preview::visibility", s,
+ true, c)
+ end)
+ self:connect_signal('mouse::leave', function()
+ awesome.emit_signal("bling::task_preview::visibility", s,
+ false, c)
+ end)
+ end,
+ layout = wibox.layout.align.vertical,
+ },
+}
+```
+
+### Theme Variables
+```lua
+theme.task_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
+theme.task_preview_widget_bg = "#000000" -- The bg color of the widget
+theme.task_preview_widget_border_color = "#ffffff" -- The border color of the widget
+theme.task_preview_widget_border_width = 3 -- The border width of the widget
+theme.task_preview_widget_margin = 0 -- The margin of the widget
+```
+
+NOTE: I recommend to only use the widget border radius theme variable when not using shadows with a compositor, as anti-aliased rounding with the outer widgets made with AwesomeWM rely on the actual bg being transparent. If you want rounding with shadows on the widget, use a compositor like [jonaburg's fork](https://github.com/jonaburg/picom).
diff --git a/.config/awesome/bling/docs/widgets/window_switcher.md b/.config/awesome/bling/docs/widgets/window_switcher.md
new file mode 100755
index 0000000..8f48257
--- /dev/null
+++ b/.config/awesome/bling/docs/widgets/window_switcher.md
@@ -0,0 +1,67 @@
+## 🎨 Window Switcher
+
+A popup with client previews that allows you to switch clients similar to the alt-tab menu in MacOS, GNOME, and Windows.
+
+
+
+*image by [No37](https://github.com/Nooo37)*
+
+### Usage
+
+To enable:
+
+```lua
+bling.widget.window_switcher.enable {
+ type = "thumbnail", -- set to anything other than "thumbnail" to disable client previews
+
+ -- keybindings (the examples provided are also the default if kept unset)
+ hide_window_switcher_key = "Escape", -- The key on which to close the popup
+ minimize_key = "n", -- The key on which to minimize the selected client
+ unminimize_key = "N", -- The key on which to unminimize all clients
+ kill_client_key = "q", -- The key on which to close the selected client
+ cycle_key = "Tab", -- The key on which to cycle through all clients
+ previous_key = "Left", -- The key on which to select the previous client
+ next_key = "Right", -- The key on which to select the next client
+ vim_previous_key = "h", -- Alternative key on which to select the previous client
+ vim_next_key = "l", -- Alternative key on which to select the next client
+
+ cycleClientsByIdx = awful.client.focus.byidx, -- The function to cycle the clients
+ filterClients = awful.widget.tasklist.filter.currenttags, -- The function to filter the viewed clients
+}
+```
+
+To run the window swicher you have to emit this signal from within your configuration (usually using a keybind).
+
+```lua
+awesome.emit_signal("bling::window_switcher::turn_on")
+```
+
+For example:
+```lua
+ awful.key({Mod1}, "Tab", function()
+ awesome.emit_signal("bling::window_switcher::turn_on")
+ end, {description = "Window Switcher", group = "bling"})
+```
+
+### Theme Variables
+```lua
+theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget
+theme.window_switcher_widget_border_width = 3 -- The border width of the widget
+theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget
+theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget
+theme.window_switcher_clients_spacing = 20 -- The space between each client item
+theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text
+theme.window_switcher_client_width = 150 -- The width of one client widget
+theme.window_switcher_client_height = 250 -- The height of one client widget
+theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget
+theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget
+theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto"
+theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget
+theme.window_switcher_name_valign = "center" -- How to vertically align one clients title
+theme.window_switcher_name_forced_width = 200 -- The width of one title
+theme.window_switcher_name_font = "sans 11" -- The font of all titles
+theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused
+theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused
+theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon
+theme.window_switcher_icon_width = 40 -- The width of one icon
+```
diff --git a/.config/awesome/bling/helpers/client.lua b/.config/awesome/bling/helpers/client.lua
new file mode 100755
index 0000000..0e14c51
--- /dev/null
+++ b/.config/awesome/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/bling/helpers/color.lua b/.config/awesome/bling/helpers/color.lua
new file mode 100755
index 0000000..4042360
--- /dev/null
+++ b/.config/awesome/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/bling/helpers/filesystem.lua b/.config/awesome/bling/helpers/filesystem.lua
new file mode 100755
index 0000000..f06139a
--- /dev/null
+++ b/.config/awesome/bling/helpers/filesystem.lua
@@ -0,0 +1,62 @@
+local Gio = require("lgi").Gio
+local awful = require("awful")
+local string = string
+
+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
+
+function _filesystem.save_image_async_curl(url, filepath, callback)
+ awful.spawn.with_line_callback(string.format("curl -L -s %s -o %s", url, filepath),
+ {
+ exit=callback
+ })
+end
+
+return _filesystem
diff --git a/.config/awesome/bling/helpers/icon_theme.lua b/.config/awesome/bling/helpers/icon_theme.lua
new file mode 100755
index 0000000..c4d4583
--- /dev/null
+++ b/.config/awesome/bling/helpers/icon_theme.lua
@@ -0,0 +1,142 @@
+local lgi = require("lgi")
+local Gio = lgi.Gio
+local Gtk = lgi.require("Gtk", "3.0")
+local gobject = require("gears.object")
+local gtable = require("gears.table")
+local setmetatable = setmetatable
+local ipairs = ipairs
+
+local icon_theme = { mt = {} }
+
+local name_lookup =
+{
+ ["jetbrains-studio"] = "android-studio"
+}
+
+local function get_icon_by_pid_command(self, client, apps)
+ local pid = client.pid
+ if pid ~= nil then
+ local handle = io.popen(string.format("ps -p %d -o comm=", pid))
+ local pid_command = handle:read("*a"):gsub("^%s*(.-)%s*$", "%1")
+ handle:close()
+
+ for _, app in ipairs(apps) do
+ local executable = app:get_executable()
+ if executable and executable:find(pid_command, 1, true) then
+ return self:get_gicon_path(app:get_icon())
+ end
+ end
+ end
+end
+
+local function get_icon_by_icon_name(self, client, apps)
+ local icon_name = client.icon_name and client.icon_name:lower() or nil
+ if icon_name ~= nil then
+ for _, app in ipairs(apps) do
+ local name = app:get_name():lower()
+ if name and name:find(icon_name, 1, true) then
+ return self:get_gicon_path(app:get_icon())
+ end
+ end
+ end
+end
+
+local function get_icon_by_class(self, client, apps)
+ if client.class ~= nil then
+ local class = name_lookup[client.class] or client.class:lower()
+
+ -- Try to remove dashes
+ local class_1 = class:gsub("[%-]", "")
+
+ -- Try to replace dashes with dot
+ local class_2 = class:gsub("[%-]", ".")
+
+ -- Try to match only the first word
+ local class_3 = class:match("(.-)-") or class
+ class_3 = class_3:match("(.-)%.") or class_3
+ class_3 = class_3:match("(.-)%s+") or class_3
+
+ local possible_icon_names = { class, class_3, class_2, class_1 }
+ for _, app in ipairs(apps) do
+ local id = app:get_id():lower()
+ for _, possible_icon_name in ipairs(possible_icon_names) do
+ if id and id:find(possible_icon_name, 1, true) then
+ return self:get_gicon_path(app:get_icon())
+ end
+ end
+ end
+ end
+end
+
+function icon_theme:get_client_icon_path(client)
+ local apps = Gio.AppInfo.get_all()
+
+ return get_icon_by_pid_command(self, client, apps) or
+ get_icon_by_icon_name(self, client, apps) or
+ get_icon_by_class(self, client, apps) or
+ client.icon or
+ self:choose_icon({"window", "window-manager", "xfwm4-default", "window_list" })
+end
+
+function icon_theme:choose_icon(icons_names)
+ local icon_info = self.gtk_theme:choose_icon(icons_names, self.icon_size, 0);
+ if icon_info then
+ local icon_path = icon_info:get_filename()
+ if icon_path then
+ return icon_path
+ end
+ end
+
+ return ""
+end
+
+function icon_theme:get_gicon_path(gicon)
+ if gicon == nil then
+ return ""
+ end
+
+ local icon_info = self.gtk_theme:lookup_by_gicon(gicon, self.icon_size, 0);
+ if icon_info then
+ local icon_path = icon_info:get_filename()
+ if icon_path then
+ return icon_path
+ end
+ end
+
+ return ""
+end
+
+function icon_theme:get_icon_path(icon_name)
+ local icon_info = self.gtk_theme:lookup_icon(icon_name, self.icon_size, 0)
+ if icon_info then
+ local icon_path = icon_info:get_filename()
+ if icon_path then
+ return icon_path
+ end
+ end
+
+ return ""
+end
+
+local function new(theme_name, icon_size)
+ local ret = gobject{}
+ gtable.crush(ret, icon_theme, true)
+
+ 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)
\ No newline at end of file
diff --git a/.config/awesome/bling/helpers/init.lua b/.config/awesome/bling/helpers/init.lua
new file mode 100755
index 0000000..f2c898e
--- /dev/null
+++ b/.config/awesome/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/bling/helpers/shape.lua b/.config/awesome/bling/helpers/shape.lua
new file mode 100755
index 0000000..fedc544
--- /dev/null
+++ b/.config/awesome/bling/helpers/shape.lua
@@ -0,0 +1,30 @@
+local gears = require("gears")
+
+local 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/bling/helpers/time.lua b/.config/awesome/bling/helpers/time.lua
new file mode 100755
index 0000000..5ab0f25
--- /dev/null
+++ b/.config/awesome/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/bling/icons/layouts/centered.png b/.config/awesome/bling/icons/layouts/centered.png
new file mode 100755
index 0000000..5e4d07f
Binary files /dev/null and b/.config/awesome/bling/icons/layouts/centered.png differ
diff --git a/.config/awesome/bling/icons/layouts/deck.png b/.config/awesome/bling/icons/layouts/deck.png
new file mode 100755
index 0000000..829d0c2
Binary files /dev/null and b/.config/awesome/bling/icons/layouts/deck.png differ
diff --git a/.config/awesome/bling/icons/layouts/equalarea.png b/.config/awesome/bling/icons/layouts/equalarea.png
new file mode 100755
index 0000000..47164a9
Binary files /dev/null and b/.config/awesome/bling/icons/layouts/equalarea.png differ
diff --git a/.config/awesome/bling/icons/layouts/horizontal.png b/.config/awesome/bling/icons/layouts/horizontal.png
new file mode 100755
index 0000000..217f4eb
Binary files /dev/null and b/.config/awesome/bling/icons/layouts/horizontal.png differ
diff --git a/.config/awesome/bling/icons/layouts/mstab.png b/.config/awesome/bling/icons/layouts/mstab.png
new file mode 100755
index 0000000..4372846
Binary files /dev/null and b/.config/awesome/bling/icons/layouts/mstab.png differ
diff --git a/.config/awesome/bling/icons/layouts/vertical.png b/.config/awesome/bling/icons/layouts/vertical.png
new file mode 100755
index 0000000..3561673
Binary files /dev/null and b/.config/awesome/bling/icons/layouts/vertical.png differ
diff --git a/.config/awesome/bling/images/bling_banner-2x.png b/.config/awesome/bling/images/bling_banner-2x.png
new file mode 100755
index 0000000..9e2ab30
Binary files /dev/null and b/.config/awesome/bling/images/bling_banner-2x.png differ
diff --git a/.config/awesome/bling/images/bling_banner.png b/.config/awesome/bling/images/bling_banner.png
new file mode 100755
index 0000000..43de6c9
Binary files /dev/null and b/.config/awesome/bling/images/bling_banner.png differ
diff --git a/.config/awesome/bling/init.lua b/.config/awesome/bling/init.lua
new file mode 100755
index 0000000..23c0acf
--- /dev/null
+++ b/.config/awesome/bling/init.lua
@@ -0,0 +1,11 @@
+--[[
+ Bling
+ Layouts, widgets and utilities for Awesome WM
+--]]
+return {
+ layout = require(... .. ".layout"),
+ module = require(... .. ".module"),
+ helpers = require(... .. ".helpers"),
+ signal = require(... .. ".signal"),
+ widget = require(... .. ".widget"),
+}
diff --git a/.config/awesome/bling/layout/centered.lua b/.config/awesome/bling/layout/centered.lua
new file mode 100755
index 0000000..7929dd8
--- /dev/null
+++ b/.config/awesome/bling/layout/centered.lua
@@ -0,0 +1,80 @@
+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 nmaster = math.min(t.master_count, #p.clients)
+ local nslaves = #p.clients - nmaster
+
+ local master_area_width = area.width * t.master_width_factor
+ if t.master_count == 0 then master_area_width = 0 end
+ local slave_width = 0.5 * (area.width - master_area_width)
+ local master_area_x = area.x + slave_width
+
+
+ -- Special case: few slaves -> make masters take more space - unless requested otherwise!
+ if nslaves < 2 and t.master_fill_policy ~= "master_width_factor" then
+ master_area_x = area.x
+
+ if nslaves == 1 then
+ slave_width = area.width - master_area_width
+ else
+ master_area_width = area.width
+ end
+ 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
+ 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
+
+ for idx = 1, nslaves 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_width,
+ height = area.height / number_of_left_sided_slaves,
+ }
+ left_iterator = left_iterator + 1
+ else
+ g = {
+ x = master_area_x + master_area_width,
+ y = area.y
+ + right_iterator
+ * (area.height / number_of_right_sided_slaves),
+ width = slave_width,
+ 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/bling/layout/deck.lua b/.config/awesome/bling/layout/deck.lua
new file mode 100755
index 0000000..e0500b9
--- /dev/null
+++ b/.config/awesome/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/bling/layout/equalarea.lua b/.config/awesome/bling/layout/equalarea.lua
new file mode 100755
index 0000000..37e972d
--- /dev/null
+++ b/.config/awesome/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/bling/layout/horizontal.lua b/.config/awesome/bling/layout/horizontal.lua
new file mode 100755
index 0000000..23f9c9e
--- /dev/null
+++ b/.config/awesome/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/bling/layout/init.lua b/.config/awesome/bling/layout/init.lua
new file mode 100755
index 0000000..de30ed6
--- /dev/null
+++ b/.config/awesome/bling/layout/init.lua
@@ -0,0 +1,46 @@
+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, beautiful.fg_normal)
+ 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)
+ if beautiful["layout_" .. layout_name] == nil then
+ beautiful["layout_" .. layout_name] = get_icon(icon_raw)
+ end
+ M[layout_name] = require(... .. "." .. layout_name)
+end
+
+return M
diff --git a/.config/awesome/bling/layout/mstab.lua b/.config/awesome/bling/layout/mstab.lua
new file mode 100755
index 0000000..88ce0cb
--- /dev/null
+++ b/.config/awesome/bling/layout/mstab.lua
@@ -0,0 +1,236 @@
+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_visibility()
+ local name = awful.layout.getname( awful.layout.get( s ) )
+ s.tabbar.visible = (name == mylayout.name)
+ end
+
+ tag.connect_signal("property::selected", adjust_visibility)
+ tag.connect_signal("property::layout", adjust_visibility)
+ tag.connect_signal("tagged", adjust_visibility)
+ tag.connect_signal("untagged", adjust_visibility)
+ tag.connect_signal("property::master_count", adjust_visibility)
+ client.connect_signal("property::minimized", adjust_visibility)
+ 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/bling/layout/vertical.lua b/.config/awesome/bling/layout/vertical.lua
new file mode 100755
index 0000000..8b6811e
--- /dev/null
+++ b/.config/awesome/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/bling/module/flash_focus.lua b/.config/awesome/bling/module/flash_focus.lua
new file mode 100755
index 0000000..35f3cf0
--- /dev/null
+++ b/.config/awesome/bling/module/flash_focus.lua
@@ -0,0 +1,39 @@
+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 and #c.screen.clients > 1 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
+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/bling/module/init.lua b/.config/awesome/bling/module/init.lua
new file mode 100755
index 0000000..ed127f6
--- /dev/null
+++ b/.config/awesome/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/bling/module/scratchpad.lua b/.config/awesome/bling/module/scratchpad.lua
new file mode 100755
index 0000000..6ef011f
--- /dev/null
+++ b/.config/awesome/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)
+ client.disconnect_signal("manage", inital_apply)
+ end
+ end
+ 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/bling/module/tabbed.lua b/.config/awesome/bling/module/tabbed.lua
new file mode 100755
index 0000000..c53ec03
--- /dev/null
+++ b/.config/awesome/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/bling/module/tiled_wallpaper.lua b/.config/awesome/bling/module/tiled_wallpaper.lua
new file mode 100755
index 0000000..75014cf
--- /dev/null
+++ b/.config/awesome/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/bling/module/wallpaper.lua b/.config/awesome/bling/module/wallpaper.lua
new file mode 100755
index 0000000..e7bddf6
--- /dev/null
+++ b/.config/awesome/bling/module/wallpaper.lua
@@ -0,0 +1,362 @@
+---------------------------------------------------------------------------
+-- 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(s)
+ gears.wallpaper.centered(
+ wallpaper_object,
+ s,
+ args.background,
+ args.scale
+ )
+ end,
+ ["tiled"] = function(s)
+ gears.wallpaper.tiled(wallpaper_object, s, args.offset)
+ end,
+ ["maximized"] = function(s)
+ gears.wallpaper.maximized(
+ wallpaper_object,
+ s,
+ args.ignore_aspect,
+ args.offset
+ )
+ end,
+ ["fit"] = function(s)
+ gears.wallpaper.fit(wallpaper_object, s, args.background)
+ end,
+ }
+ local call_func = nil
+ 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"
+ call_func = 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
+ call_func = positions[args.position]
+ else
+ gears.wallpaper.set(wallpaper_object)
+ end
+ if call_func then
+ call_func(args.screen)
+ 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
+ if type(args.screen) == 'table' then
+ for _,v in ipairs(args.screen) do
+ args.screen = v
+ apply(wallpapers[simple_index], args)
+ args.screen = nil
+ end
+ else
+ apply(wallpapers[simple_index], args)
+ end
+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)
+ if type(args.screen) == 'table' then
+ for _,v in ipairs(args.screen) do
+ args.screen = v
+ apply(wallpapers[math.random(#wallpapers)], args)
+ args.screen = nil
+ end
+ else
+ apply(wallpapers[math.random(#wallpapers)], args)
+ end
+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)
+ if type(config.screen) ~= 'table' then
+ if config.screen and s and config.screen ~= s then return end
+ config.screen = s or config.screen
+ end
+ 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" or awesome.version == "4.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/bling/module/window_swallowing.lua b/.config/awesome/bling/module/window_swallowing.lua
new file mode 100755
index 0000000..60950aa
--- /dev/null
+++ b/.config/awesome/bling/module/window_swallowing.lua
@@ -0,0 +1,128 @@
+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 parent_filter_list = beautiful.parent_filter_list
+ or beautiful.dont_swallow_classname_list
+ or { "firefox", "Gimp", "Google-chrome" }
+local child_filter_list = beautiful.child_filter_list
+ or beautiful.dont_swallow_classname_list or { }
+
+-- for boolean values the or chain way to set the values breaks with 2 vars
+-- and always defaults to true so i had to do this to se the right value...
+local swallowing_filter = true
+local filter_vars = { beautiful.swallowing_filter, beautiful.dont_swallow_filter_activated }
+for _, var in pairs(filter_vars) do
+ swallowing_filter = var
+end
+
+-- check if element exist in table
+-- returns true if it is
+local function is_in_table(element, table)
+ local res = false
+ for _, value in pairs(table) do
+ if element:match(value) then
+ res = true
+ break
+ end
+ end
+ return res
+end
+
+-- if the swallowing filter is active checks the child and parent classes
+-- against their filters
+local function check_swallow(parent, child)
+ local res = true
+ if swallowing_filter then
+ local prnt = not is_in_table(parent, parent_filter_list)
+ local chld = not is_in_table(child, child_filter_list)
+ res = ( prnt and chld )
+ end
+ return res
+end
+
+-- async function to get the parent's pid
+-- recieves a child process pid and a callback function
+-- parent_pid in format "init(1)---ancestorA(pidA)---ancestorB(pidB)...---process(pid)"
+function get_parent_pid(child_ppid, callback)
+ local ppid_cmd = string.format("pstree -A -p -s %s", child_ppid)
+ awful.spawn.easy_async(ppid_cmd, function(stdout, stderr, reason, exit_code)
+ -- primitive error checking
+ if stderr and stderr ~= "" then
+ callback(stderr)
+ return
+ end
+ local ppid = stdout
+ callback(nil, ppid)
+ end)
+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
+ elseif parent_client.type == "dialog" or parent_client.type == "splash" then
+ return
+ end
+
+ get_parent_pid(c.pid, function(err, ppid)
+ if err then
+ return
+ end
+ parent_pid = ppid
+ if
+ -- will search for "(parent_client.pid)" inside the parent_pid string
+ ( tostring(parent_pid):find("("..tostring(parent_client.pid)..")") )
+ and check_swallow(parent_client.class, c.class)
+ then
+ c:connect_signal("unmanage", function()
+ if parent_client then
+ helpers.client.turn_on(parent_client)
+ helpers.client.sync(parent_client, c)
+ end
+ end)
+
+ helpers.client.sync(c, parent_client)
+ helpers.client.turn_off(parent_client)
+ end
+ 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/bling/signal/init.lua b/.config/awesome/bling/signal/init.lua
new file mode 100755
index 0000000..c513381
--- /dev/null
+++ b/.config/awesome/bling/signal/init.lua
@@ -0,0 +1,3 @@
+return {
+ playerctl = require(... .. ".playerctl"),
+}
diff --git a/.config/awesome/bling/signal/playerctl/init.lua b/.config/awesome/bling/signal/playerctl/init.lua
new file mode 100755
index 0000000..357b02c
--- /dev/null
+++ b/.config/awesome/bling/signal/playerctl/init.lua
@@ -0,0 +1,46 @@
+local awful = require("awful")
+local gtimer = require("gears.timer")
+local beautiful = require("beautiful")
+local naughty = require("naughty")
+
+-- 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 backend = nil
+
+local function enable_wrapper(args)
+ local open = naughty.action { name = "Open" }
+
+ open:connect_signal("invoked", function()
+ awful.spawn("xdg-open https://blingcorp.github.io/bling/#/signals/pctl")
+ end)
+
+ gtimer.delayed_call(function()
+ naughty.notify({
+ title = "Bling Error",
+ text = "Global signals are deprecated! Please take a look at the playerctl documentation.",
+ app_name = "Bling Error",
+ app_icon = "system-error",
+ actions = { open }
+ })
+ end)
+
+ backend_config = (args and args.backend) or backend_config
+ backend = backends[backend_config](args)
+ return backend
+end
+
+local function disable_wrapper()
+ backend:disable()
+end
+
+return {
+ lib = backends.playerctl_lib,
+ cli = backends.playerctl_cli,
+ enable = enable_wrapper,
+ disable = disable_wrapper
+}
\ No newline at end of file
diff --git a/.config/awesome/bling/signal/playerctl/playerctl_cli.lua b/.config/awesome/bling/signal/playerctl/playerctl_cli.lua
new file mode 100755
index 0000000..d091407
--- /dev/null
+++ b/.config/awesome/bling/signal/playerctl/playerctl_cli.lua
@@ -0,0 +1,348 @@
+-- Playerctl signals
+--
+-- Provides:
+-- metadata
+-- title (string)
+-- artist (string)
+-- album_path (string)
+-- album (string)
+-- player_name (string)
+-- position
+-- interval_sec (number)
+-- length_sec (number)
+-- playback_status
+-- playing (boolean)
+-- volume
+-- volume (number)
+-- loop_status
+-- loop_status (string)
+-- shuffle
+-- shuffle (bool)
+-- no_players
+-- (No parameters)
+
+local awful = require("awful")
+local gobject = require("gears.object")
+local gtable = require("gears.table")
+local gtimer = require("gears.timer")
+local gstring = require("gears.string")
+local beautiful = require("beautiful")
+local helpers = require(tostring(...):match(".*bling") .. ".helpers")
+local setmetatable = setmetatable
+local tonumber = tonumber
+local ipairs = ipairs
+local type = type
+local capi = { awesome = awesome }
+
+local playerctl = { mt = {} }
+
+function playerctl:disable()
+ self._private.metadata_timer:stop()
+ self._private.metadata_timer = nil
+ awful.spawn.with_shell("killall playerctl")
+end
+
+function playerctl:pause(player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " pause")
+ else
+ awful.spawn.with_shell(self._private.cmd .. "pause")
+ end
+end
+
+function playerctl:play(player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " play")
+ else
+ awful.spawn.with_shell(self._private.cmd .. "play")
+ end
+end
+
+function playerctl:stop(player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " stop")
+ else
+ awful.spawn.with_shell(self._private.cmd .. "stop")
+ end
+end
+
+function playerctl:play_pause(player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " play-pause")
+ else
+ awful.spawn.with_shell(self._private.cmd .. "play-pause")
+ end
+end
+
+function playerctl:previous(player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " previous")
+ else
+ awful.spawn.with_shell(self._private.cmd .. "previous")
+ end
+end
+
+function playerctl:next(player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " next")
+ else
+ awful.spawn.with_shell(self._private.cmd .. "next")
+ end
+end
+
+function playerctl:set_loop_status(loop_status, player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " loop " .. loop_status)
+ else
+ awful.spawn.with_shell(self._private.cmd .. "loop " .. loop_status)
+ end
+end
+
+function playerctl:cycle_loop_status(player)
+ local function set_loop_status(loop_status)
+ if loop_status == "None" then
+ self:set_loop_status("Track")
+ elseif loop_status == "Track" then
+ self:set_loop_status("Playlist")
+ elseif loop_status == "Playlist" then
+ self:set_loop_status("None")
+ end
+ end
+
+ if player ~= nil then
+ awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " loop", function(stdout)
+ set_loop_status(stdout)
+ end)
+ else
+ set_loop_status(self._private.loop_status)
+ end
+end
+
+function playerctl:set_position(position, player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " position " .. position)
+ else
+ awful.spawn.with_shell(self._private.cmd .. "position " .. position)
+ end
+end
+
+function playerctl:set_shuffle(shuffle, player)
+ shuffle = shuffle and "on" or "off"
+
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " shuffle " .. shuffle)
+ else
+ awful.spawn.with_shell(self._private.cmd .. "shuffle " .. shuffle)
+ end
+end
+
+function playerctl:cycle_shuffle(player)
+ if player ~= nil then
+ awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " shuffle", function(stdout)
+ local shuffle = stdout == "on" and true or false
+ self:set_shuffle(not self._private.shuffle)
+ end)
+ else
+ self:set_shuffle(not self._private.shuffle)
+ end
+end
+
+function playerctl:set_volume(volume, player)
+ if player ~= nil then
+ awful.spawn.with_shell("playerctl --player=" .. player .. " volume " .. volume)
+ else
+ awful.spawn.with_shell(self._private.cmd .. "volume " .. volume)
+ end
+end
+
+local function emit_player_metadata(self)
+ local metadata_cmd = self._private.cmd .. "metadata --format 'title_{{title}}artist_{{artist}}art_url_{{mpris:artUrl}}player_name_{{playerName}}album_{{album}}' -F"
+
+ awful.spawn.with_line_callback(metadata_cmd, {
+ stdout = function(line)
+ local title = gstring.xml_escape(line:match('title_(.*)artist_')) or ""
+ local artist = gstring.xml_escape(line:match('artist_(.*)art_url_')) or ""
+ local art_url = line:match('art_url_(.*)player_name_') or ""
+ local player_name = line:match('player_name_(.*)album_') or ""
+ local album = gstring.xml_escape(line:match('album_(.*)')) or ""
+
+ art_url = art_url:gsub('%\n', '')
+ if player_name == "spotify" then
+ art_url = art_url:gsub("open.spotify.com", "i.scdn.co")
+ end
+
+ if self._private.metadata_timer
+ and self._private.metadata_timer.started
+ then
+ self._private.metadata_timer:stop()
+ end
+
+ self._private.metadata_timer = gtimer {
+ timeout = self.debounce_delay,
+ autostart = true,
+ single_shot = true,
+ callback = function()
+ if title and title ~= "" then
+ if art_url ~= "" then
+ local art_path = os.tmpname()
+ helpers.filesystem.save_image_async_curl(art_url, art_path, function()
+ self:emit_signal("metadata", title, artist, art_path, album, player_name)
+ capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path)
+ end)
+ else
+ self:emit_signal("metadata", title, artist, "", album, player_name)
+ capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "")
+ end
+ else
+ self:emit_signal("no_players")
+ capi.awesome.emit_signal("bling::playerctl::no_players")
+ end
+ end
+ }
+
+ collectgarbage("collect")
+ end,
+ })
+end
+
+local function emit_player_position(self)
+ local position_cmd = self._private.cmd .. "position"
+ local length_cmd = self._private.cmd .. "metadata mpris:length"
+
+ awful.widget.watch(position_cmd, self.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
+ self:emit_signal("position", interval_sec, length_sec / 1000000)
+ capi.awesome.emit_signal("bling::playerctl::position", interval_sec, length_sec / 1000000)
+ end
+ end
+ end)
+ collectgarbage("collect")
+ end)
+end
+
+local function emit_player_playback_status(self)
+ local status_cmd = self._private.cmd .. "status -F"
+
+ awful.spawn.with_line_callback(status_cmd, {
+ stdout = function(line)
+ if line:find("Playing") then
+ self:emit_signal("playback_status", true)
+ capi.awesome.emit_signal("bling::playerctl::status", true)
+ else
+ self:emit_signal("playback_status", false)
+ capi.awesome.emit_signal("bling::playerctl::status", false)
+ end
+ end,
+ })
+end
+
+local function emit_player_volume(self)
+ local volume_cmd = self._private.cmd .. "volume -F"
+
+ awful.spawn.with_line_callback(volume_cmd, {
+ stdout = function(line)
+ self:emit_signal("volume", tonumber(line))
+ end,
+ })
+end
+
+local function emit_player_loop_status(self)
+ local loop_status_cmd = self._private.cmd .. "loop -F"
+
+ awful.spawn.with_line_callback(loop_status_cmd, {
+ stdout = function(line)
+ self._private.loop_status = line
+ self:emit_signal("loop_status", line:lower())
+ end,
+ })
+end
+
+local function emit_player_shuffle(self)
+ local shuffle_cmd = self._private.cmd .. "shuffle -F"
+
+ awful.spawn.with_line_callback(shuffle_cmd, {
+ stdout = function(line)
+ if line:find("On") then
+ self._private.shuffle = true
+ self:emit_signal("shuffle", true)
+ else
+ self._private.shuffle = false
+ self:emit_signal("shuffle", false)
+ end
+ end,
+ })
+end
+
+local function parse_args(self, args)
+ if args.player then
+ self._private.cmd = self._private.cmd .. "--player="
+
+ if type(args.player) == "string" then
+ self._private.cmd = self._private.cmd .. args.player .. " "
+ elseif type(args.player) == "table" then
+ for index, player in ipairs(args.player) do
+ self._private.cmd = self._private.cmd .. player
+ if index < #args.player then
+ self._private.cmd = self._private.cmd .. ","
+ else
+ self._private.cmd = self._private.cmd .. " "
+ end
+ end
+ end
+ end
+
+ if args.ignore then
+ self._private.cmd = self._private.cmd .. "--ignore-player="
+
+ if type(args.ignore) == "string" then
+ self._private.cmd = self._private.cmd .. args.ignore .. " "
+ elseif type(args.ignore) == "table" then
+ for index, player in ipairs(args.ignore) do
+ self._private.cmd = self._private.cmd .. player
+ if index < #args.ignore then
+ self._private.cmd = self._private.cmd .. ","
+ else
+ self._private.cmd = self._private.cmd .. " "
+ end
+ end
+ end
+ end
+end
+
+local function new(args)
+ args = args or {}
+
+ local ret = gobject{}
+ gtable.crush(ret, playerctl, true)
+
+ ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
+ ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35
+
+ ret._private = {}
+ ret._private.metadata_timer = nil
+ ret._private.cmd = "playerctl "
+ parse_args(ret, args)
+
+ emit_player_metadata(ret)
+ emit_player_position(ret)
+ emit_player_playback_status(ret)
+ emit_player_volume(ret)
+ emit_player_loop_status(ret)
+ emit_player_shuffle(ret)
+
+ return ret
+end
+
+function playerctl.mt:__call(...)
+ return new(...)
+end
+
+-- On startup instead of on playerctl object init to make it
+-- possible to have more than one of these running
+awful.spawn.with_shell("killall playerctl")
+
+return setmetatable(playerctl, playerctl.mt)
diff --git a/.config/awesome/bling/signal/playerctl/playerctl_lib.lua b/.config/awesome/bling/signal/playerctl/playerctl_lib.lua
new file mode 100755
index 0000000..1df1e1f
--- /dev/null
+++ b/.config/awesome/bling/signal/playerctl/playerctl_lib.lua
@@ -0,0 +1,560 @@
+-- Playerctl signals
+--
+-- Provides:
+-- metadata
+-- title (string)
+-- artist (string)
+-- album_path (string)
+-- album (string)
+-- new (bool)
+-- player_name (string)
+-- position
+-- interval_sec (number)
+-- length_sec (number)
+-- player_name (string)
+-- playback_status
+-- playing (boolean)
+-- player_name (string)
+-- seeked
+-- position (number)
+-- player_name (string)
+-- volume
+-- volume (number)
+-- player_name (string)
+-- loop_status
+-- loop_status (string)
+-- player_name (string)
+-- shuffle
+-- shuffle (boolean)
+-- player_name (string)
+-- exit
+-- player_name (string)
+-- no_players
+-- (No parameters)
+
+local awful = require("awful")
+local gobject = require("gears.object")
+local gtable = require("gears.table")
+local gtimer = require("gears.timer")
+local gstring = require("gears.string")
+local beautiful = require("beautiful")
+local helpers = require(tostring(...):match(".*bling") .. ".helpers")
+local setmetatable = setmetatable
+local ipairs = ipairs
+local pairs = pairs
+local type = type
+local capi = { awesome = awesome }
+
+local playerctl = { mt = {} }
+
+function playerctl:disable()
+ -- Restore default settings
+ self.ignore = {}
+ self.priority = {}
+ self.update_on_activity = true
+ self.interval = 1
+ self.debounce_delay = 0.35
+
+ -- Reset timers
+ self._private.manager = nil
+ self._private.metadata_timer:stop()
+ self._private.metadata_timer = nil
+ self._private.position_timer:stop()
+ self._private.position_timer = nil
+
+ -- Reset default values
+ self._private.last_position = -1
+ self._private.last_length = -1
+ self._private.last_player = nil
+ self._private.last_title = ""
+ self._private.last_artist = ""
+ self._private.last_artUrl = ""
+end
+
+function playerctl:pause(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:pause()
+ end
+end
+
+function playerctl:play(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:play()
+ end
+end
+
+function playerctl:stop(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:stop()
+ end
+end
+
+function playerctl:play_pause(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:play_pause()
+ end
+end
+
+function playerctl:previous(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:previous()
+ end
+end
+
+function playerctl:next(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:next()
+ end
+end
+
+function playerctl:set_loop_status(loop_status, player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:set_loop_status(loop_status)
+ end
+end
+
+function playerctl:cycle_loop_status(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ if player.loop_status == "NONE" then
+ player:set_loop_status("TRACK")
+ elseif player.loop_status == "TRACK" then
+ player:set_loop_status("PLAYLIST")
+ elseif player.loop_status == "PLAYLIST" then
+ player:set_loop_status("NONE")
+ end
+ end
+end
+
+function playerctl:set_position(position, player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:set_position(position * 1000000)
+ end
+end
+
+function playerctl:set_shuffle(shuffle, player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:set_shuffle(shuffle)
+ end
+end
+
+function playerctl:cycle_shuffle(player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:set_shuffle(not player.shuffle)
+ end
+end
+
+function playerctl:set_volume(volume, player)
+ player = player or self._private.manager.players[1]
+ if player then
+ player:set_volume(volume)
+ end
+end
+
+function playerctl:get_manager()
+ return self._private.manager
+end
+
+function playerctl:get_active_player()
+ return self._private.manager.players[1]
+end
+
+function playerctl:get_player_of_name(name)
+ for _, player in ipairs(self._private.manager.players[1]) do
+ if player.name == name then
+ return player
+ end
+ end
+
+ return nil
+end
+
+local function emit_metadata_signal(self, title, artist, artUrl, album, new, player_name)
+ title = gstring.xml_escape(title)
+ artist = gstring.xml_escape(artist)
+ album = gstring.xml_escape(album)
+
+ -- Spotify client doesn't report its art URL's correctly...
+ if player_name == "spotify" then
+ artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
+ end
+
+ if artUrl ~= "" then
+ local art_path = os.tmpname()
+ helpers.filesystem.save_image_async_curl(artUrl, art_path, function()
+ self:emit_signal("metadata", title, artist, art_path, album, new, player_name)
+ capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path, player_name)
+ end)
+ else
+ capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "", player_name)
+ self:emit_signal("metadata", title, artist, "", album, new, player_name)
+ end
+end
+
+local function metadata_cb(self, player, metadata)
+ if self.update_on_activity then
+ self._private.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 ""
+ local album = data["xesam:album"] or ""
+
+ if player == self._private.manager.players[1] then
+ self._private.active_player = player
+
+ -- Callback can be called even though values we care about haven't
+ -- changed, so check to see if they have
+ if
+ player ~= self._private.last_player
+ or title ~= self._private.last_title
+ or artist ~= self._private.last_artist
+ or artUrl ~= self._private.last_artUrl
+ then
+ if (title == "" and artist == "" and artUrl == "") then return end
+
+ if self._private.metadata_timer ~= nil and self._private.metadata_timer.started then
+ self._private.metadata_timer:stop()
+ end
+
+ self._private.metadata_timer = gtimer {
+ timeout = self.debounce_delay,
+ autostart = true,
+ single_shot = true,
+ callback = function()
+ emit_metadata_signal(self, title, artist, artUrl, album, true, player.player_name)
+ end
+ }
+
+ -- Re-sync with position timer when track changes
+ self._private.position_timer:again()
+ self._private.last_player = player
+ self._private.last_title = title
+ self._private.last_artist = artist
+ self._private.last_artUrl = artUrl
+ end
+ end
+end
+
+local function position_cb(self)
+ local player = self._private.manager.players[1]
+ if player then
+
+ local position = player:get_position() / 1000000
+ local length = (player.metadata.value["mpris:length"] or 0) / 1000000
+ if position ~= self._private.last_position or length ~= self._private.last_length then
+ capi.awesome.emit_signal("bling::playerctl::position", position, length, player.player_name)
+ self:emit_signal("position", position, length, player.player_name)
+ self._private.last_position = position
+ self._private.last_length = length
+ end
+ end
+end
+
+local function playback_status_cb(self, player, status)
+ if self.update_on_activity then
+ self._private.manager:move_player_to_top(player)
+ end
+
+ if player == self._private.manager.players[1] then
+ self._private.active_player = player
+
+ -- Reported as PLAYING, PAUSED, or STOPPED
+ if status == "PLAYING" then
+ self:emit_signal("playback_status", true, player.player_name)
+ capi.awesome.emit_signal("bling::playerctl::status", true, player.player_name)
+ else
+ self:emit_signal("playback_status", false, player.player_name)
+ capi.awesome.emit_signal("bling::playerctl::status", false, player.player_name)
+ end
+ end
+end
+
+local function seeked_cb(self, player, position)
+ if self.update_on_activity then
+ self._private.manager:move_player_to_top(player)
+ end
+
+ if player == self._private.manager.players[1] then
+ self._private.active_player = player
+ self:emit_signal("seeked", position / 1000000, player.player_name)
+ end
+end
+
+local function volume_cb(self, player, volume)
+ if self.update_on_activity then
+ self._private.manager:move_player_to_top(player)
+ end
+
+ if player == self._private.manager.players[1] then
+ self._private.active_player = player
+ self:emit_signal("volume", volume, player.player_name)
+ end
+end
+
+local function loop_status_cb(self, player, loop_status)
+ if self.update_on_activity then
+ self._private.manager:move_player_to_top(player)
+ end
+
+ if player == self._private.manager.players[1] then
+ self._private.active_player = player
+ self:emit_signal("loop_status", loop_status:lower(), player.player_name)
+ end
+end
+
+local function shuffle_cb(self, player, shuffle)
+ if self.update_on_activity then
+ self._private.manager:move_player_to_top(player)
+ end
+
+ if player == self._private.manager.players[1] then
+ self._private.active_player = player
+ self:emit_signal("shuffle", shuffle, player.player_name)
+ end
+end
+
+local function exit_cb(self, player)
+ if player == self._private.manager.players[1] then
+ self:emit_signal("exit", player.player_name)
+ end
+end
+
+-- Determine if player should be managed
+local function name_is_selected(self, name)
+ if self.ignore[name.name] then
+ return false
+ end
+
+ if #self.priority > 0 then
+ for _, arg in pairs(self.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(self, name)
+ if name_is_selected(self, name) then
+ local player = self._private.lgi_Playerctl.Player.new_from_name(name)
+ self._private.manager:manage_player(player)
+ player.on_metadata = function(player, metadata)
+ metadata_cb(self, player, metadata)
+ end
+ player.on_playback_status = function(player, playback_status)
+ playback_status_cb(self, player, playback_status)
+ end
+ player.on_seeked = function(player, position)
+ seeked_cb(self, player, position)
+ end
+ player.on_volume = function(player, volume)
+ volume_cb(self, player, volume)
+ end
+ player.on_loop_status = function(player, loop_status)
+ loop_status_cb(self, player, loop_status)
+ end
+ player.on_shuffle = function(player, shuffle_status)
+ shuffle_cb(self, player, shuffle_status)
+ end
+ player.on_exit = function(player, shuffle_status)
+ exit_cb(self, player)
+ end
+
+ -- Start position timer if its not already running
+ if not self._private.position_timer.started then
+ self._private.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(self, 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(self.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(self, a, b)
+ local player_a = self._private.lgi_Playerctl.Player(a)
+ local player_b = self._private.lgi_Playerctl.Player(b)
+ return player_compare_name(self, player_a.player_name, player_b.player_name)
+end
+
+local function get_current_player_info(self, player)
+ local title = player:get_title() or ""
+ local artist = player:get_artist() or ""
+ local artUrl = player:print_metadata_prop("mpris:artUrl") or ""
+ local album = player:get_album() or ""
+
+ emit_metadata_signal(self, title, artist, artUrl, album, false, player.player_name)
+ playback_status_cb(self, player, player.playback_status)
+ volume_cb(self, player, player.volume)
+ loop_status_cb(self, player, player.loop_status)
+ shuffle_cb(self, player, player.shuffle)
+end
+
+local function start_manager(self)
+ self._private.manager = self._private.lgi_Playerctl.PlayerManager()
+
+ if #self.priority > 0 then
+ self._private.manager:set_sort_func(function(a, b)
+ return player_compare(self, a, b)
+ end)
+ end
+
+ -- Timer to update track position at specified interval
+ self._private.position_timer = gtimer {
+ timeout = self.interval,
+ callback = function()
+ position_cb(self)
+ end,
+ }
+
+ -- Manage existing players on startup
+ for _, name in ipairs(self._private.manager.player_names) do
+ init_player(self, name)
+ end
+
+ if self._private.manager.players[1] then
+ get_current_player_info(self, self._private.manager.players[1])
+ end
+
+ local _self = self
+
+ -- Callback to manage new players
+ function self._private.manager:on_name_appeared(name)
+ init_player(_self, name)
+ end
+
+ function self._private.manager:on_player_appeared(player)
+ if player == self.players[1] then
+ _self._private.active_player = player
+ end
+ end
+
+ function self._private.manager:on_player_vanished(player)
+ if #self.players == 0 then
+ _self._private.metadata_timer:stop()
+ _self._private.position_timer:stop()
+ _self:emit_signal("no_players")
+ capi.awesome.emit_signal("bling::playerctl::no_players")
+ elseif player == _self._private.active_player then
+ _self._private.active_player = self.players[1]
+ get_current_player_info(_self, self.players[1])
+ end
+ end
+end
+
+local function parse_args(self, args)
+ self.ignore = {}
+ if type(args.ignore) == "string" then
+ self.ignore[args.ignore] = true
+ elseif type(args.ignore) == "table" then
+ for _, name in pairs(args.ignore) do
+ self.ignore[name] = true
+ end
+ end
+
+ self.priority = {}
+ if type(args.player) == "string" then
+ self.priority[1] = args.player
+ elseif type(args.player) == "table" then
+ self.priority = args.player
+ end
+end
+
+local function new(args)
+ args = args or {}
+
+ local ret = gobject{}
+ gtable.crush(ret, playerctl, true)
+
+ -- 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
+ ret.update_on_activity = args.update_on_activity or
+ beautiful.playerctl_update_on_activity or true
+ ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
+ ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35
+ parse_args(ret, args)
+
+ ret._private = {}
+
+ -- Metadata callback for title, artist, and album art
+ ret._private.last_player = nil
+ ret._private.last_title = ""
+ ret._private.last_artist = ""
+ ret._private.last_artUrl = ""
+
+ -- Track position callback
+ ret._private.last_position = -1
+ ret._private.last_length = -1
+
+ -- Grab playerctl library
+ ret._private.lgi_Playerctl = require("lgi").Playerctl
+ ret._private.manager = nil
+ ret._private.metadata_timer = nil
+ ret._private.position_timer = nil
+
+ -- Ensure main event loop has started before starting player manager
+ gtimer.delayed_call(function()
+ start_manager(ret)
+ end)
+
+ return ret
+end
+
+function playerctl.mt:__call(...)
+ return new(...)
+end
+
+return setmetatable(playerctl, playerctl.mt)
diff --git a/.config/awesome/bling/theme-var-template.lua b/.config/awesome/bling/theme-var-template.lua
new file mode 100755
index 0000000..13d0f5a
--- /dev/null
+++ b/.config/awesome/bling/theme-var-template.lua
@@ -0,0 +1,105 @@
+--[[ Bling theme variables template
+
+This file has all theme variables of the bling module.
+Every variable has a small comment on what it does.
+You might just want to copy that whole part into your theme.lua and start adjusting from there.
+
+--]]
+-- LuaFormatter off
+-- window swallowing
+theme.dont_swallow_classname_list = { "firefox", "Gimp" } -- list of class names that should not be swallowed
+theme.dont_swallow_filter_activated = true -- whether the filter above should be active
+
+-- flash focus
+theme.flash_focus_start_opacity = 0.6 -- the starting opacity
+theme.flash_focus_step = 0.01 -- the step of animation
+
+-- playerctl signal
+theme.playerctl_backend = "playerctl_cli" -- backend to use
+theme.playerctl_ignore = {} -- list of players to be ignored
+theme.playerctl_player = {} -- list of players to be used in priority order
+theme.playerctl_update_on_activity = true -- whether to prioritize the most recently active players or not
+theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl
+
+-- tabbed
+theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the focused tabbing container
+
+-- tabbar general
+theme.tabbar_disable = false -- disable the tab bar entirely
+theme.tabbar_ontop = false
+theme.tabbar_radius = 0 -- border radius of the tabbar
+theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
+theme.tabbar_font = "Sans 11" -- font of the tabbar
+theme.tabbar_size = 40 -- size of the tabbar
+theme.tabbar_position = "top" -- position of the tabbar
+theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar
+theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar
+theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar
+theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar
+theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive
+theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive
+theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive
+theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive
+
+-- mstab
+theme.mstab_bar_disable = false -- disable the tabbar
+theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients
+theme.mstab_dont_resize_slaves = false -- whether the tabbed stack windows should be smaller than the
+-- currently focused stack window (set it to true if you use
+-- transparent terminals. False if you use shadows on solid ones
+theme.mstab_bar_padding = "default" -- how much padding there should be between clients and your tabbar
+-- by default it will adjust based on your useless gaps.
+-- If you want a custom value. Set it to the number of pixels (int)
+theme.mstab_border_radius = 0 -- border radius of the tabbar
+theme.mstab_bar_height = 40 -- height of the tabbar
+theme.mstab_tabbar_position = "top" -- position of the tabbar (mstab currently does not support left,right)
+theme.mstab_tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
+-- defaults to the tabbar_style so only change if you want a
+-- different style for mstab and tabbed
+
+-- the following variables are currently only for the "modern" tabbar style
+theme.tabbar_color_close = "#f9929b" -- changes the color of the close button
+theme.tabbar_color_min = "#fbdf90" -- changes the color of the minimize button
+theme.tabbar_color_float = "#ccaced" -- changes the color of the float button
+
+-- tag preview widget
+theme.tag_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
+theme.tag_preview_client_border_radius = 0 -- Border radius of each client in the widget (With AA)
+theme.tag_preview_client_opacity = 0.5 -- Opacity of each client
+theme.tag_preview_client_bg = "#000000" -- The bg color of each client
+theme.tag_preview_client_border_color = "#ffffff" -- The border color of each client
+theme.tag_preview_client_border_width = 3 -- The border width of each client
+theme.tag_preview_widget_bg = "#000000" -- The bg color of the widget
+theme.tag_preview_widget_border_color = "#ffffff" -- The border color of the widget
+theme.tag_preview_widget_border_width = 3 -- The border width of the widget
+theme.tag_preview_widget_margin = 0 -- The margin of the widget
+
+-- task preview widget
+theme.task_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
+theme.task_preview_widget_bg = "#000000" -- The bg color of the widget
+theme.task_preview_widget_border_color = "#ffffff" -- The border color of the widget
+theme.task_preview_widget_border_width = 3 -- The border width of the widget
+theme.task_preview_widget_margin = 0 -- The margin of the widget
+
+-- window switcher
+theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget
+theme.window_switcher_widget_border_width = 3 -- The border width of the widget
+theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget
+theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget
+theme.window_switcher_clients_spacing = 20 -- The space between each client item
+theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text
+theme.window_switcher_client_width = 150 -- The width of one client widget
+theme.window_switcher_client_height = 250 -- The height of one client widget
+theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget
+theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget
+theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto"
+theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget
+theme.window_switcher_name_valign = "center" -- How to vertically align one clients title
+theme.window_switcher_name_forced_width = 200 -- The width of one title
+theme.window_switcher_name_font = "Sans 11" -- The font of all titles
+theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused
+theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused
+theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon
+theme.window_switcher_icon_width = 40 -- The width of one icon
+
+-- LuaFormatter on
diff --git a/.config/awesome/bling/widget/app_launcher/init.lua b/.config/awesome/bling/widget/app_launcher/init.lua
new file mode 100755
index 0000000..b74c2ab
--- /dev/null
+++ b/.config/awesome/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:lower(), text:lower(), 1, true) ~= nil or
+ self.search_commands and string.find(entry.commandline, text:lower(), 1, true) ~= 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
+
+ -- 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.type = args.type or "dock"
+ 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 = args.type,
+ 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/bling/widget/app_launcher/prompt.lua b/.config/awesome/bling/widget/app_launcher/prompt.lua
new file mode 100755
index 0000000..fae3b86
--- /dev/null
+++ b/.config/awesome/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/bling/widget/init.lua b/.config/awesome/bling/widget/init.lua
new file mode 100755
index 0000000..d3c6ebd
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbar/boxes.lua b/.config/awesome/bling/widget/tabbar/boxes.lua
new file mode 100755
index 0000000..720f420
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbar/default.lua b/.config/awesome/bling/widget/tabbar/default.lua
new file mode 100755
index 0000000..ad6b0b1
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbar/modern.lua b/.config/awesome/bling/widget/tabbar/modern.lua
new file mode 100755
index 0000000..5f48066
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbar/pure.lua b/.config/awesome/bling/widget/tabbar/pure.lua
new file mode 100755
index 0000000..5be82e5
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbed_misc/custom_tasklist.lua b/.config/awesome/bling/widget/tabbed_misc/custom_tasklist.lua
new file mode 100755
index 0000000..ac22377
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbed_misc/init.lua b/.config/awesome/bling/widget/tabbed_misc/init.lua
new file mode 100755
index 0000000..1de3fbb
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tabbed_misc/titlebar_indicator.lua b/.config/awesome/bling/widget/tabbed_misc/titlebar_indicator.lua
new file mode 100755
index 0000000..46959cd
--- /dev/null
+++ b/.config/awesome/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/bling/widget/tag_preview.lua b/.config/awesome/bling/widget/tag_preview.lua
new file mode 100755
index 0000000..2a181b4
--- /dev/null
+++ b/.config/awesome/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/bling/widget/task_preview.lua b/.config/awesome/bling/widget/task_preview.lua
new file mode 100755
index 0000000..3b54867
--- /dev/null
+++ b/.config/awesome/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/bling/widget/window_switcher.lua b/.config/awesome/bling/widget/window_switcher.lua
new file mode 100755
index 0000000..97dbeb1
--- /dev/null
+++ b/.config/awesome/bling/widget/window_switcher.lua
@@ -0,0 +1,461 @@
+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,
+ filterClients
+)
+ filterClients = filterClients or awful.widget.tasklist.filter.currenttags
+ local tasklist_widget = type == "thumbnail"
+ and awful.widget.tasklist({
+ screen = awful.screen.focused(),
+ filter = filterClients,
+ 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 = filterClients,
+ 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 cycleClientsByIdx = opts.cycleClientsByIdx or awful.client.focus.byidx
+ local filterClients = opts.filterClients or awful.widget.tasklist.filter.currenttags
+
+ 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()
+ cycleClientsByIdx(-1)
+ end,
+ }),
+
+ awful.button({
+ modifiers = { "Any" },
+ button = scroll_next_key,
+ on_press = function()
+ cycleClientsByIdx(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()
+ cycleClientsByIdx(1)
+ end,
+
+ [previous_key] = function()
+ cycleClientsByIdx(1)
+ end,
+ [next_key] = function()
+ cycleClientsByIdx(-1)
+ end,
+
+ [vim_previous_key] = function()
+ cycleClientsByIdx(1)
+ end,
+ [vim_next_key] = function()
+ cycleClientsByIdx(-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,
+ filterClients
+ )
+ window_switcher_box.screen = awful.screen.focused()
+ window_switcher_box.visible = true
+ end)
+end
+
+return { enable = enable }
diff --git a/.config/awesome/freedesktop/LICENSE b/.config/awesome/freedesktop/LICENSE
new file mode 100755
index 0000000..23cb790
--- /dev/null
+++ b/.config/awesome/freedesktop/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {description}
+ Copyright (C) {year} {fullname}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/.config/awesome/freedesktop/README.rst b/.config/awesome/freedesktop/README.rst
new file mode 100755
index 0000000..16ca579
--- /dev/null
+++ b/.config/awesome/freedesktop/README.rst
@@ -0,0 +1,51 @@
+Awesome-Freedesktop
+===================
+
+-------------------------------------------------------------------
+Freedesktop.org menu and desktop icons support for Awesome WM 4.x
+-------------------------------------------------------------------
+
+:First author: Antonio Terceiro
+:Maintainer: Luca CPZ
+:Version: git
+:License: GNU-GPL2_
+:Source: https://github.com/lcpz/awesome-freedesktop
+
+Description
+-----------
+
+A port of awesome-freedesktop_ to Awesome_ 4.x.
+
+See branches_ for previous versions.
+
+Since the introduction of Menubar_ as a core library to provide Freedesktop.org
+functionalities in Awesome, we can now avoid the dirty work by simply exploiting
+``menubar.utils``.
+
+At the moment, the menu is complete, while the desktop icons are rather simple. Our goal
+is to add the following features:
+
+- A better way to handle desktop icons path.
+- Ability to drag and line up icons.
+- Event-based signals, in particular:
+ - Updating trash icon according to its status.
+ - Dynamic update (no need to restart Awesome to see changes on the desktop).
+
+Screenshot
+----------
+
+.. image:: screenshot.png
+ :align: center
+ :alt: Showcase of Freedesktop.org support in Awesome, using Adwaita icons
+
+Installation and usage
+----------------------
+
+Read the wiki_.
+
+.. _GNU-GPL2: http://www.gnu.org/licenses/gpl-2.0.html
+.. _awesome-freedesktop: https://github.com/terceiro/awesome-freedesktop
+.. _Awesome: https://github.com/awesomeWM/awesome
+.. _branches: https://github.com/lcpz/awesome-freedesktop/branches
+.. _Menubar: https://github.com/awesomeWM/awesome/tree/master/lib/menubar
+.. _wiki: https://github.com/lcpz/awesome-freedesktop/wiki
diff --git a/.config/awesome/freedesktop/awesome-freedesktop-scm-1.rockspec b/.config/awesome/freedesktop/awesome-freedesktop-scm-1.rockspec
new file mode 100755
index 0000000..0dde608
--- /dev/null
+++ b/.config/awesome/freedesktop/awesome-freedesktop-scm-1.rockspec
@@ -0,0 +1,19 @@
+package = "awesome-freedesktop"
+version = "scm-1"
+source = {
+ url = "git+https://github.com/lcpz/awesome-freedesktop.git",
+ tag = "master",
+}
+description = {
+ summary = "Freedesktop.org menu and desktop icons support for Awesome WM",
+ homepage = "https://github.com/lcpz/awesome-freedesktop",
+ license = "GPL-2.0"
+}
+dependencies = {
+ "lua >= 5.3",
+}
+supported_platforms = { "linux" }
+build = {
+ type = "builtin",
+ modules = { freedesktop = "init.lua" }
+}
diff --git a/.config/awesome/freedesktop/desktop.lua b/.config/awesome/freedesktop/desktop.lua
new file mode 100755
index 0000000..676ebdb
--- /dev/null
+++ b/.config/awesome/freedesktop/desktop.lua
@@ -0,0 +1,259 @@
+--[[
+
+ Awesome-Freedesktop
+ Freedesktop.org compliant desktop entries and menu
+
+ Desktop section
+
+ Licensed under GNU General Public License v2
+ * (c) 2016, Luke Bonham
+ * (c) 2009-2015, Antonio Terceiro
+
+--]]
+
+local awful = require("awful")
+local theme = require("beautiful")
+local utils = require("menubar.utils")
+local wibox = require("wibox")
+
+local io = io
+local ipairs = ipairs
+local mouse = mouse
+local os = os
+local string = string
+local screen = screen
+local table = table
+
+-- Desktop icons
+-- freedesktop.desktop
+local desktop = {
+ -- Default desktop basic icons
+ baseicons = {
+ [1] = {
+ label = "This PC",
+ icon = "computer",
+ onclick = "computer://"
+ },
+ [2] = {
+ label = "Home",
+ icon = "user-home",
+ onclick = os.getenv("HOME")
+ },
+ [3] = {
+ label = "Trash",
+ icon = "user-trash",
+ onclick = "trash://"
+ }
+ },
+ -- Default parameters
+ iconsize = { width = 48, height = 48 },
+ labelsize = { width = 140, height = 20 },
+ margin = { x = 20, y = 20 },
+}
+
+-- MIME types list
+local mime_types = {}
+
+-- Icons positioning
+desktop.current_pos = {}
+
+-- @return iterator on input pipe
+local function pipelines(...)
+ local f = assert(io.popen(...))
+ return function ()
+ local data = f:read()
+ if data == nil then f:close() end
+ return data
+ end
+end
+
+-- Adds an icon to desktop
+-- @param args settings from desktop.add_icons
+-- @param label icon string label
+-- @param icon icon string file path
+-- @param onclick function to execute on click
+function desktop.add_single_icon(args, label, icon, onclick)
+ local s = args.screen
+ local dcp = desktop.current_pos
+
+ -- define icon dimensions and position
+ if not dcp[s] then
+ dcp[s] = { x = (screen[s].geometry.x + args.iconsize.width + args.margin.x), y = screen[s].geometry.y + 20 + args.margin.y }
+ end
+
+ local tot_height = (icon and args.iconsize.height or 0) + (label and args.labelsize.height or 0)
+ if tot_height == 0 then return end
+
+ if dcp[s].y + tot_height > screen[s].geometry.y + screen[s].geometry.height - 20 - args.margin.y then
+ dcp[s].x = dcp[s].x + args.labelsize.width + args.iconsize.width + args.margin.x
+ dcp[s].y = 20 + args.margin.y
+ end
+
+ local common = { screen = s, bg = "#00000000", visible = true, type = "desktop" }
+
+ -- create icon container
+ if icon then
+ common.width = args.iconsize.width
+ common.height = args.iconsize.height
+ common.x = dcp[s].x
+ common.y = dcp[s].y
+
+ icon = wibox.widget {
+ image = icon,
+ resize = false,
+ widget = wibox.widget.imagebox
+ }
+
+ icon:buttons(awful.button({ }, 1, nil, onclick))
+
+ icon_container = wibox(common)
+ icon_container:set_widget(icon)
+
+ dcp[s].y = dcp[s].y + args.iconsize.height + 5
+ end
+
+ -- create label container
+ if label then
+ common.width = args.labelsize.width
+ common.height = args.labelsize.height
+ common.x = dcp[s].x - (args.labelsize.width/2) + args.iconsize.width/2
+ common.y = dcp[s].y
+
+ caption = wibox.widget {
+ text = label,
+ align = "center",
+ forced_width = common.width,
+ forced_height = common.height,
+ ellipsize = "middle",
+ widget = wibox.widget.textbox
+ }
+
+ caption:buttons(awful.button({ }, 1, onclick))
+ caption_container = wibox(common)
+ caption_container:set_widget(caption)
+ end
+
+ dcp[s].y = dcp[s].y + args.labelsize.height + args.margin.y
+
+ desktop.current_pos = dcp
+
+ return dcp
+end
+
+-- Adds base icons (This PC, Trash, etc) to desktop
+-- @param args settings from desktop.add_icons
+function desktop.add_base_icons(args)
+ for _,base in ipairs(args.baseicons) do
+ desktop.add_single_icon(args, base.label, utils.lookup_icon(base.icon), function()
+ awful.spawn(string.format("%s '%s'", args.open_with, base.onclick))
+ end)
+ end
+end
+
+-- Looks up a suitable icon for filename
+-- @param filename string file name
+-- @return icon file path (string)
+function desktop.lookup_file_icon(filename)
+ -- load system MIME types
+ if #mime_types == 0 then
+ for line in io.lines("/etc/mime.types") do
+ if not line:find("^#") then
+ local parsed = {}
+ for w in line:gmatch("[^%s]+") do
+ table.insert(parsed, w)
+ end
+ if #parsed > 1 then
+ for i = 2, #parsed do
+ mime_types[parsed[i]] = parsed[1]:gsub("/", "-")
+ end
+ end
+ end
+ end
+ end
+
+ -- try to search a possible icon among standards
+ local extension = filename:match("%a+$")
+ local mime = mime_types[extension] or ""
+ local mime_family = mime:match("^%a+") or ""
+
+ local possible_filenames = {
+ mime, "gnome-mime-" .. mime,
+ mime_family, "gnome-mime-" .. mime_family,
+ extension
+ }
+
+ for i, filename in ipairs(possible_filenames) do
+ local icon = utils.lookup_icon(filename)
+ if icon then return icon end
+ end
+
+ -- if we don"t find ad icon, then pretend is a plain text file
+ return utils.lookup_icon("text-x-generic")
+end
+
+-- Parse subdirectories and files list from input directory
+-- @input dir directory to parse (string)
+-- @return files table with found entries
+function desktop.parse_dirs_and_files(dir)
+ local files = {}
+ local paths = pipelines('find '..dir..' -maxdepth 1 -type d |sort|tail -n +1')
+ for path in paths do
+ if path:match("[^/]+$") then
+ local file = {}
+ file.filename = path:match("[^/]+$")
+ file.path = path
+ file.show = true
+ file.icon = utils.lookup_icon("folder")
+ table.insert(files, file)
+ end
+ end
+ local paths = pipelines('find '..dir..' -maxdepth 1 -type f')
+ for path in paths do
+ if not path:find("%.desktop$") then
+ local file = {}
+ file.filename = path:match("[^/]+$")
+ file.path = path
+ file.show = true
+ file.icon = desktop.lookup_file_icon(file.filename)
+ table.insert(files, file)
+ end
+ end
+ return files
+end
+
+-- Adds subdirectories and files icons from args.dir
+-- @param args settings from desktop.add_icons
+function desktop.add_dirs_and_files_icons(args)
+ for _, file in ipairs(desktop.parse_dirs_and_files(args.dir)) do
+ if file.show then
+ local label = args.showlabels and file.filename or nil
+ local onclick = function () awful.spawn(string.format("%s '%s'", args.open_with, file.path)) end
+ desktop.add_single_icon(args, label, file.icon, onclick)
+ end
+ end
+end
+
+-- Main function, adds base, directory and files icons
+-- @param args user defined settings, with fallback on defaults
+function desktop.add_icons(args)
+ args = args or {}
+ args.screen = args.screen or mouse.screen
+ args.dir = args.dir or os.getenv("HOME") .. "/Desktop"
+ args.showlabels = args.showlabel or true
+ args.open_with = args.open_with or "xdg_open"
+ args.baseicons = args.baseicons or desktop.baseicons
+ args.iconsize = args.iconsize or desktop.iconsize
+ args.labelsize = args.labelsize or desktop.labelsize
+ args.margin = args.margin or desktop.margin
+
+ -- trying to fallback on Adwaita if theme.icon_theme is not defined
+ -- if Adwaita is missing too, no icons will be shown
+ if not theme.icon_theme then
+ theme.icon_theme = args.icon_theme or "Adwaita"
+ end
+
+ desktop.add_base_icons(args)
+ desktop.add_dirs_and_files_icons(args)
+end
+
+return desktop
diff --git a/.config/awesome/freedesktop/init.lua b/.config/awesome/freedesktop/init.lua
new file mode 100755
index 0000000..9350b44
--- /dev/null
+++ b/.config/awesome/freedesktop/init.lua
@@ -0,0 +1,15 @@
+--[[
+
+ Awesome-Freedesktop
+ Freedesktop.org compliant desktop entries and menu
+
+ Licensed under GNU General Public License v2
+ * (c) 2016, Luke Bonham
+ * (c) 2009-2015, Antonio Terceiro
+
+--]]
+
+return {
+ desktop = require("freedesktop.desktop"),
+ menu = require("freedesktop.menu")
+}
diff --git a/.config/awesome/freedesktop/menu.lua b/.config/awesome/freedesktop/menu.lua
new file mode 100755
index 0000000..fed6e6b
--- /dev/null
+++ b/.config/awesome/freedesktop/menu.lua
@@ -0,0 +1,121 @@
+
+--[[
+
+ Awesome-Freedesktop
+ Freedesktop.org compliant desktop entries and menu
+
+ Menu section
+
+ Licensed under GNU General Public License v2
+ * (c) 2016, Luke Bonham
+ * (c) 2014, Harvey Mittens
+
+--]]
+
+local Gio = require("lgi").Gio
+local awful_menu = require("awful.menu")
+local menu_gen = require("menubar.menu_gen")
+local menu_utils = require("menubar.utils")
+
+local io, pairs, string, table, os = io, pairs, string, table, os
+
+-- Expecting a wm_name of awesome omits too many applications and tools
+menu_utils.wm_name = ""
+
+-- Menu
+-- freedesktop.menu
+local menu = {}
+
+-- Check if a path is a directory.
+-- @tparam string path The directory path
+-- @treturn boolean True if path exists and is a directory
+function menu.is_dir(path)
+ return Gio.File.new_for_path(path):query_file_type({}) == "DIRECTORY"
+end
+
+-- Remove non existent paths in order to avoid issues
+local existent_paths = {}
+for k,v in pairs(menu_gen.all_menu_dirs) do
+ if menu.is_dir(v) then
+ table.insert(existent_paths, v)
+ end
+end
+menu_gen.all_menu_dirs = existent_paths
+
+-- Determines whether an table includes a certain element
+-- @param tab a given table
+-- @param val the element to search for
+-- @return true if the given string is found within the search table; otherwise, false if not
+function menu.has_value (tab, val)
+ for index, value in pairs(tab) do
+ if val:find(value) then
+ return true
+ end
+ end
+ return false
+end
+
+-- Use MenuBar parsing utils to build a menu for Awesome
+-- @return awful.menu
+function menu.build(args)
+ local args = args or {}
+ local before = args.before or {}
+ local after = args.after or {}
+ local skip_items = args.skip_items or {}
+ local sub_menu = args.sub_menu or false
+
+ local result = {}
+ local _menu = awful_menu({ items = before })
+
+ menu_gen.generate(function(entries)
+ -- Add category icons
+ for k, v in pairs(menu_gen.all_categories) do
+ table.insert(result, { k, {}, v.icon })
+ end
+
+ -- Get items table
+ for k, v in pairs(entries) do
+ for _, cat in pairs(result) do
+ if cat[1] == v.category then
+ if not menu.has_value(skip_items, v.name) then
+ table.insert(cat[2], { v.name, v.cmdline, v.icon })
+ end
+ break
+ end
+ end
+ end
+
+ -- Cleanup things a bit
+ for i = #result, 1, -1 do
+ local v = result[i]
+ if #v[2] == 0 then
+ -- Remove unused categories
+ table.remove(result, i)
+ else
+ --Sort entries alphabetically (by name)
+ table.sort(v[2], function (a, b) return string.byte(a[1]) < string.byte(b[1]) end)
+ -- Replace category name with nice name
+ v[1] = menu_gen.all_categories[v[1]].name
+ end
+ end
+
+ -- Sort categories alphabetically also
+ table.sort(result, function(a, b) return string.byte(a[1]) < string.byte(b[1]) end)
+
+ -- Add menu item to hold the generated menu
+ if sub_menu then
+ result = {{sub_menu, result}}
+ end
+
+ -- Add items to menu
+ for _, v in pairs(result) do _menu:add(v) end
+ for _, v in pairs(after) do _menu:add(v) end
+ end)
+
+ -- Hold the menu in the module
+ menu.menu = _menu
+
+ return _menu
+end
+
+return menu
diff --git a/.config/awesome/freedesktop/screenshot.png b/.config/awesome/freedesktop/screenshot.png
new file mode 100755
index 0000000..bf254ac
Binary files /dev/null and b/.config/awesome/freedesktop/screenshot.png differ
diff --git a/.config/awesome/lain/.github/workflows/main.yml b/.config/awesome/lain/.github/workflows/main.yml
new file mode 100755
index 0000000..37957f6
--- /dev/null
+++ b/.config/awesome/lain/.github/workflows/main.yml
@@ -0,0 +1,17 @@
+name: Lain
+
+on: [push]
+
+permissions:
+ contents: read
+
+jobs:
+ linting:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo "Running tests triggered by a ${{ github.event_name }} event."
+ - run: echo "Testing ${{ github.ref }} from ${{ github.repository }} on ${{ runner.os }}"
+ - name: Check out code
+ uses: actions/checkout@v3
+ - name: Run tests
+ uses: lunarmodules/luacheck@v0
diff --git a/.config/awesome/lain/.gitmodules b/.config/awesome/lain/.gitmodules
new file mode 100755
index 0000000..d95bcb5
--- /dev/null
+++ b/.config/awesome/lain/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lain.wiki"]
+ path = wiki
+ url = https://github.com/lcpz/lain.wiki.git
diff --git a/.config/awesome/lain/.luacheckrc b/.config/awesome/lain/.luacheckrc
new file mode 100755
index 0000000..9944223
--- /dev/null
+++ b/.config/awesome/lain/.luacheckrc
@@ -0,0 +1,27 @@
+-- Only allow symbols available in all Lua versions
+std = "min"
+
+allow_defined = true
+max_line_length = false
+cache = true
+
+-- Global objects defined by the C code
+read_globals = {
+ "awesome",
+ "mousegrabber",
+ "table.unpack",
+ "unpack",
+ "utf8"
+}
+
+globals = {
+ "client",
+ "mouse",
+ "root",
+ "screen"
+}
+
+-- https://luacheck.readthedocs.io/en/stable/warnings.html
+ignore = {
+ "131"
+}
diff --git a/.config/awesome/lain/ISSUE_TEMPLATE.md b/.config/awesome/lain/ISSUE_TEMPLATE.md
new file mode 100755
index 0000000..e9dcc0b
--- /dev/null
+++ b/.config/awesome/lain/ISSUE_TEMPLATE.md
@@ -0,0 +1,33 @@
+# Please, read me!
+
+So that I can help you quickly and without having to redirect you here.
+
+# If you have an issue
+
+**Please read the [wiki](https://github.com/lcpz/lain/wiki) and search the [Issues section](https://github.com/lcpz/lain/issues) first.**
+
+If you can't find a solution there, then go ahead and provide:
+
+* output of `awesome -v` and `lua -v`
+* expected behavior and actual behavior
+* steps to reproduce the problem
+* X error log
+
+# How to provide X error log
+
+There are two ways:
+
+* (Physically) Restart X like this:
+ ```shell
+ startx -- -keeptty -nolisten tcp > $HOME/.xorg.log 2>&1
+ ```
+ the error log will be output into `$HOME/.xorg.log`.
+
+* (Virtually) Use [Xephyr](https://wikipedia.org/wiki/Xephyr):
+ ```shell
+ # set screen size as you like
+ Xephyr :1 -screen 1280x800 2> stdout.txt & DISPLAY=:1 awesome
+ ```
+ the error log will be output in the file `stdout.txt`.
+
+Before reporting, read the log and see if you can solve it yourself.
diff --git a/.config/awesome/lain/LICENSE b/.config/awesome/lain/LICENSE
new file mode 100755
index 0000000..23cb790
--- /dev/null
+++ b/.config/awesome/lain/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {description}
+ Copyright (C) {year} {fullname}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/.config/awesome/lain/README.rst b/.config/awesome/lain/README.rst
new file mode 100755
index 0000000..616c7cd
--- /dev/null
+++ b/.config/awesome/lain/README.rst
@@ -0,0 +1,40 @@
+Lain
+====
+
+.. image:: https://github.com/lcpz/lain/actions/workflows/main.yml/badge.svg
+
+-------------------------------------------------
+Layouts, widgets and utilities for Awesome WM 4.x
+-------------------------------------------------
+
+:Author: Luca CPZ
+:Version: git
+:License: GNU-GPL2_
+:Source: https://github.com/lcpz/lain
+
+Description
+-----------
+
+Successor of awesome-vain_, this module provides alternative layouts, asynchronous widgets and utility functions for Awesome_.
+
+Contributions
+-------------
+
+Constructive criticism and suggestions are welcome.
+
+If you want to create a pull request, make sure that:
+
+- Your code fits with the general style of the module. In particular, you should use the same indentation pattern that the code uses, and also avoid adding space at the ends of lines.
+
+- Your code its easy to understand, maintainable, and modularized. You should also avoid code duplication wherever possible by adding functions to or using lain.helpers_. If something is unclear, or you can not write it in such a way that it will be clear, explain it with a comment.
+
+- You test your changes before submitting to make sure that your code works and does not break other parts of the module.
+
+- You update ``wiki`` submodule with a thorough section, if necessary.
+
+Contributed widgets have to be put in ``widget/contrib``.
+
+.. _GNU-GPL2: http://www.gnu.org/licenses/gpl-2.0.html
+.. _awesome-vain: https://github.com/vain/awesome-vain
+.. _Awesome: https://github.com/awesomeWM/awesome
+.. _lain.helpers: https://github.com/lcpz/lain/blob/master/helpers.lua
diff --git a/.config/awesome/lain/helpers.lua b/.config/awesome/lain/helpers.lua
new file mode 100755
index 0000000..ef9e08b
--- /dev/null
+++ b/.config/awesome/lain/helpers.lua
@@ -0,0 +1,203 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+
+--]]
+
+local spawn = require("awful.spawn")
+local timer = require("gears.timer")
+local debug = require("debug")
+local io = { lines = io.lines,
+ open = io.open }
+local pairs = pairs
+local rawget = rawget
+local tsort = table.sort
+local unpack = unpack or table.unpack -- lua 5.1 retro-compatibility
+
+-- Lain helper functions for internal use
+-- lain.helpers
+local helpers = {}
+
+helpers.lain_dir = debug.getinfo(1, 'S').source:match[[^@(.*/).*$]]
+helpers.icons_dir = helpers.lain_dir .. 'icons/'
+helpers.scripts_dir = helpers.lain_dir .. 'scripts/'
+
+-- {{{ Modules loader
+
+function helpers.wrequire(t, k)
+ return rawget(t, k) or require(t._NAME .. '.' .. k)
+end
+
+-- }}}
+
+-- {{{ File operations
+
+-- check if the file exists and is readable
+function helpers.file_exists(path)
+ local file = io.open(path, "rb")
+ if file then file:close() end
+ return file ~= nil
+end
+
+-- get a table with all lines from a file
+function helpers.lines_from(path)
+ local lines = {}
+ for line in io.lines(path) do
+ lines[#lines + 1] = line
+ end
+ return lines
+end
+
+-- get a table with all lines from a file matching regexp
+function helpers.lines_match(regexp, path)
+ local lines = {}
+ for line in io.lines(path) do
+ if string.match(line, regexp) then
+ lines[#lines + 1] = line
+ end
+ end
+ return lines
+end
+
+-- get first line of a file
+function helpers.first_line(path)
+ local file, first = io.open(path, "rb"), nil
+ if file then
+ first = file:read("*l")
+ file:close()
+ end
+ return first
+end
+
+-- get first non empty line from a file
+function helpers.first_nonempty_line(path)
+ for line in io.lines(path) do
+ if #line then return line end
+ end
+ return nil
+end
+
+-- }}}
+
+-- {{{ Timer maker
+
+helpers.timer_table = {}
+
+function helpers.newtimer(name, timeout, fun, nostart, stoppable)
+ if not name or #name == 0 then return end
+ name = (stoppable and name) or timeout
+ if not helpers.timer_table[name] then
+ helpers.timer_table[name] = timer({ timeout = timeout })
+ helpers.timer_table[name]:start()
+ end
+ helpers.timer_table[name]:connect_signal("timeout", fun)
+ if not nostart then
+ helpers.timer_table[name]:emit_signal("timeout")
+ end
+ return stoppable and helpers.timer_table[name]
+end
+
+-- }}}
+
+-- {{{ Pipe operations
+
+-- run a command and execute a function on its output (asynchronous pipe)
+-- @param cmd the input command
+-- @param callback function to execute on cmd output
+-- @return cmd PID
+function helpers.async(cmd, callback)
+ return spawn.easy_async(cmd,
+ function (stdout, _, _, exit_code)
+ callback(stdout, exit_code)
+ end)
+end
+
+-- like above, but call spawn.easy_async with a shell
+function helpers.async_with_shell(cmd, callback)
+ return spawn.easy_async_with_shell(cmd,
+ function (stdout, _, _, exit_code)
+ callback(stdout, exit_code)
+ end)
+end
+
+-- run a command and execute a function on its output line by line
+function helpers.line_callback(cmd, callback)
+ return spawn.with_line_callback(cmd, {
+ stdout = function (line)
+ callback(line)
+ end,
+ })
+end
+
+-- }}}
+
+-- {{{ A map utility
+
+helpers.map_table = {}
+
+function helpers.set_map(element, value)
+ helpers.map_table[element] = value
+end
+
+function helpers.get_map(element)
+ return helpers.map_table[element]
+end
+
+-- }}}
+
+-- {{{ Misc
+
+-- check if an element exist on a table
+function helpers.element_in_table(element, tbl)
+ for _, i in pairs(tbl) do
+ if i == element then
+ return true
+ end
+ end
+ return false
+end
+
+-- iterate over table of records sorted by keys
+function helpers.spairs(t)
+ -- collect the keys
+ local keys = {}
+ for k in pairs(t) do keys[#keys+1] = k end
+
+ tsort(keys)
+
+ -- return the iterator function
+ local i = 0
+ return function()
+ i = i + 1
+ if keys[i] then
+ return keys[i], t[keys[i]]
+ end
+ end
+end
+
+-- create the partition of singletons of a given set
+-- example: the trivial partition set of {a, b, c}, is {{a}, {b}, {c}}
+function helpers.trivial_partition_set(set)
+ local ss = {}
+ for _,e in pairs(set) do
+ ss[#ss+1] = {e}
+ end
+ return ss
+end
+
+-- create the powerset of a given set
+function helpers.powerset(s)
+ if not s then return {} end
+ local t = {{}}
+ for i = 1, #s do
+ for j = 1, #t do
+ t[#t+1] = {s[i],unpack(t[j])}
+ end
+ end
+ return t
+end
+
+-- }}}
+
+return helpers
diff --git a/.config/awesome/lain/icons/cal/black/1.png b/.config/awesome/lain/icons/cal/black/1.png
new file mode 100755
index 0000000..d2fb62e
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/1.png differ
diff --git a/.config/awesome/lain/icons/cal/black/10.png b/.config/awesome/lain/icons/cal/black/10.png
new file mode 100755
index 0000000..507b079
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/10.png differ
diff --git a/.config/awesome/lain/icons/cal/black/11.png b/.config/awesome/lain/icons/cal/black/11.png
new file mode 100755
index 0000000..336141b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/11.png differ
diff --git a/.config/awesome/lain/icons/cal/black/12.png b/.config/awesome/lain/icons/cal/black/12.png
new file mode 100755
index 0000000..c589729
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/12.png differ
diff --git a/.config/awesome/lain/icons/cal/black/13.png b/.config/awesome/lain/icons/cal/black/13.png
new file mode 100755
index 0000000..377518b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/13.png differ
diff --git a/.config/awesome/lain/icons/cal/black/14.png b/.config/awesome/lain/icons/cal/black/14.png
new file mode 100755
index 0000000..6f4a9fe
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/14.png differ
diff --git a/.config/awesome/lain/icons/cal/black/15.png b/.config/awesome/lain/icons/cal/black/15.png
new file mode 100755
index 0000000..1a271c1
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/15.png differ
diff --git a/.config/awesome/lain/icons/cal/black/16.png b/.config/awesome/lain/icons/cal/black/16.png
new file mode 100755
index 0000000..5e65835
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/16.png differ
diff --git a/.config/awesome/lain/icons/cal/black/17.png b/.config/awesome/lain/icons/cal/black/17.png
new file mode 100755
index 0000000..f3fa0a9
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/17.png differ
diff --git a/.config/awesome/lain/icons/cal/black/18.png b/.config/awesome/lain/icons/cal/black/18.png
new file mode 100755
index 0000000..7acb37a
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/18.png differ
diff --git a/.config/awesome/lain/icons/cal/black/19.png b/.config/awesome/lain/icons/cal/black/19.png
new file mode 100755
index 0000000..a557957
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/19.png differ
diff --git a/.config/awesome/lain/icons/cal/black/2.png b/.config/awesome/lain/icons/cal/black/2.png
new file mode 100755
index 0000000..17b33e0
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/2.png differ
diff --git a/.config/awesome/lain/icons/cal/black/20.png b/.config/awesome/lain/icons/cal/black/20.png
new file mode 100755
index 0000000..558d111
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/20.png differ
diff --git a/.config/awesome/lain/icons/cal/black/21.png b/.config/awesome/lain/icons/cal/black/21.png
new file mode 100755
index 0000000..0bbedc8
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/21.png differ
diff --git a/.config/awesome/lain/icons/cal/black/22.png b/.config/awesome/lain/icons/cal/black/22.png
new file mode 100755
index 0000000..762d262
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/22.png differ
diff --git a/.config/awesome/lain/icons/cal/black/23.png b/.config/awesome/lain/icons/cal/black/23.png
new file mode 100755
index 0000000..a39dcee
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/23.png differ
diff --git a/.config/awesome/lain/icons/cal/black/24.png b/.config/awesome/lain/icons/cal/black/24.png
new file mode 100755
index 0000000..c00dbca
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/24.png differ
diff --git a/.config/awesome/lain/icons/cal/black/25.png b/.config/awesome/lain/icons/cal/black/25.png
new file mode 100755
index 0000000..dc9243c
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/25.png differ
diff --git a/.config/awesome/lain/icons/cal/black/26.png b/.config/awesome/lain/icons/cal/black/26.png
new file mode 100755
index 0000000..50bb182
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/26.png differ
diff --git a/.config/awesome/lain/icons/cal/black/27.png b/.config/awesome/lain/icons/cal/black/27.png
new file mode 100755
index 0000000..0fbf9fc
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/27.png differ
diff --git a/.config/awesome/lain/icons/cal/black/28.png b/.config/awesome/lain/icons/cal/black/28.png
new file mode 100755
index 0000000..def6ab2
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/28.png differ
diff --git a/.config/awesome/lain/icons/cal/black/29.png b/.config/awesome/lain/icons/cal/black/29.png
new file mode 100755
index 0000000..531923c
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/29.png differ
diff --git a/.config/awesome/lain/icons/cal/black/3.png b/.config/awesome/lain/icons/cal/black/3.png
new file mode 100755
index 0000000..98b552d
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/3.png differ
diff --git a/.config/awesome/lain/icons/cal/black/30.png b/.config/awesome/lain/icons/cal/black/30.png
new file mode 100755
index 0000000..ca58151
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/30.png differ
diff --git a/.config/awesome/lain/icons/cal/black/31.png b/.config/awesome/lain/icons/cal/black/31.png
new file mode 100755
index 0000000..6e8da21
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/31.png differ
diff --git a/.config/awesome/lain/icons/cal/black/4.png b/.config/awesome/lain/icons/cal/black/4.png
new file mode 100755
index 0000000..4335979
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/4.png differ
diff --git a/.config/awesome/lain/icons/cal/black/5.png b/.config/awesome/lain/icons/cal/black/5.png
new file mode 100755
index 0000000..576ec11
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/5.png differ
diff --git a/.config/awesome/lain/icons/cal/black/6.png b/.config/awesome/lain/icons/cal/black/6.png
new file mode 100755
index 0000000..56fa8ab
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/6.png differ
diff --git a/.config/awesome/lain/icons/cal/black/7.png b/.config/awesome/lain/icons/cal/black/7.png
new file mode 100755
index 0000000..7c90b3a
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/7.png differ
diff --git a/.config/awesome/lain/icons/cal/black/8.png b/.config/awesome/lain/icons/cal/black/8.png
new file mode 100755
index 0000000..9d1f28e
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/8.png differ
diff --git a/.config/awesome/lain/icons/cal/black/9.png b/.config/awesome/lain/icons/cal/black/9.png
new file mode 100755
index 0000000..00d0933
Binary files /dev/null and b/.config/awesome/lain/icons/cal/black/9.png differ
diff --git a/.config/awesome/lain/icons/cal/white/1.png b/.config/awesome/lain/icons/cal/white/1.png
new file mode 100755
index 0000000..a0faa20
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/1.png differ
diff --git a/.config/awesome/lain/icons/cal/white/10.png b/.config/awesome/lain/icons/cal/white/10.png
new file mode 100755
index 0000000..7d9343b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/10.png differ
diff --git a/.config/awesome/lain/icons/cal/white/11.png b/.config/awesome/lain/icons/cal/white/11.png
new file mode 100755
index 0000000..7af5e99
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/11.png differ
diff --git a/.config/awesome/lain/icons/cal/white/12.png b/.config/awesome/lain/icons/cal/white/12.png
new file mode 100755
index 0000000..b164f85
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/12.png differ
diff --git a/.config/awesome/lain/icons/cal/white/13.png b/.config/awesome/lain/icons/cal/white/13.png
new file mode 100755
index 0000000..fef74f3
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/13.png differ
diff --git a/.config/awesome/lain/icons/cal/white/14.png b/.config/awesome/lain/icons/cal/white/14.png
new file mode 100755
index 0000000..d747a6b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/14.png differ
diff --git a/.config/awesome/lain/icons/cal/white/15.png b/.config/awesome/lain/icons/cal/white/15.png
new file mode 100755
index 0000000..64418a6
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/15.png differ
diff --git a/.config/awesome/lain/icons/cal/white/16.png b/.config/awesome/lain/icons/cal/white/16.png
new file mode 100755
index 0000000..8b86700
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/16.png differ
diff --git a/.config/awesome/lain/icons/cal/white/17.png b/.config/awesome/lain/icons/cal/white/17.png
new file mode 100755
index 0000000..033b5ff
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/17.png differ
diff --git a/.config/awesome/lain/icons/cal/white/18.png b/.config/awesome/lain/icons/cal/white/18.png
new file mode 100755
index 0000000..0cf1c24
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/18.png differ
diff --git a/.config/awesome/lain/icons/cal/white/19.png b/.config/awesome/lain/icons/cal/white/19.png
new file mode 100755
index 0000000..bfd3530
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/19.png differ
diff --git a/.config/awesome/lain/icons/cal/white/2.png b/.config/awesome/lain/icons/cal/white/2.png
new file mode 100755
index 0000000..e7f3fa4
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/2.png differ
diff --git a/.config/awesome/lain/icons/cal/white/20.png b/.config/awesome/lain/icons/cal/white/20.png
new file mode 100755
index 0000000..9a5a1fb
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/20.png differ
diff --git a/.config/awesome/lain/icons/cal/white/21.png b/.config/awesome/lain/icons/cal/white/21.png
new file mode 100755
index 0000000..266ab9f
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/21.png differ
diff --git a/.config/awesome/lain/icons/cal/white/22.png b/.config/awesome/lain/icons/cal/white/22.png
new file mode 100755
index 0000000..f486289
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/22.png differ
diff --git a/.config/awesome/lain/icons/cal/white/23.png b/.config/awesome/lain/icons/cal/white/23.png
new file mode 100755
index 0000000..244dceb
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/23.png differ
diff --git a/.config/awesome/lain/icons/cal/white/24.png b/.config/awesome/lain/icons/cal/white/24.png
new file mode 100755
index 0000000..0ce1c75
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/24.png differ
diff --git a/.config/awesome/lain/icons/cal/white/25.png b/.config/awesome/lain/icons/cal/white/25.png
new file mode 100755
index 0000000..48d279c
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/25.png differ
diff --git a/.config/awesome/lain/icons/cal/white/26.png b/.config/awesome/lain/icons/cal/white/26.png
new file mode 100755
index 0000000..7535855
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/26.png differ
diff --git a/.config/awesome/lain/icons/cal/white/27.png b/.config/awesome/lain/icons/cal/white/27.png
new file mode 100755
index 0000000..2aa9074
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/27.png differ
diff --git a/.config/awesome/lain/icons/cal/white/28.png b/.config/awesome/lain/icons/cal/white/28.png
new file mode 100755
index 0000000..0201976
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/28.png differ
diff --git a/.config/awesome/lain/icons/cal/white/29.png b/.config/awesome/lain/icons/cal/white/29.png
new file mode 100755
index 0000000..9305b9b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/29.png differ
diff --git a/.config/awesome/lain/icons/cal/white/3.png b/.config/awesome/lain/icons/cal/white/3.png
new file mode 100755
index 0000000..f1eb5de
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/3.png differ
diff --git a/.config/awesome/lain/icons/cal/white/30.png b/.config/awesome/lain/icons/cal/white/30.png
new file mode 100755
index 0000000..1ba61aa
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/30.png differ
diff --git a/.config/awesome/lain/icons/cal/white/31.png b/.config/awesome/lain/icons/cal/white/31.png
new file mode 100755
index 0000000..e9a873b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/31.png differ
diff --git a/.config/awesome/lain/icons/cal/white/4.png b/.config/awesome/lain/icons/cal/white/4.png
new file mode 100755
index 0000000..ee1ed6a
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/4.png differ
diff --git a/.config/awesome/lain/icons/cal/white/5.png b/.config/awesome/lain/icons/cal/white/5.png
new file mode 100755
index 0000000..466aa71
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/5.png differ
diff --git a/.config/awesome/lain/icons/cal/white/6.png b/.config/awesome/lain/icons/cal/white/6.png
new file mode 100755
index 0000000..0a7bf4d
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/6.png differ
diff --git a/.config/awesome/lain/icons/cal/white/7.png b/.config/awesome/lain/icons/cal/white/7.png
new file mode 100755
index 0000000..e971951
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/7.png differ
diff --git a/.config/awesome/lain/icons/cal/white/8.png b/.config/awesome/lain/icons/cal/white/8.png
new file mode 100755
index 0000000..cb03d0b
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/8.png differ
diff --git a/.config/awesome/lain/icons/cal/white/9.png b/.config/awesome/lain/icons/cal/white/9.png
new file mode 100755
index 0000000..fca554a
Binary files /dev/null and b/.config/awesome/lain/icons/cal/white/9.png differ
diff --git a/.config/awesome/lain/icons/layout/default/cascade.png b/.config/awesome/lain/icons/layout/default/cascade.png
new file mode 100755
index 0000000..292a057
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/cascade.png differ
diff --git a/.config/awesome/lain/icons/layout/default/cascadetile.png b/.config/awesome/lain/icons/layout/default/cascadetile.png
new file mode 100755
index 0000000..ba30f43
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/cascadetile.png differ
diff --git a/.config/awesome/lain/icons/layout/default/cascadetilew.png b/.config/awesome/lain/icons/layout/default/cascadetilew.png
new file mode 100755
index 0000000..d15eb70
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/cascadetilew.png differ
diff --git a/.config/awesome/lain/icons/layout/default/cascadew.png b/.config/awesome/lain/icons/layout/default/cascadew.png
new file mode 100755
index 0000000..da64bd6
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/cascadew.png differ
diff --git a/.config/awesome/lain/icons/layout/default/centerfair.png b/.config/awesome/lain/icons/layout/default/centerfair.png
new file mode 100755
index 0000000..188c243
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/centerfair.png differ
diff --git a/.config/awesome/lain/icons/layout/default/centerfairw.png b/.config/awesome/lain/icons/layout/default/centerfairw.png
new file mode 100755
index 0000000..ed4bcf5
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/centerfairw.png differ
diff --git a/.config/awesome/lain/icons/layout/default/centerwork.png b/.config/awesome/lain/icons/layout/default/centerwork.png
new file mode 100755
index 0000000..51e06bc
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/centerwork.png differ
diff --git a/.config/awesome/lain/icons/layout/default/centerworkh.png b/.config/awesome/lain/icons/layout/default/centerworkh.png
new file mode 100755
index 0000000..c59092f
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/centerworkh.png differ
diff --git a/.config/awesome/lain/icons/layout/default/centerworkhw.png b/.config/awesome/lain/icons/layout/default/centerworkhw.png
new file mode 100755
index 0000000..7820f8c
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/centerworkhw.png differ
diff --git a/.config/awesome/lain/icons/layout/default/centerworkw.png b/.config/awesome/lain/icons/layout/default/centerworkw.png
new file mode 100755
index 0000000..85e6996
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/centerworkw.png differ
diff --git a/.config/awesome/lain/icons/layout/default/termfair.png b/.config/awesome/lain/icons/layout/default/termfair.png
new file mode 100755
index 0000000..06226c1
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/termfair.png differ
diff --git a/.config/awesome/lain/icons/layout/default/termfairw.png b/.config/awesome/lain/icons/layout/default/termfairw.png
new file mode 100755
index 0000000..0a8b576
Binary files /dev/null and b/.config/awesome/lain/icons/layout/default/termfairw.png differ
diff --git a/.config/awesome/lain/icons/layout/zenburn/cascade.png b/.config/awesome/lain/icons/layout/zenburn/cascade.png
new file mode 100755
index 0000000..fbe4fac
Binary files /dev/null and b/.config/awesome/lain/icons/layout/zenburn/cascade.png differ
diff --git a/.config/awesome/lain/icons/layout/zenburn/cascadetile.png b/.config/awesome/lain/icons/layout/zenburn/cascadetile.png
new file mode 100755
index 0000000..2e03a80
Binary files /dev/null and b/.config/awesome/lain/icons/layout/zenburn/cascadetile.png differ
diff --git a/.config/awesome/lain/icons/layout/zenburn/centerfair.png b/.config/awesome/lain/icons/layout/zenburn/centerfair.png
new file mode 100755
index 0000000..75dc993
Binary files /dev/null and b/.config/awesome/lain/icons/layout/zenburn/centerfair.png differ
diff --git a/.config/awesome/lain/icons/layout/zenburn/centerwork.png b/.config/awesome/lain/icons/layout/zenburn/centerwork.png
new file mode 100755
index 0000000..af7a863
Binary files /dev/null and b/.config/awesome/lain/icons/layout/zenburn/centerwork.png differ
diff --git a/.config/awesome/lain/icons/layout/zenburn/centerworkh.png b/.config/awesome/lain/icons/layout/zenburn/centerworkh.png
new file mode 100755
index 0000000..88019b3
Binary files /dev/null and b/.config/awesome/lain/icons/layout/zenburn/centerworkh.png differ
diff --git a/.config/awesome/lain/icons/layout/zenburn/termfair.png b/.config/awesome/lain/icons/layout/zenburn/termfair.png
new file mode 100755
index 0000000..f7640b5
Binary files /dev/null and b/.config/awesome/lain/icons/layout/zenburn/termfair.png differ
diff --git a/.config/awesome/lain/icons/mail.png b/.config/awesome/lain/icons/mail.png
new file mode 100755
index 0000000..9c0c7a3
Binary files /dev/null and b/.config/awesome/lain/icons/mail.png differ
diff --git a/.config/awesome/lain/icons/no_net.png b/.config/awesome/lain/icons/no_net.png
new file mode 100755
index 0000000..3613372
Binary files /dev/null and b/.config/awesome/lain/icons/no_net.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/01d.png b/.config/awesome/lain/icons/openweathermap/01d.png
new file mode 100755
index 0000000..569965e
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/01d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/01n.png b/.config/awesome/lain/icons/openweathermap/01n.png
new file mode 100755
index 0000000..ce5b135
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/01n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/02d.png b/.config/awesome/lain/icons/openweathermap/02d.png
new file mode 100755
index 0000000..2ba9799
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/02d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/02n.png b/.config/awesome/lain/icons/openweathermap/02n.png
new file mode 100755
index 0000000..12e4283
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/02n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/03d.png b/.config/awesome/lain/icons/openweathermap/03d.png
new file mode 100755
index 0000000..1cf0e9d
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/03d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/03n.png b/.config/awesome/lain/icons/openweathermap/03n.png
new file mode 100755
index 0000000..89a42b8
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/03n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/04d.png b/.config/awesome/lain/icons/openweathermap/04d.png
new file mode 100755
index 0000000..e7fb67f
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/04d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/04n.png b/.config/awesome/lain/icons/openweathermap/04n.png
new file mode 100755
index 0000000..e7fb67f
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/04n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/09d.png b/.config/awesome/lain/icons/openweathermap/09d.png
new file mode 100755
index 0000000..cfa066a
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/09d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/09n.png b/.config/awesome/lain/icons/openweathermap/09n.png
new file mode 100755
index 0000000..cfa066a
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/09n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/10d.png b/.config/awesome/lain/icons/openweathermap/10d.png
new file mode 100755
index 0000000..712d0c8
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/10d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/10n.png b/.config/awesome/lain/icons/openweathermap/10n.png
new file mode 100755
index 0000000..712d0c8
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/10n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/11d.png b/.config/awesome/lain/icons/openweathermap/11d.png
new file mode 100755
index 0000000..3b62f7c
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/11d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/11n.png b/.config/awesome/lain/icons/openweathermap/11n.png
new file mode 100755
index 0000000..3b62f7c
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/11n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/13d.png b/.config/awesome/lain/icons/openweathermap/13d.png
new file mode 100755
index 0000000..e265b01
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/13d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/13n.png b/.config/awesome/lain/icons/openweathermap/13n.png
new file mode 100755
index 0000000..e265b01
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/13n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/50d.png b/.config/awesome/lain/icons/openweathermap/50d.png
new file mode 100755
index 0000000..905ace3
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/50d.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/50n.png b/.config/awesome/lain/icons/openweathermap/50n.png
new file mode 100755
index 0000000..905ace3
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/50n.png differ
diff --git a/.config/awesome/lain/icons/openweathermap/README.md b/.config/awesome/lain/icons/openweathermap/README.md
new file mode 100755
index 0000000..f908fbd
--- /dev/null
+++ b/.config/awesome/lain/icons/openweathermap/README.md
@@ -0,0 +1,3 @@
+[Plain Weather Icons](http://merlinthered.deviantart.com/art/plain-weather-icons-157162192), created by [MerlinTheRed](http://merlinthered.deviantart.com/).
+
+
diff --git a/.config/awesome/lain/icons/openweathermap/na.png b/.config/awesome/lain/icons/openweathermap/na.png
new file mode 100755
index 0000000..1cc5132
Binary files /dev/null and b/.config/awesome/lain/icons/openweathermap/na.png differ
diff --git a/.config/awesome/lain/icons/taskwarrior.png b/.config/awesome/lain/icons/taskwarrior.png
new file mode 100755
index 0000000..c64fe86
Binary files /dev/null and b/.config/awesome/lain/icons/taskwarrior.png differ
diff --git a/.config/awesome/lain/init.lua b/.config/awesome/lain/init.lua
new file mode 100755
index 0000000..b59d5dd
--- /dev/null
+++ b/.config/awesome/lain/init.lua
@@ -0,0 +1,15 @@
+--[[
+
+ Lain
+ Layouts, widgets and utilities for Awesome WM
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+
+--]]
+
+return {
+ layout = require("lain.layout"),
+ util = require("lain.util"),
+ widget = require("lain.widget")
+}
diff --git a/.config/awesome/lain/lain-scm-1.rockspec b/.config/awesome/lain/lain-scm-1.rockspec
new file mode 100755
index 0000000..70b0f43
--- /dev/null
+++ b/.config/awesome/lain/lain-scm-1.rockspec
@@ -0,0 +1,22 @@
+package = "lain"
+version = "scm-1"
+source = {
+ url = "git+https://github.com/lcpz/lain.git",
+ tag = "master"
+}
+description = {
+ summary = "Layout, widgets and utilities for Awesome WM",
+ detailed = "Alternative layouts, asynchronous widgets and utility functions for Awesome WM. Non-Lua dependency: curl (for IMAP, MPD and weather widgets).",
+ homepage = "https://github.com/lcpz/lain",
+ license = "GPL2"
+}
+dependencies = {
+ "lua >= 5.3",
+ "dkjson >= 2.6-1"
+}
+supported_platforms = { "linux" }
+build = {
+ type = "builtin",
+ modules = { lain = "init.lua" }
+}
+
diff --git a/.config/awesome/lain/layout/cascade.lua b/.config/awesome/lain/layout/cascade.lua
new file mode 100755
index 0000000..cbc3877
--- /dev/null
+++ b/.config/awesome/lain/layout/cascade.lua
@@ -0,0 +1,172 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2014, projektile
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local floor = math.floor
+local screen = screen
+
+local cascade = {
+ name = "cascade",
+ nmaster = 0,
+ offset_x = 32,
+ offset_y = 8,
+ tile = {
+ name = "cascadetile",
+ nmaster = 0,
+ ncol = 0,
+ mwfact = 0,
+ offset_x = 5,
+ offset_y = 32,
+ extra_padding = 0
+ }
+}
+
+local function do_cascade(p, tiling)
+ local t = p.tag or screen[p.screen].selected_tag
+ local wa = p.workarea
+ local cls = p.clients
+
+ if #cls == 0 then return end
+
+ if not tiling then
+ -- Cascade windows.
+
+ local num_c
+ if cascade.nmaster > 0 then
+ num_c = cascade.nmaster
+ else
+ num_c = t.master_count
+ end
+
+ -- Opening a new window will usually force all existing windows to
+ -- get resized. This wastes a lot of CPU time. So let's set a lower
+ -- bound to "how_many": This wastes a little screen space but you'll
+ -- get a much better user experience.
+ local how_many = (#cls >= num_c and #cls) or num_c
+
+ local current_offset_x = cascade.offset_x * (how_many - 1)
+ local current_offset_y = cascade.offset_y * (how_many - 1)
+
+ -- Iterate.
+ for i = 1,#cls,1 do
+ local c = cls[i]
+ local g = {}
+
+ g.x = wa.x + (how_many - i) * cascade.offset_x
+ g.y = wa.y + (i - 1) * cascade.offset_y
+ g.width = wa.width - current_offset_x
+ g.height = wa.height - current_offset_y
+
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+
+ p.geometries[c] = g
+ end
+ else
+ -- Layout with one fixed column meant for a master window. Its
+ -- width is calculated according to mwfact. Other clients are
+ -- cascaded or "tabbed" in a slave column on the right.
+
+ -- (1) (2) (3) (4)
+ -- +----------+---+ +----------+---+ +----------+---+ +----------+---+
+ -- | | | | | 3 | | | 4 | | +---+|
+ -- | | | -> | | | -> | +---++ -> | +---+|+
+ -- | 1 | 2 | | 1 +---++ | 1 | 3 || | 1 +---+|+|
+ -- | | | | | 2 || | +---++| | +---+|+ |
+ -- | | | | | || | | 2 | | | | 2 |+ |
+ -- +----------+---+ +---------+---++ +--------+---+-+ +------+---+---+
+
+ local mwfact
+ if cascade.tile.mwfact > 0 then
+ mwfact = cascade.tile.mwfact
+ else
+ mwfact = t.master_width_factor
+ end
+
+ -- Make slave windows overlap main window? Do this if ncol is 1.
+ local overlap_main
+ if cascade.tile.ncol > 0 then
+ overlap_main = cascade.tile.ncol
+ else
+ overlap_main = t.column_count
+ end
+
+ -- Minimum space for slave windows? See cascade.tile.lua.
+ local num_c
+ if cascade.tile.nmaster > 0 then
+ num_c = cascade.tile.nmaster
+ else
+ num_c = t.master_count
+ end
+
+ local how_many = (#cls - 1 >= num_c and (#cls - 1)) or num_c
+
+ local current_offset_x = cascade.tile.offset_x * (how_many - 1)
+ local current_offset_y = cascade.tile.offset_y * (how_many - 1)
+
+ if #cls <= 0 then return end
+
+ -- Main column, fixed width and height.
+ local c = cls[1]
+ local g = {}
+ -- Rounding is necessary to prevent the rendered size of slavewid
+ -- from being 1 pixel off when the result is not an integer.
+ local mainwid = floor(wa.width * mwfact)
+ local slavewid = wa.width - mainwid
+
+ if overlap_main == 1 then
+ g.width = wa.width
+
+ -- The size of the main window may be reduced a little bit.
+ -- This allows you to see if there are any windows below the
+ -- main window.
+ -- This only makes sense, though, if the main window is
+ -- overlapping everything else.
+ g.width = g.width - cascade.tile.extra_padding
+ else
+ g.width = mainwid
+ end
+
+ g.height = wa.height
+ g.x = wa.x
+ g.y = wa.y
+
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+
+ p.geometries[c] = g
+
+ -- Remaining clients stacked in slave column, new ones on top.
+ if #cls <= 1 then return end
+ for i = 2,#cls do
+ c = cls[i]
+ g = {}
+
+ g.width = slavewid - current_offset_x
+ g.height = wa.height - current_offset_y
+
+ g.x = wa.x + mainwid + (how_many - (i - 1)) * cascade.tile.offset_x
+ g.y = wa.y + (i - 2) * cascade.tile.offset_y
+
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+
+ p.geometries[c] = g
+ end
+ end
+end
+
+function cascade.tile.arrange(p)
+ return do_cascade(p, true)
+end
+
+function cascade.arrange(p)
+ return do_cascade(p, false)
+end
+
+return cascade
diff --git a/.config/awesome/lain/layout/centerwork.lua b/.config/awesome/lain/layout/centerwork.lua
new file mode 100755
index 0000000..c105676
--- /dev/null
+++ b/.config/awesome/lain/layout/centerwork.lua
@@ -0,0 +1,276 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2018, Eugene Pakhomov
+ * (c) 2016, Henrik Antonsson
+ * (c) 2015, Joerg Jaspert
+ * (c) 2014, projektile
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local floor = math.floor
+local max = math.max
+local mouse = mouse
+local mousegrabber = mousegrabber
+local screen = screen
+
+local centerwork = {
+ name = "centerwork",
+ horizontal = { name = "centerworkh" }
+}
+
+local function arrange(p, layout)
+ 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 g = {}
+
+ -- Main column, fixed width and height
+ local mwfact = t.master_width_factor
+ local mainhei = floor(wa.height * mwfact)
+ local mainwid = floor(wa.width * mwfact)
+ local slavewid = wa.width - mainwid
+ local slaveLwid = floor(slavewid / 2)
+ local slaveRwid = slavewid - slaveLwid
+ local slavehei = wa.height - mainhei
+ local slaveThei = floor(slavehei / 2)
+ local slaveBhei = slavehei - slaveThei
+ local nbrFirstSlaves = floor(#cls / 2)
+ local nbrSecondSlaves = floor((#cls - 1) / 2)
+
+ local slaveFirstDim, slaveSecondDim = 0, 0
+
+ if layout.name == "centerwork" then -- vertical
+ if nbrFirstSlaves > 0 then slaveFirstDim = floor(wa.height / nbrFirstSlaves) end
+ if nbrSecondSlaves > 0 then slaveSecondDim = floor(wa.height / nbrSecondSlaves) end
+
+ g.height = wa.height
+ g.width = mainwid
+
+ g.x = wa.x + slaveLwid
+ g.y = wa.y
+ else -- horizontal
+ if nbrFirstSlaves > 0 then slaveFirstDim = floor(wa.width / nbrFirstSlaves) end
+ if nbrSecondSlaves > 0 then slaveSecondDim = floor(wa.width / nbrSecondSlaves) end
+
+ g.height = mainhei
+ g.width = wa.width
+
+ g.x = wa.x
+ g.y = wa.y + slaveThei
+ end
+
+ g.width = max(g.width, 1)
+ g.height = max(g.height, 1)
+
+ p.geometries[cls[1]] = g
+
+ -- Auxiliary clients
+ if #cls <= 1 then return end
+ for i = 2, #cls do
+ g = {}
+ local idxChecker, dimToAssign
+
+ local rowIndex = floor(i/2)
+
+ if layout.name == "centerwork" then
+ if i % 2 == 0 then -- left slave
+ g.x = wa.x
+ g.y = wa.y + (rowIndex - 1) * slaveFirstDim
+ g.width = slaveLwid
+
+ idxChecker, dimToAssign = nbrFirstSlaves, slaveFirstDim
+ else -- right slave
+ g.x = wa.x + slaveLwid + mainwid
+ g.y = wa.y + (rowIndex - 1) * slaveSecondDim
+ g.width = slaveRwid
+
+ idxChecker, dimToAssign = nbrSecondSlaves, slaveSecondDim
+ end
+
+ -- if last slave in row, use remaining space for it
+ if rowIndex == idxChecker then
+ g.height = wa.y + wa.height - g.y
+ else
+ g.height = dimToAssign
+ end
+ else
+ if i % 2 == 0 then -- top slave
+ g.x = wa.x + (rowIndex - 1) * slaveFirstDim
+ g.y = wa.y
+ g.height = slaveThei
+
+ idxChecker, dimToAssign = nbrFirstSlaves, slaveFirstDim
+ else -- bottom slave
+ g.x = wa.x + (rowIndex - 1) * slaveSecondDim
+ g.y = wa.y + slaveThei + mainhei
+ g.height = slaveBhei
+
+ idxChecker, dimToAssign = nbrSecondSlaves, slaveSecondDim
+ end
+
+ -- if last slave in row, use remaining space for it
+ if rowIndex == idxChecker then
+ g.width = wa.x + wa.width - g.x
+ else
+ g.width = dimToAssign
+ end
+ end
+
+ g.width = max(g.width, 1)
+ g.height = max(g.height, 1)
+
+ p.geometries[cls[i]] = g
+ end
+end
+
+local function mouse_resize_handler(c, _, _, _, orientation)
+ local wa = c.screen.workarea
+ local mwfact = c.screen.selected_tag.master_width_factor
+ local g = c:geometry()
+ local offset = 0
+ local cursor = "cross"
+
+ local corner_coords
+
+ if orientation == 'vertical' then
+ if g.height + 15 >= wa.height then
+ offset = g.height * .5
+ cursor = "sb_h_double_arrow"
+ elseif g.y + g.height + 15 <= wa.y + wa.height then
+ offset = g.height
+ end
+ corner_coords = { x = wa.x + wa.width * (1 - mwfact) / 2, y = g.y + offset }
+ else
+ if g.width + 15 >= wa.width then
+ offset = g.width * .5
+ cursor = "sb_v_double_arrow"
+ elseif g.x + g.width + 15 <= wa.x + wa.width then
+ offset = g.width
+ end
+ corner_coords = { y = wa.y + wa.height * (1 - mwfact) / 2, x = g.x + offset }
+ end
+
+ mouse.coords(corner_coords)
+
+ local prev_coords = {}
+
+ mousegrabber.run(function(m)
+ if not c.valid then return false end
+ for _, v in ipairs(m.buttons) do
+ if v then
+ prev_coords = { x = m.x, y = m.y }
+ local new_mwfact
+ if orientation == 'vertical' then
+ new_mwfact = 1 - (m.x - wa.x) / wa.width * 2
+ else
+ new_mwfact = 1 - (m.y - wa.y) / wa.height * 2
+ end
+ c.screen.selected_tag.master_width_factor = math.min(math.max(new_mwfact, 0.01), 0.99)
+ return true
+ end
+ end
+ return prev_coords.x == m.x and prev_coords.y == m.y
+ end, cursor)
+end
+
+function centerwork.arrange(p)
+ return arrange(p, centerwork)
+end
+
+function centerwork.horizontal.arrange(p)
+ return arrange(p, centerwork.horizontal)
+end
+
+function centerwork.mouse_resize_handler(c, corner, x, y)
+ return mouse_resize_handler(c, corner, x, y, 'vertical')
+end
+
+function centerwork.horizontal.mouse_resize_handler(c, corner, x, y)
+ return mouse_resize_handler(c, corner, x, y, 'horizontal')
+end
+
+
+--[[
+Make focus.byidx and swap.byidx behave more consistently with other layouts.
+--]]
+
+local awful = require("awful")
+local gears = require("gears")
+local client = client
+
+local function compare_position(a, b)
+ if a.x == b.x then
+ return a.y < b.y
+ else
+ return a.x < b.x
+ end
+end
+
+local function clients_by_position()
+ local this = client.focus
+ if this then
+ local sorted = {}
+ for _, c in ipairs(client.focus.first_tag:clients()) do
+ if not c.minimized then sorted[#sorted+1] = c end
+ end
+ table.sort(sorted, compare_position)
+
+ local idx = 0
+ for i, that in ipairs(sorted) do
+ if this.window == that.window then
+ idx = i
+ end
+ end
+
+ if idx > 0 then
+ return { sorted = sorted, idx = idx }
+ end
+ end
+ return {}
+end
+
+local function in_centerwork()
+ return client.focus and client.focus.first_tag.layout.name == "centerwork"
+end
+
+centerwork.focus = {}
+
+
+--[[
+Drop in replacements for awful.client.focus.byidx and awful.client.swap.byidx
+that behaves consistently with other layouts.
+--]]
+
+function centerwork.focus.byidx(i)
+ if in_centerwork() then
+ local cls = clients_by_position()
+ if cls.idx then
+ local target = cls.sorted[gears.math.cycle(#cls.sorted, cls.idx + i)]
+ awful.client.focus.byidx(0, target)
+ end
+ else
+ awful.client.focus.byidx(i)
+ end
+end
+
+centerwork.swap = {}
+
+function centerwork.swap.byidx(i)
+ if in_centerwork() then
+ local cls = clients_by_position()
+ if cls.idx then
+ local target = cls.sorted[gears.math.cycle(#cls.sorted, cls.idx + i)]
+ client.focus:swap(target)
+ end
+ else
+ awful.client.swap.byidx(i)
+ end
+end
+
+return centerwork
diff --git a/.config/awesome/lain/layout/init.lua b/.config/awesome/lain/layout/init.lua
new file mode 100755
index 0000000..6478b06
--- /dev/null
+++ b/.config/awesome/lain/layout/init.lua
@@ -0,0 +1,19 @@
+--[[
+
+ Lain
+ Layouts, widgets and utilities for Awesome WM
+
+ Layouts section
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local wrequire = require("lain.helpers").wrequire
+local setmetatable = setmetatable
+
+local layout = { _NAME = "lain.layout" }
+
+return setmetatable(layout, { __index = wrequire })
diff --git a/.config/awesome/lain/layout/termfair.lua b/.config/awesome/lain/layout/termfair.lua
new file mode 100755
index 0000000..cf018ef
--- /dev/null
+++ b/.config/awesome/lain/layout/termfair.lua
@@ -0,0 +1,282 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2014, projektile
+ * (c) 2013, Luca CPZ
+ * (c) 2010, Nicolas Estibals
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local math = math
+local screen = screen
+local tonumber = tonumber
+
+local termfair = { name = "termfair" }
+termfair.center = { name = "centerfair" }
+termfair.stable = { name = "stablefair" }
+
+local function do_fair(p, orientation)
+ local t = p.tag or screen[p.screen].selected_tag
+ local wa = p.workarea
+ local cls = p.clients
+
+ if #cls == 0 then return end
+
+ -- How many vertical columns? Read from nmaster on the tag.
+ local num_x = tonumber(termfair.nmaster) or t.master_count
+ local ncol = tonumber(termfair.ncol) or t.column_count
+ if num_x <= 2 then num_x = 2 end
+ if ncol <= 1 then ncol = 1 end
+ local width = math.floor(wa.width/num_x)
+
+ if orientation == "west" then
+ -- Layout with fixed number of vertical columns (read from nmaster).
+ -- New windows align from left to right. When a row is full, a new
+ -- one above it is created. Like this:
+
+ -- (1) (2) (3)
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+ -- | | | | | | | | | | | |
+ -- | 1 | | | -> | 1 | 2 | | -> | 1 | 2 | 3 | ->
+ -- | | | | | | | | | | | |
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+
+ -- (4) (5) (6)
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+ -- | 1 | | | | 1 | 2 | | | 1 | 2 | 3 |
+ -- +---+---+---+ -> +---+---+---+ -> +---+---+---+
+ -- | 2 | 3 | 4 | | 3 | 4 | 5 | | 4 | 5 | 6 |
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+
+ local num_y = math.max(math.ceil(#cls / num_x), ncol)
+ local height = math.floor(wa.height/num_y)
+ local cur_num_x = num_x
+ local at_x = 0
+ local at_y = 0
+
+ local remaining_clients = #cls
+
+ -- We start the first row. Left-align by limiting the number of
+ -- available slots.
+ if remaining_clients < num_x then
+ cur_num_x = remaining_clients
+ end
+
+ -- Iterate in reversed order.
+ for i = #cls,1,-1 do
+ -- Get x and y position.
+ local c = cls[i]
+ local this_x = cur_num_x - at_x - 1
+ local this_y = num_y - at_y - 1
+
+ -- Calculate geometry.
+ local g = {}
+ if this_x == (num_x - 1) then
+ g.width = wa.width - (num_x - 1)*width
+ else
+ g.width = width
+ end
+
+ if this_y == (num_y - 1) then
+ g.height = wa.height - (num_y - 1)*height
+ else
+ g.height = height
+ end
+
+ g.x = wa.x + this_x*width
+ g.y = wa.y + this_y*height
+
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+
+ p.geometries[c] = g
+
+ remaining_clients = remaining_clients - 1
+
+ -- Next grid position.
+ at_x = at_x + 1
+ if at_x == num_x then
+ -- Row full, create a new one above it.
+ at_x = 0
+ at_y = at_y + 1
+
+ -- We start a new row. Left-align.
+ if remaining_clients < num_x then
+ cur_num_x = remaining_clients
+ end
+ end
+ end
+ elseif orientation == "stable" then
+ -- Layout with fixed number of vertical columns (read from nmaster).
+ -- New windows align from left to right. When a row is full, a new
+ -- one below it is created. Like this:
+
+ -- (1) (2) (3)
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+ -- | | | | | | | | | | | |
+ -- | 1 | | | -> | 1 | 2 | | -> | 1 | 2 | 3 | ->
+ -- | | | | | | | | | | | |
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+
+ -- (4) (5) (6)
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+ -- | 1 | 2 | 3 | | 1 | 2 | 3 | | 1 | 2 | 3 |
+ -- +---+---+---+ +---+---+---+ +---+---+---+
+ -- | 4 | | | | 4 | 5 | | | 4 | 5 | 6 |
+ -- +---+---+---+ -> +---+---+---+ -> +---+---+---+
+
+ local num_y = math.max(math.ceil(#cls / num_x), ncol)
+ local height = math.floor(wa.height/num_y)
+
+ for i = #cls,1,-1 do
+ -- Get x and y position.
+ local c = cls[i]
+ local this_x = (i - 1) % num_x
+ local this_y = math.floor((i - this_x - 1) / num_x)
+
+ -- Calculate geometry.
+ local g = {}
+ if this_x == (num_x - 1) then
+ g.width = wa.width - (num_x - 1)*width
+ else
+ g.width = width
+ end
+
+ if this_y == (num_y - 1) then
+ g.height = wa.height - (num_y - 1)*height
+ else
+ g.height = height
+ end
+
+ g.x = wa.x + this_x*width
+ g.y = wa.y + this_y*height
+
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+
+ p.geometries[c] = g
+ end
+ elseif orientation == "center" then
+ -- Layout with fixed number of vertical columns (read from nmaster).
+ -- Cols are centerded until there is nmaster columns, then windows
+ -- are stacked in the slave columns, with at most ncol clients per
+ -- column if possible.
+
+ -- with nmaster=3 and ncol=1 you'll have
+ -- (1) (2) (3)
+ -- +---+---+---+ +-+---+---+-+ +---+---+---+
+ -- | | | | | | | | | | | | |
+ -- | | 1 | | -> | | 1 | 2 | | -> | 1 | 2 | 3 | ->
+ -- | | | | | | | | | | | | |
+ -- +---+---+---+ +-+---+---+-+ +---+---+---+
+
+ -- (4) (5)
+ -- +---+---+---+ +---+---+---+
+ -- | | | 3 | | | 2 | 4 |
+ -- + 1 + 2 +---+ -> + 1 +---+---+
+ -- | | | 4 | | | 3 | 5 |
+ -- +---+---+---+ +---+---+---+
+
+ if #cls < num_x then
+ -- Less clients than the number of columns, let's center it!
+ local offset_x = wa.x + (wa.width - #cls*width) / 2
+ for i = 1, #cls do
+ local g = { y = wa.y }
+ g.width = width
+ g.height = wa.height
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+ g.x = offset_x + (i - 1) * width
+ p.geometries[cls[i]] = g
+ end
+ else
+ -- More clients than the number of columns, let's arrange it!
+ -- Master client deserves a special treatement
+ local g = {}
+ g.width = wa.width - (num_x - 1)*width
+ g.height = wa.height
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+ g.x = wa.x
+ g.y = wa.y
+ p.geometries[cls[1]] = g
+
+ -- Treat the other clients
+
+ -- Compute distribution of clients among columns
+ local num_y = {}
+ local remaining_clients = #cls-1
+ local ncol_min = math.ceil(remaining_clients/(num_x-1))
+
+ if ncol >= ncol_min then
+ for i = (num_x-1), 1, -1 do
+ if (remaining_clients-i+1) < ncol then
+ num_y[i] = remaining_clients-i + 1
+ else
+ num_y[i] = ncol
+ end
+ remaining_clients = remaining_clients - num_y[i]
+ end
+ else
+ local rem = remaining_clients % (num_x-1)
+ if rem == 0 then
+ for i = 1, num_x-1 do
+ num_y[i] = ncol_min
+ end
+ else
+ for i = 1, num_x-1 do
+ num_y[i] = ncol_min - 1
+ end
+ for i = 0, rem-1 do
+ num_y[num_x-1-i] = num_y[num_x-1-i] + 1
+ end
+ end
+ end
+
+ -- Compute geometry of the other clients
+ local nclient = 2 -- we start with the 2nd client
+ local wx = g.x + g.width
+ for i = 1, (num_x-1) do
+ local height = math.floor(wa.height / num_y[i])
+ local wy = wa.y
+ for _ = 0, (num_y[i]-2) do
+ g = {}
+ g.x = wx
+ g.y = wy
+ g.height = height
+ g.width = width
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+ p.geometries[cls[nclient]] = g
+ nclient = nclient + 1
+ wy = wy + height
+ end
+ g = {}
+ g.x = wx
+ g.y = wy
+ g.height = wa.height - (num_y[i] - 1)*height
+ g.width = width
+ if g.width < 1 then g.width = 1 end
+ if g.height < 1 then g.height = 1 end
+ p.geometries[cls[nclient]] = g
+ nclient = nclient + 1
+ wx = wx + width
+ end
+ end
+ end
+end
+
+function termfair.center.arrange(p)
+ return do_fair(p, "center")
+end
+
+function termfair.stable.arrange(p)
+ return do_fair(p, "stable")
+end
+
+function termfair.arrange(p)
+ return do_fair(p, "west")
+end
+
+return termfair
diff --git a/.config/awesome/lain/util/dkjson.lua b/.config/awesome/lain/util/dkjson.lua
new file mode 100755
index 0000000..61cccb9
--- /dev/null
+++ b/.config/awesome/lain/util/dkjson.lua
@@ -0,0 +1,747 @@
+-- Module options:
+local always_use_lpeg = false
+local register_global_module_table = false
+local global_module_name = 'json'
+
+--[==[
+
+David Kolf's JSON module for Lua 5.1 - 5.4
+
+Version 2.6
+
+
+For the documentation see the corresponding readme.txt or visit
+.
+
+You can contact the author by sending an e-mail to 'david' at the
+domain 'dkolf.de'.
+
+
+Copyright (C) 2010-2021 David Heiko Kolf
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+--]==]
+
+-- global dependencies:
+local pairs, type, tostring, tonumber, getmetatable, setmetatable =
+ pairs, type, tostring, tonumber, getmetatable, setmetatable
+local error, require, pcall, select = error, require, pcall, select
+local floor, huge = math.floor, math.huge
+local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
+ string.rep, string.gsub, string.sub, string.byte, string.char,
+ string.find, string.len, string.format
+local strmatch = string.match
+local concat = table.concat
+
+local json = { version = "dkjson 2.6" }
+
+local jsonlpeg = {}
+
+if register_global_module_table then
+ if always_use_lpeg then
+ _G[global_module_name] = jsonlpeg
+ else
+ _G[global_module_name] = json
+ end
+end
+
+_ENV = nil -- blocking globals in Lua 5.2 and later
+
+pcall (function()
+ -- Enable access to blocked metatables.
+ -- Don't worry, this module doesn't change anything in them.
+ local debmeta = require "debug".getmetatable
+ if debmeta then getmetatable = debmeta end
+end)
+
+json.null = setmetatable ({}, {
+ __tojson = function () return "null" end
+})
+
+local function isarray (tbl)
+ local max, n, arraylen = 0, 0, 0
+ for k,v in pairs (tbl) do
+ if k == 'n' and type(v) == 'number' then
+ arraylen = v
+ if v > max then
+ max = v
+ end
+ else
+ if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
+ return false
+ end
+ if k > max then
+ max = k
+ end
+ n = n + 1
+ end
+ end
+ if max > 10 and max > arraylen and max > n * 2 then
+ return false -- don't create an array with too many holes
+ end
+ return true, max
+end
+
+local escapecodes = {
+ ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
+ ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
+}
+
+local function escapeutf8 (uchar)
+ local value = escapecodes[uchar]
+ if value then
+ return value
+ end
+ local a, b, c, d = strbyte (uchar, 1, 4)
+ a, b, c, d = a or 0, b or 0, c or 0, d or 0
+ if a <= 0x7f then
+ value = a
+ elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
+ value = (a - 0xc0) * 0x40 + b - 0x80
+ elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
+ value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
+ elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
+ value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
+ else
+ return ""
+ end
+ if value <= 0xffff then
+ return strformat ("\\u%.4x", value)
+ elseif value <= 0x10ffff then
+ -- encode as UTF-16 surrogate pair
+ value = value - 0x10000
+ local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
+ return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
+ else
+ return ""
+ end
+end
+
+local function fsub (str, pattern, repl)
+ -- gsub always builds a new string in a buffer, even when no match
+ -- exists. First using find should be more efficient when most strings
+ -- don't contain the pattern.
+ if strfind (str, pattern) then
+ return gsub (str, pattern, repl)
+ else
+ return str
+ end
+end
+
+local function quotestring (value)
+ -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
+ value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
+ if strfind (value, "[\194\216\220\225\226\239]") then
+ value = fsub (value, "\194[\128-\159\173]", escapeutf8)
+ value = fsub (value, "\216[\128-\132]", escapeutf8)
+ value = fsub (value, "\220\143", escapeutf8)
+ value = fsub (value, "\225\158[\180\181]", escapeutf8)
+ value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
+ value = fsub (value, "\226\129[\160-\175]", escapeutf8)
+ value = fsub (value, "\239\187\191", escapeutf8)
+ value = fsub (value, "\239\191[\176-\191]", escapeutf8)
+ end
+ return "\"" .. value .. "\""
+end
+json.quotestring = quotestring
+
+local function replace(str, o, n)
+ local i, j = strfind (str, o, 1, true)
+ if i then
+ return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
+ else
+ return str
+ end
+end
+
+-- locale independent num2str and str2num functions
+local decpoint, numfilter
+
+local function updatedecpoint ()
+ decpoint = strmatch(tostring(0.5), "([^05+])")
+ -- build a filter that can be used to remove group separators
+ numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
+end
+
+updatedecpoint()
+
+local function num2str (num)
+ return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
+end
+
+local function str2num (str)
+ local num = tonumber(replace(str, ".", decpoint))
+ if not num then
+ updatedecpoint()
+ num = tonumber(replace(str, ".", decpoint))
+ end
+ return num
+end
+
+local function addnewline2 (level, buffer, buflen)
+ buffer[buflen+1] = "\n"
+ buffer[buflen+2] = strrep (" ", level)
+ buflen = buflen + 2
+ return buflen
+end
+
+function json.addnewline (state)
+ if state.indent then
+ state.bufferlen = addnewline2 (state.level or 0,
+ state.buffer, state.bufferlen or #(state.buffer))
+ end
+end
+
+local encode2 -- forward declaration
+
+local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ local kt = type (key)
+ if kt ~= 'string' and kt ~= 'number' then
+ return nil, "type '" .. kt .. "' is not supported as a key by JSON."
+ end
+ if prev then
+ buflen = buflen + 1
+ buffer[buflen] = ","
+ end
+ if indent then
+ buflen = addnewline2 (level, buffer, buflen)
+ end
+ buffer[buflen+1] = quotestring (key)
+ buffer[buflen+2] = ":"
+ return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
+end
+
+local function appendcustom(res, buffer, state)
+ local buflen = state.bufferlen
+ if type (res) == 'string' then
+ buflen = buflen + 1
+ buffer[buflen] = res
+ end
+ return buflen
+end
+
+local function exception(reason, value, state, buffer, buflen, defaultmessage)
+ defaultmessage = defaultmessage or reason
+ local handler = state.exception
+ if not handler then
+ return nil, defaultmessage
+ else
+ state.bufferlen = buflen
+ local ret, msg = handler (reason, value, state, defaultmessage)
+ if not ret then return nil, msg or defaultmessage end
+ return appendcustom(ret, buffer, state)
+ end
+end
+
+function json.encodeexception(_reason, _value, _state, defaultmessage)
+ return quotestring("<" .. defaultmessage .. ">")
+end
+
+encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
+ local valtype = type (value)
+ local valmeta = getmetatable (value)
+ valmeta = type (valmeta) == 'table' and valmeta -- only tables
+ local valtojson = valmeta and valmeta.__tojson
+ if valtojson then
+ if tables[value] then
+ return exception('reference cycle', value, state, buffer, buflen)
+ end
+ tables[value] = true
+ state.bufferlen = buflen
+ local ret, msg = valtojson (value, state)
+ if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
+ tables[value] = nil
+ buflen = appendcustom(ret, buffer, state)
+ elseif value == nil then
+ buflen = buflen + 1
+ buffer[buflen] = "null"
+ elseif valtype == 'number' then
+ local s
+ if value ~= value or value >= huge or -value >= huge then
+ -- This is the behaviour of the original JSON implementation.
+ s = "null"
+ else
+ s = num2str (value)
+ end
+ buflen = buflen + 1
+ buffer[buflen] = s
+ elseif valtype == 'boolean' then
+ buflen = buflen + 1
+ buffer[buflen] = value and "true" or "false"
+ elseif valtype == 'string' then
+ buflen = buflen + 1
+ buffer[buflen] = quotestring (value)
+ elseif valtype == 'table' then
+ if tables[value] then
+ return exception('reference cycle', value, state, buffer, buflen)
+ end
+ tables[value] = true
+ level = level + 1
+ local isa, n = isarray (value)
+ if n == 0 and valmeta and valmeta.__jsontype == 'object' then
+ isa = false
+ end
+ local msg
+ if isa then -- JSON array
+ buflen = buflen + 1
+ buffer[buflen] = "["
+ for i = 1, n do
+ buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ if i < n then
+ buflen = buflen + 1
+ buffer[buflen] = ","
+ end
+ end
+ buflen = buflen + 1
+ buffer[buflen] = "]"
+ else -- JSON object
+ local prev = false
+ buflen = buflen + 1
+ buffer[buflen] = "{"
+ local order = valmeta and valmeta.__jsonorder or globalorder
+ if order then
+ local used = {}
+ n = #order
+ for i = 1, n do
+ local k = order[i]
+ local v = value[k]
+ if v ~= nil then
+ used[k] = true
+ buflen, _msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ prev = true -- add a seperator before the next element
+ end
+ end
+ for k,v in pairs (value) do
+ if not used[k] then
+ buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ prev = true -- add a seperator before the next element
+ end
+ end
+ else -- unordered
+ for k,v in pairs (value) do
+ buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ prev = true -- add a seperator before the next element
+ end
+ end
+ if indent then
+ buflen = addnewline2 (level - 1, buffer, buflen)
+ end
+ buflen = buflen + 1
+ buffer[buflen] = "}"
+ end
+ tables[value] = nil
+ else
+ return exception ('unsupported type', value, state, buffer, buflen,
+ "type '" .. valtype .. "' is not supported by JSON.")
+ end
+ return buflen
+end
+
+function json.encode (value, state)
+ state = state or {}
+ local oldbuffer = state.buffer
+ local buffer = oldbuffer or {}
+ state.buffer = buffer
+ updatedecpoint()
+ local ret, msg = encode2 (value, state.indent, state.level or 0,
+ buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
+ if not ret then
+ error (msg, 2)
+ elseif oldbuffer == buffer then
+ state.bufferlen = ret
+ return true
+ else
+ state.bufferlen = nil
+ state.buffer = nil
+ return concat (buffer)
+ end
+end
+
+local function loc (str, where)
+ local line, pos, linepos = 1, 1, 0
+ while true do
+ pos = strfind (str, "\n", pos, true)
+ if pos and pos < where then
+ line = line + 1
+ linepos = pos
+ pos = pos + 1
+ else
+ break
+ end
+ end
+ return "line " .. line .. ", column " .. (where - linepos)
+end
+
+local function unterminated (str, what, where)
+ return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
+end
+
+local function scanwhite (str, pos)
+ while true do
+ pos = strfind (str, "%S", pos)
+ if not pos then return nil end
+ local sub2 = strsub (str, pos, pos + 1)
+ if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
+ -- UTF-8 Byte Order Mark
+ pos = pos + 3
+ elseif sub2 == "//" then
+ pos = strfind (str, "[\n\r]", pos + 2)
+ if not pos then return nil end
+ elseif sub2 == "/*" then
+ pos = strfind (str, "*/", pos + 2)
+ if not pos then return nil end
+ pos = pos + 2
+ else
+ return pos
+ end
+ end
+end
+
+local escapechars = {
+ ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
+ ["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
+}
+
+local function unichar (value)
+ if value < 0 then
+ return nil
+ elseif value <= 0x007f then
+ return strchar (value)
+ elseif value <= 0x07ff then
+ return strchar (0xc0 + floor(value/0x40),
+ 0x80 + (floor(value) % 0x40))
+ elseif value <= 0xffff then
+ return strchar (0xe0 + floor(value/0x1000),
+ 0x80 + (floor(value/0x40) % 0x40),
+ 0x80 + (floor(value) % 0x40))
+ elseif value <= 0x10ffff then
+ return strchar (0xf0 + floor(value/0x40000),
+ 0x80 + (floor(value/0x1000) % 0x40),
+ 0x80 + (floor(value/0x40) % 0x40),
+ 0x80 + (floor(value) % 0x40))
+ else
+ return nil
+ end
+end
+
+local function scanstring (str, pos)
+ local lastpos = pos + 1
+ local buffer, n = {}, 0
+ while true do
+ local nextpos = strfind (str, "[\"\\]", lastpos)
+ if not nextpos then
+ return unterminated (str, "string", pos)
+ end
+ if nextpos > lastpos then
+ n = n + 1
+ buffer[n] = strsub (str, lastpos, nextpos - 1)
+ end
+ if strsub (str, nextpos, nextpos) == "\"" then
+ lastpos = nextpos + 1
+ break
+ else
+ local escchar = strsub (str, nextpos + 1, nextpos + 1)
+ local value
+ if escchar == "u" then
+ value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
+ if value then
+ local value2
+ if 0xD800 <= value and value <= 0xDBff then
+ -- we have the high surrogate of UTF-16. Check if there is a
+ -- low surrogate escaped nearby to combine them.
+ if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
+ value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
+ if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
+ value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
+ else
+ value2 = nil -- in case it was out of range for a low surrogate
+ end
+ end
+ end
+ value = value and unichar (value)
+ if value then
+ if value2 then
+ lastpos = nextpos + 12
+ else
+ lastpos = nextpos + 6
+ end
+ end
+ end
+ end
+ if not value then
+ value = escapechars[escchar] or escchar
+ lastpos = nextpos + 2
+ end
+ n = n + 1
+ buffer[n] = value
+ end
+ end
+ if n == 1 then
+ return buffer[1], lastpos
+ elseif n > 1 then
+ return concat (buffer), lastpos
+ else
+ return "", lastpos
+ end
+end
+
+local scanvalue -- forward declaration
+
+local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
+ local tbl, n = {}, 0
+ local pos = startpos + 1
+ if what == 'object' then
+ setmetatable (tbl, objectmeta)
+ else
+ setmetatable (tbl, arraymeta)
+ end
+ while true do
+ pos = scanwhite (str, pos)
+ if not pos then return unterminated (str, what, startpos) end
+ local char = strsub (str, pos, pos)
+ if char == closechar then
+ return tbl, pos + 1
+ end
+ local val1, err
+ val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
+ if err then return nil, pos, err end
+ pos = scanwhite (str, pos)
+ if not pos then return unterminated (str, what, startpos) end
+ char = strsub (str, pos, pos)
+ if char == ":" then
+ if val1 == nil then
+ return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
+ end
+ pos = scanwhite (str, pos + 1)
+ if not pos then return unterminated (str, what, startpos) end
+ local val2
+ val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
+ if err then return nil, pos, err end
+ tbl[val1] = val2
+ pos = scanwhite (str, pos)
+ if not pos then return unterminated (str, what, startpos) end
+ char = strsub (str, pos, pos)
+ else
+ n = n + 1
+ tbl[n] = val1
+ end
+ if char == "," then
+ pos = pos + 1
+ end
+ end
+end
+
+scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
+ pos = pos or 1
+ pos = scanwhite (str, pos)
+ if not pos then
+ return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
+ end
+ local char = strsub (str, pos, pos)
+ if char == "{" then
+ return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
+ elseif char == "[" then
+ return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
+ elseif char == "\"" then
+ return scanstring (str, pos)
+ else
+ local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
+ if pstart then
+ local number = str2num (strsub (str, pstart, pend))
+ if number then
+ return number, pend + 1
+ end
+ end
+ pstart, pend = strfind (str, "^%a%w*", pos)
+ if pstart then
+ local name = strsub (str, pstart, pend)
+ if name == "true" then
+ return true, pend + 1
+ elseif name == "false" then
+ return false, pend + 1
+ elseif name == "null" then
+ return nullval, pend + 1
+ end
+ end
+ return nil, pos, "no valid JSON value at " .. loc (str, pos)
+ end
+end
+
+local function optionalmetatables(...)
+ if select("#", ...) > 0 then
+ return ...
+ else
+ return {__jsontype = 'object'}, {__jsontype = 'array'}
+ end
+end
+
+function json.decode (str, pos, nullval, ...)
+ local objectmeta, arraymeta = optionalmetatables(...)
+ return scanvalue (str, pos, nullval, objectmeta, arraymeta)
+end
+
+function json.use_lpeg ()
+ local g = require ("lpeg")
+
+ if g.version() == "0.11" then
+ error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
+ end
+
+ local pegmatch = g.match
+ local P, S, R = g.P, g.S, g.R
+
+ local function ErrorCall (str, pos, msg, state)
+ if not state.msg then
+ state.msg = msg .. " at " .. loc (str, pos)
+ state.pos = pos
+ end
+ return false
+ end
+
+ local function Err (msg)
+ return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
+ end
+
+ local function ErrorUnterminatedCall (str, pos, what, state)
+ return ErrorCall (str, pos - 1, "unterminated " .. what, state)
+ end
+
+ local SingleLineComment = P"//" * (1 - S"\n\r")^0
+ local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
+ local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
+
+ local function ErrUnterminated (what)
+ return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall)
+ end
+
+ local PlainChar = 1 - S"\"\\\n\r"
+ local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
+ local HexDigit = R("09", "af", "AF")
+ local function UTF16Surrogate (_match, _pos, high, low)
+ high, low = tonumber (high, 16), tonumber (low, 16)
+ if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
+ return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
+ else
+ return false
+ end
+ end
+ local function UTF16BMP (hex)
+ return unichar (tonumber (hex, 16))
+ end
+ local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
+ local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
+ local Char = UnicodeEscape + EscapeSequence + PlainChar
+ local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string")
+ local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
+ local Fractal = P"." * R"09"^0
+ local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
+ local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
+ local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
+ local SimpleValue = Number + String + Constant
+ local ArrayContent, ObjectContent
+
+ -- The functions parsearray and parseobject parse only a single value/pair
+ -- at a time and store them directly to avoid hitting the LPeg limits.
+ local function parsearray (str, pos, nullval, state)
+ local obj, cont
+ local start = pos
+ local npos
+ local t, nt = {}, 0
+ repeat
+ obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
+ if cont == 'end' then
+ return ErrorUnterminatedCall (str, start, "array", state)
+ end
+ pos = npos
+ if cont == 'cont' or cont == 'last' then
+ nt = nt + 1
+ t[nt] = obj
+ end
+ until cont ~= 'cont'
+ return pos, setmetatable (t, state.arraymeta)
+ end
+
+ local function parseobject (str, pos, nullval, state)
+ local obj, key, cont
+ local start = pos
+ local npos
+ local t = {}
+ repeat
+ key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
+ if cont == 'end' then
+ return ErrorUnterminatedCall (str, start, "object", state)
+ end
+ pos = npos
+ if cont == 'cont' or cont == 'last' then
+ t[key] = obj
+ end
+ until cont ~= 'cont'
+ return pos, setmetatable (t, state.objectmeta)
+ end
+
+ local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray)
+ local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject)
+ local Value = Space * (Array + Object + SimpleValue)
+ local ExpectedValue = Value + Space * Err "value expected"
+ local ExpectedKey = String + Err "key expected"
+ local End = P(-1) * g.Cc'end'
+ local ErrInvalid = Err "invalid JSON"
+ ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp()
+ local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue)
+ ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp()
+ local DecodeValue = ExpectedValue * g.Cp ()
+
+ jsonlpeg.version = json.version
+ jsonlpeg.encode = json.encode
+ jsonlpeg.null = json.null
+ jsonlpeg.quotestring = json.quotestring
+ jsonlpeg.addnewline = json.addnewline
+ jsonlpeg.encodeexception = json.encodeexception
+ jsonlpeg.using_lpeg = true
+
+ function jsonlpeg.decode (str, pos, nullval, ...)
+ local state = {}
+ state.objectmeta, state.arraymeta = optionalmetatables(...)
+ local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
+ if state.msg then
+ return nil, state.pos, state.msg
+ else
+ return obj, retpos
+ end
+ end
+
+ -- cache result of this function:
+ json.use_lpeg = function () return jsonlpeg end
+ jsonlpeg.use_lpeg = json.use_lpeg
+
+ return jsonlpeg
+end
+
+if always_use_lpeg then
+ return json.use_lpeg()
+end
+
+return json
+
diff --git a/.config/awesome/lain/util/init.lua b/.config/awesome/lain/util/init.lua
new file mode 100755
index 0000000..5ae0523
--- /dev/null
+++ b/.config/awesome/lain/util/init.lua
@@ -0,0 +1,190 @@
+--[[
+
+ Lain
+ Layouts, widgets and utilities for Awesome WM
+
+ Utilities section
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local awful = require("awful")
+local sqrt = math.sqrt
+local pairs = pairs
+local client = client
+local tonumber = tonumber
+local wrequire = require("lain.helpers").wrequire
+local setmetatable = setmetatable
+
+-- Lain utilities submodule
+-- lain.util
+local util = { _NAME = "lain.util" }
+
+-- Like awful.menu.clients, but only show clients of currently selected tags
+function util.menu_clients_current_tags(menu, args)
+ -- List of currently selected tags.
+ local cls_tags = awful.screen.focused().selected_tags
+
+ if cls_tags == nil then return nil end
+
+ -- Final list of menu items.
+ local cls_t = {}
+
+ -- For each selected tag get all clients of that tag and add them to
+ -- the menu. A click on a menu item will raise that client.
+ for i = 1,#cls_tags do
+ local t = cls_tags[i]
+ local cls = t:clients()
+
+ for _, c in pairs(cls) do
+ cls_t[#cls_t + 1] = { awful.util.escape(c.name) or "",
+ function ()
+ c.minimized = false
+ client.focus = c
+ c:raise()
+ end,
+ c.icon }
+ end
+ end
+
+ -- No clients? Then quit.
+ if #cls_t <= 0 then return nil end
+
+ -- menu may contain some predefined values, otherwise start with a
+ -- fresh menu.
+ if not menu then menu = {} end
+
+ -- Set the list of items and show the menu.
+ menu.items = cls_t
+ local m = awful.menu(menu)
+ m:show(args)
+
+ return m
+end
+
+-- Magnify a client: set it to "float" and resize it.
+function util.magnify_client(c, width_f, height_f)
+ if c and not c.floating then
+ util.magnified_client = c
+ util.mc(c, width_f, height_f)
+ else
+ util.magnified_client = nil
+ c.floating = false
+ end
+end
+
+-- https://github.com/lcpz/lain/issues/195
+function util.mc(c, width_f, height_f)
+ c = c or util.magnified_client
+ if not c then return end
+
+ c.floating = true
+ local s = awful.screen.focused()
+ local mg = s.workarea
+ local g = {}
+ local mwfact = width_f or s.selected_tag.master_width_factor or 0.5
+ g.width = sqrt(mwfact) * mg.width
+ g.height = sqrt(height_f or mwfact) * mg.height
+ g.x = mg.x + (mg.width - g.width) / 2
+ g.y = mg.y + (mg.height - g.height) / 2
+
+ if c then c:geometry(g) end -- if c is still a valid object
+end
+
+-- Non-empty tag browsing
+-- direction in {-1, 1} <-> {previous, next} non-empty tag
+function util.tag_view_nonempty(direction,sc)
+ direction = direction or 1
+ local s = sc or awful.screen.focused()
+ local tags = s.tags
+ local sel = s.selected_tag
+
+ local i = sel.index
+ repeat
+ i = i + direction
+
+ -- Wrap around when we reach one of the bounds
+ if i > #tags then
+ i = i - #tags
+ end
+ if i < 1 then
+ i = i + #tags
+ end
+
+ local t = tags[i]
+
+ -- Stop when we get back to where we started
+ if t == sel then
+ break
+ end
+
+ -- If it's The One, view it.
+ if #t:clients() > 0 then
+ t:view_only()
+ return
+ end
+ until false
+end
+
+-- {{{ Dynamic tagging
+
+-- Add a new tag
+function util.add_tag(layout)
+ awful.prompt.run {
+ prompt = "New tag name: ",
+ textbox = awful.screen.focused().mypromptbox.widget,
+ exe_callback = function(name)
+ if not name or #name == 0 then return end
+ awful.tag.add(name, { screen = awful.screen.focused(), layout = layout or awful.layout.suit.tile }):view_only()
+ end
+ }
+end
+
+-- Rename current tag
+function util.rename_tag()
+ awful.prompt.run {
+ prompt = "Rename tag: ",
+ textbox = awful.screen.focused().mypromptbox.widget,
+ exe_callback = function(new_name)
+ if not new_name or #new_name == 0 then return end
+ local t = awful.screen.focused().selected_tag
+ if t then
+ t.name = new_name
+ end
+ end
+ }
+end
+
+-- Move current tag
+-- pos in {-1, 1} <-> {previous, next} tag position
+function util.move_tag(pos)
+ local tag = awful.screen.focused().selected_tag
+ if tonumber(pos) <= -1 then
+ awful.tag.move(tag.index - 1, tag)
+ else
+ awful.tag.move(tag.index + 1, tag)
+ end
+end
+
+-- Delete current tag
+-- Any rule set on the tag shall be broken
+function util.delete_tag()
+ local t = awful.screen.focused().selected_tag
+ if not t then return end
+ t:delete()
+end
+
+-- }}}
+
+-- On the fly useless gaps change
+function util.useless_gaps_resize(thatmuch, s, t)
+ local scr = s or awful.screen.focused()
+ local tag = t or scr.selected_tag
+ tag.gap = tag.gap + tonumber(thatmuch)
+ awful.layout.arrange(scr)
+end
+
+return setmetatable(util, { __index = wrequire })
diff --git a/.config/awesome/lain/util/markup.lua b/.config/awesome/lain/util/markup.lua
new file mode 100755
index 0000000..63f9486
--- /dev/null
+++ b/.config/awesome/lain/util/markup.lua
@@ -0,0 +1,66 @@
+--[[
+
+ Licensed under MIT License
+ * (c) 2013, Luca CPZ
+ * (c) 2009, Uli Schlachter
+ * (c) 2009, Majic
+
+--]]
+
+local format = string.format
+local setmetatable = setmetatable
+
+-- Lain markup util submodule
+-- lain.util.markup
+local markup = { fg = {}, bg = {} }
+
+-- Convenience tags
+function markup.bold(text) return format("%s", text) end
+function markup.italic(text) return format("%s", text) end
+function markup.strike(text) return format("%s", text) end
+function markup.underline(text) return format("%s", text) end
+function markup.monospace(text) return format("%s", text) end
+function markup.big(text) return format("%s", text) end
+function markup.small(text) return format("%s", text) end
+
+-- Set the font
+function markup.font(font, text)
+ return format("%s", font, text)
+end
+
+-- Set the foreground
+function markup.fg.color(color, text)
+ return format("%s", color, text)
+end
+
+-- Set the background
+function markup.bg.color(color, text)
+ return format("%s", color, text)
+end
+
+-- Set foreground and background
+function markup.color(fg, bg, text)
+ return format("%s", fg, bg, text)
+end
+
+-- Set font and foreground
+function markup.fontfg(font, fg, text)
+ return format("%s", font, fg, text)
+end
+
+-- Set font and background
+function markup.fontbg(font, bg, text)
+ return format("%s", font, bg, text)
+end
+
+-- Set font, foreground and background
+function markup.fontcolor(font, fg, bg, text)
+ return format("%s", font, fg, bg, text)
+end
+
+-- link markup.{fg,bg}(...) calls to markup.{fg,bg}.color(...)
+setmetatable(markup.fg, { __call = function(_, ...) return markup.fg.color(...) end })
+setmetatable(markup.bg, { __call = function(_, ...) return markup.bg.color(...) end })
+
+-- link markup(...) calls to markup.fg.color(...)
+return setmetatable(markup, { __call = function(_, ...) return markup.fg.color(...) end })
diff --git a/.config/awesome/lain/util/menu_iterator.lua b/.config/awesome/lain/util/menu_iterator.lua
new file mode 100755
index 0000000..d457473
--- /dev/null
+++ b/.config/awesome/lain/util/menu_iterator.lua
@@ -0,0 +1,144 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2017, Simon Désaulniers
+ * (c) 2017, Uli Schlachter
+ * (c) 2017, Jeferson Siqueira
+
+--]]
+
+-- Menu iterator with Naughty notifications
+-- lain.util.menu_iterator
+
+local naughty = require("naughty")
+local helpers = require("lain.helpers")
+local atable = require("awful.util").table
+local assert = assert
+local pairs = pairs
+local tconcat = table.concat
+local unpack = unpack or table.unpack -- lua 5.1 retro-compatibility
+
+local state = { cid = nil }
+
+local function naughty_destroy_callback(reason)
+ local closed = naughty.notificationClosedReason
+ if reason == closed.expired or reason == closed.dismissedByUser then
+ local actions = state.index and state.menu[state.index - 1][2]
+ if actions then
+ for _,action in pairs(actions) do
+ -- don't try to call nil callbacks
+ if action then action() end
+ end
+ state.index = nil
+ end
+ end
+end
+
+-- Iterates over a menu.
+-- After the timeout, callbacks associated to the last visited choice are
+-- executed. Inputs:
+-- * menu: a list of {label, {callbacks}} pairs
+-- * timeout: time to wait before confirming the menu selection
+-- * icon: icon to display in the notification of the chosen label
+local function iterate(menu, timeout, icon)
+ timeout = timeout or 4 -- default timeout for each menu entry
+ icon = icon or nil -- icon to display on the menu
+
+ -- Build the list of choices
+ if not state.index then
+ state.menu = menu
+ state.index = 1
+ end
+
+ -- Select one and display the appropriate notification
+ local label
+ local next = state.menu[state.index]
+ state.index = state.index + 1
+
+ if not next then
+ label = "Cancel"
+ state.index = nil
+ else
+ label, _ = unpack(next)
+ end
+
+ state.cid = naughty.notify({
+ text = label,
+ icon = icon,
+ timeout = timeout,
+ screen = mouse.screen,
+ replaces_id = state.cid,
+ destroy = naughty_destroy_callback
+ }).id
+end
+
+-- Generates a menu compatible with the first argument of `iterate` function and
+-- suitable for the following cases:
+-- * all possible choices individually (partition of singletons);
+-- * all possible subsets of the set of choices (powerset).
+--
+-- Inputs:
+-- * args: an array containing the following members:
+-- * choices: Array of choices (string) on which the menu will be
+-- generated.
+-- * name: Displayed name of the menu (in the form "name: choices").
+-- * selected_cb: Callback to execute for each selected choice. Takes
+-- the choice as a string argument. Can be `nil` (no action
+-- to execute).
+-- * rejected_cb: Callback to execute for each rejected choice (possible
+-- choices which are not selected). Takes the choice as a
+-- string argument. Can be `nil` (no action to execute).
+-- * extra_choices: An array of extra { choice_str, callback_fun } pairs to be
+-- added to the menu. Each callback_fun can be `nil`.
+-- * combination: The combination of choices to generate. Possible values:
+-- "powerset" and "single" (default).
+-- Output:
+-- * m: menu to be iterated over.
+local function menu(args)
+ local choices = assert(args.choices or args[1])
+ local name = assert(args.name or args[2])
+ local selected_cb = args.selected_cb
+ local rejected_cb = args.rejected_cb
+ local extra_choices = args.extra_choices or {}
+
+ local ch_combinations = args.combination == "powerset" and helpers.powerset(choices) or helpers.trivial_partition_set(choices)
+
+ for _, c in pairs(extra_choices) do
+ ch_combinations = atable.join(ch_combinations, {{c[1]}})
+ end
+
+ local m = {} -- the menu
+
+ for _,c in pairs(ch_combinations) do
+ if #c > 0 then
+ local cbs = {}
+
+ -- selected choices
+ for _,ch in pairs(c) do
+ if atable.hasitem(choices, ch) then
+ cbs[#cbs + 1] = selected_cb and function() selected_cb(ch) end or nil
+ end
+ end
+
+ -- rejected choices
+ for _,ch in pairs(choices) do
+ if not atable.hasitem(c, ch) and atable.hasitem(choices, ch) then
+ cbs[#cbs + 1] = rejected_cb and function() rejected_cb(ch) end or nil
+ end
+ end
+
+ -- add user extra choices (like the choice "None" for example)
+ for _,x in pairs(extra_choices) do
+ if x[1] == c[1] then
+ cbs[#cbs + 1] = x[2]
+ end
+ end
+
+ m[#m + 1] = { name .. ": " .. tconcat(c, " + "), cbs }
+ end
+ end
+
+ return m
+end
+
+return { iterate = iterate, menu = menu }
diff --git a/.config/awesome/lain/util/quake.lua b/.config/awesome/lain/util/quake.lua
new file mode 100755
index 0000000..8bc68a7
--- /dev/null
+++ b/.config/awesome/lain/util/quake.lua
@@ -0,0 +1,179 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2016, Luca CPZ
+ * (c) 2015, unknown
+
+--]]
+
+local awful = require("awful")
+local capi = { client = client }
+local math = math
+local string = string
+local pairs = pairs
+local screen = screen
+local setmetatable = setmetatable
+
+-- Quake-like Dropdown application spawn
+local quake = {}
+
+-- If you have a rule like "awful.client.setslave" for your terminals,
+-- ensure you use an exception for QuakeDD. Otherwise, you may
+-- run into problems with focus.
+
+function quake:display()
+ if self.followtag then self.screen = awful.screen.focused() end
+
+ -- First, we locate the client
+ local client = nil
+ local i = 0
+ for c in awful.client.iterate(function (c)
+ -- c.name may be changed!
+ return c.instance == self.name
+ end)
+ do
+ i = i + 1
+ if i == 1 then
+ client = c
+ else
+ -- Additional matching clients, let's remove the sticky bit
+ -- which may persist between awesome restarts. We don't close
+ -- them as they may be valuable. They will just turn into
+ -- normal clients.
+ c.sticky = false
+ c.ontop = false
+ c.above = false
+ end
+ end
+
+ if not client and not self.visible then return end
+
+ if not client then
+ -- The client does not exist, we spawn it
+ local cmd = string.format("%s %s %s", self.app,
+ string.format(self.argname, self.name), self.extra)
+ awful.spawn(cmd, { tag = self.screen.selected_tag })
+ return
+ end
+
+ -- Set geometry
+ client.floating = true
+ client.border_width = self.border
+ client.size_hints_honor = false
+ local maximized = client.maximized
+ local fullscreen = client.fullscreen
+ client:geometry(self.geometry[self.screen.index] or self:compute_size())
+
+ -- Set not sticky and on top
+ client.sticky = false
+ client.ontop = true
+ client.above = true
+ client.skip_taskbar = true
+
+ -- Additional user settings
+ if self.settings then self.settings(client) end
+
+ -- Toggle display
+ if self.visible then
+ client.hidden = false
+ client.maximized = self.maximized
+ client.fullscreen = self.fullscreen
+ client:raise()
+ self.last_tag = self.screen.selected_tag
+ client:tags({self.screen.selected_tag})
+ capi.client.focus = client
+ else
+ self.maximized = maximized
+ self.fullscreen = fullscreen
+ client.maximized = false
+ client.fullscreen = false
+ client.hidden = true
+ local ctags = client:tags()
+ for j, _ in pairs(ctags) do
+ ctags[j] = nil
+ end
+ client:tags(ctags)
+ end
+
+ return client
+end
+
+function quake:compute_size()
+ -- skip if we already have a geometry for this screen
+ if not self.geometry[self.screen.index] then
+ local geom
+ if not self.overlap then
+ geom = screen[self.screen.index].workarea
+ else
+ geom = screen[self.screen.index].geometry
+ end
+ local width, height = self.width, self.height
+ if width <= 1 then width = math.floor(geom.width * width) - 2 * self.border end
+ if height <= 1 then height = math.floor(geom.height * height) end
+ local x, y
+ if self.horiz == "left" then x = geom.x
+ elseif self.horiz == "right" then x = geom.width + geom.x - width
+ else x = geom.x + (geom.width - width)/2 end
+ if self.vert == "top" then y = geom.y
+ elseif self.vert == "bottom" then y = geom.height + geom.y - height
+ else y = geom.y + (geom.height - height)/2 end
+ self.geometry[self.screen.index] = { x = x, y = y, width = width, height = height }
+ end
+ return self.geometry[self.screen.index]
+end
+
+function quake:toggle()
+ if self.followtag then self.screen = awful.screen.focused() end
+ local current_tag = self.screen.selected_tag
+ if current_tag and self.last_tag ~= current_tag and self.visible then
+ local c=self:display()
+ if c then
+ c:move_to_tag(current_tag)
+ end
+ else
+ self.visible = not self.visible
+ self:display()
+ end
+end
+
+function quake.new(conf)
+ conf = conf or {}
+
+ conf.app = conf.app or "xterm" -- application to spawn
+ conf.name = conf.name or "QuakeDD" -- window name
+ conf.argname = conf.argname or "-name %s" -- how to specify window name
+ conf.extra = conf.extra or "" -- extra arguments
+ conf.border = conf.border or 1 -- client border width
+ conf.visible = conf.visible or false -- initially not visible
+ conf.followtag = conf.followtag or false -- spawn on currently focused screen
+ conf.overlap = conf.overlap or false -- overlap wibox
+ conf.screen = conf.screen or awful.screen.focused()
+ conf.settings = conf.settings
+
+ -- If width or height <= 1 this is a proportion of the workspace
+ conf.height = conf.height or 0.25 -- height
+ conf.width = conf.width or 1 -- width
+ conf.vert = conf.vert or "top" -- top, bottom or center
+ conf.horiz = conf.horiz or "left" -- left, right or center
+ conf.geometry = {} -- internal use
+
+ conf.maximized = false
+ conf.fullscreen = false
+
+ local dropdown = setmetatable(conf, { __index = quake })
+
+ capi.client.connect_signal("manage", function(c)
+ if c.instance == dropdown.name and c.screen == dropdown.screen then
+ dropdown:display()
+ end
+ end)
+ capi.client.connect_signal("unmanage", function(c)
+ if c.instance == dropdown.name and c.screen == dropdown.screen then
+ dropdown.visible = false
+ end
+ end)
+
+ return dropdown
+end
+
+return setmetatable(quake, { __call = function(_, ...) return quake.new(...) end })
diff --git a/.config/awesome/lain/util/separators.lua b/.config/awesome/lain/util/separators.lua
new file mode 100755
index 0000000..04402bb
--- /dev/null
+++ b/.config/awesome/lain/util/separators.lua
@@ -0,0 +1,118 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2015, Luca CPZ
+ * (c) 2015, plotnikovanton
+
+--]]
+
+local wibox = require("wibox")
+local gears = require("gears")
+local beautiful = require("beautiful")
+
+-- Lain Cairo separators util submodule
+-- lain.util.separators
+local separators = { height = beautiful.separators_height or 0, width = beautiful.separators_width or 9 }
+
+-- [[ Arrow
+
+-- Right
+function separators.arrow_right(col1, col2)
+ local widget = wibox.widget.base.make_widget()
+ widget.col1 = col1
+ widget.col2 = col2
+
+ widget.fit = function(_, _, _)
+ return separators.width, separators.height
+ end
+
+ widget.update = function(_, _)
+ widget.col1 = col1
+ widget.col2 = col2
+ widget:emit_signal("widget::redraw_needed")
+ end
+
+ widget.draw = function(_, _, cr, width, height)
+ if widget.col2 ~= "alpha" then
+ cr:set_source_rgba(gears.color.parse_color(widget.col2))
+ cr:new_path()
+ cr:move_to(0, 0)
+ cr:line_to(width, height/2)
+ cr:line_to(width, 0)
+ cr:close_path()
+ cr:fill()
+
+ cr:new_path()
+ cr:move_to(0, height)
+ cr:line_to(width, height/2)
+ cr:line_to(width, height)
+ cr:close_path()
+ cr:fill()
+ end
+
+ if widget.col1 ~= "alpha" then
+ cr:set_source_rgba(gears.color.parse_color(widget.col1))
+ cr:new_path()
+ cr:move_to(0, 0)
+ cr:line_to(width, height/2)
+ cr:line_to(0, height)
+ cr:close_path()
+ cr:fill()
+ end
+ end
+
+ return widget
+end
+
+-- Left
+function separators.arrow_left(col1, col2)
+ local widget = wibox.widget.base.make_widget()
+ widget.col1 = col1
+ widget.col2 = col2
+
+ widget.fit = function(_, _, _)
+ return separators.width, separators.height
+ end
+
+ widget.update = function(c1, c2)
+ widget.col1 = c1
+ widget.col2 = c2
+ widget:emit_signal("widget::redraw_needed")
+ end
+
+ widget.draw = function(_, _, cr, width, height)
+ if widget.col1 ~= "alpha" then
+ cr:set_source_rgba(gears.color.parse_color(widget.col1))
+ cr:new_path()
+ cr:move_to(width, 0)
+ cr:line_to(0, height/2)
+ cr:line_to(0, 0)
+ cr:close_path()
+ cr:fill()
+
+ cr:new_path()
+ cr:move_to(width, height)
+ cr:line_to(0, height/2)
+ cr:line_to(0, height)
+ cr:close_path()
+ cr:fill()
+ end
+
+ if widget.col2 ~= "alpha" then
+ cr:new_path()
+ cr:move_to(width, 0)
+ cr:line_to(0, height/2)
+ cr:line_to(width, height)
+ cr:close_path()
+
+ cr:set_source_rgba(gears.color.parse_color(widget.col2))
+ cr:fill()
+ end
+ end
+
+ return widget
+end
+
+-- ]]
+
+return separators
diff --git a/.config/awesome/lain/widget/alsa.lua b/.config/awesome/lain/widget/alsa.lua
new file mode 100755
index 0000000..202dc98
--- /dev/null
+++ b/.config/awesome/lain/widget/alsa.lua
@@ -0,0 +1,54 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010, Adrian C.
+
+--]]
+
+local helpers = require("lain.helpers")
+local shell = require("awful.util").shell
+local wibox = require("wibox")
+local string = string
+
+-- ALSA volume
+-- lain.widget.alsa
+
+local function factory(args)
+ args = args or {}
+ local alsa = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 5
+ local settings = args.settings or function() end
+
+ alsa.cmd = args.cmd or "amixer"
+ alsa.channel = args.channel or "Master"
+ alsa.togglechannel = args.togglechannel
+
+ local format_cmd = string.format("%s get %s", alsa.cmd, alsa.channel)
+
+ if alsa.togglechannel then
+ format_cmd = { shell, "-c", string.format("%s get %s; %s get %s",
+ alsa.cmd, alsa.channel, alsa.cmd, alsa.togglechannel) }
+ end
+
+ alsa.last = {}
+
+ function alsa.update()
+ helpers.async(format_cmd, function(mixer)
+ local l,s = string.match(mixer, "([%d]+)%%.*%[([%l]*)")
+ l = tonumber(l)
+ if alsa.last.level ~= l or alsa.last.status ~= s then
+ volume_now = { level = l, status = s }
+ widget = alsa.widget
+ settings()
+ alsa.last = volume_now
+ end
+ end)
+ end
+
+ helpers.newtimer(string.format("alsa-%s-%s", alsa.cmd, alsa.channel), timeout, alsa.update)
+
+ return alsa
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/alsabar.lua b/.config/awesome/lain/widget/alsabar.lua
new file mode 100755
index 0000000..8e8cd3a
--- /dev/null
+++ b/.config/awesome/lain/widget/alsabar.lua
@@ -0,0 +1,166 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2013, Rman
+
+--]]
+
+local helpers = require("lain.helpers")
+local awful = require("awful")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local math = math
+local string = string
+local type = type
+local tonumber = tonumber
+
+-- ALSA volume bar
+-- lain.widget.alsabar
+
+local function factory(args)
+ local alsabar = {
+ colors = {
+ background = "#000000",
+ mute = "#EB8F8F",
+ unmute = "#A4CE8A"
+ },
+
+ _current_level = 0,
+ _playback = "off"
+ }
+
+ args = args or {}
+
+ local timeout = args.timeout or 5
+ local settings = args.settings or function() end
+ local width = args.width or 63
+ local height = args.height or 1
+ local margins = args.margins or 1
+ local ticks = args.ticks or false
+ local ticks_size = args.ticks_size or 7
+ local tick = args.tick or "|"
+ local tick_pre = args.tick_pre or "["
+ local tick_post = args.tick_post or "]"
+ local tick_none = args.tick_none or " "
+
+ alsabar.cmd = args.cmd or "amixer"
+ alsabar.channel = args.channel or "Master"
+ alsabar.togglechannel = args.togglechannel
+ alsabar.colors = args.colors or alsabar.colors
+ alsabar.followtag = args.followtag or false
+ alsabar.notification_preset = args.notification_preset
+
+ if not alsabar.notification_preset then
+ alsabar.notification_preset = { font = "Monospace 10" }
+ end
+
+ local format_cmd = string.format("%s get %s", alsabar.cmd, alsabar.channel)
+
+ if alsabar.togglechannel then
+ format_cmd = { awful.util.shell, "-c", string.format("%s get %s; %s get %s",
+ alsabar.cmd, alsabar.channel, alsabar.cmd, alsabar.togglechannel) }
+ end
+
+ alsabar.bar = wibox.widget {
+ color = alsabar.colors.unmute,
+ background_color = alsabar.colors.background,
+ forced_height = height,
+ forced_width = width,
+ margins = margins,
+ paddings = margins,
+ ticks = ticks,
+ ticks_size = ticks_size,
+ widget = wibox.widget.progressbar
+ }
+
+ alsabar.tooltip = awful.tooltip({ objects = { alsabar.bar } })
+
+ function alsabar.update(callback)
+ helpers.async(format_cmd, function(mixer)
+ local vol, playback = string.match(mixer, "([%d]+)%%.*%[([%l]*)")
+
+ if not vol or not playback then return end
+
+ if vol ~= alsabar._current_level or playback ~= alsabar._playback then
+ alsabar._current_level = tonumber(vol)
+ alsabar.bar:set_value(alsabar._current_level / 100)
+ if alsabar._current_level == 0 or playback == "off" then
+ alsabar._playback = playback
+ alsabar.tooltip:set_text("[Muted]")
+ alsabar.bar.color = alsabar.colors.mute
+ else
+ alsabar._playback = "on"
+ alsabar.tooltip:set_text(string.format("%s: %s", alsabar.channel, vol))
+ alsabar.bar.color = alsabar.colors.unmute
+ end
+
+ volume_now = {
+ level = alsabar._current_level,
+ status = alsabar._playback
+ }
+
+ settings()
+
+ if type(callback) == "function" then callback() end
+ end
+ end)
+ end
+
+ function alsabar.notify()
+ alsabar.update(function()
+ local preset = alsabar.notification_preset
+
+ preset.title = string.format("%s - %s%%", alsabar.channel, alsabar._current_level)
+
+ if alsabar._playback == "off" then
+ preset.title = preset.title .. " Muted"
+ end
+
+ -- tot is the maximum number of ticks to display in the notification
+ local tot = alsabar.notification_preset.max_ticks
+
+ if not tot then
+ local wib = awful.screen.focused().mywibox
+ -- if we can grab mywibox, tot is defined as its height if
+ -- horizontal, or width otherwise
+ if wib then
+ if wib.position == "left" or wib.position == "right" then
+ tot = wib.width
+ else
+ tot = wib.height
+ end
+ -- fallback: default horizontal wibox height
+ else
+ tot = 20
+ end
+ end
+
+ local int = math.modf((alsabar._current_level / 100) * tot)
+ preset.text = string.format(
+ "%s%s%s%s",
+ tick_pre,
+ string.rep(tick, int),
+ string.rep(tick_none, tot - int),
+ tick_post
+ )
+
+ if alsabar.followtag then preset.screen = awful.screen.focused() end
+
+ if not alsabar.notification then
+ alsabar.notification = naughty.notify {
+ preset = preset,
+ destroy = function() alsabar.notification = nil end
+ }
+ else
+ naughty.replace_text(alsabar.notification, preset.title, preset.text)
+ end
+ end)
+ end
+
+ helpers.newtimer(string.format("alsabar-%s-%s", alsabar.cmd, alsabar.channel), timeout, alsabar.update)
+
+ return alsabar
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/bat.lua b/.config/awesome/lain/widget/bat.lua
new file mode 100755
index 0000000..260f4b9
--- /dev/null
+++ b/.config/awesome/lain/widget/bat.lua
@@ -0,0 +1,236 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local helpers = require("lain.helpers")
+local fs = require("gears.filesystem")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local math = math
+local string = string
+local ipairs = ipairs
+local tonumber = tonumber
+
+-- Battery infos
+-- lain.widget.bat
+
+local function factory(args)
+ local pspath = args.pspath or "/sys/class/power_supply/"
+
+ if not fs.is_dir(pspath) then
+ naughty.notify { text = "lain.widget.bat: invalid power supply path", timeout = 0 }
+ return
+ end
+
+ args = args or {}
+
+ local bat = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 30
+ local notify = args.notify or "on"
+ local full_notify = args.full_notify or notify
+ local n_perc = args.n_perc or { 5, 15 }
+ local batteries = args.batteries or (args.battery and {args.battery}) or {}
+ local ac = args.ac or "AC0"
+ local settings = args.settings or function() end
+
+ function bat.get_batteries()
+ helpers.line_callback("ls -1 " .. pspath, function(line)
+ local bstr = string.match(line, "BAT%w+")
+ if bstr then
+ batteries[#batteries + 1] = bstr
+ else
+ ac = string.match(line, "A%w+") or ac
+ end
+ end)
+ end
+
+ if #batteries == 0 then bat.get_batteries() end
+
+ bat_notification_critical_preset = {
+ title = "Battery exhausted",
+ text = "Shutdown imminent",
+ timeout = 15,
+ fg = "#000000",
+ bg = "#FFFFFF"
+ }
+
+ bat_notification_low_preset = {
+ title = "Battery low",
+ text = "Plug the cable!",
+ timeout = 15,
+ fg = "#202020",
+ bg = "#CDCDCD"
+ }
+
+ bat_notification_charged_preset = {
+ title = "Battery full",
+ text = "You can unplug the cable",
+ timeout = 15,
+ fg = "#202020",
+ bg = "#CDCDCD"
+ }
+
+ bat_now = {
+ status = "N/A",
+ ac_status = "N/A",
+ perc = "N/A",
+ time = "N/A",
+ watt = "N/A",
+ capacity = "N/A"
+ }
+
+ bat_now.n_status = {}
+ bat_now.n_perc = {}
+ bat_now.n_capacity = {}
+ for i = 1, #batteries do
+ bat_now.n_status[i] = "N/A"
+ bat_now.n_perc[i] = 0
+ bat_now.n_capacity[i] = 0
+ end
+
+ -- used to notify full charge only once before discharging
+ local fullnotification = false
+
+ function bat.update()
+ -- luacheck: globals bat_now
+ local sum_rate_current = 0
+ local sum_rate_voltage = 0
+ local sum_rate_power = 0
+ local sum_rate_energy = 0
+ local sum_energy_now = 0
+ local sum_energy_full = 0
+ local sum_charge_full = 0
+ local sum_charge_design = 0
+
+ for i, battery in ipairs(batteries) do
+ local bstr = pspath .. battery
+ local present = helpers.first_line(bstr .. "/present")
+
+ if tonumber(present) == 1 then
+ -- current_now(I)[uA], voltage_now(U)[uV], power_now(P)[uW]
+ local rate_current = tonumber(helpers.first_line(bstr .. "/current_now"))
+ local rate_voltage = tonumber(helpers.first_line(bstr .. "/voltage_now"))
+ local rate_power = tonumber(helpers.first_line(bstr .. "/power_now"))
+ local charge_full = tonumber(helpers.first_line(bstr .. "/charge_full"))
+ local charge_design = tonumber(helpers.first_line(bstr .. "/charge_full_design"))
+
+ -- energy_now(P)[uWh], charge_now(I)[uAh]
+ local energy_now = tonumber(helpers.first_line(bstr .. "/energy_now") or
+ helpers.first_line(bstr .. "/charge_now"))
+
+ -- energy_full(P)[uWh], charge_full(I)[uAh]
+ local energy_full = tonumber(helpers.first_line(bstr .. "/energy_full") or
+ charge_full)
+
+ local energy_percentage = tonumber(helpers.first_line(bstr .. "/capacity")) or
+ math.floor((energy_now / energy_full) * 100)
+
+ bat_now.n_status[i] = helpers.first_line(bstr .. "/status") or "N/A"
+ bat_now.n_perc[i] = energy_percentage or bat_now.n_perc[i]
+
+ if not charge_design or charge_design == 0 then
+ bat_now.n_capacity[i] = 0
+ else
+ bat_now.n_capacity[i] = math.floor((charge_full / charge_design) * 100)
+ end
+
+ sum_rate_current = sum_rate_current + (rate_current or 0)
+ sum_rate_voltage = sum_rate_voltage + (rate_voltage or 0)
+ sum_rate_power = sum_rate_power + (rate_power or 0)
+ sum_rate_energy = sum_rate_energy + (rate_power or (((rate_voltage or 0) * (rate_current or 0)) / 1e6))
+ sum_energy_now = sum_energy_now + (energy_now or 0)
+ sum_energy_full = sum_energy_full + (energy_full or 0)
+ sum_charge_full = sum_charge_full + (charge_full or 0)
+ sum_charge_design = sum_charge_design + (charge_design or 0)
+ end
+ end
+
+ bat_now.capacity = math.floor(math.min(100, (sum_charge_full / sum_charge_design) * 100))
+
+ -- When one of the battery is charging, others' status are either
+ -- "Full", "Unknown" or "Charging". When the laptop is not plugged in,
+ -- one or more of the batteries may be full, but only one battery
+ -- discharging suffices to set global status to "Discharging".
+ bat_now.status = bat_now.n_status[1] or "N/A"
+ for _,status in ipairs(bat_now.n_status) do
+ if status == "Discharging" or status == "Charging" then
+ bat_now.status = status
+ end
+ end
+ bat_now.ac_status = tonumber(helpers.first_line(string.format("%s%s/online", pspath, ac))) or "N/A"
+
+ if bat_now.status ~= "N/A" then
+ if bat_now.status ~= "Full" and sum_rate_power == 0 and bat_now.ac_status == 1 then
+ bat_now.perc = math.floor(math.min(100, (sum_energy_now / sum_energy_full) * 100))
+ bat_now.time = "00:00"
+ bat_now.watt = 0
+
+ -- update {perc,time,watt} iff battery not full and rate > 0
+ elseif bat_now.status ~= "Full" then
+ local rate_time = 0
+ -- Calculate time and watt if rates are greater then 0
+ if (sum_rate_power > 0 or sum_rate_current > 0) then
+ local div = (sum_rate_power > 0 and sum_rate_power) or sum_rate_current
+
+ if bat_now.status == "Charging" then
+ rate_time = (sum_energy_full - sum_energy_now) / div
+ else -- Discharging
+ rate_time = sum_energy_now / div
+ end
+
+ if 0 < rate_time and rate_time < 0.01 then -- check for magnitude discrepancies (#199)
+ rate_time_magnitude = math.abs(math.floor(math.log10(rate_time)))
+ rate_time = rate_time * 10^(rate_time_magnitude - 2)
+ end
+ end
+
+ local hours = math.floor(rate_time)
+ local minutes = math.floor((rate_time - hours) * 60)
+ bat_now.perc = math.floor(math.min(100, (sum_energy_now / sum_energy_full) * 100))
+ bat_now.time = string.format("%02d:%02d", hours, minutes)
+ bat_now.watt = tonumber(string.format("%.2f", sum_rate_energy / 1e6))
+ elseif bat_now.status == "Full" then
+ bat_now.perc = 100
+ bat_now.time = "00:00"
+ bat_now.watt = 0
+ end
+ end
+
+ widget = bat.widget
+ settings()
+
+ -- notifications for critical, low, and full levels
+ if notify == "on" then
+ if bat_now.status == "Discharging" then
+ if tonumber(bat_now.perc) <= n_perc[1] then
+ bat.id = naughty.notify({
+ preset = bat_notification_critical_preset,
+ replaces_id = bat.id
+ }).id
+ elseif tonumber(bat_now.perc) <= n_perc[2] then
+ bat.id = naughty.notify({
+ preset = bat_notification_low_preset,
+ replaces_id = bat.id
+ }).id
+ end
+ fullnotification = false
+ elseif bat_now.status == "Full" and full_notify == "on" and not fullnotification then
+ bat.id = naughty.notify({
+ preset = bat_notification_charged_preset,
+ replaces_id = bat.id
+ }).id
+ fullnotification = true
+ end
+ end
+ end
+
+ helpers.newtimer("batteries", timeout, bat.update)
+
+ return bat
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/cal.lua b/.config/awesome/lain/widget/cal.lua
new file mode 100755
index 0000000..7543684
--- /dev/null
+++ b/.config/awesome/lain/widget/cal.lua
@@ -0,0 +1,190 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2018, Luca CPZ
+
+--]]
+
+local helpers = require("lain.helpers")
+local markup = require("lain.util.markup")
+local awful = require("awful")
+local naughty = require("naughty")
+local floor = math.floor
+local os = os
+local pairs = pairs
+local string = string
+local tconcat = table.concat
+local type = type
+local tonumber = tonumber
+local tostring = tostring
+local utf8 = utf8
+
+-- Calendar notification
+-- lain.widget.cal
+
+local function factory(args)
+ args = args or {}
+ local cal = {
+ attach_to = args.attach_to or {},
+ week_start = args.week_start or 2,
+ three = args.three or false,
+ followtag = args.followtag or false,
+ week_number = args.week_number or "none",
+ week_number_format = args.week_number_format or args.week_number == "left" and "%3d | " or "| %-3d",
+ icons = args.icons or helpers.icons_dir .. "cal/white/",
+ notification_preset = args.notification_preset or {
+ font = "Monospace 10", fg = "#FFFFFF", bg = "#000000"
+ }
+ }
+
+ function cal.get_week_number(m, st_day, x)
+ local date = os.date("*t", m)
+
+ local week_step = (x ~= 0 and floor((x + st_day) / 7) - 1 or 0);
+
+ local display_time = os.time {
+ year = date.year, month = date.month, day = date.day + 7 * week_step
+ }
+
+ return string.format(cal.week_number_format, os.date("%V", display_time))
+ end
+
+ function cal.sum_week_days(x, y)
+ return (x + y) % 7
+ end
+
+ function cal.build(month, year)
+ local current_month, current_year = tonumber(os.date("%m")), tonumber(os.date("%Y"))
+ local is_current_month = (not month or not year) or (month == current_month and year == current_year)
+ local today = is_current_month and tonumber(os.date("%d")) -- otherwise nil and not highlighted
+ local t = os.time { year = year or current_year, month = month and month+1 or current_month+1, day = 0 }
+ local d = os.date("*t", t)
+ local mth_days, st_day, this_month = d.day, (d.wday-d.day-cal.week_start+1)%7, os.date("%B %Y", t)
+ local notifytable = { [1] = string.format("%s%s\n", string.rep(" ", floor((28 - this_month:len())/2)), markup.bold(this_month)) }
+ for x = 0,6 do notifytable[#notifytable+1] = os.date("%a", os.time { year=2006, month=1, day=x+cal.week_start }):sub(1, utf8.offset(1, 3)) .. " " end
+ notifytable[#notifytable] = string.format("%s\n%s", notifytable[#notifytable]:sub(1, -2), string.rep(" ", st_day*4))
+ local strx
+ for x = 1,mth_days do
+ strx = x
+ if x == today then
+ if x < 10 then x = " " .. x end
+ strx = markup.bold(markup.color(cal.notification_preset.bg, cal.notification_preset.fg, x) .. " ")
+ end
+ strx = string.format("%s%s", string.rep(" ", 3 - tostring(x):len()), strx)
+ notifytable[#notifytable+1] = string.format("%-4s%s", strx, (x+st_day)%7==0 and x ~= mth_days and "\n" or "")
+ end
+ if string.len(cal.icons or "") > 0 and today then cal.icon = cal.icons .. today .. ".png" end
+ cal.month, cal.year = d.month, d.year
+
+ if cal.week_number ~= "none" then
+ local m = os.time { year = year or current_year, month = month and month or current_month, day = 1 }
+ local head_prepend = string.rep(" ", tostring(string.format(cal.week_number_format, 0)):len())
+
+ if cal.week_number == "left" then
+ notifytable[1] = head_prepend .. notifytable[1] -- month-year row
+ notifytable[2] = head_prepend .. notifytable[2] -- weekdays row
+ notifytable[8] = notifytable[8]:gsub("\n", "\n" .. cal.get_week_number(m, st_day, 0)) -- first week of the month
+
+ for x = 10,#notifytable do
+ if cal.sum_week_days(st_day, x) == 2 then
+ notifytable[x] = cal.get_week_number(m, st_day, x) .. notifytable[x]
+ end
+ end
+ elseif cal.week_number == "right" then
+ notifytable[8] = notifytable[8]:gsub("\n", head_prepend .. "\n") -- weekdays row
+ for x = 9,#notifytable do
+ if cal.sum_week_days(st_day, x) == 1 then
+ notifytable[x] = notifytable[x]:gsub("\n", cal.get_week_number(m, st_day, x - 7) .. "\n")
+ end
+ end
+ -- last week of the month
+ local end_days = cal.sum_week_days(st_day, mth_days)
+ if end_days ~= 0 then end_days = 7 - end_days end
+ notifytable[#notifytable] = notifytable[#notifytable] .. string.rep(" ", 4 * end_days) .. cal.get_week_number(m, st_day, mth_days + end_days)
+ end
+ end
+
+ return notifytable
+ end
+
+ function cal.getdate(month, year, offset)
+ if not month or not year then
+ month = tonumber(os.date("%m"))
+ year = tonumber(os.date("%Y"))
+ end
+
+ month = month + offset
+
+ while month > 12 do
+ month = month - 12
+ year = year + 1
+ end
+
+ while month < 1 do
+ month = month + 12
+ year = year - 1
+ end
+
+ return month, year
+ end
+
+ function cal.hide()
+ if not cal.notification then return end
+ naughty.destroy(cal.notification)
+ cal.notification = nil
+ end
+
+ function cal.show(seconds, month, year, scr)
+ local text = tconcat(cal.build(month, year))
+
+ if cal.three then
+ local current_month, current_year = cal.month, cal.year
+ local prev_month, prev_year = cal.getdate(cal.month, cal.year, -1)
+ local next_month, next_year = cal.getdate(cal.month, cal.year, 1)
+ text = string.format("%s\n\n%s\n\n%s",
+ tconcat(cal.build(prev_month, prev_year)), text,
+ tconcat(cal.build(next_month, next_year)))
+ cal.month, cal.year = current_month, current_year
+ end
+
+ if cal.notification then
+ local title = cal.notification_preset.title or nil
+ naughty.replace_text(cal.notification, title, text)
+ return
+ end
+
+ cal.notification = naughty.notify {
+ preset = cal.notification_preset,
+ screen = cal.followtag and awful.screen.focused() or scr or 1,
+ icon = cal.icon,
+ timeout = type(seconds) == "number" and seconds or cal.notification_preset.timeout or 5,
+ text = text
+ }
+ end
+
+ function cal.hover_on() cal.show(0) end
+ function cal.move(offset)
+ offset = offset or 0
+ cal.month, cal.year = cal.getdate(cal.month, cal.year, offset)
+ cal.show(0, cal.month, cal.year)
+ end
+ function cal.prev() cal.move(-1) end
+ function cal.next() cal.move( 1) end
+
+ function cal.attach(widget)
+ widget:connect_signal("mouse::enter", cal.hover_on)
+ widget:connect_signal("mouse::leave", cal.hide)
+ widget:buttons(awful.util.table.join(
+ awful.button({}, 1, cal.prev),
+ awful.button({}, 3, cal.next),
+ awful.button({}, 2, cal.hover_on),
+ awful.button({}, 5, cal.prev),
+ awful.button({}, 4, cal.next)))
+ end
+
+ for _, widget in pairs(cal.attach_to) do cal.attach(widget) end
+
+ return cal
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/contrib/init.lua b/.config/awesome/lain/widget/contrib/init.lua
new file mode 100755
index 0000000..9e863a5
--- /dev/null
+++ b/.config/awesome/lain/widget/contrib/init.lua
@@ -0,0 +1,18 @@
+--[[
+
+ Lain
+ Layouts, widgets and utilities for Awesome WM
+
+ Users contributed widgets section
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+
+--]]
+
+local wrequire = require("lain.helpers").wrequire
+local setmetatable = setmetatable
+
+local widget = { _NAME = "lain.widget.contrib" }
+
+return setmetatable(widget, { __index = wrequire })
diff --git a/.config/awesome/lain/widget/contrib/moc.lua b/.config/awesome/lain/widget/contrib/moc.lua
new file mode 100755
index 0000000..ad6452e
--- /dev/null
+++ b/.config/awesome/lain/widget/contrib/moc.lua
@@ -0,0 +1,97 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2014, anticlockwise
+
+--]]
+
+local helpers = require("lain.helpers")
+local shell = require("awful.util").shell
+local focused = require("awful.screen").focused
+local escape_f = require("awful.util").escape
+local naughty = require("naughty")
+local wibox = require("wibox")
+local os = os
+local string = string
+
+-- MOC audio player
+-- lain.widget.contrib.moc
+
+local function factory(args)
+ args = args or {}
+
+ local moc = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 2
+ local music_dir = args.music_dir or os.getenv("HOME") .. "/Music"
+ local cover_pattern = args.cover_pattern or "*\\.(jpg|jpeg|png|gif)$"
+ local cover_size = args.cover_size or 100
+ local default_art = args.default_art or ""
+ local followtag = args.followtag or false
+ local settings = args.settings or function() end
+
+ moc_notification_preset = { title = "Now playing", timeout = 6 }
+
+ helpers.set_map("current moc track", nil)
+
+ function moc.update()
+ helpers.async("mocp -i", function(f)
+ moc_now = {
+ state = "N/A",
+ file = "N/A",
+ artist = "N/A",
+ title = "N/A",
+ album = "N/A",
+ elapsed = "N/A",
+ total = "N/A"
+ }
+
+ for line in string.gmatch(f, "[^\n]+") do
+ for k, v in string.gmatch(line, "([%w]+):[%s](.*)$") do
+ if k == "State" then moc_now.state = v
+ elseif k == "File" then moc_now.file = v
+ elseif k == "Artist" then moc_now.artist = escape_f(v)
+ elseif k == "SongTitle" then moc_now.title = escape_f(v)
+ elseif k == "Album" then moc_now.album = escape_f(v)
+ elseif k == "CurrentTime" then moc_now.elapsed = escape_f(v)
+ elseif k == "TotalTime" then moc_now.total = escape_f(v)
+ end
+ end
+ end
+
+ moc_notification_preset.text = string.format("%s (%s) - %s\n%s", moc_now.artist,
+ moc_now.album, moc_now.total, moc_now.title)
+ widget = moc.widget
+ settings()
+
+ if moc_now.state == "PLAY" then
+ if moc_now.title ~= helpers.get_map("current moc track") then
+ helpers.set_map("current moc track", moc_now.title)
+
+ if followtag then moc_notification_preset.screen = focused() end
+
+ local common = {
+ preset = moc_notification_preset,
+ icon = default_art,
+ icon_size = cover_size,
+ replaces_id = moc.id,
+ }
+
+ local path = string.format("%s/%s", music_dir, string.match(moc_now.file, ".*/"))
+ local cover = string.format("find '%s' -maxdepth 1 -type f | egrep -i -m1 '%s'", path, cover_pattern)
+ helpers.async({ shell, "-c", cover }, function(current_icon)
+ common.icon = current_icon:gsub("\n", "")
+ moc.id = naughty.notify(common).id
+ end)
+ end
+ elseif moc_now.state ~= "PAUSE" then
+ helpers.set_map("current moc track", nil)
+ end
+ end)
+ end
+
+ moc.timer = helpers.newtimer("moc", timeout, moc.update, true, true)
+
+ return moc
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/contrib/redshift.lua b/.config/awesome/lain/widget/contrib/redshift.lua
new file mode 100755
index 0000000..d91d941
--- /dev/null
+++ b/.config/awesome/lain/widget/contrib/redshift.lua
@@ -0,0 +1,54 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2017, Luca CPZ
+ * (c) 2014, blueluke
+
+--]]
+
+local async = require("lain.helpers").async
+local awful = require("awful")
+local execute = os.execute
+local type = type
+
+-- Redshift
+-- lain.widget.contrib.redshift
+local redshift = { active = false, pid = nil }
+
+function redshift.start()
+ execute("pkill redshift")
+ awful.spawn.with_shell("redshift -x") -- clear adjustments
+ redshift.pid = awful.spawn.with_shell("redshift")
+ redshift.active = true
+ if type(redshift.update_fun) == "function" then
+ redshift.update_fun(redshift.active)
+ end
+end
+
+function redshift.toggle()
+ async({ awful.util.shell, "-c", string.format("ps -p %d -o pid=", redshift.pid) }, function(f)
+ if f and #f > 0 then -- redshift is running
+ -- Sending -USR1 toggles redshift (See project website)
+ execute("pkill -USR1 redshift")
+ redshift.active = not redshift.active
+ else -- not started or killed, (re)start it
+ redshift.start()
+ end
+ redshift.update_fun(redshift.active)
+ end)
+end
+
+-- Attach to a widget
+-- Provides a button which toggles redshift on/off on click
+-- @param widget: Widget to attach to.
+-- @param fun: Function to be run each time redshift is toggled (optional).
+-- Use it to update widget text or icons on status change.
+function redshift.attach(widget, fun)
+ redshift.update_fun = fun or function() end
+ if not redshift.pid then redshift.start() end
+ if widget then
+ widget:buttons(awful.util.table.join(awful.button({}, 1, function () redshift.toggle() end)))
+ end
+end
+
+return redshift
diff --git a/.config/awesome/lain/widget/contrib/task.lua b/.config/awesome/lain/widget/contrib/task.lua
new file mode 100755
index 0000000..2311996
--- /dev/null
+++ b/.config/awesome/lain/widget/contrib/task.lua
@@ -0,0 +1,92 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Jan Xie
+
+--]]
+
+local helpers = require("lain.helpers")
+local markup = require("lain.util").markup
+local awful = require("awful")
+local naughty = require("naughty")
+local mouse = mouse
+
+-- Taskwarrior notification
+-- lain.widget.contrib.task
+local task = {}
+
+function task.hide()
+ if not task.notification then return end
+ naughty.destroy(task.notification)
+ task.notification = nil
+end
+
+function task.show(scr)
+ task.notification_preset.screen = task.followtag and awful.screen.focused() or scr or 1
+
+ helpers.async({ awful.util.shell, "-c", task.show_cmd }, function(f)
+ local widget_focused = true
+
+ if mouse.current_widgets then
+ widget_focused = false
+ for _,v in ipairs(mouse.current_widgets) do
+ if task.widget == v then
+ widget_focused = true
+ break
+ end
+ end
+ end
+
+ if widget_focused then
+ task.hide()
+ task.notification = naughty.notify {
+ preset = task.notification_preset,
+ title = "task next",
+ text = markup.font(task.notification_preset.font,
+ awful.util.escape(f:gsub("\n*$", "")))
+ }
+ end
+ end)
+end
+
+function task.prompt()
+ awful.prompt.run {
+ prompt = task.prompt_text,
+ textbox = awful.screen.focused().mypromptbox.widget,
+ exe_callback = function(t)
+ helpers.async(t, function(f)
+ naughty.notify {
+ preset = task.notification_preset,
+ title = t,
+ text = markup.font(task.notification_preset.font,
+ awful.util.escape(f:gsub("\n*$", "")))
+ }
+ end)
+ end,
+ history_path = awful.util.getdir("cache") .. "/history_task"
+ }
+end
+
+function task.attach(widget, args)
+ args = args or {}
+
+ task.show_cmd = args.show_cmd or "task next"
+ task.prompt_text = args.prompt_text or "Enter task command: "
+ task.followtag = args.followtag or false
+ task.notification_preset = args.notification_preset
+ task.widget = widget
+
+ if not task.notification_preset then
+ task.notification_preset = {
+ font = "Monospace 10",
+ icon = helpers.icons_dir .. "/taskwarrior.png"
+ }
+ end
+
+ if widget then
+ widget:connect_signal("mouse::enter", function () task.show() end)
+ widget:connect_signal("mouse::leave", function () task.hide() end)
+ end
+end
+
+return task
diff --git a/.config/awesome/lain/widget/contrib/tp_smapi.lua b/.config/awesome/lain/widget/contrib/tp_smapi.lua
new file mode 100755
index 0000000..87c5e51
--- /dev/null
+++ b/.config/awesome/lain/widget/contrib/tp_smapi.lua
@@ -0,0 +1,147 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2018, Luca CPZ
+ * (c) 2013, Conor Heine
+
+--]]
+
+local helpers = require("lain.helpers")
+local focused = require("awful.screen").focused
+local naughty = require("naughty")
+local wibox = require("wibox")
+local string = string
+local type = type
+
+-- ThinkPad battery infos and widget creator
+-- http://www.thinkwiki.org/wiki/Tp_smapi
+-- lain.widget.contrib.tp_smapi
+
+local function factory(apipath)
+ local tp_smapi = {
+ path = apipath or "/sys/devices/platform/smapi"
+ }
+
+ function tp_smapi.get(batid, feature)
+ return helpers.first_line(string.format("%s/%s/%s", tp_smapi.path, batid or "BAT0", feature or ""))
+ end
+
+ function tp_smapi.installed(batid)
+ return tp_smapi.get(batid, "installed") == "1"
+ end
+
+ function tp_smapi.status(batid)
+ return tp_smapi.get(batid, "state")
+ end
+
+ function tp_smapi.percentage(batid)
+ return tp_smapi.get(batid, "remaining_percent")
+ end
+
+ -- either running or charging time
+ function tp_smapi.time(batid)
+ local status = tp_smapi.status(batid)
+ local mins_left = tp_smapi.get(batid, string.match(string.lower(status), "discharging") and "remaining_running_time" or "remaining_charging_time")
+ if not string.find(mins_left, "^%d+") then return "N/A" end
+ return string.format("%02d:%02d", math.floor(mins_left / 60), mins_left % 60) -- HH:mm
+ end
+
+ function tp_smapi.hide()
+ if not tp_smapi.notification then return end
+ naughty.destroy(tp_smapi.notification)
+ tp_smapi.notification = nil
+ end
+
+ function tp_smapi.show(batid, seconds, scr)
+ if not tp_smapi.installed(batid) then return end
+
+ local mfgr = tp_smapi.get(batid, "manufacturer") or "no_mfgr"
+ local model = tp_smapi.get(batid, "model") or "no_model"
+ local chem = tp_smapi.get(batid, "chemistry") or "no_chem"
+ local status = tp_smapi.get(batid, "state")
+ local time = tp_smapi.time(batid)
+ local msg
+
+ if status and status ~= "idle" then
+ msg = string.format("[%s] %s %s", status, time ~= "N/A" and time or "unknown remaining time",
+ string.lower(status):gsub(" ", ""):gsub("\n", "") == "charging" and " until charged" or " remaining")
+ else
+ msg = "On AC power"
+ end
+
+ tp_smapi.hide()
+ tp_smapi.notification = naughty.notify {
+ title = string.format("%s: %s %s (%s)", batid, mfgr, model, chem),
+ text = msg,
+ timeout = type(seconds) == "number" and seconds or 0,
+ screen = scr or focused()
+ }
+ end
+
+ function tp_smapi.create_widget(args)
+ args = args or {}
+
+ local pspath = args.pspath or "/sys/class/power_supply/"
+ local batteries = args.batteries or (args.battery and {args.battery}) or {}
+ local timeout = args.timeout or 30
+ local settings = args.settings or function() end
+
+ if #batteries == 0 then
+ helpers.line_callback("ls -1 " .. pspath, function(line)
+ local bstr = string.match(line, "BAT%w+")
+ if bstr then batteries[#batteries + 1] = bstr end
+ end)
+ end
+
+ local all_batteries_installed = true
+
+ for _, battery in ipairs(batteries) do
+ if not tp_smapi.installed(battery) then
+ naughty.notify {
+ preset = naughty.config.critical,
+ title = "tp_smapi: error while creating widget",
+ text = string.format("battery %s is not installed", battery)
+ }
+ all_batteries_installed = false
+ break
+ end
+ end
+
+ if not all_batteries_installed then return end
+
+ tpbat = {
+ batteries = batteries,
+ widget = args.widget or wibox.widget.textbox()
+ }
+
+ function tpbat.update()
+ tpbat_now = {
+ n_status = {},
+ n_perc = {},
+ n_time = {},
+ status = "N/A"
+ }
+
+ for i = 1, #batteries do
+ tpbat_now.n_status[i] = tp_smapi.status(batteries[i]) or "N/A"
+ tpbat_now.n_perc[i] = tp_smapi.percentage(batteries[i])
+ tpbat_now.n_time[i] = tp_smapi.time(batteries[i]) or "N/A"
+
+ if not tpbat_now.n_status[i]:lower():match("full") then
+ tpbat_now.status = tpbat_now.n_status[i]
+ end
+ end
+
+ widget = tpbat.widget -- backwards compatibility
+ settings()
+ end
+
+ helpers.newtimer("thinkpad-batteries", timeout, tpbat.update)
+
+ return tpbat
+ end
+
+ return tp_smapi
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/cpu.lua b/.config/awesome/lain/widget/cpu.lua
new file mode 100755
index 0000000..6c51115
--- /dev/null
+++ b/.config/awesome/lain/widget/cpu.lua
@@ -0,0 +1,75 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local helpers = require("lain.helpers")
+local wibox = require("wibox")
+local math = math
+local string = string
+
+-- CPU usage
+-- lain.widget.cpu
+
+local function factory(args)
+ args = args or {}
+
+ local cpu = { core = {}, widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 2
+ local settings = args.settings or function() end
+
+ function cpu.update()
+ -- Read the amount of time the CPUs have spent performing
+ -- different kinds of work. Read the first line of /proc/stat
+ -- which is the sum of all CPUs.
+ for index,time in pairs(helpers.lines_match("cpu","/proc/stat")) do
+ local coreid = index - 1
+ local core = cpu.core[coreid] or
+ { last_active = 0 , last_total = 0, usage = 0 }
+ local at = 1
+ local idle = 0
+ local total = 0
+
+ for field in string.gmatch(time, "[%s]+([^%s]+)") do
+ -- 4 = idle, 5 = ioWait. Essentially, the CPUs have done
+ -- nothing during these times.
+ if at == 4 or at == 5 then
+ idle = idle + field
+ end
+ total = total + field
+ at = at + 1
+ end
+
+ local active = total - idle
+
+ if core.last_active ~= active or core.last_total ~= total then
+ -- Read current data and calculate relative values.
+ local dactive = active - core.last_active
+ local dtotal = total - core.last_total
+ local usage = math.ceil(math.abs((dactive / dtotal) * 100))
+
+ core.last_active = active
+ core.last_total = total
+ core.usage = usage
+
+ -- Save current data for the next run.
+ cpu.core[coreid] = core
+ end
+ end
+
+ cpu_now = cpu.core
+ cpu_now.usage = cpu_now[0].usage
+ widget = cpu.widget
+
+ settings()
+ end
+
+ helpers.newtimer("cpu", timeout, cpu.update)
+
+ return cpu
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/fs.lua b/.config/awesome/lain/widget/fs.lua
new file mode 100755
index 0000000..b3a2dad
--- /dev/null
+++ b/.config/awesome/lain/widget/fs.lua
@@ -0,0 +1,156 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2018, Uli Schlacter
+ * (c) 2018, Otto Modinos
+ * (c) 2013, Luca CPZ
+
+--]]
+
+local helpers = require("lain.helpers")
+local Gio = require("lgi").Gio
+local focused = require("awful.screen").focused
+local wibox = require("wibox")
+local naughty = require("naughty")
+local gears = require("gears")
+local math = math
+local string = string
+local tconcat = table.concat
+local type = type
+local query_size = Gio.FILE_ATTRIBUTE_FILESYSTEM_SIZE
+local query_free = Gio.FILE_ATTRIBUTE_FILESYSTEM_FREE
+local query_used = Gio.FILE_ATTRIBUTE_FILESYSTEM_USED
+local query = query_size .. "," .. query_free .. "," .. query_used
+
+-- File systems info
+-- lain.widget.fs
+
+local function factory(args)
+ args = args or {}
+
+ local fs = {
+ widget = args.widget or wibox.widget.textbox(),
+ units = {
+ [1] = "Kb", [2] = "Mb", [3] = "Gb",
+ [4] = "Tb", [5] = "Pb", [6] = "Eb",
+ [7] = "Zb", [8] = "Yb"
+ }
+ }
+
+ function fs.hide()
+ if not fs.notification then return end
+ naughty.destroy(fs.notification)
+ fs.notification = nil
+ end
+
+ function fs.show(seconds, scr)
+ fs.hide()
+ fs.update(function()
+ fs.notification_preset.screen = fs.followtag and focused() or scr or 1
+ fs.notification = naughty.notify {
+ preset = fs.notification_preset,
+ timeout = type(seconds) == "number" and seconds or 5
+ }
+ end)
+ end
+
+ local timeout = args.timeout or 600
+ local partition = args.partition
+ local threshold = args.threshold or 99
+ local showpopup = args.showpopup or "on"
+ local settings = args.settings or function() end
+
+ fs.followtag = args.followtag or false
+ fs.notification_preset = args.notification_preset
+
+ if not fs.notification_preset then
+ fs.notification_preset = {
+ font = "Monospace 10",
+ fg = "#FFFFFF",
+ bg = "#000000"
+ }
+ end
+
+ local function update_synced()
+ local pathlen = 10
+ fs_now = {}
+
+ local notifypaths = {}
+ for _, mount in ipairs(Gio.unix_mounts_get()) do
+ local path = Gio.unix_mount_get_mount_path(mount)
+ local root = Gio.File.new_for_path(path)
+ local info = root:query_filesystem_info(query)
+
+ if info then
+ local size = info:get_attribute_uint64(query_size)
+ local used = info:get_attribute_uint64(query_used)
+ local free = info:get_attribute_uint64(query_free)
+
+ if size > 0 then
+ local units = math.floor(math.log(size)/math.log(1024))
+
+ fs_now[path] = {
+ units = fs.units[units],
+ percentage = math.floor(100 * used / size), -- used percentage
+ size = size / math.pow(1024, units),
+ used = used / math.pow(1024, units),
+ free = free / math.pow(1024, units)
+ }
+
+ if fs_now[path].percentage > 0 then -- don't notify unused file systems
+ notifypaths[#notifypaths+1] = path
+
+ if #path > pathlen then
+ pathlen = #path
+ end
+ end
+ end
+ end
+ end
+
+ widget = fs.widget
+ settings()
+
+ if partition and fs_now[partition] and fs_now[partition].percentage >= threshold then
+ if not helpers.get_map(partition) then
+ naughty.notify {
+ preset = naughty.config.presets.critical,
+ title = "Warning",
+ text = string.format("%s is above %d%% (%d%%)", partition, threshold, fs_now[partition].percentage)
+ }
+ helpers.set_map(partition, true)
+ else
+ helpers.set_map(partition, false)
+ end
+ end
+
+ local fmt = "%-" .. tostring(pathlen) .. "s %4s\t%6s\t%6s\n"
+ local notifytable = { [1] = string.format(fmt, "path", "used", "free", "size") }
+ fmt = "\n%-" .. tostring(pathlen) .. "s %3s%%\t%6.2f\t%6.2f %s"
+ for _, path in ipairs(notifypaths) do
+ notifytable[#notifytable+1] = string.format(fmt, path, fs_now[path].percentage, fs_now[path].free, fs_now[path].size, fs_now[path].units)
+ end
+
+ fs.notification_preset.text = tconcat(notifytable)
+ end
+
+ function fs.update(callback)
+ Gio.Async.start(gears.protected_call.call)(function()
+ update_synced()
+ if type(callback) == "function" and callback then
+ callback()
+ end
+ end)
+ end
+
+ if showpopup == "on" then
+ fs.widget:connect_signal('mouse::enter', function () fs.show(0) end)
+ fs.widget:connect_signal('mouse::leave', function () fs.hide() end)
+ end
+
+ helpers.newtimer(partition or "fs", timeout, fs.update)
+
+ return fs
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/imap.lua b/.config/awesome/lain/widget/imap.lua
new file mode 100755
index 0000000..e3f7baa
--- /dev/null
+++ b/.config/awesome/lain/widget/imap.lua
@@ -0,0 +1,94 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+
+--]]
+
+local helpers = require("lain.helpers")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local awful = require("awful")
+local string = string
+local type = type
+local tonumber = tonumber
+
+-- Mail IMAP check
+-- lain.widget.imap
+
+local function factory(args)
+ args = args or {}
+
+ local imap = { widget = args.widget or wibox.widget.textbox() }
+ local server = args.server
+ local mail = args.mail
+ local password = args.password
+ local port = args.port or 993
+ local timeout = args.timeout or 60
+ local pwdtimeout = args.pwdtimeout or 10
+ local is_plain = args.is_plain or false
+ local followtag = args.followtag or false
+ local notify = args.notify or "on"
+ local settings = args.settings or function() end
+
+ local head_command = "curl --connect-timeout 3 -fsm 3"
+ local request = "-X 'STATUS INBOX (MESSAGES RECENT UNSEEN)'"
+
+ if not server or not mail or not password then return end
+
+ mail_notification_preset = {
+ icon = helpers.icons_dir .. "mail.png",
+ position = "top_left"
+ }
+
+ helpers.set_map(mail, 0)
+
+ if not is_plain then
+ if type(password) == "string" or type(password) == "table" then
+ helpers.async(password, function(f) password = f:gsub("\n", "") end)
+ elseif type(password) == "function" then
+ imap.pwdtimer = helpers.newtimer(mail .. "-password", pwdtimeout, function()
+ local retrieved_password, try_again = password()
+ if not try_again then
+ imap.pwdtimer:stop() -- stop trying to retrieve
+ password = retrieved_password or "" -- failsafe
+ end
+ end, true, true)
+ end
+ end
+
+ function imap.update()
+ -- do not update if the password has not been retrieved yet
+ if type(password) ~= "string" then return end
+
+ local curl = string.format("%s --url imaps://%s:%s/INBOX -u %s:'%s' %s -k",
+ head_command, server, port, mail, password, request)
+
+ helpers.async(curl, function(f)
+ imap_now = { ["MESSAGES"] = 0, ["RECENT"] = 0, ["UNSEEN"] = 0 }
+
+ for s,d in f:gmatch("(%w+)%s+(%d+)") do imap_now[s] = tonumber(d) end
+ mailcount = imap_now["UNSEEN"] -- backwards compatibility
+ widget = imap.widget
+
+ settings()
+
+ if notify == "on" and mailcount and mailcount >= 1 and mailcount > helpers.get_map(mail) then
+ if followtag then mail_notification_preset.screen = awful.screen.focused() end
+ naughty.notify {
+ preset = mail_notification_preset,
+ text = string.format("%s has %d new message%s", mail, mailcount, mailcount == 1 and "" or "s")
+ }
+ end
+
+ helpers.set_map(mail, imap_now["UNSEEN"])
+ end)
+
+ end
+
+ imap.timer = helpers.newtimer(mail, timeout, imap.update, true, true)
+
+ return imap
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/init.lua b/.config/awesome/lain/widget/init.lua
new file mode 100755
index 0000000..57b86bb
--- /dev/null
+++ b/.config/awesome/lain/widget/init.lua
@@ -0,0 +1,19 @@
+--[[
+
+ Lain
+ Layouts, widgets and utilities for Awesome WM
+
+ Widgets section
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local wrequire = require("lain.helpers").wrequire
+local setmetatable = setmetatable
+
+local widget = { _NAME = "lain.widget" }
+
+return setmetatable(widget, { __index = wrequire })
diff --git a/.config/awesome/lain/widget/mem.lua b/.config/awesome/lain/widget/mem.lua
new file mode 100755
index 0000000..0318494
--- /dev/null
+++ b/.config/awesome/lain/widget/mem.lua
@@ -0,0 +1,51 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local helpers = require("lain.helpers")
+local wibox = require("wibox")
+local gmatch, lines, floor = string.gmatch, io.lines, math.floor
+
+-- Memory usage (ignoring caches)
+-- lain.widget.mem
+
+local function factory(args)
+ args = args or {}
+
+ local mem = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 2
+ local settings = args.settings or function() end
+
+ function mem.update()
+ mem_now = {}
+ for line in lines("/proc/meminfo") do
+ for k, v in gmatch(line, "([%a]+):[%s]+([%d]+).+") do
+ if k == "MemTotal" then mem_now.total = floor(v / 1024 + 0.5)
+ elseif k == "MemFree" then mem_now.free = floor(v / 1024 + 0.5)
+ elseif k == "Buffers" then mem_now.buf = floor(v / 1024 + 0.5)
+ elseif k == "Cached" then mem_now.cache = floor(v / 1024 + 0.5)
+ elseif k == "SwapTotal" then mem_now.swap = floor(v / 1024 + 0.5)
+ elseif k == "SwapFree" then mem_now.swapf = floor(v / 1024 + 0.5)
+ elseif k == "SReclaimable" then mem_now.srec = floor(v / 1024 + 0.5)
+ end
+ end
+ end
+
+ mem_now.used = mem_now.total - mem_now.free - mem_now.buf - mem_now.cache - mem_now.srec
+ mem_now.swapused = mem_now.swap - mem_now.swapf
+ mem_now.perc = math.floor(mem_now.used / mem_now.total * 100)
+
+ widget = mem.widget
+ settings()
+ end
+
+ helpers.newtimer("mem", timeout, mem.update)
+
+ return mem
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/mpd.lua b/.config/awesome/lain/widget/mpd.lua
new file mode 100755
index 0000000..55d3649
--- /dev/null
+++ b/.config/awesome/lain/widget/mpd.lua
@@ -0,0 +1,135 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010, Adrian C.
+
+--]]
+
+local helpers = require("lain.helpers")
+local shell = require("awful.util").shell
+local escape_f = require("awful.util").escape
+local focused = require("awful.screen").focused
+local naughty = require("naughty")
+local wibox = require("wibox")
+local os = os
+local string = string
+
+-- MPD infos
+-- lain.widget.mpd
+
+local function factory(args)
+ args = args or {}
+
+ local mpd = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 2
+ local password = (args.password and #args.password > 0 and string.format("password %s\\n", args.password)) or ""
+ local host = args.host or os.getenv("MPD_HOST") or "127.0.0.1"
+ local port = args.port or os.getenv("MPD_PORT") or "6600"
+ local music_dir = args.music_dir or os.getenv("HOME") .. "/Music"
+ local cover_pattern = args.cover_pattern or "*\\.(jpg|jpeg|png|gif)$"
+ local cover_size = args.cover_size or 100
+ local default_art = args.default_art
+ local notify = args.notify or "on"
+ local followtag = args.followtag or false
+ local settings = args.settings or function() end
+
+ local mpdh = string.format("telnet://%s:%s", host, port)
+ local echo = string.format("printf \"%sstatus\\ncurrentsong\\nclose\\n\"", password)
+ local cmd = string.format("%s | curl --connect-timeout 1 -fsm 3 %s", echo, mpdh)
+
+ mpd_notification_preset = { title = "Now playing", timeout = 6 }
+
+ helpers.set_map("current mpd track", nil)
+
+ function mpd.update()
+ helpers.async({ shell, "-c", cmd }, function(f)
+ mpd_now = {
+ random_mode = false,
+ single_mode = false,
+ repeat_mode = false,
+ consume_mode = false,
+ pls_pos = "N/A",
+ pls_len = "N/A",
+ state = "N/A",
+ file = "N/A",
+ name = "N/A",
+ artist = "N/A",
+ title = "N/A",
+ album = "N/A",
+ genre = "N/A",
+ track = "N/A",
+ date = "N/A",
+ time = "N/A",
+ elapsed = "N/A",
+ volume = "N/A"
+ }
+
+ for line in string.gmatch(f, "[^\n]+") do
+ for k, v in string.gmatch(line, "([%w]+):[%s](.*)$") do
+ if k == "state" then mpd_now.state = v
+ elseif k == "file" then mpd_now.file = v
+ elseif k == "Name" then mpd_now.name = escape_f(v)
+ elseif k == "Artist" then mpd_now.artist = escape_f(v)
+ elseif k == "Title" then mpd_now.title = escape_f(v)
+ elseif k == "Album" then mpd_now.album = escape_f(v)
+ elseif k == "Genre" then mpd_now.genre = escape_f(v)
+ elseif k == "Track" then mpd_now.track = escape_f(v)
+ elseif k == "Date" then mpd_now.date = escape_f(v)
+ elseif k == "Time" then mpd_now.time = v
+ elseif k == "elapsed" then mpd_now.elapsed = string.match(v, "%d+")
+ elseif k == "song" then mpd_now.pls_pos = v
+ elseif k == "playlistlength" then mpd_now.pls_len = v
+ elseif k == "repeat" then mpd_now.repeat_mode = v ~= "0"
+ elseif k == "single" then mpd_now.single_mode = v ~= "0"
+ elseif k == "random" then mpd_now.random_mode = v ~= "0"
+ elseif k == "consume" then mpd_now.consume_mode = v ~= "0"
+ elseif k == "volume" then mpd_now.volume = v
+ end
+ end
+ end
+
+ mpd_notification_preset.text = string.format("%s (%s) - %s\n%s", mpd_now.artist,
+ mpd_now.album, mpd_now.date, mpd_now.title)
+ widget = mpd.widget
+ settings()
+
+ if mpd_now.state == "play" then
+ if notify == "on" and mpd_now.title ~= helpers.get_map("current mpd track") then
+ helpers.set_map("current mpd track", mpd_now.title)
+
+ if followtag then mpd_notification_preset.screen = focused() end
+
+ local common = {
+ preset = mpd_notification_preset,
+ icon = default_art,
+ icon_size = cover_size,
+ replaces_id = mpd.id
+ }
+
+ if not string.match(mpd_now.file, "http.*://") then -- local file instead of http stream
+ local path = string.format("%s/%s", music_dir, string.match(mpd_now.file, ".*/"))
+ local cover = string.format("find '%s' -maxdepth 1 -type f | egrep -i -m1 '%s'",
+ path:gsub("'", "'\\''"), cover_pattern)
+ helpers.async({ shell, "-c", cover }, function(current_icon)
+ common.icon = current_icon:gsub("\n", "")
+ if #common.icon == 0 then common.icon = nil end
+ mpd.id = naughty.notify(common).id
+ end)
+ else
+ mpd.id = naughty.notify(common).id
+ end
+
+ end
+ elseif mpd_now.state ~= "pause" then
+ helpers.set_map("current mpd track", nil)
+ end
+ end)
+ end
+
+ mpd.timer = helpers.newtimer("mpd", timeout, mpd.update, true, true)
+
+ return mpd
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/net.lua b/.config/awesome/lain/widget/net.lua
new file mode 100755
index 0000000..9b7b165
--- /dev/null
+++ b/.config/awesome/lain/widget/net.lua
@@ -0,0 +1,122 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local helpers = require("lain.helpers")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local string = string
+
+-- Network infos
+-- lain.widget.net
+
+local function factory(args)
+ args = args or {}
+
+ local net = { widget = args.widget or wibox.widget.textbox(), devices = {} }
+ local timeout = args.timeout or 2
+ local units = args.units or 1024 -- KB
+ local notify = args.notify or "on"
+ local wifi_state = args.wifi_state or "off"
+ local eth_state = args.eth_state or "off"
+ local screen = args.screen or 1
+ local format = args.format or "%.1f"
+ local settings = args.settings or function() end
+
+ -- Compatibility with old API where iface was a string corresponding to 1 interface
+ net.iface = (args.iface and (type(args.iface) == "string" and {args.iface}) or
+ (type(args.iface) == "table" and args.iface)) or {}
+
+ function net.get_devices()
+ net.iface = {} -- reset at every call
+ helpers.line_callback("ip link", function(line)
+ net.iface[#net.iface + 1] = not string.match(line, "LOOPBACK") and string.match(line, "(%w+): <") or nil
+ end)
+ end
+
+ if #net.iface == 0 then net.get_devices() end
+
+ function net.update()
+ -- These are the totals over all specified interfaces
+ net_now = {
+ devices = {},
+ -- Bytes since last iteration
+ sent = 0,
+ received = 0
+ }
+
+ for _, dev in ipairs(net.iface) do
+ local dev_now = {}
+ local dev_before = net.devices[dev] or { last_t = 0, last_r = 0 }
+ local now_t = tonumber(helpers.first_line(string.format("/sys/class/net/%s/statistics/tx_bytes", dev)) or 0)
+ local now_r = tonumber(helpers.first_line(string.format("/sys/class/net/%s/statistics/rx_bytes", dev)) or 0)
+
+ dev_now.carrier = helpers.first_line(string.format("/sys/class/net/%s/carrier", dev)) or "0"
+ dev_now.state = helpers.first_line(string.format("/sys/class/net/%s/operstate", dev)) or "down"
+
+ dev_now.sent = (now_t - dev_before.last_t) / timeout / units
+ dev_now.received = (now_r - dev_before.last_r) / timeout / units
+
+ net_now.sent = net_now.sent + dev_now.sent
+ net_now.received = net_now.received + dev_now.received
+
+ dev_now.sent = string.format(format, dev_now.sent)
+ dev_now.received = string.format(format, dev_now.received)
+
+ dev_now.last_t = now_t
+ dev_now.last_r = now_r
+
+ if wifi_state == "on" and helpers.first_line(string.format("/sys/class/net/%s/uevent", dev)) == "DEVTYPE=wlan" then
+ dev_now.wifi = true
+ if string.match(dev_now.carrier, "1") then
+ dev_now.signal = tonumber(string.match(helpers.lines_from("/proc/net/wireless")[3], "(%-%d+%.)")) or nil
+ end
+ else
+ dev_now.wifi = false
+ end
+
+ if eth_state == "on" and helpers.first_line(string.format("/sys/class/net/%s/uevent", dev)) ~= "DEVTYPE=wlan" then
+ dev_now.ethernet = true
+ else
+ dev_now.ethernet = false
+ end
+
+ net.devices[dev] = dev_now
+
+ -- Notify only once when connection is lost
+ if string.match(dev_now.carrier, "0") and notify == "on" and helpers.get_map(dev) then
+ naughty.notify {
+ title = dev,
+ text = "No carrier",
+ icon = helpers.icons_dir .. "no_net.png",
+ screen = screen
+ }
+ helpers.set_map(dev, false)
+ elseif string.match(dev_now.carrier, "1") then
+ helpers.set_map(dev, true)
+ end
+
+ net_now.carrier = dev_now.carrier
+ net_now.state = dev_now.state
+ net_now.devices[dev] = dev_now
+ -- net_now.sent and net_now.received will be
+ -- the totals across all specified devices
+ end
+
+ net_now.sent = string.format(format, net_now.sent)
+ net_now.received = string.format(format, net_now.received)
+
+ widget = net.widget
+ settings()
+ end
+
+ helpers.newtimer("network", timeout, net.update)
+
+ return net
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/pulse.lua b/.config/awesome/lain/widget/pulse.lua
new file mode 100755
index 0000000..69f4d70
--- /dev/null
+++ b/.config/awesome/lain/widget/pulse.lua
@@ -0,0 +1,58 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2016, Luca CPZ
+
+--]]
+
+local helpers = require("lain.helpers")
+local shell = require("awful.util").shell
+local wibox = require("wibox")
+local string = string
+local type = type
+
+-- PulseAudio volume
+-- lain.widget.pulse
+
+local function factory(args)
+ args = args or {}
+
+ local pulse = { widget = args.widget or wibox.widget.textbox(), device = "N/A" }
+ local timeout = args.timeout or 5
+ local settings = args.settings or function() end
+
+ pulse.devicetype = args.devicetype or "sink"
+ pulse.cmd = args.cmd or "pacmd list-" .. pulse.devicetype .. "s | sed -n -e '/*/,$!d' -e '/index/p' -e '/base volume/d' -e '/volume:/p' -e '/muted:/p' -e '/device\\.string/p'"
+
+ function pulse.update()
+ helpers.async({ shell, "-c", type(pulse.cmd) == "string" and pulse.cmd or pulse.cmd() },
+ function(s)
+ volume_now = {
+ index = string.match(s, "index: (%S+)") or "N/A",
+ device = string.match(s, "device.string = \"(%S+)\"") or "N/A",
+ muted = string.match(s, "muted: (%S+)") or "N/A"
+ }
+
+ pulse.device = volume_now.index
+
+ local ch = 1
+ volume_now.channel = {}
+ for v in string.gmatch(s, ":.-(%d+)%%") do
+ volume_now.channel[ch] = v
+ ch = ch + 1
+ end
+
+ volume_now.left = volume_now.channel[1] or "N/A"
+ volume_now.right = volume_now.channel[2] or "N/A"
+
+ widget = pulse.widget
+ settings()
+ end)
+ end
+
+ helpers.newtimer("pulse", timeout, pulse.update)
+
+ return pulse
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/pulsebar.lua b/.config/awesome/lain/widget/pulsebar.lua
new file mode 100755
index 0000000..19e73b9
--- /dev/null
+++ b/.config/awesome/lain/widget/pulsebar.lua
@@ -0,0 +1,175 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2013, Rman
+
+--]]
+
+local helpers = require("lain.helpers")
+local awful = require("awful")
+local naughty = require("naughty")
+local wibox = require("wibox")
+local math = math
+local string = string
+local type = type
+local tonumber = tonumber
+
+-- PulseAudio volume bar
+-- lain.widget.pulsebar
+
+local function factory(args)
+ local pulsebar = {
+ colors = {
+ background = "#000000",
+ mute_background = "#000000",
+ mute = "#EB8F8F",
+ unmute = "#A4CE8A"
+ },
+
+ _current_level = 0,
+ _mute = "no",
+ device = "N/A"
+ }
+
+ args = args or {}
+
+ local timeout = args.timeout or 5
+ local settings = args.settings or function() end
+ local width = args.width or 63
+ local height = args.height or 1
+ local margins = args.margins or 1
+ local paddings = args.paddings or 1
+ local ticks = args.ticks or false
+ local ticks_size = args.ticks_size or 7
+ local tick = args.tick or "|"
+ local tick_pre = args.tick_pre or "["
+ local tick_post = args.tick_post or "]"
+ local tick_none = args.tick_none or " "
+
+ pulsebar.colors = args.colors or pulsebar.colors
+ pulsebar.followtag = args.followtag or false
+ pulsebar.notification_preset = args.notification_preset
+ pulsebar.devicetype = args.devicetype or "sink"
+ pulsebar.cmd = args.cmd or "pacmd list-" .. pulsebar.devicetype .. "s | sed -n -e '/*/,$!d' -e '/index/p' -e '/base volume/d' -e '/volume:/p' -e '/muted:/p' -e '/device\\.string/p'"
+
+ if not pulsebar.notification_preset then
+ pulsebar.notification_preset = {
+ font = "Monospace 10"
+ }
+ end
+
+ pulsebar.bar = wibox.widget {
+ color = pulsebar.colors.unmute,
+ background_color = pulsebar.colors.background,
+ forced_height = height,
+ forced_width = width,
+ margins = margins,
+ paddings = paddings,
+ ticks = ticks,
+ ticks_size = ticks_size,
+ widget = wibox.widget.progressbar,
+ }
+
+ pulsebar.tooltip = awful.tooltip({ objects = { pulsebar.bar } })
+
+ function pulsebar.update(callback)
+ helpers.async({ awful.util.shell, "-c", type(pulsebar.cmd) == "string" and pulsebar.cmd or pulsebar.cmd() },
+ function(s)
+ volume_now = {
+ index = string.match(s, "index: (%S+)") or "N/A",
+ device = string.match(s, "device.string = \"(%S+)\"") or "N/A",
+ muted = string.match(s, "muted: (%S+)") or "N/A"
+ }
+
+ pulsebar.device = volume_now.index
+
+ local ch = 1
+ volume_now.channel = {}
+ for v in string.gmatch(s, ":.-(%d+)%%") do
+ volume_now.channel[ch] = v
+ ch = ch + 1
+ end
+
+ volume_now.left = volume_now.channel[1] or "N/A"
+ volume_now.right = volume_now.channel[2] or "N/A"
+
+ local volu = volume_now.left
+ local mute = volume_now.muted
+
+ if volu:match("N/A") or mute:match("N/A") then return end
+
+ if volu ~= pulsebar._current_level or mute ~= pulsebar._mute then
+ pulsebar._current_level = tonumber(volu)
+ pulsebar.bar:set_value(pulsebar._current_level / 100)
+ if pulsebar._current_level == 0 or mute == "yes" then
+ pulsebar._mute = mute
+ pulsebar.tooltip:set_text ("[muted]")
+ pulsebar.bar.color = pulsebar.colors.mute
+ pulsebar.bar.background_color = pulsebar.colors.mute_background
+ else
+ pulsebar._mute = "no"
+ pulsebar.tooltip:set_text(string.format("%s %s: %s", pulsebar.devicetype, pulsebar.device, volu))
+ pulsebar.bar.color = pulsebar.colors.unmute
+ pulsebar.bar.background_color = pulsebar.colors.background
+ end
+
+ settings()
+
+ if type(callback) == "function" then callback() end
+ end
+ end)
+ end
+
+ function pulsebar.notify()
+ pulsebar.update(function()
+ local preset = pulsebar.notification_preset
+
+ preset.title = string.format("%s %s - %s%%", pulsebar.devicetype, pulsebar.device, pulsebar._current_level)
+
+ if pulsebar._mute == "yes" then
+ preset.title = preset.title .. " muted"
+ end
+
+ -- tot is the maximum number of ticks to display in the notification
+ -- fallback: default horizontal wibox height
+ local wib, tot = awful.screen.focused().mywibox, 20
+
+ -- if we can grab mywibox, tot is defined as its height if
+ -- horizontal, or width otherwise
+ if wib then
+ if wib.position == "left" or wib.position == "right" then
+ tot = wib.width
+ else
+ tot = wib.height
+ end
+ end
+
+ local int = math.modf((pulsebar._current_level / 100) * tot)
+ preset.text = string.format(
+ "%s%s%s%s",
+ tick_pre,
+ string.rep(tick, int),
+ string.rep(tick_none, tot - int),
+ tick_post
+ )
+
+ if pulsebar.followtag then preset.screen = awful.screen.focused() end
+
+ if not pulsebar.notification then
+ pulsebar.notification = naughty.notify {
+ preset = preset,
+ destroy = function() pulsebar.notification = nil end
+ }
+ else
+ naughty.replace_text(pulsebar.notification, preset.title, preset.text)
+ end
+ end)
+ end
+
+ helpers.newtimer(string.format("pulsebar-%s-%s", pulsebar.devicetype, pulsebar.device), timeout, pulsebar.update)
+
+ return pulsebar
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/sysload.lua b/.config/awesome/lain/widget/sysload.lua
new file mode 100755
index 0000000..7260524
--- /dev/null
+++ b/.config/awesome/lain/widget/sysload.lua
@@ -0,0 +1,39 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+ * (c) 2010-2012, Peter Hofmann
+
+--]]
+
+local helpers = require("lain.helpers")
+local wibox = require("wibox")
+local open, match = io.open, string.match
+
+-- System load
+-- lain.widget.sysload
+
+local function factory(args)
+ args = args or {}
+
+ local sysload = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 2
+ local settings = args.settings or function() end
+
+ function sysload.update()
+ local f = open("/proc/loadavg")
+ local ret = f:read("*all")
+ f:close()
+
+ load_1, load_5, load_15 = match(ret, "([^%s]+) ([^%s]+) ([^%s]+)")
+
+ widget = sysload.widget
+ settings()
+ end
+
+ helpers.newtimer("sysload", timeout, sysload.update)
+
+ return sysload
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/temp.lua b/.config/awesome/lain/widget/temp.lua
new file mode 100755
index 0000000..99f8700
--- /dev/null
+++ b/.config/awesome/lain/widget/temp.lua
@@ -0,0 +1,50 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2013, Luca CPZ
+
+--]]
+
+local helpers = require("lain.helpers")
+local wibox = require("wibox")
+local tonumber = tonumber
+
+-- {thermal,core} temperature info
+-- lain.widget.temp
+
+local function factory(args)
+ args = args or {}
+
+ local temp = { widget = args.widget or wibox.widget.textbox() }
+ local timeout = args.timeout or 30
+ local tempfile = args.tempfile or "/sys/devices/virtual/thermal/thermal_zone0/temp"
+ local format = args.format or "%.1f"
+ local settings = args.settings or function() end
+
+ function temp.update()
+ helpers.async({"find", "/sys/devices", "-type", "f", "-name", "*temp*"}, function(f)
+ temp_now = {}
+ local temp_fl, temp_value
+ for t in f:gmatch("[^\n]+") do
+ temp_fl = helpers.first_line(t)
+ if temp_fl then
+ temp_value = tonumber(temp_fl)
+ temp_now[t] = temp_value and temp_value/1e3 or temp_fl
+ end
+ end
+ if temp_now[tempfile] then
+ coretemp_now = string.format(format, temp_now[tempfile])
+ else
+ coretemp_now = "N/A"
+ end
+ widget = temp.widget
+ settings()
+ end)
+ end
+
+ helpers.newtimer("thermal", timeout, temp.update)
+
+ return temp
+end
+
+return factory
diff --git a/.config/awesome/lain/widget/weather.lua b/.config/awesome/lain/widget/weather.lua
new file mode 100755
index 0000000..c683d42
--- /dev/null
+++ b/.config/awesome/lain/widget/weather.lua
@@ -0,0 +1,146 @@
+--[[
+
+ Licensed under GNU General Public License v2
+ * (c) 2015, Luca CPZ
+
+--]]
+
+local helpers = require("lain.helpers")
+local json = require("lain.util").dkjson
+local focused = require("awful.screen").focused
+local naughty = require("naughty")
+local wibox = require("wibox")
+local math = math
+local os = os
+local string = string
+local type = type
+local tonumber = tonumber
+
+-- OpenWeatherMap
+-- current weather and X-days forecast
+-- lain.widget.weather
+
+local function factory(args)
+ args = args or {}
+
+ local weather = { widget = args.widget or wibox.widget.textbox() }
+ local APPID = args.APPID -- mandatory
+ local timeout = args.timeout or 60 * 15 -- 15 min
+ local current_call = args.current_call or "curl -s 'https://api.openweathermap.org/data/2.5/weather?id=%s&units=%s&lang=%s&APPID=%s'"
+ local forecast_call = args.forecast_call or "curl -s 'https://api.openweathermap.org/data/2.5/forecast?id=%s&units=%s&lang=%s&APPID=%s'"
+ local city_id = args.city_id or 0 -- placeholder
+ local units = args.units or "metric"
+ local lang = args.lang or "en"
+ local cnt = args.cnt or 5
+ local icons_path = args.icons_path or helpers.icons_dir .. "openweathermap/"
+ local notification_preset = args.notification_preset or {}
+ local notification_text_fun = args.notification_text_fun or
+ function (wn)
+ local day = os.date("%a %d", wn["dt"])
+ local temp = math.floor(wn["main"]["temp"])
+ local desc = wn["weather"][1]["description"]
+ return string.format("%s: %s, %d ", day, desc, temp)
+ end
+ local weather_na_markup = args.weather_na_markup or " N/A "
+ local followtag = args.followtag or false
+ local showpopup = args.showpopup or "on"
+ local settings = args.settings or function() end
+
+ weather.widget:set_markup(weather_na_markup)
+ weather.icon_path = icons_path .. "na.png"
+ weather.icon = wibox.widget.imagebox(weather.icon_path)
+
+ function weather.show(seconds)
+ weather.hide()
+
+ if followtag then
+ notification_preset.screen = focused()
+ end
+
+ if not weather.notification_text then
+ weather.update()
+ weather.forecast_update()
+ end
+
+ weather.notification = naughty.notify {
+ preset = notification_preset,
+ text = weather.notification_text,
+ icon = weather.icon_path,
+ timeout = type(seconds) == "number" and seconds or notification_preset.timeout
+ }
+ end
+
+ function weather.hide()
+ if weather.notification then
+ naughty.destroy(weather.notification)
+ weather.notification = nil
+ end
+ end
+
+ function weather.attach(obj)
+ obj:connect_signal("mouse::enter", function()
+ weather.show(0)
+ end)
+ obj:connect_signal("mouse::leave", function()
+ weather.hide()
+ end)
+ end
+
+ function weather.forecast_update()
+ local cmd = string.format(forecast_call, city_id, units, lang, APPID)
+ helpers.async(cmd, function(f)
+ local err
+ weather_now, _, err = json.decode(f, 1, nil)
+
+ if not err and type(weather_now) == "table" and tonumber(weather_now["cod"]) == 200 then
+ weather.notification_text = ""
+ for i = 1, weather_now["cnt"], weather_now["cnt"]//cnt do
+ weather.notification_text = weather.notification_text ..
+ notification_text_fun(weather_now["list"][i])
+ if i < weather_now["cnt"] then
+ weather.notification_text = weather.notification_text .. "\n"
+ end
+ end
+ end
+ end)
+ end
+
+ function weather.update()
+ local cmd = string.format(current_call, city_id, units, lang, APPID)
+ helpers.async(cmd, function(f)
+ local err
+ weather_now, _, err = json.decode(f, 1, nil)
+
+ if not err and type(weather_now) == "table" and tonumber(weather_now["cod"]) == 200 then
+ local sunrise = tonumber(weather_now["sys"]["sunrise"])
+ local sunset = tonumber(weather_now["sys"]["sunset"])
+ local icon = weather_now["weather"][1]["icon"]
+ local loc_now = os.time()
+
+ if sunrise <= loc_now and loc_now <= sunset then
+ icon = string.gsub(icon, "n", "d")
+ else
+ icon = string.gsub(icon, "d", "n")
+ end
+
+ weather.icon_path = icons_path .. icon .. ".png"
+ widget = weather.widget
+ settings()
+ else
+ weather.icon_path = icons_path .. "na.png"
+ weather.widget:set_markup(weather_na_markup)
+ end
+
+ weather.icon:set_image(weather.icon_path)
+ end)
+ end
+
+ if showpopup == "on" then weather.attach(weather.widget) end
+
+ weather.timer = helpers.newtimer("weather-" .. city_id, timeout, weather.update, false, true)
+ weather.timer_forecast = helpers.newtimer("weather_forecast-" .. city_id, timeout, weather.forecast_update, false, true)
+
+ return weather
+end
+
+return factory
diff --git a/.config/awesome/lain/wiki/Home.md b/.config/awesome/lain/wiki/Home.md
new file mode 100755
index 0000000..c31550f
--- /dev/null
+++ b/.config/awesome/lain/wiki/Home.md
@@ -0,0 +1,44 @@
+Welcome to the Lain wiki!
+
+If you spot a typo or have a suggestion to improve these pages, please notify me opening an [issue](https://github.com/lcpz/lain/issues) format. Thank you.
+
+Dependencies
+------------
+
+Package | Requested by | Reasons of choice
+--- | --- | ---
+[curl](https://curl.haxx.se) | `imap`, `mpd`, and `weather` widgets | 1. faster and simpler to use than [LuaSocket](https://github.com/diegonehab/luasocket); 2. it's in the core of almost every distro; 3. can be called [asynchronously](https://awesomewm.org/doc/api/libraries/awful.spawn.html#easy_async)
+
+Installation
+------------
+
+### Arch Linux
+
+[AUR package](https://aur.archlinux.org/packages/lain-git/)
+
+### Other distributions
+
+```shell
+git clone https://github.com/lcpz/lain.git ~/.config/awesome/lain
+```
+
+Also available via [LuaRocks](https://luarocks.org/modules/lcpz/lain):
+
+```shell
+luarocks install lcpz/lain
+```
+
+Usage
+--------
+
+First, include it into your `rc.lua`:
+
+```lua
+local lain = require("lain")
+```
+
+Then check out the submodules you want:
+
+- [Layouts](https://github.com/lcpz/lain/wiki/Layouts)
+- [Widgets](https://github.com/lcpz/lain/wiki/Widgets)
+- [Utilities](https://github.com/lcpz/lain/wiki/Utilities)
diff --git a/.config/awesome/lain/wiki/Layouts.md b/.config/awesome/lain/wiki/Layouts.md
new file mode 100755
index 0000000..0286d4b
--- /dev/null
+++ b/.config/awesome/lain/wiki/Layouts.md
@@ -0,0 +1,255 @@
+
+ lain/layout
+ .
+ |-- termfair
+ |-- termfair.center
+ |-- cascade
+ |-- cascade.tile
+ |-- centerwork
+ |-- centerwork.horizontal
+
+Usage
+=====
+
+As usual, specify your favourites in `awful.layout.layouts`, or set them on specific tags with [`awful.layout.set`](https://awesomewm.org/doc/api/libraries/awful.layout.html#set).
+
+```lua
+awful.layout.set(lain.layout.termfair, tag)
+```
+
+How do layouts work?
+====================
+
+`termfair`
+--------
+
+This layout restricts the size of each window. Each window will have the
+same width but is variable in height. Furthermore, windows are
+left-aligned. The basic workflow is as follows (the number above the
+screen is the number of open windows, the number in a cell is the fixed
+number of a client):
+
+ (1) (2) (3)
+ +---+---+---+ +---+---+---+ +---+---+---+
+ | | | | | | | | | | | |
+ | 1 | | | -> | 2 | 1 | | -> | 3 | 2 | 1 | ->
+ | | | | | | | | | | | |
+ +---+---+---+ +---+---+---+ +---+---+---+
+
+ (4) (5) (6)
+ +---+---+---+ +---+---+---+ +---+---+---+
+ | 4 | | | | 5 | 4 | | | 6 | 5 | 4 |
+ +---+---+---+ -> +---+---+---+ -> +---+---+---+
+ | 3 | 2 | 1 | | 3 | 2 | 1 | | 3 | 2 | 1 |
+ +---+---+---+ +---+---+---+ +---+---+---+
+
+The first client will be located in the left column. When opening
+another window, this new window will be placed in the left column while
+moving the first window into the middle column. Once a row is full,
+another row above it will be created.
+
+Default number of columns and rows are respectively taken from `nmaster`
+and `ncol` values in `awful.tag`, but you can set your own.
+
+For example, this sets `termfair` to 3 columns and at least 1 row:
+
+```lua
+lain.layout.termfair.nmaster = 3
+lain.layout.termfair.ncol = 1
+```
+
+`termfair.center`
+----------
+
+Similar to `termfair`, but with fixed number of vertical columns. Cols are centerded until there are `nmaster` columns, then windows are stacked as slaves, with possibly `ncol` clients per column at most.
+
+ (1) (2) (3)
+ +---+---+---+ +-+---+---+-+ +---+---+---+
+ | | | | | | | | | | | | |
+ | | 1 | | -> | | 1 | 2 | | -> | 1 | 2 | 3 | ->
+ | | | | | | | | | | | | |
+ +---+---+---+ +-+---+---+-+ +---+---+---+
+
+ (4) (5)
+ +---+---+---+ +---+---+---+
+ | | | 3 | | | 2 | 4 |
+ + 1 + 2 +---+ -> + 1 +---+---+
+ | | | 4 | | | 3 | 5 |
+ +---+---+---+ +---+---+---+
+
+Like `termfair`, default number of columns and rows are respectively taken from `nmaster`
+and `ncol` values in `awful.tag`, but you can set your own.
+
+For example, this sets `termfair.center` to 3 columns and at least 1 row:
+
+```lua
+lain.layout.termfair.center.nmaster = 3
+lain.layout.termfair.center.ncol = 1
+```
+
+`cascade`
+-------
+
+Cascade all windows of a tag.
+
+You can control the offsets by setting these two variables:
+
+```lua
+lain.layout.cascade.offset_x = 64
+lain.layout.cascade.offset_y = 16
+```
+
+The following reserves space for 5 windows:
+
+```lua
+lain.layout.cascade.nmaster = 5
+```
+
+That is, no window will get resized upon the creation of a new window,
+unless there's more than 5 windows.
+
+`cascade.tile`
+-----------
+
+Similar to `awful.layout.suit.tile` layout, however, clients in the slave
+column are cascaded instead of tiled.
+
+Left column size can be set, otherwise is controlled by `mwfact` of the
+tag. Additional windows will be opened in another column on the right.
+New windows are placed above old windows.
+
+Whether the slave column is placed on top of the master window or not is
+controlled by the value of `ncol`. A value of 1 means "overlapping slave column"
+and anything else means "don't overlap windows".
+
+Usage example:
+
+```lua
+lain.layout.cascade.tile.offset_x = 2
+lain.layout.cascade.tile.offset_y = 32
+lain.layout.cascade.tile.extra_padding = 5
+lain.layout.cascade.tile.nmaster = 5
+lain.layout.cascade.tile.ncol = 2
+```
+
+`extra_padding` reduces the size of the master window if "overlapping
+slave column" is activated. This allows you to see if there are any
+windows in your slave column.
+
+Setting `offset_x` to a very small value or even 0 is recommended to avoid wasting space.
+
+`centerwork`
+----------
+
+You start with one window, centered horizontally:
+
+ +--------------------------+
+ | +----------+ |
+ | | | |
+ | | | |
+ | | | |
+ | | MAIN | |
+ | | | |
+ | | | |
+ | | | |
+ | | | |
+ | +----------+ |
+ +--------------------------+
+
+This is your main working window. You do most of the work right here.
+Sometimes, you may want to open up additional windows. They're put on left and right, alternately.
+
+ +--------------------------+
+ | +---+ +----------+ +---+ |
+ | | | | | | | |
+ | | | | | | | |
+ | | | | | | | |
+ | +---+ | MAIN | +---+ |
+ | +---+ | | +---+ |
+ | | | | | | | |
+ | | | | | | | |
+ | | | | | | | |
+ | +---+ +----------+ +---+ |
+ +--------------------------+
+
+*Please note:* If you use Awesome's default configuration, navigation in
+this layout may be very confusing. How do you get from the main window
+to satellite ones depends on the order in which the windows are opened.
+Thus, use of `awful.client.focus.bydirection()` is suggested.
+Here's an example:
+
+```lua
+globalkeys = awful.util.table.join(
+ -- [...]
+ awful.key({ modkey }, "j",
+ function()
+ awful.client.focus.bydirection("down")
+ if client.focus then client.focus:raise() end
+ end),
+ awful.key({ modkey }, "k",
+ function()
+ awful.client.focus.bydirection("up")
+ if client.focus then client.focus:raise() end
+ end),
+ awful.key({ modkey }, "h",
+ function()
+ awful.client.focus.bydirection("left")
+ if client.focus then client.focus:raise() end
+ end),
+ awful.key({ modkey }, "l",
+ function()
+ awful.client.focus.bydirection("right")
+ if client.focus then client.focus:raise() end
+ end),
+ -- [...]
+)
+```
+
+`centerwork.horizontal`
+-----------
+
+Same as `centerwork`, except that the main
+window expands horizontally, and the additional windows
+are put ontop/below it. Useful if you have a screen turned 90°.
+
+Pre 4.0 `uselesstile` patches
+=============================
+
+In branch 3.5, this module provided useless gaps layouts. Since useless gaps have been implemented in Awesome 4.0, those layouts have been removed.
+
+Following are a couple of `uselesstile` variants that were not part of lain. They are kept only for reference and are not supported.
+
+Xmonad-like
+-----------
+
+If you want to have `awful.layout.suit.tile` behave like xmonad, with internal gaps two times wider than external ones, download [this](https://gist.github.com/lcpz/9e56dcfbe66bfe14967c) as `lain/layout/uselesstile`.
+
+Inverted master
+---------------
+
+Want to invert master window position? Use [this](https://gist.github.com/lcpz/c59dc59c9f99d98218eb) version. You can set `single_gap` with `width` and `height` in your `theme.lua`, in order to define the window geometry when there's only one client, otherwise it goes maximized. An example:
+
+ theme.single_gap = { width = 600, height = 100 }
+
+What about layout icons?
+========================
+
+They are located in ``lain/icons/layout``.
+
+To use them, define new `layout_*` variables in your ``theme.lua``. For instance:
+
+```lua
+theme.lain_icons = os.getenv("HOME") ..
+ "/.config/awesome/lain/icons/layout/default/"
+theme.layout_termfair = theme.lain_icons .. "termfair.png"
+theme.layout_centerfair = theme.lain_icons .. "centerfair.png" -- termfair.center
+theme.layout_cascade = theme.lain_icons .. "cascade.png"
+theme.layout_cascadetile = theme.lain_icons .. "cascadetile.png" -- cascade.tile
+theme.layout_centerwork = theme.lain_icons .. "centerwork.png"
+theme.layout_centerworkh = theme.lain_icons .. "centerworkh.png" -- centerwork.horizontal
+```
+
+Credit goes to [Nicolas Estibals](https://github.com/nestibal) for creating
+layout icons for default theme.
+
+You can use them as a template for your custom versions.
\ No newline at end of file
diff --git a/.config/awesome/lain/wiki/Utilities.md b/.config/awesome/lain/wiki/Utilities.md
new file mode 100755
index 0000000..af0ec28
--- /dev/null
+++ b/.config/awesome/lain/wiki/Utilities.md
@@ -0,0 +1,339 @@
+Quake
+-----
+
+A Quake-like dropdown container for your favourite application.
+
+**Usage**
+
+Define it globally to have a single instance for all screens:
+
+```lua
+local quake = lain.util.quake()
+```
+
+or define it in `connect_for_each_screen` to have one instance for each screen:
+
+```lua
+awful.screen.connect_for_each_screen(function(s)
+ -- Quake application
+ s.quake = lain.util.quake()
+ -- [...]
+```
+
+**Keybinding example**
+
+If using a global instance:
+```lua
+awful.key({ modkey, }, "z", function () quake:toggle() end),
+```
+
+If using a per-screen instance:
+```lua
+awful.key({ modkey, }, "z", function () awful.screen.focused().quake:toggle() end),
+```
+
+**Input table**
+
+Variable | Meaning | Type | Default
+--- | --- | --- | ---
+`app` | client to spawn | string | "xterm"
+`name` | client name | string | "QuakeDD"
+`argname` | how to specify client name | string | "-name %s"
+`extra` | extra `app` arguments | string | empty string
+`border` | border width | integer | 1
+`visible` | initially visible | boolean | false
+`followtag` | always spawn on currently focused screen | boolean | false
+`overlap` | Overlap the wibox or not | boolean | false
+`settings` | Additional settings to make on the client | function | `nil`
+`screen` | screen where to spawn the client | integer | `awful.screen.focused()`
+`height` | dropdown client height | float in [0,1] or exact pixels number | 0.25
+`width` | dropdown client width | float in [0,1] or exact pixels number | 1
+`vert` | vertical position | string, possible values: "top", "bottom", "center" | "top"
+`horiz` | horizontal position | string, possible values: "left", "right", "center" | "left"
+
+`height` and `width` express a fraction of the workspace.
+
+`settings` is a function which takes the client as input, and can be used to customize its properties. For instance:
+
+```lua
+-- set the client sticky
+s.quake = lain.util.quake { settings = function(c) c.sticky = true end }
+```
+
+Read [here](https://awesomewm.org/doc/api/classes/client.html#Object_properties) for the complete list of properties.
+
+**Notes**
+
+* [Does not work](https://github.com/lcpz/lain/issues/358) with `gnome-terminal`, `konsole`, or any other terminal which is strictly designed for a Desktop Environment. Just pick a better terminal, [there's plenty](https://wiki.archlinux.org/index.php/List_of_applications#Terminal_emulators).
+* Set `followtag = true` if [experiencing issues with multiple screens](https://github.com/lcpz/lain/issues/346).
+* If you have a `awful.client.setslave` rule for your application, ensure you use an exception for `QuakeDD` (or your defined `name`). Otherwise, you may run into problems with focus.
+* If you are using a VTE-based terminal like `termite`, be sure to set [`argname = "--name %s"`](https://github.com/lcpz/lain/issues/211).
+* If you are using a terminal that does not always set its `class_id` to your specified `name`, such as Alacritty, then toggling will not work. You can solve this issue as explained [here](https://github.com/lcpz/lain/issues/536#issuecomment-1180155486).
+
+Separators
+----------
+
+Adds Cairo separators.
+
+```lua
+local separators = lain.util.separators
+```
+
+A separator function `separators.separator` takes two color arguments, defined as strings. `"alpha"` argument is allowed. Example:
+
+```lua
+arrl_dl = separators.arrow_left(beautiful.bg_focus, "alpha")
+arrl_ld = separators.arrow_left("alpha", beautiful.bg_focus)
+```
+
+You can customize height and width by setting `separators_height` and `separators_width` in your `theme.lua`. Default values are 0 and 9, respectively.
+
+List of functions:
+
+ +-- separators
+ |
+ |`-- arrow_right() Draw a right arrow.
+ `-- arrow_left() Draw a left arrow.
+
+Markup
+------
+
+Easier markup.
+
+```lua
+local markup = lain.util.markup
+```
+
+List of functions:
+
+ +-- markup
+ |
+ |`-- bold() Set bold.
+ |`-- italic() Set italicized text.
+ |`-- strike() Set strikethrough text.
+ |`-- underline() Set underlined text.
+ |`-- monospace() Set monospaced text.
+ |`-- big() Set bigger text.
+ |`-- small() Set smaller text.
+ |`-- font() Set the font of the text.
+ |`-- font() Set the font of the text.
+ |`-- color() Set background and foreground color.
+ |`-- fontfg() Set font and foreground color.
+ |`-- fontbg() Set font and background color.
+ `-- fontcolor() Set font, plus background and foreground colors.
+ |
+ |`--+ bg
+ | |
+ | `-- color() Set background color.
+ |
+ `--+ fg
+ |
+ `-- color() Set foreground color.
+
+they all take one argument, which is the text to markup, except the following:
+
+```lua
+markup.font(font, text)
+markup.color(fg, bg, text)
+markup.fontfg(font, fg, text)
+markup.fontbg(font, bg, text)
+markup.fontcolor(font, fg, bg, text)
+markup.fg.color(color, text)
+markup.bg.color(color, text)
+```
+
+Dynamic tagging
+---------------
+
+That is:
+
+- add a new tag;
+- rename current tag;
+- move current tag;
+- delete current tag.
+
+If you delete a tag, any rule set on it shall be broken, so be careful.
+
+Use it with key bindings like these:
+
+```lua
+awful.key({ modkey, "Shift" }, "n", function () lain.util.add_tag(mylayout) end),
+awful.key({ modkey, "Shift" }, "r", function () lain.util.rename_tag() end),
+awful.key({ modkey, "Shift" }, "Left", function () lain.util.move_tag(1) end), -- move to next tag
+awful.key({ modkey, "Shift" }, "Right", function () lain.util.move_tag(-1) end), -- move to previous tag
+awful.key({ modkey, "Shift" }, "d", function () lain.util.delete_tag() end),
+```
+
+The argument in `lain.util.add_tag` represents the tag layout, and is optional: if not present, it will be defaulted to `awful.layout.suit.tile`.
+
+Useless gaps resize
+---------------------
+
+Changes `beautiful.useless_gaps` on the fly.
+
+```lua
+lain.util.useless_gap_resize(thatmuch, s, t)
+```
+
+The argument `thatmuch` is the number of pixel to add to/substract from gaps (integer).
+
+The arguments `s` and `t` are the `awful.screen` and `awful.tag` in which you want to change the gap. They are optional.
+
+Following are example keybindings for changing client gaps on current screen and tag.
+
+Example 1:
+
+```lua
+-- On the fly useless gaps change
+awful.key({ altkey, "Control" }, "+", function () lain.util.useless_gaps_resize(1) end),
+awful.key({ altkey, "Control" }, "-", function () lain.util.useless_gaps_resize(-1) end),
+```
+
+where `altkey = Mod1`. Example 2:
+
+```lua
+mywidget:buttons(awful.util.table.join (
+ awful.button({}, 4, function() lain.util.useless_gaps_resize(-1) end),
+ awful.button({}, 5, function() lain.util.useless_gaps_resize(1) end)
+ end)
+))
+```
+
+so when hovering the mouse over `mywidget`, you can adjust useless gaps size by scrolling with the mouse wheel.
+
+tag\_view\_nonempty
+-------------------
+
+This function lets you jump to the next/previous non-empty tag.
+It takes two arguments:
+
+* `direction`: `1` for next non-empty tag, `-1` for previous.
+* `sc`: Screen which the taglist is in. Default is `mouse.screen` or `1`. This
+ argument is optional.
+
+You can use it with key bindings like these:
+
+```lua
+-- Non-empty tag browsing
+awful.key({ altkey }, "Left", function () lain.util.tag_view_nonempty(-1) end),
+awful.key({ altkey }, "Right", function () lain.util.tag_view_nonempty(1) end),
+```
+
+where `altkey = "Mod1"`.
+
+magnify\_client
+---------------
+
+Set a client to floating and resize it in the same way the "magnifier"
+layout does it. Place it on the "current" screen (derived from the mouse
+position). This allows you to magnify any client you wish, regardless of
+the currently used layout. Use it with a client keybinding like this:
+
+```lua
+clientkeys = awful.util.table.join(
+ -- [...]
+ awful.key({ modkey, "Control" }, "m", lain.util.magnify_client),
+ -- [...]
+)
+```
+
+If you want to "de-magnify" it, just retype the keybinding.
+
+If you want magnified client to respond to `incmwfact`, read [here](https://github.com/lcpz/lain/issues/195).
+
+menu\_clients\_current\_tags
+----------------------------
+
+Similar to `awful.menu.clients`, but this menu only shows the clients
+of currently visible tags. Use it with a key binding like this:
+
+```lua
+awful.key({ "Mod1" }, "Tab", function()
+ lain.util.menu_clients_current_tags({ width = 350 }, { keygrabber = true })
+end),
+```
+
+menu\_iterator
+--------------
+
+A generic menu utility which enables iteration over lists of possible
+actions to execute. The perfect example is a menu for choosing what
+configuration to apply to X with `xrandr`, as suggested on the [Awesome wiki page](https://awesomewm.org/recipes/xrandr).
+
+