Add awesome extras

This commit is contained in:
2021-12-15 13:09:25 +00:00
parent 2aa8f732d6
commit 36bfe02555
49 changed files with 6173 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
trim_trailing_whitespace = true
[*.lua]
indent_size = 4
indent_style = space
max_line_length = 80
[*.yml]
indent_size = 2
indent_style = space
[*.{html,css}]
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,53 @@
local Gio = require("lgi").Gio
local _filesystem = {}
--- Get a list of files from a given directory.
-- @string path The directory to search.
-- @tparam[opt] table exts Specific extensions to limit the search to. eg:`{ "jpg", "png" }`
-- If ommited, all files are considered.
-- @bool[opt=false] recursive List files from subdirectories
-- @staticfct bling.helpers.filesystem.get_random_file_from_dir
function _filesystem.list_directory_files(path, exts, recursive)
recursive = recursive or false
local files, valid_exts = {}, {}
-- Transforms { "jpg", ... } into { [jpg] = #, ... }
if exts then
for i, j in ipairs(exts) do
valid_exts[j:lower()] = i
end
end
-- Build a table of files from the path with the required extensions
local file_list = Gio.File.new_for_path(path):enumerate_children(
"standard::*",
0
)
if file_list then
for file in function()
return file_list:next_file()
end do
local file_type = file:get_file_type()
if file_type == "REGULAR" then
local file_name = file:get_display_name()
if
not exts
or valid_exts[file_name:lower():match(".+%.(.*)$") or ""]
then
table.insert(files, file_name)
end
elseif recursive and file_type == "DIRECTORY" then
local file_name = file:get_display_name()
files = gears.table.join(
files,
list_directory_files(file_name, exts, recursive)
)
end
end
end
return files
end
return _filesystem

View File

@@ -0,0 +1,134 @@
local Gio = require("lgi").Gio
local Gtk = require("lgi").Gtk
local gobject = require("gears.object")
local gtable = require("gears.table")
local helpers = require("helpers")
local setmetatable = setmetatable
local ipairs = ipairs
local icon_theme = { mt = {} }
function icon_theme:get_client_icon_path(client)
local function find_icon(class)
if self._private.client_icon_cache[class] ~= nil then
return self._private.client_icon_cache[class]
end
for _, app in ipairs(Gio.AppInfo.get_all()) do
local id = Gio.AppInfo.get_id(app)
if id:match(helpers.misc.case_insensitive_pattern(class)) then
self._private.client_icon_cache[class] = self:get_gicon_path(Gio.AppInfo.get_icon(app))
return self._private.client_icon_cache[class]
end
end
return nil
end
local class = client.class
if class == "jetbrains-studio" then
class = "android-studio"
end
local icon = self:get_icon_path("gnome-window-manager")
if class ~= nil then
class = class:gsub("[%-]", "%%%0")
icon = find_icon(class) or icon
class = client.class
class = class:gsub("[%-]", "")
icon = find_icon(class) or icon
class = client.class
class = class:gsub("[%-]", ".")
icon = find_icon(class) or icon
class = client.class
class = class:match("(.-)-") or class
class = class:match("(.-)%.") or class
class = class:match("(.-)%s+") or class
class = class:gsub("[%-]", "%%%0")
icon = find_icon(class) or icon
end
return icon
end
function icon_theme:choose_icon(icons_names)
local icon_info = Gtk.IconTheme.choose_icon(self.gtk_theme, icons_names, self.icon_size, 0);
if icon_info then
local icon_path = Gtk.IconInfo.get_filename(icon_info)
if icon_path then
return icon_path
end
end
return ""
end
function icon_theme:get_gicon_path(gicon)
if gicon == nil then
return ""
end
if self._private.icon_cache[gicon] ~= nil then
return self._private.icon_cache[gicon]
end
local icon_info = Gtk.IconTheme.lookup_by_gicon(self.gtk_theme, gicon, self.icon_size, 0);
if icon_info then
local icon_path = Gtk.IconInfo.get_filename(icon_info)
if icon_path then
self._private.icon_cache[gicon] = icon_path
return icon_path
end
end
return ""
end
function icon_theme:get_icon_path(icon_name)
if self._private.icon_cache[icon_name] ~= nil then
return self._private.icon_cache[icon_name]
end
local icon_info = Gtk.IconTheme.lookup_icon(self.gtk_theme, icon_name, self.icon_size, 0);
if icon_info then
local icon_path = Gtk.IconInfo.get_filename(icon_info)
if icon_path then
self._private.icon_cache[icon_name] = icon_path
return icon_path
end
end
return ""
end
local function new(theme_name, icon_size)
local ret = gobject{}
gtable.crush(ret, icon_theme, true)
ret._private = {}
ret._private.client_icon_cache = {}
ret._private.icon_cache = {}
ret.name = theme_name or nil
ret.icon_size = icon_size or 48
if theme_name then
ret.gtk_theme = Gtk.IconTheme.new()
Gtk.IconTheme.set_custom_theme(ret.gtk_theme, theme_name);
else
ret.gtk_theme = Gtk.IconTheme.get_default()
end
return ret
end
function icon_theme.mt:__call(...)
return new(...)
end
return setmetatable(icon_theme, icon_theme.mt)

View File

@@ -0,0 +1,7 @@
return {
client = require(... .. ".client"),
color = require(... .. ".color"),
filesystem = require(... .. ".filesystem"),
shape = require(... .. ".shape"),
time = require(... .. ".time"),
}

View File

@@ -0,0 +1,30 @@
local gears = require("gears")
shape = {}
-- Create rounded rectangle shape (in one line)
function shape.rrect(radius)
return function(cr, width, height)
gears.shape.rounded_rect(cr, width, height, radius)
end
end
-- Create partially rounded rect
function shape.prrect(radius, tl, tr, br, bl)
return function(cr, width, height)
gears.shape.partially_rounded_rect(
cr,
width,
height,
tl,
tr,
br,
bl,
radius
)
end
end
return shape

View File

@@ -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

BIN
.config/awesome/extras/bling/icons/layouts/centered.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/icons/layouts/deck.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/icons/layouts/equalarea.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/icons/layouts/horizontal.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/icons/layouts/mstab.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/icons/layouts/vertical.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/images/bling_banner-2x.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
.config/awesome/extras/bling/images/bling_banner.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
return {
layout = require(... .. ".layout"),
--module = require(... .. ".module"),
--helpers = require(... .. ".helpers"),
--signal = require(... .. ".signal"),
--widget = require(... .. ".widget"),
}

View File

@@ -0,0 +1,84 @@
local awful = require("awful")
local math = math
local mylayout = {}
mylayout.name = "centered"
function mylayout.arrange(p)
local area = p.workarea
local t = p.tag or screen[p.screen].selected_tag
local mwfact = t.master_width_factor
local nmaster = math.min(t.master_count, #p.clients)
local nslaves = #p.clients - nmaster
local master_area_width = area.width * mwfact
local slave_area_width = area.width - master_area_width
local master_area_x = area.x + 0.5 * slave_area_width
local number_of_left_sided_slaves = math.floor(nslaves / 2)
local number_of_right_sided_slaves = nslaves - number_of_left_sided_slaves
local left_iterator = 0
local right_iterator = 0
-- Special case: no maters -> rrelapse into awesomes fair layout
if t.master_count == 0 then
awful.layout.suit.fair.arrange(p)
return
end
-- Special case: one slave -> relapse into awesomes masterstack tile layout
if nslaves == 1 then
awful.layout.suit.tile.right.arrange(p)
return
end
-- Special case: no slaves -> fullscreen master area
if nslaves < 1 then
master_area_width = area.width
master_area_x = area.x
end
-- iterate through masters
for idx = 1, nmaster do
local c = p.clients[idx]
local g
g = {
x = master_area_x,
y = area.y + (nmaster - idx) * (area.height / nmaster),
width = master_area_width,
height = area.height / nmaster,
}
p.geometries[c] = g
end
-- iterate through slaves
for idx = 1, nslaves do -- idx=nmaster+1,#p.clients do
local c = p.clients[idx + nmaster]
local g
if idx % 2 == 0 then
g = {
x = area.x,
y = area.y
+ left_iterator
* (area.height / number_of_left_sided_slaves),
width = slave_area_width / 2,
height = area.height / number_of_left_sided_slaves,
}
left_iterator = left_iterator + 1
else
g = {
x = area.x + master_area_width + slave_area_width / 2,
y = area.y
+ right_iterator
* (area.height / number_of_right_sided_slaves),
width = slave_area_width / 2,
height = area.height / number_of_right_sided_slaves,
}
right_iterator = right_iterator + 1
end
p.geometries[c] = g
end
end
return mylayout

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,44 @@
local beautiful = require("beautiful")
local gears = require("gears")
local M = {}
local relative_lua_path = tostring(...)
local function get_layout_icon_path(name)
local relative_icon_path = relative_lua_path
:match("^.*bling"):gsub("%.", "/")
.. "/icons/layouts/" .. name .. ".png"
for p in package.path:gmatch('([^;]+)') do
p = p:gsub("?.*", "")
local absolute_icon_path = p .. relative_icon_path
if gears.filesystem.file_readable(absolute_icon_path) then
return absolute_icon_path
end
end
end
local function get_icon(icon_raw)
if icon_raw ~= nil then
return gears.color.recolor_image(icon_raw, "#e6e6e6")
else
return nil
end
end
local layouts = {
"mstab",
"vertical",
"horizontal",
"centered",
"equalarea",
"deck"
}
for _, layout_name in ipairs(layouts) do
local icon_raw = get_layout_icon_path(layout_name)
beautiful["layout_" .. layout_name] = get_icon(icon_raw)
M[layout_name] = require(... .. "." .. layout_name)
end
return M

View File

@@ -0,0 +1,249 @@
local awful = require("awful")
local gears = require("gears")
local wibox = require("wibox")
local beautiful = require("beautiful")
local mylayout = {}
mylayout.name = "mstab"
local tabbar_disable = beautiful.mstab_bar_disable or false
local tabbar_ontop = beautiful.mstab_bar_ontop or false
local tabbar_padding = beautiful.mstab_bar_padding or "default"
local border_radius = beautiful.mstab_border_radius
or beautiful.border_radius
or 0
local tabbar_position = beautiful.mstab_tabbar_position
or beautiful.tabbar_position
or "top"
local bar_style = beautiful.mstab_tabbar_style
or beautiful.tabbar_style
or "default"
local bar = require(
tostring(...):match(".*bling") .. ".widget.tabbar." .. bar_style
)
local tabbar_size = bar.size
or beautiful.mstab_bar_height
or beautiful.tabbar_size
or 40
local dont_resize_slaves = beautiful.mstab_dont_resize_slaves or false
-- The top_idx is the idx of the slave clients (excluding all master clients)
-- that should be on top of all other slave clients ("the focused slave")
-- by creating a variable outside of the arrange function, this layout can "remember" that client
-- by creating it as a new property of every tag, this layout can be active on different tags and
-- still have different "focused slave clients"
for idx, tag in ipairs(root.tags()) do
tag.top_idx = 1
end
-- Haven't found a signal that is emitted when a new tag is added. That should work though
-- since you can't use a layout on a tag that you haven't selected previously
tag.connect_signal("property::selected", function(t)
if not t.top_idx then
t.top_idx = 1
end
end)
function update_tabbar(
clients,
t,
top_idx,
area,
master_area_width,
slave_area_width
)
local s = t.screen
-- create the list of clients for the tabbar
local clientlist = bar.layout()
for idx, c in ipairs(clients) do
-- focus with right click, kill with mid click, minimize with left click
local buttons = gears.table.join(
awful.button({}, 1, function()
c:raise()
client.focus = c
end),
awful.button({}, 2, function()
c:kill()
end),
awful.button({}, 3, function()
c.minimized = true
end)
)
local client_box = bar.create(c, (idx == top_idx), buttons)
clientlist:add(client_box)
end
-- if no tabbar exists, create one
if not s.tabbar then
s.tabbar = wibox({
ontop = tabbar_ontop,
shape = function(cr, width, height)
gears.shape.rounded_rect(cr, width, height, border_radius)
end,
bg = bar.bg_normal,
visible = true,
})
-- Change visibility of the tab bar when layout, selected tag or number of clients (visible, master, slave) changes
local function adjust_visiblity(t)
s.tabbar.visible = (#t:clients() - t.master_count > 1)
and (t.layout.name == mylayout.name)
end
tag.connect_signal("property::selected", function(t)
adjust_visiblity(t)
end)
tag.connect_signal("property::layout", function(t, layout)
adjust_visiblity(t)
end)
tag.connect_signal("tagged", function(t, c)
adjust_visiblity(t)
end)
tag.connect_signal("untagged", function(t, c)
adjust_visiblity(t)
end)
tag.connect_signal("property::master_count", function(t)
adjust_visiblity(t)
end)
client.connect_signal("property::minimized", function(c)
local t = c.first_tag
adjust_visiblity(t)
end)
end
-- update the tabbar size and position (to support gap size change on the fly)
if tabbar_position == "top" then
s.tabbar.x = area.x + master_area_width + t.gap
s.tabbar.y = area.y + t.gap
s.tabbar.width = slave_area_width - 2 * t.gap
s.tabbar.height = tabbar_size
elseif tabbar_position == "bottom" then
s.tabbar.x = area.x + master_area_width + t.gap
s.tabbar.y = area.y + area.height - tabbar_size - t.gap
s.tabbar.width = slave_area_width - 2 * t.gap
s.tabbar.height = tabbar_size
elseif tabbar_position == "left" then
s.tabbar.x = area.x + master_area_width + t.gap
s.tabbar.y = area.y + t.gap
s.tabbar.width = tabbar_size
s.tabbar.height = area.height - 2 * t.gap
elseif tabbar_position == "right" then
s.tabbar.x = area.x
+ master_area_width
+ slave_area_width
- tabbar_size
- t.gap
s.tabbar.y = area.y + t.gap
s.tabbar.width = tabbar_size
s.tabbar.height = area.height - 2 * t.gap
end
-- update clientlist
s.tabbar:setup({ layout = wibox.layout.flex.horizontal, clientlist })
end
function mylayout.arrange(p)
local area = p.workarea
local t = p.tag or screen[p.screen].selected_tag
local s = t.screen
local mwfact = t.master_width_factor
local nmaster = math.min(t.master_count, #p.clients)
local nslaves = #p.clients - nmaster
local master_area_width = area.width * mwfact
local slave_area_width = area.width - master_area_width
-- "default" means that it uses standard useless gap size
if tabbar_padding == "default" then
tabbar_padding = 2 * t.gap
end
-- Special case: No masters -> full screen slave width
if nmaster == 0 then
master_area_width = 1
slave_area_width = area.width
end
-- Special case: One or zero slaves -> no tabbar (essentially tile right)
if nslaves <= 1 then
-- since update_tabbar isnt called that way we have to hide it manually
if s.tabbar then
s.tabbar.visible = false
end
-- otherwise just do tile right
awful.layout.suit.tile.right.arrange(p)
return
end
-- Iterate through masters
for idx = 1, nmaster do
local c = p.clients[idx]
local g = {
x = area.x,
y = area.y + (idx - 1) * (area.height / nmaster),
width = master_area_width,
height = area.height / nmaster,
}
p.geometries[c] = g
end
local tabbar_size_change = 0
local tabbar_width_change = 0
local tabbar_y_change = 0
local tabbar_x_change = 0
if not tabbar_disable then
if tabbar_position == "top" then
tabbar_size_change = tabbar_size + tabbar_padding
tabbar_y_change = tabbar_size + tabbar_padding
elseif tabbar_position == "bottom" then
tabbar_size_change = tabbar_size + tabbar_padding
elseif tabbar_position == "left" then
tabbar_width_change = tabbar_size + tabbar_padding
tabbar_x_change = tabbar_size + tabbar_padding
elseif tabbar_position == "right" then
tabbar_width_change = tabbar_size + tabbar_padding
end
end
-- Iterate through slaves
-- (also creates a list of all slave clients for update_tabbar)
local slave_clients = {}
for idx = 1, nslaves do
local c = p.clients[idx + nmaster]
slave_clients[#slave_clients + 1] = c
if c == client.focus then
t.top_idx = #slave_clients
end
local g = {
x = area.x + master_area_width + tabbar_x_change,
y = area.y + tabbar_y_change,
width = slave_area_width - tabbar_width_change,
height = area.height - tabbar_size_change,
}
if not dont_resize_slaves and idx ~= t.top_idx then
g = {
x = area.x + master_area_width + slave_area_width / 4,
y = area.y + tabbar_size + area.height / 4,
width = slave_area_width / 2,
height = area.height / 4 - tabbar_size,
}
end
p.geometries[c] = g
end
if not tabbar_disable then
update_tabbar(
slave_clients,
t,
t.top_idx,
area,
master_area_width,
slave_area_width
)
end
end
return mylayout

View File

@@ -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

View File

@@ -0,0 +1,44 @@
local gears = require("gears")
local beautiful = require("beautiful")
local op = beautiful.flash_focus_start_opacity or 0.6
local stp = beautiful.flash_focus_step or 0.01
local flashfocus = function(c)
if c then
c.opacity = op
local q = op
local g = gears.timer({
timeout = stp,
call_now = false,
autostart = true,
})
g:connect_signal("timeout", function()
if not c.valid then
return
end
if q >= 1 then
c.opacity = 1
g:stop()
else
c.opacity = q
q = q + stp
end
end)
end
-- Bring the focused client to the top
if c then
c:raise()
end
end
local enable = function()
client.connect_signal("focus", flashfocus)
end
local disable = function()
client.disconnect_signal("focus", flashfocus)
end
return { enable = enable, disable = disable, flashfocus = flashfocus }

View File

@@ -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"),
}

View File

@@ -0,0 +1,374 @@
local awful = require("awful")
local gears = require("gears")
local naughty = require("naughty")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local capi = { awesome = awesome, client = client }
local ruled = capi.awesome.version ~= "v4.3" and require("ruled") or nil
local pairs = pairs
local Scratchpad = { mt = {} }
--- Called when the turn off animation has ended
local function on_animate_turn_off_end(self, tag)
-- When toggling off a scratchpad that's present on multiple tags
-- depsite still being unminizmied on the other tags it will become invisible
-- as it's position could be outside the screen from the animation
self.client:geometry({
x = self.geometry.x + self.client.screen.geometry.x,
y = self.geometry.y + self.client.screen.geometry.y,
width = self.geometry.width,
height = self.geometry.height,
})
helpers.client.turn_off(self.client, tag)
self.turning_off = false
self:emit_signal("turn_off", self.client)
end
--- The turn off animation
local function animate_turn_off(self, anim, axis)
self.screen_on_toggled_scratchpad = self.client.screen
self.tag_on_toggled_scratchpad = self.screen_on_toggled_scratchpad.selected_tag
if self.client.floating == false then
-- Save the client geometry before floating it
local non_floating_x = self.client.x
local non_floating_y = self.client.y
local non_floating_width = self.client.width
local non_floating_height = self.client.height
-- Can't animate non floating clients
self.client.floating = true
-- Set the client geometry back to what it was before floating it
self.client:geometry({
x = non_floating_x,
y = non_floating_y,
width = non_floating_width,
height = non_floating_height,
})
end
if axis == "x" then
anim.pos = self.client.x
else
anim.pos = self.client.y
end
anim:set(anim:initial())
end
-- Handles changing tag mid animation
local function abort_if_tag_was_switched(self)
-- Check for the following scenerio:
-- Toggle on scratchpad at tag 1
-- Toggle on scratchpad at tag 2
-- Toggle off scratchpad at tag 1
-- Switch to tag 2
-- Outcome: The client will remain on tag 1 and will instead be removed from tag 2
if (self.turning_off) and (self.screen_on_toggled_scratchpad and
self.screen_on_toggled_scratchpad.selected_tag) ~= self.tag_on_toggled_scratchpad
then
if self.rubato.x then
self.rubato.x:abort()
end
if self.rubato.y then
self.rubato.y:abort()
end
on_animate_turn_off_end(self, self.tag_on_toggled_scratchpad)
self.screen_on_toggled_scratchpad.selected_tag = nil
self.tag_on_toggled_scratchpad = nil
end
end
--- The turn on animation
local function animate_turn_on(self, anim, axis)
-- Check for the following scenerio:
-- Toggle on scratchpad at tag 1
-- Toggle on scratchpad at tag 2
-- The animation will instantly end
-- as the timer pos is already at the on position
-- from toggling on the scratchpad at tag 1
if axis == "x" and anim.pos == self.geometry.x then
anim.pos = anim:initial()
else
if anim.pos == self.geometry.y then
anim.pos = anim:initial()
end
end
if axis == "x" then
anim:set(self.geometry.x)
else
anim:set(self.geometry.y)
end
end
--- Creates a new scratchpad object based on the argument
--
-- @param args A table of possible arguments
-- @return The new scratchpad object
function Scratchpad:new(args)
args = args or {}
if args.awestore then
naughty.notify({
title = "Bling Error",
text = "Awestore is no longer supported! Please take a look at the scratchpad documentation and use rubato for animations instead.",
})
end
args.rubato = args.rubato or {}
local ret = gears.object{}
gears.table.crush(ret, Scratchpad)
gears.table.crush(ret, args)
if ret.rubato.x then
ret.rubato.x:subscribe(function(pos)
if ret.client and ret.client.valid then
ret.client.x = pos
end
abort_if_tag_was_switched(ret)
end)
ret.rubato.x.ended:subscribe(function()
if ((ret.rubato.y and ret.rubato.y.state == false) or (ret.rubato.y == nil)) and ret.turning_off == true then
on_animate_turn_off_end(ret)
end
end)
end
if ret.rubato.y then
ret.rubato.y:subscribe(function(pos)
if ret.client and ret.client.valid then
ret.client.y = pos
end
abort_if_tag_was_switched(ret)
end)
ret.rubato.y.ended:subscribe(function()
if ((ret.rubato.x and ret.rubato.x.state == false) or (ret.rubato.x == nil)) and ret.turning_off == true then
on_animate_turn_off_end(ret)
end
end)
end
return ret
end
--- Find all clients that satisfy the the rule
--
-- @return A list of all clients that satisfy the rule
function Scratchpad:find()
return helpers.client.find(self.rule)
end
--- Applies the objects scratchpad properties to a given client
--
-- @param c A client to which to apply the properties
function Scratchpad:apply(c)
if not c or not c.valid then
return
end
c.floating = self.floating
c.sticky = self.sticky
c.fullscreen = false
c.maximized = false
c:geometry({
x = self.geometry.x + awful.screen.focused().geometry.x,
y = self.geometry.y + awful.screen.focused().geometry.y,
width = self.geometry.width,
height = self.geometry.height,
})
if self.autoclose then
c:connect_signal("unfocus", function(c1)
c1.sticky = false -- client won't turn off if sticky
helpers.client.turn_off(c1)
end)
end
end
--- Turns the scratchpad on
function Scratchpad:turn_on()
self.client = self:find()[1]
local anim_x = self.rubato.x
local anim_y = self.rubato.y
local in_anim = false
if (anim_x and anim_x.state == true) or (anim_y and anim_y.state == true) then
in_anim = true
end
if self.client and not in_anim and self.client.first_tag and self.client.first_tag.selected then
self.client:raise()
capi.client.focus = self.client
return
end
if self.client and not in_anim then
-- if a client was found, turn it on
if self.reapply then
self:apply(self.client)
end
-- c.sticky was set to false in turn_off so it has to be reapplied anyway
self.client.sticky = self.sticky
if anim_x then
animate_turn_on(self, anim_x, "x")
end
if anim_y then
animate_turn_on(self, anim_y, "y")
end
helpers.client.turn_on(self.client)
self:emit_signal("turn_on", self.client)
return
end
if not self.client then
-- if no client was found, spawn one, find the corresponding window,
-- apply the properties only once (until the next closing)
local pid = awful.spawn.with_shell(self.command)
if capi.awesome.version ~= "v4.3" then
ruled.client.append_rule({
id = "scratchpad",
rule = self.rule,
properties = {
-- If a scratchpad is opened it should spawn at the current tag
-- the same way it will behave if the client was already open
tag = awful.screen.focused().selected_tag,
switch_to_tags = false,
-- Hide the client until the gemoetry rules are applied
hidden = true,
minimized = true,
},
callback = function(c)
-- For a reason I can't quite get the gemotery rules will fail to apply unless we use this timer
gears.timer({
timeout = 0.15,
autostart = true,
single_shot = true,
callback = function()
self.client = c
self:apply(c)
c.hidden = false
c.minimized = false
-- Some clients fail to gain focus
c:activate({})
if anim_x then
animate_turn_on(self, anim_x, "x")
end
if anim_y then
animate_turn_on(self, anim_y, "y")
end
self:emit_signal("inital_apply", c)
-- Discord spawns 2 windows, so keep the rule until the 2nd window shows
if c.name ~= "Discord Updater" then
ruled.client.remove_rule("scratchpad")
end
-- In a case Discord is killed before the second window spawns
c:connect_signal("request::unmanage", function()
ruled.client.remove_rule("scratchpad")
end)
end,
})
end,
})
else
local function inital_apply(c1)
if helpers.client.is_child_of(c1, pid) then
self.client = c1
self:apply(c1)
if anim_x then
animate_turn_on(self, anim_x, "x")
end
if anim_y then
animate_turn_on(self, anim_y, "y")
end
self:emit_signal("inital_apply", c1)
self.client.disconnect_signal("manage", inital_apply)
end
end
self.client.connect_signal("manage", inital_apply)
end
end
end
--- Turns the scratchpad off
function Scratchpad:turn_off()
self.client = self:find()[1]
-- Get the tweens
local anim_x = self.rubato.x
local anim_y = self.rubato.y
local in_anim = false
if (anim_x and anim_x.state == true) or (anim_y and anim_y.state == true) then
in_anim = true
end
if self.client and not in_anim then
if anim_x then
self.turning_off = true
animate_turn_off(self, anim_x, "x")
end
if anim_y then
self.turning_off = true
animate_turn_off(self, anim_y, "y")
end
if not anim_x and not anim_y then
helpers.client.turn_off(self.client)
self:emit_signal("turn_off", self.client)
end
end
end
--- Turns the scratchpad off if it is focused otherwise it raises the scratchpad
function Scratchpad:toggle()
local is_turn_off = false
local c = self:find()[1]
if self.dont_focus_before_close then
if c then
if c.sticky and #c:tags() > 0 then
is_turn_off = true
else
local current_tag = c.screen.selected_tag
for k, tag in pairs(c:tags()) do
if tag == current_tag then
is_turn_off = true
break
else
is_turn_off = false
end
end
end
end
else
is_turn_off = capi.client.focus
and awful.rules.match(capi.client.focus, self.rule)
end
if is_turn_off then
self:turn_off()
else
self:turn_on()
end
end
--- Make the module callable without putting a `:new` at the end of it
--
-- @param args A table of possible arguments
-- @return The new scratchpad object
function Scratchpad.mt:__call(...)
return Scratchpad:new(...)
end
return setmetatable(Scratchpad, Scratchpad.mt)

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,339 @@
---------------------------------------------------------------------------
-- High-level declarative function for setting your wallpaper.
--
--
-- An easy way to setup a complex wallpaper with slideshow, random, schedule, extensibility.
--
-- @usage
-- local wallpaper = require("wallpaper")
-- -- A silly example
-- wallpaper.setup { -- I want a wallpaper
-- change_timer = 500, -- changing every 5 minutes
-- set_function = wallpaper.setters.random, -- in a random way
-- wallpaper = {"#abcdef",
-- "~/Pictures",
-- wallpaper.setters.awesome}, -- from this list (a color, a directory with pictures and the Awesome wallpaper)
-- recursive = false, -- do not read subfolders of "~/Pictures"
-- position = "centered", -- center it on the screen (for pictures)
-- scale = 2, -- 2 time bigger (for pictures)
-- }
--
-- @author Grumph
-- @copyright 2021 Grumph
--
---------------------------------------------------------------------------
local awful = require("awful")
local beautiful = require("beautiful")
local gears = require("gears")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local setters = {}
--- Apply a wallpaper.
--
-- This function is a helper that will apply a wallpaper_object,
-- either using gears.wallpaper.set or gears.wallpaper.* higher level functions when applicable.
-- @param wallpaper_object A wallpaper object, either
-- a `pattern` (see `gears.wallpaper.set`)
-- a `surf` (see `gears.wallpaper.centered`)
-- a function that actually sets the wallpaper.
-- @tparam table args The argument table containing any of the arguments below.
-- @int[opt=nil] args.screen The screen to use (as used in `gears.wallpaper` functions)
-- @string[opt=nil or "centered"] args.position The `gears.wallpaper` position function to use.
-- Must be set when wallpaper is a file.
-- It can be `"centered"`, `"fit"`, `"tiled"` or `"maximized"`.
-- @string[opt=beautiful.bg_normal or "black"] args.background See `gears.wallpaper`.
-- @bool[opt=false] args.ignore_aspect See `gears.wallpaper`.
-- @tparam[opt={x=0,y=0}] table args.offset See `gears.wallpaper`.
-- @int[opt=1] args.scale See `gears.wallpaper`.
function apply(wallpaper_object, args)
args.background = args.background or beautiful.bg_normal or "black"
args.ignore_aspect = args.ignore_aspect or false -- false = keep aspect ratio
args.offset = args.offset or { x = 0, y = 0 }
args.scale = args.scale or 1
local positions = {
["centered"] = function()
gears.wallpaper.centered(
wallpaper_object,
args.screen,
args.background,
args.scale
)
end,
["tiled"] = function()
gears.wallpaper.tiled(wallpaper_object, args.screen, args.offset)
end,
["maximized"] = function()
gears.wallpaper.maximized(
wallpaper_object,
args.screen,
args.ignore_aspect,
args.offset
)
end,
["fit"] = function()
gears.wallpaper.fit(wallpaper_object, args.screen, args.background)
end,
}
if
type(wallpaper_object) == "string"
and gears.filesystem.file_readable(wallpaper_object)
then
-- path of an image file, we use a position function
local p = args.position or "centered"
positions[p]()
elseif type(wallpaper_object) == "function" then
-- function
wallpaper_object(args)
elseif
(not gears.color.ensure_pango_color(wallpaper_object, nil))
and args.position
then
-- if the user sets a position function, wallpaper_object should be a cairo surface
positions[args.position]()
else
gears.wallpaper.set(wallpaper_object)
end
end
--- Converts `args.wallpaper` to a list of `wallpaper_objects` readable by `apply` function).
--
-- @tparam table args The argument table containing the argument below.
-- @param[opt=`beautiful.wallpaper_path` or `"black"`] args.wallpaper A wallpaper object.
-- It can be a color or a cairo pattern (what `gears.wallpaper.set` understands),
-- a cairo suface (set with gears.wallpaper.set if `args.position` is nil, or with
-- `gears.wallpaper` position functions, see `args.position`),
-- a function similar to args.set_function that will effectively set a wallpaper (usually
-- with `gears.wallpaper` functions),
-- a path to a file,
-- path to a directory containing images,
-- or a list with any of the previous choices.
-- @tparam[opt=`{"jpg", "jpeg", "png", "bmp"}`] table args.image_formats A list of
-- file extensions to filter when `args.wallpaper` is a directory.
-- @bool[opt=true] args.recursive Either to recurse or not when `args.wallpaper` is a directory.
-- @treturn table A list of `wallpaper_objects` (what `apply` can read).
-- @see apply
function prepare_list(args)
args.image_formats = args.image_formats or { "jpg", "jpeg", "png", "bmp" }
args.recursive = args.recursive or true
local wallpapers = (args.wallpaper or beautiful.wallpaper_path or "black")
local res = {}
if type(wallpapers) ~= "table" then
wallpapers = { wallpapers }
end
for _, w in ipairs(wallpapers) do
-- w is either:
-- - a directory path (string)
-- - an image path or a color (string)
-- - a cairo surface or a cairo pattern
-- - a function for setting the wallpaper
if type(w) == "string" and gears.filesystem.dir_readable(w) then
local file_list = helpers.filesystem.list_directory_files(
w,
args.image_formats,
args.recursive
)
for _, f in ipairs(file_list) do
res[#res + 1] = w .. "/" .. f
end
else
res[#res + 1] = w
end
end
return res
end
local simple_index = 0
--- Set the next wallpaper in a list.
--
-- @tparam table args See `prepare_list` and `apply` arguments
-- @see apply
-- @see prepare_list
function setters.simple(args)
local wallpapers = prepare_list(args)
simple_index = (simple_index % #wallpapers) + 1
apply(wallpapers[simple_index], args)
end
--- Set a random wallpaper from a list.
--
-- @tparam table args See `prepare_list` and `apply` arguments
-- @see apply
-- @see prepare_list
function setters.random(args)
local wallpapers = prepare_list(args)
apply(wallpapers[math.random(#wallpapers)], args)
end
local simple_schedule_object = nil
--- A schedule setter.
--
-- This simple schedule setter was freely inspired by [dynamic-wallpaper](https://github.com/manilarome/awesome-glorious-widgets/blob/master/dynamic-wallpaper/init.lua).
-- @tparam table args The argument table containing any of the arguments below.
-- @tparam table args.wallpaper The schedule table, with the form
-- {
-- ["HH:MM:SS"] = wallpaper,
-- ["HH:MM:SS"] = wallpaper2,
-- }
-- The wallpapers definition can be anything the `schedule_set_function` can read
-- (what you would place in `args.wallpaper` for this function),
-- @tparam[opt=`setters.simple`] function args.wallpaper_set_function The set_function used by default
function setters.simple_schedule(args)
local function update_wallpaper()
local fake_args = gears.table.join(args, {
wallpaper = args.wallpaper[simple_schedule_object.closest_lower_time],
})
simple_schedule_object.schedule_set_function(fake_args)
end
if not simple_schedule_object then
simple_schedule_object = {}
-- initialize the schedule object, so we don't do it for every call
simple_schedule_object.schedule_set_function = args.schedule_set_function
or setters.simple
-- we get the sorted time keys
simple_schedule_object.times = {}
for k in pairs(args.wallpaper) do
table.insert(simple_schedule_object.times, k)
end
table.sort(simple_schedule_object.times)
-- now we get the closest time which is below current time (the current applicable period)
local function update_timer()
local current_time = os.date("%H:%M:%S")
local next_time = simple_schedule_object.times[1]
simple_schedule_object.closest_lower_time =
simple_schedule_object.times[#simple_schedule_object.times]
for _, k in ipairs(simple_schedule_object.times) do
if k > current_time then
next_time = k
break
end
simple_schedule_object.closest_lower_time = k
end
simple_schedule_object.timer.timeout = helpers.time.time_diff(
next_time,
current_time
)
if simple_schedule_object.timer.timeout < 0 then
-- the next_time is the day after, so we add 24 hours to the timer
simple_schedule_object.timer.timeout = simple_schedule_object.timer.timeout
+ 86400
end
simple_schedule_object.timer:again()
update_wallpaper()
end
simple_schedule_object.timer = gears.timer({
callback = update_timer,
})
update_timer()
else
-- if called again (usually when the change_timer is set), we just change the wallpaper depending on current parameters
update_wallpaper()
end
end
--- Set the AWESOME wallpaper.
--
-- @tparam table args The argument table containing the argument below.
-- @param[opt=`beautiful.bg_normal`] args.colors.bg The bg color.
-- If the default is used, the color is darkened if `beautiful.bg_normal` is light
-- or lightned if `beautiful.bg_normal` is dark.
-- @param[opt=`beautiful.fg_normal`] args.colors.fg The fg color.
-- @param[opt=`beautiful.fg_focus`] args.colors.alt_fg The alt_fg color.
--
-- see beautiful.theme_assets.wallpaper
function setters.awesome_wallpaper(args)
local colors = {
bg = beautiful.bg_normal,
fg = beautiful.fg_normal,
alt_fg = beautiful.bg_focus,
}
colors.bg = helpers.color.is_dark(beautiful.bg_normal)
and helpers.color.lighten(colors.bg)
or helpers.color.darken(colors.bg)
if type(args.colors) == "table" then
colors.bg = args.colors.bg or colors.bg
colors.fg = args.colors.fg or colors.fg
colors.alt_fg = args.colors.alt_fg or colors.alt_fg
end
-- Generate wallpaper:
if not args.screen then
for s in screen do
gears.wallpaper.set(
beautiful.theme_assets.wallpaper(
colors.bg,
colors.fg,
colors.alt_fg,
s
)
)
end
else
gears.wallpaper.set(
beautiful.theme_assets.wallpaper(
colors.bg,
colors.fg,
colors.alt_fg,
args.screen
)
)
end
end
--- Setup a wallpaper.
--
-- @tparam table args Parameters for the wallpaper. It may also contain all parameters your `args.set_function` needs
-- @int[opt=nil] args.screen The screen to use (as used in `gears.wallpaper` functions)
-- @int[opt=nil] args.change_timer Time in seconds for wallpaper changes
-- @tparam[opt=`setters.awesome` or `setters.simple`] function args.set_function A function to set the wallpaper
-- It takes args as parameter (the same args as the setup function).
-- This function is called at `"request::wallpaper"` `screen` signals and at `args.change_timer` timeouts.
-- There is no obligation, but for consistency, the function should use `args.wallpaper` as a feeder.
-- If `args.wallpaper` is defined, the default function is `setters.simple`, else it will be `setters.awesome`.
--
-- @usage
-- local wallpaper = require("wallpaper")
-- wallpaper.setup {
-- change_timer = 631, -- Prime number is better
-- set_function = wallpaper.setters.random,
-- -- parameters for the random setter
-- wallpaper = '/data/pictures/wallpapers',
-- position = "maximized",
-- }
--
-- @see apply
-- @see prepare_list
-- @see setters.simple
function setup(args)
local config = args or {}
config.set_function = config.set_function
or (config.wallpaper and setters.simple or setters.awesome_wallpaper)
local function set_wallpaper(s)
config.screen = s or config.screen
config.set_function(config)
end
if config.change_timer and config.change_timer > 0 then
gears.timer({
timeout = config.change_timer,
call_now = false,
autostart = true,
callback = function()
set_wallpaper()
end,
})
end
if awesome.version == "v4.3" then
awful.screen.connect_for_each_screen(set_wallpaper)
else
screen.connect_signal("request::wallpaper", set_wallpaper)
end
end
return {
setup = setup,
setters = setters,
apply = apply,
prepare_list = prepare_list,
}

View File

@@ -0,0 +1,88 @@
local awful = require("awful")
local gears = require("gears")
local beautiful = require("beautiful")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
-- It might actually swallow too much, that's why there is a filter option by classname
-- without the don't-swallow-list it would also swallow for example
-- file pickers or new firefox windows spawned by an already existing one
local window_swallowing_activated = false
-- you might want to add or remove applications here
local dont_swallow_classname_list = beautiful.dont_swallow_classname_list
or { "firefox", "Gimp", "Google-chrome" }
local activate_dont_swallow_filter = beautiful.dont_swallow_filter_activated
or true
-- checks if client classname matches with any entry of the dont-swallow-list
local function check_if_swallow(c)
if not activate_dont_swallow_filter then
return true
end
for _, classname in ipairs(dont_swallow_classname_list) do
if classname == c.class then
return false
end
end
return true
end
-- the function that will be connected to / disconnected from the spawn client signal
local function manage_clientspawn(c)
-- get the last focused window to check if it is a parent window
local parent_client = awful.client.focus.history.get(c.screen, 1)
if not parent_client then
return
end
-- io.popen is normally discouraged. Should probably be changed
local handle = io.popen(
[[pstree -T -p -a -s ]]
.. tostring(c.pid)
.. [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']]
)
local parent_pid = handle:read("*a")
handle:close()
if
(tostring(parent_pid) == tostring(parent_client.pid))
and check_if_swallow(c)
then
c:connect_signal("unmanage", function()
helpers.client.turn_on(parent_client)
helpers.client.sync(parent_client, c)
end)
helpers.client.sync(c, parent_client)
helpers.client.turn_off(parent_client)
end
end
-- without the following functions that module would be autoloaded by require("bling")
-- a toggle window swallowing hotkey is also possible that way
local function start()
client.connect_signal("manage", manage_clientspawn)
window_swallowing_activated = true
end
local function stop()
client.disconnect_signal("manage", manage_clientspawn)
window_swallowing_activated = false
end
local function toggle()
if window_swallowing_activated then
stop()
else
start()
end
end
return {
start = start,
stop = stop,
toggle = toggle,
}

View File

@@ -0,0 +1 @@
return { playerctl = require(... .. ".playerctl") }

View File

@@ -0,0 +1,19 @@
local beautiful = require("beautiful")
-- Use CLI backend as default as it is supported on most if not all systems
local backend_config = beautiful.playerctl_backend or "playerctl_cli"
local backends = {
playerctl_cli = require(... .. ".playerctl_cli"),
playerctl_lib = require(... .. ".playerctl_lib"),
}
local function enable_wrapper(args)
backend_config = (args and args.backend) or backend_config
backends[backend_config].enable(args)
end
local function disable_wrapper()
backends[backend_config].disable()
end
return { enable = enable_wrapper, disable = disable_wrapper }

View File

@@ -0,0 +1,151 @@
--
-- Provides:
-- bling::playerctl::status
-- playing (boolean)
-- bling::playerctl::title_artist_album
-- title (string)
-- artist (string)
-- album_path (string)
-- bling::playerctl::position
-- interval_sec (number)
-- length_sec (number)
-- bling::playerctl::no_players
--
local awful = require("awful")
local beautiful = require("beautiful")
local interval = beautiful.playerctl_position_update_interval or 1
local function emit_player_status()
local status_cmd = "playerctl status -F"
-- Follow status
awful.spawn.easy_async({
"pkill",
"--full",
"--uid",
os.getenv("USER"),
"^playerctl status",
}, function()
awful.spawn.with_line_callback(status_cmd, {
stdout = function(line)
local playing = false
if line:find("Playing") then
playing = true
else
playing = false
end
awesome.emit_signal("bling::playerctl::status", playing)
end,
})
collectgarbage("collect")
end)
end
local function emit_player_info()
local art_script = [[
sh -c '
tmp_dir="$XDG_CACHE_HOME/awesome/"
if [ -z ${XDG_CACHE_HOME} ]; then
tmp_dir="$HOME/.cache/awesome/"
fi
tmp_cover_path=${tmp_dir}"cover.png"
if [ ! -d $tmp_dir ]; then
mkdir -p $tmp_dir
fi
link="$(playerctl metadata mpris:artUrl)"
curl -s "$link" --output $tmp_cover_path
echo "$tmp_cover_path"
']]
-- Command that lists artist and title in a format to find and follow
local song_follow_cmd =
"playerctl metadata --format 'artist_{{artist}}title_{{title}}' -F"
-- Progress Cmds
local prog_cmd = "playerctl position"
local length_cmd = "playerctl metadata mpris:length"
awful.widget.watch(prog_cmd, interval, function(_, interval)
awful.spawn.easy_async_with_shell(length_cmd, function(length)
local length_sec = tonumber(length) -- in microseconds
local interval_sec = tonumber(interval) -- in seconds
if length_sec and interval_sec then
if interval_sec >= 0 and length_sec > 0 then
awesome.emit_signal(
"bling::playerctl::position",
interval_sec,
length_sec / 1000000
)
end
end
end)
collectgarbage("collect")
end)
-- Follow title
awful.spawn.easy_async({
"pkill",
"--full",
"--uid",
os.getenv("USER"),
"^playerctl metadata",
}, function()
awful.spawn.with_line_callback(song_follow_cmd, {
stdout = function(line)
local album_path = ""
awful.spawn.easy_async_with_shell(art_script, function(out)
-- Get album path
album_path = out:gsub("%\n", "")
-- Get title and artist
local artist = line:match("artist_(.*)title_")
local title = line:match("title_(.*)")
-- If the title is nil or empty then the players stopped
if title and title ~= "" then
awesome.emit_signal(
"bling::playerctl::title_artist_album",
title,
artist,
album_path
)
else
awesome.emit_signal("bling::playerctl::no_players")
end
end)
collectgarbage("collect")
end,
})
collectgarbage("collect")
end)
end
-- Emit info
-- emit_player_status()
-- emit_player_info()
local enable = function(args)
interval = (args and args.interval) or interval
emit_player_status()
emit_player_info()
end
local disable = function()
awful.spawn.with_shell(
"pkill --full --uid " .. os.getenv("USER") .. " '^playerctl status -F'"
)
awful.spawn.with_shell(
"pkill --full --uid "
.. os.getenv("USER")
.. " '^playerctl metadata --format'"
)
end
return { enable = enable, disable = disable }

View File

@@ -0,0 +1,350 @@
-- Playerctl signals
--
-- Provides:
-- bling::playerctl::status
-- playing (boolean)
-- player_name (string)
-- bling::playerctl::title_artist_album
-- title (string)
-- artist (string)
-- album_path (string)
-- player_name (string)
-- bling::playerctl::position
-- interval_sec (number)
-- length_sec (number)
-- player_name (string)
-- bling::playerctl::no_players
-- (No parameters)
local gears = require("gears")
local awful = require("awful")
local beautiful = require("beautiful")
local Playerctl = nil
local manager = nil
local metadata_timer = nil
local position_timer = nil
local ignore = {}
local priority = {}
local update_on_activity = true
local interval = 1
-- Track position callback
local last_position = -1
local last_length = -1
local function position_cb()
local player = manager.players[1]
if player then
local position = player:get_position() / 1000000
local length = (player.metadata.value["mpris:length"] or 0) / 1000000
if position ~= last_position or length ~= last_length then
awesome.emit_signal(
"bling::playerctl::position",
position,
length,
player.player_name
)
last_position = position
last_length = length
end
end
end
local function get_album_art(url)
return awful.util.shell
.. [[ -c '
tmp_dir="$XDG_CACHE_HOME/awesome/"
if [ -z "$XDG_CACHE_HOME" ]; then
tmp_dir="$HOME/.cache/awesome/"
fi
tmp_cover_path="${tmp_dir}cover.png"
if [ ! -d "$tmp_dir" ]; then
mkdir -p $tmp_dir
fi
curl -s ']]
.. url
.. [[' --output $tmp_cover_path
echo "$tmp_cover_path"
']]
end
-- Metadata callback for title, artist, and album art
local last_player = nil
local last_title = ""
local last_artist = ""
local last_artUrl = ""
local function metadata_cb(player, metadata)
if update_on_activity then
manager:move_player_to_top(player)
end
local data = metadata.value
local title = data["xesam:title"] or ""
local artist = data["xesam:artist"][1] or ""
for i = 2, #data["xesam:artist"] do
artist = artist .. ", " .. data["xesam:artist"][i]
end
local artUrl = data["mpris:artUrl"] or ""
-- Spotify client doesn't report its art URL's correctly...
if player.player_name == "spotify" then
artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
end
if player == manager.players[1] then
-- Callback can be called even though values we care about haven't
-- changed, so check to see if they have
if
player ~= last_player
or title ~= last_title
or artist ~= last_artist
or artUrl ~= last_artUrl
then
if title == "" and artist == "" and artUrl == "" then
return
end
if metadata_timer ~= nil then
if metadata_timer.started then
metadata_timer:stop()
end
end
metadata_timer = gears.timer({
timeout = 0.3,
autostart = true,
single_shot = true,
callback = function()
if artUrl ~= "" then
awful.spawn.with_line_callback(get_album_art(artUrl), {
stdout = function(line)
awesome.emit_signal(
"bling::playerctl::title_artist_album",
title,
artist,
line,
player.player_name
)
end,
})
else
awesome.emit_signal(
"bling::playerctl::title_artist_album",
title,
artist,
"",
player.player_name
)
end
end,
})
-- Re-sync with position timer when track changes
position_timer:again()
last_player = player
last_title = title
last_artist = artist
last_artUrl = artUrl
end
end
end
-- Playback status callback
-- Reported as PLAYING, PAUSED, or STOPPED
local function playback_status_cb(player, status)
if update_on_activity then
manager:move_player_to_top(player)
end
if player == manager.players[1] then
if status == "PLAYING" then
awesome.emit_signal(
"bling::playerctl::status",
true,
player.player_name
)
else
awesome.emit_signal(
"bling::playerctl::status",
false,
player.player_name
)
end
end
end
-- Determine if player should be managed
local function name_is_selected(name)
if ignore[name.name] then
return false
end
if #priority > 0 then
for _, arg in pairs(priority) do
if arg == name.name or arg == "%any" then
return true
end
end
return false
end
return true
end
-- Create new player and connect it to callbacks
local function init_player(name)
if name_is_selected(name) then
local player = Playerctl.Player.new_from_name(name)
manager:manage_player(player)
player.on_playback_status = playback_status_cb
player.on_metadata = metadata_cb
-- Start position timer if its not already running
if not position_timer.started then
position_timer:again()
end
end
end
-- Determine if a player name comes before or after another according to the
-- priority order
local function player_compare_name(name_a, name_b)
local any_index = math.huge
local a_match_index = nil
local b_match_index = nil
if name_a == name_b then
return 0
end
for index, name in ipairs(priority) do
if name == "%any" then
any_index = (any_index == math.huge) and index or any_index
elseif name == name_a then
a_match_index = a_match_index or index
elseif name == name_b then
b_match_index = b_match_index or index
end
end
if not a_match_index and not b_match_index then
return 0
elseif not a_match_index then
return (b_match_index < any_index) and 1 or -1
elseif not b_match_index then
return (a_match_index < any_index) and -1 or 1
elseif a_match_index == b_match_index then
return 0
else
return (a_match_index < b_match_index) and -1 or 1
end
end
-- Sorting function used by manager if a priority order is specified
local function player_compare(a, b)
local player_a = Playerctl.Player(a)
local player_b = Playerctl.Player(b)
return player_compare_name(player_a.player_name, player_b.player_name)
end
local function start_manager()
manager = Playerctl.PlayerManager()
if #priority > 0 then
manager:set_sort_func(player_compare)
end
-- Timer to update track position at specified interval
position_timer = gears.timer({
timeout = interval,
callback = position_cb,
})
-- Manage existing players on startup
for _, name in ipairs(manager.player_names) do
init_player(name)
end
-- Callback to manage new players
function manager:on_name_appeared(name)
init_player(name)
end
-- Callback to check if all players have exited
function manager:on_name_vanished(name)
if #manager.players == 0 then
metadata_timer:stop()
position_timer:stop()
awesome.emit_signal("bling::playerctl::no_players")
end
end
end
-- Parse arguments
local function parse_args(args)
if args then
update_on_activity = args.update_on_activity or update_on_activity
interval = args.interval or interval
if type(args.ignore) == "string" then
ignore[args.ignore] = true
elseif type(args.ignore) == "table" then
for _, name in pairs(args.ignore) do
ignore[name] = true
end
end
if type(args.player) == "string" then
priority[1] = args.player
elseif type(args.player) == "table" then
priority = args.player
end
end
end
local function playerctl_enable(args)
args = args or {}
-- Grab settings from beautiful variables if not set explicitly
args.ignore = args.ignore or beautiful.playerctl_ignore
args.player = args.player or beautiful.playerctl_player
args.update_on_activity = args.update_on_activity
or beautiful.playerctl_update_on_activity
args.interval = args.interval
or beautiful.playerctl_position_update_interval
parse_args(args)
-- Grab playerctl library
Playerctl = require("lgi").Playerctl
-- Ensure main event loop has started before starting player manager
gears.timer.delayed_call(start_manager)
end
local function playerctl_disable()
-- Remove manager and timer
manager = nil
metadata_timer:stop()
metadata_timer = nil
position_timer:stop()
position_timer = nil
-- Restore default settings
ignore = {}
priority = {}
update_on_activity = true
interval = 1
-- Reset default values
last_position = -1
last_length = -1
last_player = nil
last_title = ""
last_artist = ""
last_artUrl = ""
end
return { enable = playerctl_enable, disable = playerctl_disable }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,656 @@
---------------------------------------------------------------------------
--- Modified Prompt module.
-- @author Julien Danjou &lt;julien@danjou.info&gt;
-- @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 .. "<span background=\"" .. cursor_color ..
"\" foreground=\"" .. text_color .. "\" underline=\"" .. underline ..
"\">" .. char .. "</span>" .. 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)

View File

@@ -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"),
}

View File

@@ -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,
}

View File

@@ -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 = "<span foreground='"
.. fg_temp
.. "'>"
.. title_temp
.. "</span>"
c:connect_signal("property::name", function(_)
local title_temp = c.name or c.class or "-"
text_temp.markup = "<span foreground='"
.. fg_temp
.. "'>"
.. title_temp
.. "</span>"
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,
}

View File

@@ -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 = "<span foreground='"
.. fg_temp
.. "'>"
.. title_temp
.. "</span>"
c:connect_signal("property::name", function(_)
local title_temp = c.name or c.class or "-"
text_temp.markup = "<span foreground='"
.. fg_temp
.. "'>"
.. title_temp
.. "</span>"
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,
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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"
),
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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 }

View File

@@ -0,0 +1,454 @@
local cairo = require("lgi").cairo
local awful = require("awful")
local gears = require("gears")
local wibox = require("wibox")
local beautiful = require("beautiful")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local dpi = beautiful.xresources.apply_dpi
local window_switcher_first_client -- The client that was focused when the window_switcher was activated
local window_switcher_minimized_clients = {} -- The clients that were minimized when the window switcher was activated
local window_switcher_grabber
local get_num_clients = function()
local minimized_clients_in_tag = 0
local matcher = function(c)
return awful.rules.match(
c,
{
minimized = true,
skip_taskbar = false,
hidden = false,
first_tag = awful.screen.focused().selected_tag,
}
)
end
for c in awful.client.iterate(matcher) do
minimized_clients_in_tag = minimized_clients_in_tag + 1
end
return minimized_clients_in_tag + #awful.screen.focused().clients
end
local window_switcher_hide = function(window_switcher_box)
-- Add currently focused client to history
if client.focus then
local window_switcher_last_client = client.focus
awful.client.focus.history.add(window_switcher_last_client)
-- Raise client that was focused originally
-- Then raise last focused client
if
window_switcher_first_client and window_switcher_first_client.valid
then
window_switcher_first_client:raise()
window_switcher_last_client:raise()
end
end
-- Minimize originally minimized clients
local s = awful.screen.focused()
for _, c in pairs(window_switcher_minimized_clients) do
if c and c.valid and not (client.focus and client.focus == c) then
c.minimized = true
end
end
-- Reset helper table
window_switcher_minimized_clients = {}
-- Resume recording focus history
awful.client.focus.history.enable_tracking()
-- Stop and hide window_switcher
awful.keygrabber.stop(window_switcher_grabber)
window_switcher_box.visible = false
window_switcher_box.widget = nil
collectgarbage("collect")
end
local function draw_widget(
type,
background,
border_width,
border_radius,
border_color,
clients_spacing,
client_icon_horizontal_spacing,
client_width,
client_height,
client_margins,
thumbnail_margins,
thumbnail_scale,
name_margins,
name_valign,
name_forced_width,
name_font,
name_normal_color,
name_focus_color,
icon_valign,
icon_width,
mouse_keys
)
local tasklist_widget = type == "thumbnail"
and awful.widget.tasklist({
screen = awful.screen.focused(),
filter = awful.widget.tasklist.filter.currenttags,
buttons = mouse_keys,
style = {
font = name_font,
fg_normal = name_normal_color,
fg_focus = name_focus_color,
},
layout = {
layout = wibox.layout.flex.horizontal,
spacing = clients_spacing,
},
widget_template = {
widget = wibox.container.background,
id = "bg_role",
forced_width = client_width,
forced_height = client_height,
create_callback = function(self, c, _, __)
local content = gears.surface(c.content)
local cr = cairo.Context(content)
local x, y, w, h = cr:clip_extents()
local img = cairo.ImageSurface.create(
cairo.Format.ARGB32,
w - x,
h - y
)
cr = cairo.Context(img)
cr:set_source_surface(content, 0, 0)
cr.operator = cairo.Operator.SOURCE
cr:paint()
self:get_children_by_id("thumbnail")[1].image =
gears.surface.load(
img
)
end,
{
{
{
horizontal_fit_policy = thumbnail_scale == true
and "fit"
or "auto",
vertical_fit_policy = thumbnail_scale == true
and "fit"
or "auto",
id = "thumbnail",
widget = wibox.widget.imagebox,
},
margins = thumbnail_margins,
widget = wibox.container.margin,
},
{
{
{
id = "icon_role",
widget = wibox.widget.imagebox,
},
forced_width = icon_width,
valign = icon_valign,
widget = wibox.container.place,
},
{
{
forced_width = name_forced_width,
valign = name_valign,
id = "text_role",
widget = wibox.widget.textbox,
},
margins = name_margins,
widget = wibox.container.margin,
},
spacing = client_icon_horizontal_spacing,
layout = wibox.layout.fixed.horizontal,
},
layout = wibox.layout.flex.vertical,
},
},
})
or awful.widget.tasklist({
screen = awful.screen.focused(),
filter = awful.widget.tasklist.filter.currenttags,
buttons = mouse_keys,
style = {
font = name_font,
fg_normal = name_normal_color,
fg_focus = name_focus_color,
},
layout = {
layout = wibox.layout.fixed.vertical,
spacing = clients_spacing,
},
widget_template = {
widget = wibox.container.background,
id = "bg_role",
forced_width = client_width,
forced_height = client_height,
{
{
{
id = "icon_role",
widget = wibox.widget.imagebox,
},
forced_width = icon_width,
valign = icon_valign,
widget = wibox.container.place,
},
{
{
forced_width = name_forced_width,
valign = name_valign,
id = "text_role",
widget = wibox.widget.textbox,
},
margins = name_margins,
widget = wibox.container.margin,
},
spacing = client_icon_horizontal_spacing,
layout = wibox.layout.fixed.horizontal,
},
},
})
return wibox.widget({
{
tasklist_widget,
margins = client_margins,
widget = wibox.container.margin,
},
shape_border_width = border_width,
shape_border_color = border_color,
bg = background,
shape = helpers.shape.rrect(border_radius),
widget = wibox.container.background,
})
end
local enable = function(opts)
local opts = opts or {}
local type = opts.type or "thumbnail"
local background = beautiful.window_switcher_widget_bg or "#000000"
local border_width = beautiful.window_switcher_widget_border_width or dpi(3)
local border_radius = beautiful.window_switcher_widget_border_radius
or dpi(0)
local border_color = beautiful.window_switcher_widget_border_color
or "#ffffff"
local clients_spacing = beautiful.window_switcher_clients_spacing or dpi(20)
local client_icon_horizontal_spacing = beautiful.window_switcher_client_icon_horizontal_spacing
or dpi(5)
local client_width = beautiful.window_switcher_client_width
or dpi(type == "thumbnail" and 150 or 500)
local client_height = beautiful.window_switcher_client_height
or dpi(type == "thumbnail" and 250 or 50)
local client_margins = beautiful.window_switcher_client_margins or dpi(10)
local thumbnail_margins = beautiful.window_switcher_thumbnail_margins
or dpi(5)
local thumbnail_scale = beautiful.thumbnail_scale or false
local name_margins = beautiful.window_switcher_name_margins or dpi(10)
local name_valign = beautiful.window_switcher_name_valign or "center"
local name_forced_width = beautiful.window_switcher_name_forced_width
or dpi(type == "thumbnail" and 200 or 550)
local name_font = beautiful.window_switcher_name_font or beautiful.font
local name_normal_color = beautiful.window_switcher_name_normal_color
or "#FFFFFF"
local name_focus_color = beautiful.window_switcher_name_focus_color
or "#FF0000"
local icon_valign = beautiful.window_switcher_icon_valign or "center"
local icon_width = beautiful.window_switcher_icon_width or dpi(40)
local hide_window_switcher_key = opts.hide_window_switcher_key or "Escape"
local select_client_key = opts.select_client_key or 1
local minimize_key = opts.minimize_key or "n"
local unminimize_key = opts.unminimize_key or "N"
local kill_client_key = opts.kill_client_key or "q"
local cycle_key = opts.cycle_key or "Tab"
local previous_key = opts.previous_key or "Left"
local next_key = opts.next_key or "Right"
local vim_previous_key = opts.vim_previous_key or "h"
local vim_next_key = opts.vim_next_key or "l"
local scroll_previous_key = opts.scroll_previous_key or 4
local scroll_next_key = opts.scroll_next_key or 5
local window_switcher_box = awful.popup({
bg = "#00000000",
visible = false,
ontop = true,
placement = awful.placement.centered,
screen = awful.screen.focused(),
widget = wibox.container.background, -- A dummy widget to make awful.popup not scream
widget = {
{
draw_widget(),
margins = client_margins,
widget = wibox.container.margin,
},
shape_border_width = border_width,
shape_border_color = border_color,
bg = background,
shape = helpers.shape.rrect(border_radius),
widget = wibox.container.background,
},
})
local mouse_keys = gears.table.join(
awful.button({
modifiers = { "Any" },
button = select_client_key,
on_press = function(c)
client.focus = c
end,
}),
awful.button({
modifiers = { "Any" },
button = scroll_previous_key,
on_press = function()
awful.client.focus.byidx(-1)
end,
}),
awful.button({
modifiers = { "Any" },
button = scroll_next_key,
on_press = function()
awful.client.focus.byidx(1)
end,
})
)
local keyboard_keys = {
[hide_window_switcher_key] = function()
window_switcher_hide(window_switcher_box)
end,
[minimize_key] = function()
if client.focus then
client.focus.minimized = true
end
end,
[unminimize_key] = function()
if awful.client.restore() then
client.focus = awful.client.restore()
end
end,
[kill_client_key] = function()
if client.focus then
client.focus:kill()
end
end,
[cycle_key] = function()
awful.client.focus.byidx(1)
end,
[previous_key] = function()
awful.client.focus.byidx(1)
end,
[next_key] = function()
awful.client.focus.byidx(-1)
end,
[vim_previous_key] = function()
awful.client.focus.byidx(1)
end,
[vim_next_key] = function()
awful.client.focus.byidx(-1)
end,
}
window_switcher_box:connect_signal("property::width", function()
if window_switcher_box.visible and get_num_clients() == 0 then
window_switcher_hide(window_switcher_box)
end
end)
window_switcher_box:connect_signal("property::height", function()
if window_switcher_box.visible and get_num_clients() == 0 then
window_switcher_hide(window_switcher_box)
end
end)
awesome.connect_signal("bling::window_switcher::turn_on", function()
local number_of_clients = get_num_clients()
if number_of_clients == 0 then
return
end
-- Store client that is focused in a variable
window_switcher_first_client = client.focus
-- Stop recording focus history
awful.client.focus.history.disable_tracking()
-- Go to previously focused client (in the tag)
awful.client.focus.history.previous()
-- Track minimized clients
-- Unminimize them
-- Lower them so that they are always below other
-- originally unminimized windows
local clients = awful.screen.focused().selected_tag:clients()
for _, c in pairs(clients) do
if c.minimized then
table.insert(window_switcher_minimized_clients, c)
c.minimized = false
c:lower()
end
end
-- Start the keygrabber
window_switcher_grabber = awful.keygrabber.run(function(_, key, event)
if event == "release" then
-- Hide if the modifier was released
-- We try to match Super or Alt or Control since we do not know which keybind is
-- used to activate the window switcher (the keybind is set by the user in keys.lua)
if
key:match("Super")
or key:match("Alt")
or key:match("Control")
then
window_switcher_hide(window_switcher_box)
end
-- Do nothing
return
end
-- Run function attached to key, if it exists
if keyboard_keys[key] then
keyboard_keys[key]()
end
end)
window_switcher_box.widget = draw_widget(
type,
background,
border_width,
border_radius,
border_color,
clients_spacing,
client_icon_horizontal_spacing,
client_width,
client_height,
client_margins,
thumbnail_margins,
thumbnail_scale,
name_margins,
name_valign,
name_forced_width,
name_font,
name_normal_color,
name_focus_color,
icon_valign,
icon_width,
mouse_keys
)
window_switcher_box.visible = true
end)
end
return { enable = enable }

Submodule .config/awesome/extras/nice added at 810aa72bbe