diff options
author | delta <darkussdelta@gmail.com> | 2025-07-04 00:38:29 +0200 |
---|---|---|
committer | delta <darkussdelta@gmail.com> | 2025-07-04 00:38:29 +0200 |
commit | b3530d7c4a102935fa26498a160ee1dc6c1e9c03 (patch) | |
tree | d7751206a694bc5de2d6b34b0c077cfcd1855798 /.config/awesome | |
parent | df75ec5ed5e3848c497f0439acb43ec9246ad3e7 (diff) |
:3
Diffstat (limited to '.config/awesome')
92 files changed, 4776 insertions, 2245 deletions
diff --git a/.config/awesome/.nvim.lua b/.config/awesome/.nvim.lua new file mode 100644 index 0000000..9c7049d --- /dev/null +++ b/.config/awesome/.nvim.lua @@ -0,0 +1,2 @@ +vim.env.GIT_DIR = vim.fn.expand("~/.dots") +vim.env.GIT_WORK_TREE = vim.fn.expand("~") diff --git a/.config/awesome/.styluaignore b/.config/awesome/.styluaignore new file mode 100644 index 0000000..cbf9e27 --- /dev/null +++ b/.config/awesome/.styluaignore @@ -0,0 +1,2 @@ +lib/bling +lib/rubato diff --git a/.config/awesome/assets/custom/init.lua b/.config/awesome/assets/custom/init.lua new file mode 100644 index 0000000..fd1dca5 --- /dev/null +++ b/.config/awesome/assets/custom/init.lua @@ -0,0 +1,6 @@ +local gfs = require "gears.filesystem" +local qiconset = require "quarrel.iconset" + +local simpleicons_dir = gfs.get_configuration_dir() .. "assets/custom/" + +return qiconset(simpleicons_dir) diff --git a/.config/awesome/assets/custom/vinyl-record-fill.svg b/.config/awesome/assets/custom/vinyl-record-fill.svg new file mode 100644 index 0000000..0d68cbf --- /dev/null +++ b/.config/awesome/assets/custom/vinyl-record-fill.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="28" + height="28" + fill="#000000" + viewBox="0 0 256 256" + version="1.1" + id="svg1" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <path + d="M 128,0 A 128,128 0 1 0 256,128 128.13538,128.13538 0 0 0 128,0 Z M 59.076923,128 A 9.846154,9.8461541 0 0 1 39.384615,128 88.713846,88.713846 0 0 1 128,39.384615 9.846154,9.846154 0 0 1 128,59.076923 68.996923,68.996923 0 0 0 59.076923,128 Z m 39.384615,0 A 29.538462,29.538462 0 1 1 128,157.53846 29.538462,29.538462 0 0 1 98.461538,128 Z M 128,216.61538 a 9.8461538,9.8461538 0 0 1 0,-19.6923 A 68.996923,68.996923 0 0 0 196.92308,128 9.8461538,9.8461538 0 0 1 216.61538,128 88.713846,88.713846 0 0 1 128,216.61538 Z" + id="path1" + style="stroke-width:1.23077" /> +</svg> diff --git a/.config/awesome/assets/phosphor/browser-fill.svg b/.config/awesome/assets/phosphor/browser-fill.svg index 68d95e5..1f64b69 100644 --- a/.config/awesome/assets/phosphor/browser-fill.svg +++ b/.config/awesome/assets/phosphor/browser-fill.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#000000" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V88H40V56Z"></path></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V88H40V56Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/phosphor/calendar-dots-fill.svg b/.config/awesome/assets/phosphor/calendar-dots-fill.svg new file mode 100644 index 0000000..1c25fff --- /dev/null +++ b/.config/awesome/assets/phosphor/calendar-dots-fill.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM84,184a12,12,0,1,1,12-12A12,12,0,0,1,84,184Zm44,0a12,12,0,1,1,12-12A12,12,0,0,1,128,184Zm0-40a12,12,0,1,1,12-12A12,12,0,0,1,128,144Zm44,40a12,12,0,1,1,12-12A12,12,0,0,1,172,184Zm0-40a12,12,0,1,1,12-12A12,12,0,0,1,172,144Zm36-64H48V48H72v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/phosphor/caret-down-bold.svg b/.config/awesome/assets/phosphor/caret-down-bold.svg new file mode 100644 index 0000000..cc995f8 --- /dev/null +++ b/.config/awesome/assets/phosphor/caret-down-bold.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/phosphor/caret-up-bold.svg b/.config/awesome/assets/phosphor/caret-up-bold.svg new file mode 100644 index 0000000..eae0ae7 --- /dev/null +++ b/.config/awesome/assets/phosphor/caret-up-bold.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216.49,168.49a12,12,0,0,1-17,0L128,97,56.49,168.49a12,12,0,0,1-17-17l80-80a12,12,0,0,1,17,0l80,80A12,12,0,0,1,216.49,168.49Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/phosphor/init.lua b/.config/awesome/assets/phosphor/init.lua index 421ceb2..2f0f6fc 100644 --- a/.config/awesome/assets/phosphor/init.lua +++ b/.config/awesome/assets/phosphor/init.lua @@ -1,14 +1,6 @@ local gfs = require "gears.filesystem" -local qfs = require "quarrel.fs" +local qiconset = require "quarrel.iconset" ----@type table<string, string> -local icons = {} local phosphor_dir = gfs.get_configuration_dir() .. "assets/phosphor/" -for _, icon in ipairs(qfs.ls_files(phosphor_dir)) do - if icon:match ".+%.(.+)" == "svg" then - icons[icon:match("(.+)%..+"):gsub("-", "_")] = phosphor_dir .. icon - end -end - -return icons +return qiconset(phosphor_dir) diff --git a/.config/awesome/assets/phosphor/speaker-hifi-fill.svg b/.config/awesome/assets/phosphor/speaker-hifi-fill.svg new file mode 100644 index 0000000..dce7932 --- /dev/null +++ b/.config/awesome/assets/phosphor/speaker-hifi-fill.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M152,160a24,24,0,1,1-24-24A24,24,0,0,1,152,160ZM208,40V216a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V40A16,16,0,0,1,64,24H192A16,16,0,0,1,208,40ZM116,68a12,12,0,1,0,12-12A12,12,0,0,0,116,68Zm52,92a40,40,0,1,0-40,40A40,40,0,0,0,168,160Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/phosphor/timer-fill.svg b/.config/awesome/assets/phosphor/timer-fill.svg new file mode 100644 index 0000000..530646d --- /dev/null +++ b/.config/awesome/assets/phosphor/timer-fill.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,40a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,40Zm45.66,61.66-40,40a8,8,0,0,1-11.32-11.32l40-40a8,8,0,0,1,11.32,11.32ZM96,16a8,8,0,0,1,8-8h48a8,8,0,0,1,0,16H104A8,8,0,0,1,96,16Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/phosphor/waveform-fill.svg b/.config/awesome/assets/phosphor/waveform-fill.svg new file mode 100644 index 0000000..dd4d8ab --- /dev/null +++ b/.config/awesome/assets/phosphor/waveform-fill.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM72,152a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm32,32a8,8,0,0,1-16,0V72a8,8,0,0,1,16,0Zm32-16a8,8,0,0,1-16,0V88a8,8,0,0,1,16,0Zm32-16a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm32,8a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Z"></path></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/simpleicons/firefox.svg b/.config/awesome/assets/simpleicons/firefox.svg new file mode 100644 index 0000000..3cbc89c --- /dev/null +++ b/.config/awesome/assets/simpleicons/firefox.svg @@ -0,0 +1 @@ +<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Firefox Browser</title><path d="M8.824 7.287c.008 0 .004 0 0 0zm-2.8-1.4c.006 0 .003 0 0 0zm16.754 2.161c-.505-1.215-1.53-2.528-2.333-2.943.654 1.283 1.033 2.57 1.177 3.53l.002.02c-1.314-3.278-3.544-4.6-5.366-7.477-.091-.147-.184-.292-.273-.446a3.545 3.545 0 01-.13-.24 2.118 2.118 0 01-.172-.46.03.03 0 00-.027-.03.038.038 0 00-.021 0l-.006.001a.037.037 0 00-.01.005L15.624 0c-2.585 1.515-3.657 4.168-3.932 5.856a6.197 6.197 0 00-2.305.587.297.297 0 00-.147.37c.057.162.24.24.396.17a5.622 5.622 0 012.008-.523l.067-.005a5.847 5.847 0 011.957.222l.095.03a5.816 5.816 0 01.616.228c.08.036.16.073.238.112l.107.055a5.835 5.835 0 01.368.211 5.953 5.953 0 012.034 2.104c-.62-.437-1.733-.868-2.803-.681 4.183 2.09 3.06 9.292-2.737 9.02a5.164 5.164 0 01-1.513-.292 4.42 4.42 0 01-.538-.232c-1.42-.735-2.593-2.121-2.74-3.806 0 0 .537-2 3.845-2 .357 0 1.38-.998 1.398-1.287-.005-.095-2.029-.9-2.817-1.677-.422-.416-.622-.616-.8-.767a3.47 3.47 0 00-.301-.227 5.388 5.388 0 01-.032-2.842c-1.195.544-2.124 1.403-2.8 2.163h-.006c-.46-.584-.428-2.51-.402-2.913-.006-.025-.343.176-.389.206-.406.29-.787.616-1.136.974-.397.403-.76.839-1.085 1.303a9.816 9.816 0 00-1.562 3.52c-.003.013-.11.487-.19 1.073-.013.09-.026.181-.037.272a7.8 7.8 0 00-.069.667l-.002.034-.023.387-.001.06C.386 18.795 5.593 24 12.016 24c5.752 0 10.527-4.176 11.463-9.661.02-.149.035-.298.052-.448.232-1.994-.025-4.09-.753-5.844z"/></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/simpleicons/init.lua b/.config/awesome/assets/simpleicons/init.lua new file mode 100644 index 0000000..4aaa2d3 --- /dev/null +++ b/.config/awesome/assets/simpleicons/init.lua @@ -0,0 +1,6 @@ +local gfs = require "gears.filesystem" +local qiconset = require "quarrel.iconset" + +local simpleicons_dir = gfs.get_configuration_dir() .. "assets/simpleicons/" + +return qiconset(simpleicons_dir) diff --git a/.config/awesome/assets/simpleicons/librewolf.svg b/.config/awesome/assets/simpleicons/librewolf.svg new file mode 100644 index 0000000..a6dab52 --- /dev/null +++ b/.config/awesome/assets/simpleicons/librewolf.svg @@ -0,0 +1 @@ +<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LibreWolf</title><path d="M12 0C5.3726 0 0 5.3726 0 12s5.3726 12 12 12 12-5.3726 12-12A12 12 0 0 0 12 0m.0306 2.4174c2.539 0 4.7927.9097 6.619 2.6718.8799.849 1.47 1.654 1.9752 2.6953.6834 1.4085.96 2.6337.96 4.2503 0 2.1282-.6144 3.9841-1.891 5.7107-.3792.513-1.1202 1.2925-1.612 1.6955-1.3487 1.1057-3.068 1.8537-4.7739 2.0774-.5894.0772-1.982.0868-2.5232.0174v.0001c-1.681-.2156-3.2887-.8859-4.6919-1.9555-.4576-.3489-1.3163-1.213-1.6911-1.702-1.0497-1.3694-1.7025-2.9237-1.9179-4.5663-.0922-.7032-.0916-1.996.0011-2.6387.4635-3.213 2.4855-5.9688 5.389-7.3449 1.3505-.64 2.5868-.9111 4.1567-.9111m-.0315 1.4514c-1.4755-.0014-2.7693.3367-4.0497 1.0586-1.1657.6572-2.3708 1.8637-3.0308 3.0342-.486.862-.8417 1.8852-.9806 2.82-.091.613-.0929 1.8301-.0036 2.417.1786 1.1746.6241 2.3354 1.27 3.3091.3527.5317.3416.5259.6528.3479.5566-.3184.7018-.4726 1.005-1.0666.3546-.6946.7336-1.3016 1.0762-1.7232.3953-.4867.4496-.5732.648-1.0327.0997-.2312.2788-.5708.3978-.7546l.2165-.3341-.28-.2753c-.372-.3655-.6814-.7802-.7395-.991-.0595-.2152.0108-.3353.2902-.4967.2825-.1632.4845-.21 1.2396-.2877.3677-.0377.7668-.1069.8925-.1547.125-.0475.4988-.251.8305-.452.9577-.5807 1.0388-.6107 1.613-.5983.485.0106.4859.0103.7734-.1635.6026-.3642 1.4681-1.0215 2.607-1.9797.2183-.1838.4286-.328.4674-.3206.161.031.457.757.5019 1.2315.0272.2878-.0285.5725-.1992 1.018-.0466.1215-.068.2375-.0477.2577.0489.0486.2165-.1607.2165-.2703 0-.1397.0924-.1557.2182-.0377.0915.0859.1705.1105.3545.1105.201 0 .2507.0186.329.1232.1332.1775.1644.6616.0625.9682-.099.2983-.3986.6423-.6704.77-.2431.1143-.7354.5951-.9658.9432-.0882.1332-.2536.411-.3675.6172-.114.2063-.305.518-.4246.6925-.2354.3436-.437.823-.6316 1.5017-.1143.3987-.1238.5025-.1285 1.3918-.0057 1.0742-.0613 1.3378-.3518 1.6686-.3301.376-.6596 1.2018-.6873 1.7225-.0175.3286-.0682.649-.1401.8862-.0317.1045.0275.1032.38-.0079 1.027-.3237 1.8638-.7588 2.6718-1.3889.4296-.335 1.1722-1.0847 1.4835-1.4978.8445-1.1202 1.419-2.5235 1.5881-3.8785.0814-.6526.0342-2.0332-.0901-2.6325-.3293-1.5875-1.039-2.9284-2.1566-4.0759-.6968-.7154-1.3311-1.187-2.1473-1.5961-1.2384-.621-2.2994-.8718-3.6932-.8733m1.516 5.314-.6964.0976-.433.5544.6966-.0978z"/></svg>
\ No newline at end of file diff --git a/.config/awesome/assets/simpleicons/spotify.svg b/.config/awesome/assets/simpleicons/spotify.svg new file mode 100644 index 0000000..8d4d095 --- /dev/null +++ b/.config/awesome/assets/simpleicons/spotify.svg @@ -0,0 +1 @@ +<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spotify</title><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>
\ No newline at end of file diff --git a/.config/awesome/curious.lua b/.config/awesome/curious.lua index 3a0bf5d..66c33da 100644 --- a/.config/awesome/curious.lua +++ b/.config/awesome/curious.lua @@ -15,11 +15,11 @@ local interface = Gio.DBusInterfaceInfo { out_args = { Gio.DBusArgInfo { name = "widget_tree", - signature = "av" - } - } - } - } + signature = "av", + }, + }, + }, + }, } -- iterate with ipairs, get the length of the array, check if key value from pairs is a number lower than the length and if so, use as a dict key cause it's not gonna work with :add anyway @@ -43,17 +43,17 @@ local function serialize(builder, t) signature = "b" data = v elseif _type == "thread" or _type == "function" or (_type == "userdata" and not getmetatable(v).__tostring) then - local address = tonumber(tostring(v):match("%a+: 0x(%w+)"), 16) + local address = tonumber(tostring(v):match "%a+: 0x(%w+)", 16) signature = "(s(t))" data = { _type, address } elseif _type == "userdata" then if getmetatable(v).emit_signal then -- One of the properties available on any awesome luaobject - local _, _, content = tostring(v):match("%a+/(.*)") + local _, _, content = tostring(v):match "%a+/(.*)" local stringified = tostring(v) if content then stringified = content end - local type, extra, address = stringified:match("(%a+)(.*): 0x(%w+)") + local type, extra, address = stringified:match "(%a+)(.*): 0x(%w+)" if extra ~= "" then extra:sub(2, -2) signature = "(s(st))" @@ -64,7 +64,7 @@ local function serialize(builder, t) end else signature = "(s())" - data = { _type, {}} + data = { _type, {} } end end @@ -72,16 +72,16 @@ local function serialize(builder, t) builder:add("v", GLib.Variant(signature, data)) else print(type(signature), type(data)) - local variant = GLib.Variant("{sd}", { "test", 4}) + local variant = GLib.Variant("{sd}", { "test", 4 }) builder:add_value(GLib.Variant("v", variant)) end end end function methods.Select(parameters, invocation) - local builder = GLib.VariantBuilder(GLib.VariantType("av")) + local builder = GLib.VariantBuilder(GLib.VariantType "av") serialize(builder, { "test", some_key = "some_value" }) - local tuple_builder = GLib.VariantBuilder(GLib.VariantType("(av)")) + local tuple_builder = GLib.VariantBuilder(GLib.VariantType "(av)") tuple_builder:add_value(builder:_end()) -- builder -- invocation:return_value(GLib.Variant("(av)", { builder })) @@ -89,7 +89,10 @@ function methods.Select(parameters, invocation) end local function handle_call(_, _, _, _, method, parameters, invocation) - if not methods[method] then print("invalid"); return end + if not methods[method] then + print "invalid" + return + end gpcall(methods[method], parameters, invocation) end @@ -97,4 +100,9 @@ local function acquire(connection) connection:register_object("/com/twoexem/delta/Curious", interface, GObject.Closure(handle_call)) end -local connection = Gio.bus_own_name(Gio.BusType.SESSION, "com.twoexem.delta.Curious", Gio.BusNameOwnerFlags.NONE, GObject.Closure(acquire)) +local connection = Gio.bus_own_name( + Gio.BusType.SESSION, + "com.twoexem.delta.Curious", + Gio.BusNameOwnerFlags.NONE, + GObject.Closure(acquire) +) diff --git a/.config/awesome/lib/lit b/.config/awesome/lib/lit new file mode 160000 +Subproject 06b6581137c04520cc4ffccb8f1103b961a187f diff --git a/.config/awesome/misc/autostart.lua b/.config/awesome/misc/autostart.lua index b089068..291e39d 100644 --- a/.config/awesome/misc/autostart.lua +++ b/.config/awesome/misc/autostart.lua @@ -1,7 +1,7 @@ local awful = require "awful" -local quarrel = require "quarrel" +local qpersistent = require "quarrel.persistent" -if quarrel.is_restart() then +if qpersistent.is_restart() then return end @@ -11,9 +11,10 @@ local programs = { "clipcatd", "wezterm", "wezterm start --class code_term", - "firefox", - "discord", - "LD_PRELOAD=/usr/lib/spotify-adblock.so spotify", + "librewolf", + "keepassxc", + "fractal", + -- "LD_PRELOAD=/usr/lib/spotify-adblock.so spotify", } for _, program in ipairs(programs) do diff --git a/.config/awesome/misc/cfg.lua b/.config/awesome/misc/cfg.lua index aceaa08..f62612f 100644 --- a/.config/awesome/misc/cfg.lua +++ b/.config/awesome/misc/cfg.lua @@ -3,21 +3,26 @@ local awful = require "awful" local cfg = { terminal = "wezterm", tags = { + -- home { layout = awful.layout.suit.floating, selected = true, }, + -- browser { + -- layout = awful.layout.suit.fair, layout = awful.layout.suit.floating, }, + -- messaging { layout = awful.layout.suit.tile.left, master_width_factor = 0.7, }, - { - layout = awful.layout.suit.tile.top, - master_width_factor = 0.2, - }, + + -- { + -- layout = awful.layout.suit.tile.top, + -- master_width_factor = 0.2, + -- }, { layout = awful.layout.suit.tile.right, -- master_width_factor = 0.7, diff --git a/.config/awesome/misc/keys.lua b/.config/awesome/misc/keys.lua index 9d994ce..8d703b8 100644 --- a/.config/awesome/misc/keys.lua +++ b/.config/awesome/misc/keys.lua @@ -3,21 +3,22 @@ local backlight = require "services.backlight" local beautiful = require "beautiful" local cfg = require "misc.cfg" local fresnel = require "ui.fresnel" +local gtable = require "gears.table" local gtimer = require "gears.timer" local insightful = require "ui.insightful" local naughty = require "naughty" +-- local mpris = (require "services.mpris") +local mpris = require "ui.statusbar.panel.widgets.mpris" local playerctl = require "services.playerctl" local powermenu = require "ui.powermenu" local qbind = require "quarrel.bind" -local qstore = require "quarrel.store" -local qvars = require "quarrel.vars" local recording = { false, "" } client.connect_signal("request::default_mousebindings", function() awful.mouse.append_client_mousebindings { - qbind:new { - triggers = qvars.btns.left, + qbind { + triggers = qbind.btns.left, press = function(c) c:activate { context = "mouse_click", @@ -26,9 +27,9 @@ client.connect_signal("request::default_mousebindings", function() group = "client", desc = "raise client", }, - qbind:new { - mods = qvars.mods.M, - triggers = qvars.btns.left, + qbind { + mods = qbind.mods.M, + triggers = qbind.btns.left, press = function(c) c:activate { context = "mouse_click", @@ -38,9 +39,9 @@ client.connect_signal("request::default_mousebindings", function() group = "client", desc = "move client", }, - qbind:new { - mods = qvars.mods.M, - triggers = qvars.btns.right, + qbind { + mods = qbind.mods.M, + triggers = qbind.btns.right, press = function(c) c:activate { context = "mouse_click", @@ -55,8 +56,8 @@ end) client.connect_signal("request::default_keybindings", function() awful.keyboard.append_client_keybindings { - qbind:new { - mods = qvars.mods.MC, + qbind { + mods = qbind.mods.MC, triggers = "q", press = function(c) c:kill() @@ -64,8 +65,8 @@ client.connect_signal("request::default_keybindings", function() group = "client", desc = "close", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "m", press = function(c) c.maximized = not c.maximized @@ -73,8 +74,8 @@ client.connect_signal("request::default_keybindings", function() group = "client", desc = "(un)maximize", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "n", press = function(c) gtimer.delayed_call(function() @@ -84,8 +85,8 @@ client.connect_signal("request::default_keybindings", function() group = "client", desc = "minimize", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "f", press = function(c) c.fullscreen = not c.fullscreen @@ -97,15 +98,15 @@ client.connect_signal("request::default_keybindings", function() end) awful.keyboard.append_global_keybindings { - qbind:new { - mods = qvars.mods.MC, + qbind { + mods = qbind.mods.MC, triggers = "r", press = awesome.restart, group = "awesome", desc = "restart awesome", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "F1", press = function() insightful:toggle() @@ -113,8 +114,8 @@ awful.keyboard.append_global_keybindings { group = "awesome", desc = "toggle insightful", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "space", press = function() fresnel:show() @@ -122,17 +123,17 @@ awful.keyboard.append_global_keybindings { group = "awesome", desc = "toggle fresnel", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "p", press = function() - qstore.panel_toggle:press() + awful.screen.focused().bar:toggle() end, group = "awesome", desc = "toggle bar panel", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "l", press = function() powermenu:toggle() @@ -141,7 +142,7 @@ awful.keyboard.append_global_keybindings { desc = "toggle powermenu", }, - qbind:new { + qbind { triggers = "XF86AudioMute", press = function() awful.spawn "wpctl set-mute @DEFAULT_SINK@ toggle" @@ -149,46 +150,65 @@ awful.keyboard.append_global_keybindings { group = "audio", desc = "mute", }, - qbind:new { + qbind { triggers = { { "XF86AudioRaiseVolume", true }, { "XF86AudioLowerVolume", false }, }, press = function(up) if up then - awful.spawn "wpctl set-volume @DEFAULT_SINK@ 5%+" + -- awful.spawn "wpctl set-volume @DEFAULT_SINK@ 5%+" + print "up" else - awful.spawn "wpctl set-volume @DEFAULT_SINK@ 5%-" + print "down" + -- awful.spawn "wpctl set-volume @DEFAULT_SINK@ 5%-" end end, group = "audio", desc = "increase/decrease volume", }, - qbind:new { + qbind { triggers = { { "XF86AudioNext", true }, { "XF86AudioPrev", false }, }, press = function(next) + local active_player = playerctl:list()[mpris.active_player_index].instance if next then - playerctl:next() + playerctl:next(active_player) else - playerctl:previous() + playerctl:previous(active_player) end end, group = "audio", desc = "previous/next song", }, - qbind:new { + qbind { triggers = "XF86AudioPlay", press = function() - playerctl:play_pause() + playerctl:play_pause(playerctl:list()[mpris.active_player_index].instance) end, group = "audio", desc = "(un)pause song", }, + qbind { + mods = qbind.mods.M, + triggers = { + { "Next", true }, + { "Prior", false }, + }, + press = function(next) + if next then + mpris.next_player() + else + mpris.previous_player() + end + end, + group = "audio", + desc = "previous/next player", + }, - qbind:new { + qbind { triggers = { { "XF86MonBrightnessUp", true }, { "XF86MonBrightnessDown", false }, @@ -204,8 +224,8 @@ awful.keyboard.append_global_keybindings { desc = "increase/decrease brightness", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "Return", press = function() awful.spawn(cfg.terminal) @@ -214,7 +234,7 @@ awful.keyboard.append_global_keybindings { desc = "launch terminal", }, - qbind:new { + qbind { triggers = "Print", press = function() local date = os.date "%Y%m%d_%H%M%S" @@ -235,8 +255,8 @@ awful.keyboard.append_global_keybindings { group = "screenshot", desc = "take fullscreen screenshot", }, - qbind:new { - mods = qvars.mods.S, + qbind { + mods = qbind.mods.S, triggers = "Print", press = function() local date = os.date "%Y%m%d_%H%M%S" @@ -256,8 +276,8 @@ awful.keyboard.append_global_keybindings { desc = "take region screenshot", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "Print", press = function() if recording[1] then @@ -283,8 +303,8 @@ awful.keyboard.append_global_keybindings { desc = "toggle recording", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "k", press = function() awful.spawn "xkblayout-state set +1" @@ -292,8 +312,8 @@ awful.keyboard.append_global_keybindings { group = "keyboard", desc = "next keyboard layout", }, - qbind:new { - mods = qvars.mods.MS, + qbind { + mods = qbind.mods.MS, triggers = "k", press = function() awful.spawn "xkblayout-state set -1" @@ -302,22 +322,35 @@ awful.keyboard.append_global_keybindings { desc = "previous keyboard layout", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "Up", press = awful.tag.viewprev, group = "tag", desc = "switch to previous", }, - qbind:new { - mods = qvars.mods.M, + qbind { + mods = qbind.mods.M, triggers = "Down", press = awful.tag.viewnext, group = "tag", desc = "switch to next", }, - qbind:new { - mods = qvars.mods.MC, + qbind { + mods = qbind.mods.M, + triggers = gtable.join(awful.key.keygroups.numrow, awful.key.keygroups.numpad), + press = function(idx) + local tag = awful.screen.focused().tags[idx] + if not tag then + return + end + tag:view_only() + end, + group = "tag", + desc = "switch to the specified tag (if it exists)", + }, + qbind { + mods = qbind.mods.MC, triggers = "x", press = function() local tag = awful.screen.focused().selected_tag @@ -326,4 +359,16 @@ awful.keyboard.append_global_keybindings { group = "tag", desc = "reset master width", }, + qbind { + mods = qbind.mods.M, + triggers = { + { "XF86AudioRaiseVolume", true }, -- volume roller produces these events + { "XF86AudioLowerVolume", false }, + }, + press = function(up) + awful.spawn("xdotool click " .. (up and "4" or "5")) + end, + group = "misc", + desc = "scroll up/down", + }, } diff --git a/.config/awesome/misc/rules.lua b/.config/awesome/misc/rules.lua index 92ac596..18b11f1 100644 --- a/.config/awesome/misc/rules.lua +++ b/.config/awesome/misc/rules.lua @@ -20,6 +20,17 @@ ruled.client.connect_signal("request::rules", function() }, { + id = "unlock_db", + rule = { + name = "Unlock Database - KeePassXC", + }, + properties = { + sticky = true, + floating = true, + }, + }, + + { id = "titlebars", rule_any = { type = { @@ -56,7 +67,7 @@ ruled.client.connect_signal("request::rules", function() { id = "chat_tag", rule_any = { - class = { "discord" }, + class = { "fractal" }, }, properties = { screen = 1, @@ -64,17 +75,17 @@ ruled.client.connect_signal("request::rules", function() }, }, - { - id = "music_tag", - rule_any = { - class = { "Spotify" }, - }, - properties = { - screen = 1, - tag = awful.screen.focused().tags[4], - titlebars_enabled = false, - }, - }, + -- { + -- id = "music_tag", + -- rule_any = { + -- class = { "Spotify" }, + -- }, + -- properties = { + -- screen = 1, + -- tag = awful.screen.focused().tags[4], + -- titlebars_enabled = false, + -- }, + -- }, { id = "code_tag", @@ -83,7 +94,7 @@ ruled.client.connect_signal("request::rules", function() }, properties = { screen = 1, - tag = awful.screen.focused().tags[5], + tag = awful.screen.focused().tags[4], }, }, @@ -94,7 +105,7 @@ ruled.client.connect_signal("request::rules", function() }, properties = { screen = 1, - tag = awful.screen.focused().tags[6], + tag = awful.screen.focused().tags[5], }, }, } diff --git a/.config/awesome/prismite.lua b/.config/awesome/prismite.lua index 5de089f..497a1b9 100644 --- a/.config/awesome/prismite.lua +++ b/.config/awesome/prismite.lua @@ -1,39 +1,42 @@ local naughty = require "naughty" -local qvars = require "quarrel.vars" +local qcolor = require "quarrel.color" +local qui = require "quarrel.ui" local xresources = require "beautiful.xresources" local dpi = xresources.apply_dpi local theme = {} -theme.font = qvars.font +theme.font = qui.font() -theme.bg_normal = qvars.colors.bg -theme.bg_focus = qvars.colors.bg -theme.bg_urgent = qvars.colors.bg -theme.bg_minimize = qvars.colors.bg -theme.bg_systray = qvars.colors.bg +theme.bg_normal = qcolor.palette.bg() +theme.bg_focus = qcolor.palette.bg() +theme.bg_urgent = qcolor.palette.bg() +theme.bg_minimize = qcolor.palette.bg() +theme.bg_systray = qcolor.palette.bg() -theme.fg_normal = qvars.colors.fg -theme.fg_focus = qvars.colors.fg -theme.fg_urgent = qvars.colors.fg -theme.fg_minimize = qvars.colors.fg +theme.fg_normal = qcolor.palette.fg() +theme.fg_focus = qcolor.palette.fg() +theme.fg_urgent = qcolor.palette.fg() +theme.fg_minimize = qcolor.palette.fg() -theme.useless_gap = dpi(2) -theme.border_width = qvars.border_width -theme.border_normal = qvars.colors.bright.black -theme.border_focus = qvars.colors.bright.black -theme.border_marked = qvars.colors.bright.black +-- theme.useless_gap = dpi(2) +theme.useless_gap = qui.PADDING / 2 +theme.border_width = qui.BORDER_WIDTH +theme.border_normal = qcolor.palette.border() +theme.border_focus = qcolor.palette.border() +theme.border_marked = qcolor.palette.border() theme.notification_icon_size = dpi(32) -theme.notification_border_width = qvars.border_width +theme.notification_border_width = qui.BORDER_WIDTH theme.notification_border_color = theme.border_normal -theme.notification_max_width = qvars.char_width * 48 -theme.notification_shape = qvars.shape +theme.notification_max_width = qui.CHAR_WIDTH * 48 +-- theme.notification_max_width = qui.CHAR_WIDTH * 48 * 4 +theme.notification_shape = qui.shape theme.notification_spacing = theme.useless_gap * 2 -naughty.config.defaults.timeout = qvars.notif_timeout +-- naughty.config.defaults.timeout = qvars.notif_timeout naughty.config.defaults.position = "bottom_right" -naughty.config.defaults.border_width = qvars.border_width +-- naughty.config.defaults.border_width = qui.BORDER_WIDTH theme.tasklist_plain_task_name = true theme.enable_spawn_cursor = false diff --git a/.config/awesome/quarrel/animation/bezier.lua b/.config/awesome/quarrel/animation/bezier.lua new file mode 100644 index 0000000..a505e48 --- /dev/null +++ b/.config/awesome/quarrel/animation/bezier.lua @@ -0,0 +1,89 @@ +local gtable = require "gears.table" + +-- port of https://github.com/WebKit/WebKit/blob/da934454c84ac2dcbf9fca9e5f4ac2644ef25d72/Source/WebCore/platform/graphics/UnitBezier.h + +local bezier = {} + +function bezier:sample_x(t) + -- `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((self.ax * t + self.bx) * t + self.cx) * t +end + +function bezier:sample_y(t) + return ((self.ay * t + self.by) * t + self.cy) * t +end + +function bezier:sample_derivative_x(t) + return (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx +end + +function bezier:solve_x(x, epsilon) + local x2, d2 + local t2 = x + + -- First try a few iterations of Newton's method -- normally very fast. + for _ = 1, 8 do + x2 = self:sample_x(t2) - x + if math.abs(x2) < epsilon then + return t2 + end + d2 = self:sample_derivative_x(t2) + if math.abs(d2) < 1e-6 then + break + end + t2 = t2 - x2 / d2 + end + + -- Fall back to the bisection method for reliability. + local t0 = 0 + local t1 = 1 + t2 = x + + if t2 < t0 then + return t0 + end + if t2 > t1 then + return t1 + end + + while t0 < t1 do + x2 = self:sample_x(t2) + if math.abs(x2 - x) < epsilon then + return t2 + end + if x > x2 then + t0 = t2 + else + t1 = t2 + end + t2 = (t1 - t0) * 0.5 + t0 + end + + -- Failure. + return t2 +end + +function bezier:solve(x, epsilon) + return self:sample_y(self:solve_x(x, epsilon)) +end + +local function new(x1, y1, x2, y2) + local obj = gtable.crush({}, bezier) + + -- Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). + obj.cx = 3.0 * x1 + obj.bx = 3.0 * (x2 - x1) - obj.cx + obj.ax = 1.0 - obj.cx - obj.bx + + obj.cy = 3.0 * y1 + obj.by = 3.0 * (y2 - y1) - obj.cy + obj.ay = 1.0 - obj.cy - obj.by + + return obj +end + +return setmetatable(bezier, { + __call = function(_, ...) + return new(...) + end, +}) diff --git a/.config/awesome/quarrel/animation/init.lua b/.config/awesome/quarrel/animation/init.lua new file mode 100644 index 0000000..c57b01a --- /dev/null +++ b/.config/awesome/quarrel/animation/init.lua @@ -0,0 +1,249 @@ +------------------------------------------- +-- @author https://github.com/Kasper24 +-- @copyright 2021-2025 Kasper24 +------------------------------------------- +local GLib = require("lgi").GLib +local gobject = require "gears.object" +local gpcall = require "gears.protected_call" +local gtable = require "gears.table" +local gtimer = require "gears.timer" +local subscribable = require "quarrel.animation.subscribable" +-- local qconsts = require "quarrel.consts" +local qtween = require "quarrel.animation.tween" +-- local qbezier = require "quarrel.animation.bezier" +local ipairs = ipairs +local table = table +local pairs = pairs + +local animation_manager = {} + +-- local easing_bezier = qbezier(0.2, 0, 0, 1) +-- animation_manager.consts = qconsts.protect(function() +-- local duration = 0.3 +-- return { +-- DURATION = duration, +-- INTRO = duration / 4, +-- EASING = function(t, b, c, d) +-- local epsilon = 1000 / d +-- return c * easing_bezier:solve(t/d, epsilon) + b +-- end +-- } +-- end) + +local animation = {} + +local function second_to_micro(sec) + return sec * 1000000 +end + +local function framerate_tomilli(framerate) + return 1000 / framerate +end + +local function on_no_running_animations(self, callback) + gtimer.start_new(0.1, function() + if #self._private.animations <= 0 then + callback() + return false + else + local has_non_looped_anim = false + for _, animation in ipairs(self._private.animations) do + if animation.loop == false then + has_non_looped_anim = true + end + end + + if has_non_looped_anim == false then + callback() + return false + end + end + + return true + end) +end + +local function animation_loop(self) + self._private.source_id = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + framerate_tomilli(self._private.framerate), + function() + for index, animation in ipairs(self._private.animations) do + if animation.state == true then + -- compute delta time + local time = GLib.get_monotonic_time() + local delta = time - animation.last_elapsed + animation.last_elapsed = time + + -- If pos is true, the animation has ended + local pos = gpcall(animation.tween.update, animation.tween, delta) + if pos == true then + -- Loop the animation, don't end it. + -- Useful for widgets like the spinning cicle + if animation.loop == true then + animation.tween:reset() + else + animation.state = false + + -- Snap to end + animation.pos = animation.tween.target + + gpcall(animation.emit_signal, animation, "update", animation.pos) + gpcall(animation.fire, animation, animation.pos) + + gpcall(animation.emit_signal, animation, "ended", animation.pos) + gpcall(animation.ended.fire, animation, animation.pos) + + table.remove(self._private.animations, index) + end + -- Animation in process, keep updating + else + animation.pos = pos + + gpcall(animation.emit_signal, animation, "update", animation.pos) + gpcall(animation.fire, animation, animation.pos) + end + else + table.remove(self._private.animations, index) + end + end + + -- call again the function after cooldown + return true + end + ) +end + +function animation:set(args) + args = args or {} + + -- Awestoer/Rubbto compatibility + -- I'd rather this always be a table, but Awestore/Rubbto + -- except the :set() method to have 1 number value parameter + -- used to set the target + local is_table = type(args) == "table" + local initial = is_table and (args.pos or self.pos) or self.pos + local subject = is_table and (args.subject or self.subject) or self.subject + local target = is_table and (args.target or self.target) or args + local duration = is_table and (args.duration or self.duration) or self.duration + local easing = is_table and (args.easing or self.easing) or self.easing + + if self.tween == nil or self.reset_on_stop == true then + self.tween = qtween.new { + initial = initial, + subject = subject, + target = target, + duration = second_to_micro(duration), + easing = easing, + } + end + + if self._private.anim_manager._private.instant and self.override_instant ~= true then + self.pos = self.tween.target + self:fire(self.pos) + self:emit_signal("update", self.pos) + + self.state = false + self.ended:fire(self.pos) + self:emit_signal("ended", self.pos) + return + end + + if self._private.anim_manager._private.animations[self.index] == nil then + table.insert(self._private.anim_manager._private.animations, self) + end + + self.state = true + self.last_elapsed = GLib.get_monotonic_time() + + self.started:fire() + self:emit_signal "started" +end + +-- Rubato compatibility +function animation:abort() + self.state = false +end + +function animation:stop() + self.state = false +end + +function animation:initial() + return self._private.initial +end + +function animation_manager:set_instant(value) + if value == true and self._private.instant == false then + on_no_running_animations(self, function() + -- GLib.source_remove(self._private.source_id) + self._private.instant = true + end) + elseif self._private.instant == true then + self._private.instant = false + -- animation_loop(self) + end +end + +function animation_manager:set_framerate(value) + self._private.framerate = value + -- if self._private.instant == false then + on_no_running_animations(self, function() + GLib.source_remove(self._private.source_id) + animation_loop(self) + end) + -- end +end + +function animation_manager:new(args) + args = args or {} + + args.pos = args.pos or 0 + args.subject = args.subject or nil + args.target = args.target or nil + args.duration = args.duration or 0 + args.easing = args.easing or nil + args.loop = args.loop or false + args.signals = args.signals or {} + args.update = args.update or nil + args.reset_on_stop = args.reset_on_stop == nil and true or args.reset_on_stop + + -- Awestoer/Rubbto compatibility + args.subscribed = args.subscribed or nil + local ret = subscribable() + ret.started = subscribable() + ret.ended = subscribable() + if args.subscribed ~= nil then + ret:subscribe(args.subscribed) + end + + for sig, sigfun in pairs(args.signals) do + ret:connect_signal(sig, sigfun) + end + if args.update ~= nil then + ret:connect_signal("update", args.update) + end + + gtable.crush(ret, args, true) + gtable.crush(ret, animation, true) + + ret._private = {} + ret._private.anim_manager = self + ret._private.initial = args.pos + -- Can't have it private for rubato compatibility + ret.state = false + + return ret +end + +local instance = gobject {} +gtable.crush(instance, animation_manager, true) + +instance._private = {} +instance._private.animations = {} +instance._private.instant = false +instance._private.framerate = 60 + +animation_loop(instance) + +return instance diff --git a/.config/awesome/quarrel/animation/subscribable/init.lua b/.config/awesome/quarrel/animation/subscribable/init.lua new file mode 100644 index 0000000..9542704 --- /dev/null +++ b/.config/awesome/quarrel/animation/subscribable/init.lua @@ -0,0 +1,47 @@ +------------------------------------------- +-- @author https://github.com/Kasper24 +-- @copyright 2021-2025 Kasper24 +-- Adopted from rubato +------------------------------------------- +local gobject = require "gears.object" + +-- Kidna copying awesotre's stores on a surface level for added compatibility +local function subscribable(args) + local ret = gobject {} + local subscribed = {} + + -- Subscrubes a function to the object so that it's called when `fire` is + -- Calls subscribe_callback if it exists as well + function ret:subscribe(func) + local id = tostring(func):gsub("function: ", "") + subscribed[id] = func + + if self.subscribe_callback then + self.subscribe_callback(func) + end + end + + -- Unsubscribes a function and calls unsubscribe_callback if it exists + function ret:unsubscribe(func) + if not func then + subscribed = {} + else + local id = tostring(func):gsub("function: ", "") + subscribed[id] = nil + end + + if self.unsubscribe_callback then + self.unsubscribe_callback(func) + end + end + + function ret:fire(...) + for _, func in pairs(subscribed) do + func(...) + end + end + + return ret +end + +return subscribable diff --git a/.config/awesome/quarrel/animation/tween/init.lua b/.config/awesome/quarrel/animation/tween/init.lua new file mode 100644 index 0000000..5f4ce51 --- /dev/null +++ b/.config/awesome/quarrel/animation/tween/init.lua @@ -0,0 +1,554 @@ +------------------------------------------- +-- @author https://github.com/Kasper24 +-- @copyright 2021-2025 Kasper24 +------------------------------------------- +-- easing +-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits. +-- For all easing functions: +-- t = time == how much time has to pass for the tweening to complete +-- b = begin == starting property value +-- c = change == ending - beginning +-- d = duration == running time. How much time has passed *right now* +-- local Color = require "external.lua-color" +local gobject = require "gears.object" +local gtable = require "gears.table" +local tostring = tostring +local assert = assert +local table = table +local pairs = pairs +local error = error + +local type = type + +local tween = { + _VERSION = "tween 2.1.1", + _DESCRIPTION = "tweening for lua", + _URL = "https://github.com/kikito/tween.lua", + _LICENSE = [[ + MIT LICENSE + Copyright (c) 2014 Enrique GarcÃa Cota, Yuichi Tateno, Emmanuel Oga + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]], +} + +local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin + +-- linear +local function linear(t, b, c, d) + return c * t / d + b +end + +-- quad +local function inQuad(t, b, c, d) + return c * pow(t / d, 2) + b +end +local function outQuad(t, b, c, d) + t = t / d + return -c * t * (t - 2) + b +end +local function inOutQuad(t, b, c, d) + t = t / d * 2 + if t < 1 then + return c / 2 * pow(t, 2) + b + end + return -c / 2 * ((t - 1) * (t - 3) - 1) + b +end +local function outInQuad(t, b, c, d) + if t < d / 2 then + return outQuad(t * 2, b, c / 2, d) + end + return inQuad((t * 2) - d, b + c / 2, c / 2, d) +end + +-- cubic +local function inCubic(t, b, c, d) + return c * pow(t / d, 3) + b +end +local function outCubic(t, b, c, d) + return c * (pow(t / d - 1, 3) + 1) + b +end +local function inOutCubic(t, b, c, d) + t = t / d * 2 + if t < 1 then + return c / 2 * t * t * t + b + end + t = t - 2 + return c / 2 * (t * t * t + 2) + b +end +local function outInCubic(t, b, c, d) + if t < d / 2 then + return outCubic(t * 2, b, c / 2, d) + end + return inCubic((t * 2) - d, b + c / 2, c / 2, d) +end + +-- quart +local function inQuart(t, b, c, d) + return c * pow(t / d, 4) + b +end +local function outQuart(t, b, c, d) + return -c * (pow(t / d - 1, 4) - 1) + b +end +local function inOutQuart(t, b, c, d) + t = t / d * 2 + if t < 1 then + return c / 2 * pow(t, 4) + b + end + return -c / 2 * (pow(t - 2, 4) - 2) + b +end +local function outInQuart(t, b, c, d) + if t < d / 2 then + return outQuart(t * 2, b, c / 2, d) + end + return inQuart((t * 2) - d, b + c / 2, c / 2, d) +end + +-- quint +local function inQuint(t, b, c, d) + return c * pow(t / d, 5) + b +end +local function outQuint(t, b, c, d) + return c * (pow(t / d - 1, 5) + 1) + b +end +local function inOutQuint(t, b, c, d) + t = t / d * 2 + if t < 1 then + return c / 2 * pow(t, 5) + b + end + return c / 2 * (pow(t - 2, 5) + 2) + b +end +local function outInQuint(t, b, c, d) + if t < d / 2 then + return outQuint(t * 2, b, c / 2, d) + end + return inQuint((t * 2) - d, b + c / 2, c / 2, d) +end + +-- sine +local function inSine(t, b, c, d) + return -c * cos(t / d * (pi / 2)) + c + b +end +local function outSine(t, b, c, d) + return c * sin(t / d * (pi / 2)) + b +end +local function inOutSine(t, b, c, d) + return -c / 2 * (cos(pi * t / d) - 1) + b +end +local function outInSine(t, b, c, d) + if t < d / 2 then + return outSine(t * 2, b, c / 2, d) + end + return inSine((t * 2) - d, b + c / 2, c / 2, d) +end + +-- expo +local function inExpo(t, b, c, d) + if t == 0 then + return b + end + return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001 +end +local function outExpo(t, b, c, d) + if t == d then + return b + c + end + return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b +end +local function inOutExpo(t, b, c, d) + if t == 0 then + return b + end + if t == d then + return b + c + end + t = t / d * 2 + if t < 1 then + return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 + end + return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b +end +local function outInExpo(t, b, c, d) + if t < d / 2 then + return outExpo(t * 2, b, c / 2, d) + end + return inExpo((t * 2) - d, b + c / 2, c / 2, d) +end + +-- circ +local function inCirc(t, b, c, d) + return (-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) +end +local function outCirc(t, b, c, d) + return (c * sqrt(1 - pow(t / d - 1, 2)) + b) +end +local function inOutCirc(t, b, c, d) + t = t / d * 2 + if t < 1 then + return -c / 2 * (sqrt(1 - t * t) - 1) + b + end + t = t - 2 + return c / 2 * (sqrt(1 - t * t) + 1) + b +end +local function outInCirc(t, b, c, d) + if t < d / 2 then + return outCirc(t * 2, b, c / 2, d) + end + return inCirc((t * 2) - d, b + c / 2, c / 2, d) +end + +-- elastic +local function calculatePAS(p, a, c, d) + p, a = p or d * 0.3, a or 0 + if a < abs(c) then + return p, c, p / 4 + end -- p, a, s + return p, a, p / (2 * pi) * asin(c / a) -- p,a,s +end +local function inElastic(t, b, c, d, a, p) + local s + if t == 0 then + return b + end + t = t / d + if t == 1 then + return b + c + end + p, a, s = calculatePAS(p, a, c, d) + t = t - 1 + return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b +end +local function outElastic(t, b, c, d, a, p) + local s + if t == 0 then + return b + end + t = t / d + if t == 1 then + return b + c + end + p, a, s = calculatePAS(p, a, c, d) + return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b +end +local function inOutElastic(t, b, c, d, a, p) + local s + if t == 0 then + return b + end + t = t / d * 2 + if t == 2 then + return b + c + end + p, a, s = calculatePAS(p, a, c, d) + t = t - 1 + if t < 0 then + return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b + end + return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) * 0.5 + c + b +end +local function outInElastic(t, b, c, d, a, p) + if t < d / 2 then + return outElastic(t * 2, b, c / 2, d, a, p) + end + return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p) +end + +-- back +local function inBack(t, b, c, d, s) + s = s or 1.70158 + t = t / d + return c * t * t * ((s + 1) * t - s) + b +end +local function outBack(t, b, c, d, s) + s = s or 1.70158 + t = t / d - 1 + return c * (t * t * ((s + 1) * t + s) + 1) + b +end +local function inOutBack(t, b, c, d, s) + s = (s or 1.70158) * 1.525 + t = t / d * 2 + if t < 1 then + return c / 2 * (t * t * ((s + 1) * t - s)) + b + end + t = t - 2 + return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b +end +local function outInBack(t, b, c, d, s) + if t < d / 2 then + return outBack(t * 2, b, c / 2, d, s) + end + return inBack((t * 2) - d, b + c / 2, c / 2, d, s) +end + +-- bounce +local function outBounce(t, b, c, d) + t = t / d + if t < 1 / 2.75 then + return c * (7.5625 * t * t) + b + end + if t < 2 / 2.75 then + t = t - (1.5 / 2.75) + return c * (7.5625 * t * t + 0.75) + b + elseif t < 2.5 / 2.75 then + t = t - (2.25 / 2.75) + return c * (7.5625 * t * t + 0.9375) + b + end + t = t - (2.625 / 2.75) + return c * (7.5625 * t * t + 0.984375) + b +end +local function inBounce(t, b, c, d) + return c - outBounce(d - t, 0, c, d) + b +end +local function inOutBounce(t, b, c, d) + if t < d / 2 then + return inBounce(t * 2, 0, c, d) * 0.5 + b + end + return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b +end +local function outInBounce(t, b, c, d) + if t < d / 2 then + return outBounce(t * 2, b, c / 2, d) + end + return inBounce((t * 2) - d, b + c / 2, c / 2, d) +end + +tween.easing = { + linear = linear, + inQuad = inQuad, + outQuad = outQuad, + inOutQuad = inOutQuad, + outInQuad = outInQuad, + inCubic = inCubic, + outCubic = outCubic, + inOutCubic = inOutCubic, + outInCubic = outInCubic, + inQuart = inQuart, + outQuart = outQuart, + inOutQuart = inOutQuart, + outInQuart = outInQuart, + inQuint = inQuint, + outQuint = outQuint, + inOutQuint = inOutQuint, + outInQuint = outInQuint, + inSine = inSine, + outSine = outSine, + inOutSine = inOutSine, + outInSine = outInSine, + inExpo = inExpo, + outExpo = outExpo, + inOutExpo = inOutExpo, + outInExpo = outInExpo, + inCirc = inCirc, + outCirc = outCirc, + inOutCirc = inOutCirc, + outInCirc = outInCirc, + inElastic = inElastic, + outElastic = outElastic, + inOutElastic = inOutElastic, + outInElastic = outInElastic, + inBack = inBack, + outBack = outBack, + inOutBack = inOutBack, + outInBack = outInBack, + inBounce = inBounce, + outBounce = outBounce, + inOutBounce = inOutBounce, + outInBounce = outInBounce, +} + +-- Private interface +local function copyTables(destination, keysTable, valuesTable) + valuesTable = valuesTable or keysTable + local mt = getmetatable(keysTable) + if mt and getmetatable(destination) == nil then + setmetatable(destination, mt) + end + + for k, v in pairs(keysTable) do + if type(v) == "table" then + destination[k] = copyTables({}, v, valuesTable[k]) + else + destination[k] = valuesTable[k] + end + end + return destination +end + +local function checkSubjectAndTargetRecursively(subject, target, path) + path = path or {} + local targetType, newPath + for k, targetValue in pairs(target) do + targetType, newPath = type(targetValue), copyTables({}, path) + table.insert(newPath, tostring(k)) + if targetType == "number" then + assert( + type(subject[k]) == "number", + "Parameter '" .. table.concat(newPath, "/") .. "' is missing from subject or isn't a number" + ) + elseif targetType == "table" then + checkSubjectAndTargetRecursively(subject[k], targetValue, newPath) + else + assert( + targetType == "number", + "Parameter '" .. table.concat(newPath, "/") .. "' must be a number or table of numbers" + ) + end + end +end + +local function checkNewParams(initial, duration, subject, target, easing) + -- assert(type(initial) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration)) + -- assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration)) + assert(type(easing) == "function", "easing must be a function. Was " .. tostring(easing)) + + if subject and target then + local tsubject = type(subject) + assert( + tsubject == "table" or tsubject == "userdata", + "subject must be a table or userdata. Was " .. tostring(subject) + ) + assert(type(target) == "table", "target must be a table. Was " .. tostring(target)) + checkSubjectAndTargetRecursively(subject, target) + end +end + +local function getEasingFunction(easing) + easing = easing or "linear" + if type(easing) == "string" then + local name = easing + easing = tween.easing[name] + if type(easing) ~= "function" then + error("The easing function name '" .. name .. "' is invalid") + end + end + return easing +end + +local function performEasingOnSubject(subject, target, initial, clock, duration, easing) + local t, b, c, d + for k, v in pairs(target) do + if type(v) == "table" then + performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing) + else + t, b, c, d = clock, initial[k], v - initial[k], duration + subject[k] = easing(t, b, c, d) + end + end +end + +-- local function performEasingOnColor(initial, target, clock, duration, easing) +-- initial = Color(initial) +-- target = Color(target) +-- +-- local r = easing(clock, initial.r, target.r - initial.r, duration) +-- local g = easing(clock, initial.g, target.g - initial.g, duration) +-- local b = easing(clock, initial.b, target.b - initial.b, duration) +-- local a = easing(clock, initial.a, target.a - initial.a, duration) +-- +-- return tostring(Color { r = r, g = g, b = b, a = a }) +-- end + +local function performEasing(table, initial, target, clock, duration, easing) + if type(target) == "table" then + local t, b, c, d + for k, target in pairs(target) do + if type(target) == "table" then + table[k] = {} + performEasing(table[k], initial[k], target, clock, duration, easing) + -- elseif type(target) == "string" and target:sub(1, 1) == "#" then + -- table[k] = performEasingOnColor(initial[k], target, clock, duration, easing) + else + t, b, c, d = clock, initial[k], target - initial[k], duration + table[k] = easing(t, b, c, d) + end + end + + return table + -- elseif type(target) == "string" and target:sub(1, 1) == "#" then + -- return performEasingOnColor(initial, target, clock, duration, easing) + else + local t, b, c, d = clock, initial, target - initial, duration + return easing(t, b, c, d) + end +end + +-- Public interface +local Tween = {} + +function Tween:set(clock) + assert(type(clock) == "number", "clock must be a positive number or 0") + + if self.subject and self.initial == 0 then + self.initial = copyTables({}, self.target, self.subject) + end + + self.clock = clock + + if self.clock <= 0 then + self.clock = 0 + if self.subject then + copyTables(self.subject, self.initial) + end + elseif self.clock >= self.duration then -- the tween has expired + self.clock = self.duration + + if self.subject then + copyTables(self.subject, self.target) + end + else + if self.subject then + performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing) + else + local pos = {} + return performEasing(pos, self.initial, self.target, self.clock, self.duration, self.easing) + end + end + + return self.clock >= self.duration +end + +function Tween:update(dt) + assert(type(dt) == "number", "dt must be a number") + return self:set(self.clock + dt) +end + +function Tween:reset() + return self:set(0) +end + +function tween.new(args) + args = args or {} + + args.initial = args.initial or 0 + args.subject = args.subject or nil + args.target = args.target or nil + args.duration = args.duration or 0 + args.easing = args.easing or nil + + args.easing = getEasingFunction(args.easing) + checkNewParams(args.initial, args.duration, args.subject, args.target, args.easing) + + local ret = gobject {} + ret.clock = 0 + + gtable.crush(ret, args, true) + gtable.crush(ret, Tween, true) + + return ret +end + +return tween diff --git a/.config/awesome/quarrel/bezier.lua b/.config/awesome/quarrel/bezier.lua deleted file mode 100644 index 4229961..0000000 --- a/.config/awesome/quarrel/bezier.lua +++ /dev/null @@ -1,343 +0,0 @@ ---------------------------------------------------------------------------- ---- A helper module for computations involving Bézier curves --- --- @author Alex Belykh <albel727@ngs.ru> --- @copyright 2021 Alex Belykh --- @submodule gears.math ---------------------------------------------------------------------------- - -local table_insert = table.insert - -local bezier = {} - ---- Compute the value of a Bézier curve at a given value of the t parameter. --- --- This function evaluates the given curve `B` of an arbitrary degree --- at a given point t. --- --- @tparam {number,...} c The table of control points of the curve. --- @tparam number t The value of the t parameter to evaluate the curve at. --- @treturn[1] number The value of `B(t)`. --- @treturn[2] nil `nil`, if c is empty. --- @staticfct gears.math.bezier.curve_evaluate_at --- @see wibox.widget.graph.step_hook -function bezier.curve_evaluate_at(c, t) - local from = c - local tmp = { nil, nil, nil, nil } - while #from > 1 do - for i = 1, #from - 1 do - tmp[i] = from[i] * (1 - t) + from[i + 1] * t - end - tmp[#from] = nil - from = tmp - end - - return from[1] -end - ---- Split a Bézier curve into two curves at a given value of the t parameter. --- --- This function splits the given curve `B` of an arbitrary degree at a point t --- into two curves of the same degree `B_left` and `B_right`, such that --- `B_left(0)=B(0)`, `B_left(1)=B(t)=B_right(0)`, `B_right(1)=B(1)`. --- --- @tparam {number,...} c The table of control points of the curve. --- @tparam number t The value of the t parameter to split the curve at. --- @treturn {number,...} The table of control points for `B_left`. --- @treturn {number,...} The table of control points for `B_right`. --- @staticfct gears.math.bezier.curve_split_at --- @see wibox.widget.graph.step_hook -function bezier.curve_split_at(c, t) - local coefs_left, coefs_right = {}, {} - local from = c - local tmp = { nil, nil, nil, nil } - while #from > 0 do - table_insert(coefs_left, from[1]) - table_insert(coefs_right, 1, from[#from]) - for i = 1, #from - 1 do - tmp[i] = from[i] * (1 - t) + from[i + 1] * t - end - tmp[#from] = nil - from = tmp - end - - return coefs_left, coefs_right -end - ---- Get the n-th derivative Bézier curve of a Bézier curve. --- --- This function computes control points for the curve that is --- the derivative of order `n` in `t`, i.e. `B^(n)(t)`, --- of the given curve `B(t)` of an arbitrary degree. --- --- @tparam {number,...} c The table of control points of the curve. --- @tparam[opt=1] integer n The order of the derivative to take. --- @treturn[1] {number,...} The table of control points of `B^(n)(t)`. --- @treturn[2] nil If n is less than 0. --- @staticfct gears.math.bezier.curve_derivative --- @see wibox.widget.graph.step_hook -function bezier.curve_derivative(c, n) - n = n or 1 - if n < 0 then - return - end - if n < 1 then - return c - end - local c_len = #c - if c_len < n + 1 then - return {} - end - - local from = c - local tmp = {} - - for l = c_len - 1, c_len - n, -1 do - for i = 1, l do - tmp[i] = (from[i + 1] - from[i]) * l - end - tmp[l + 1] = nil - from = tmp - end - - return from -end - --- This is used instead of plain 0 to try and be compatible --- with objects that implement their own arithmetic via metatables. -local function get_zero(c, zero) - return c and c * 0 or zero -end - ---- Compute the value of the n-th derivative of a Bézier curve ---- at a given value of the t parameter. --- --- This is roughly the same as --- `curve_evaluate_at(curve_derivative(c, n), t)`, but the latter --- would throw errors or return nil instead of 0 in some cases. --- --- @tparam {number,...} c The table of control points of the curve. --- @tparam number t The value of the t parameter to compute the derivative at. --- @tparam[opt=1] integer n The order of the derivative to take. --- @tparam[opt=nil] number|nil zero The value to return if c is empty. --- @treturn[1] number The value of `B^(n)(t)`. --- @treturn[2] nil nil, if n is less than 0. --- @treturn[3] number|nil The value of the zero parameter, if c is empty. --- @staticfct gears.math.bezier.curve_derivative_at --- @see wibox.widget.graph.step_hook -function bezier.curve_derivative_at(c, t, n, zero) - local d = bezier.curve_derivative(c, n) - if not d then - return - end - - return bezier.curve_evaluate_at(d, t) or get_zero(c[1], zero) -end - ---- Compute the value of the 1-st derivative of a Bézier curve at t=0. --- --- This is the same as `curve_derivative_at(c, 0)`, but since it's particularly --- computationally simple and useful in practice, it has its own function. --- --- @tparam {number,...} c The table of control points of the curve. --- @tparam[opt=nil] number|nil zero The value to return if c is empty. --- @treturn[1] number The value of `B'(0)`. --- @treturn[2] number|nil The value of the zero parameter, if c is empty. --- @staticfct gears.math.bezier.curve_derivative_at_zero --- @see wibox.widget.graph.step_hook -function bezier.curve_derivative_at_zero(c, zero) - local l = #c - if l < 2 then - return get_zero(c[1], zero) - end - return (c[2] - c[1]) * (l - 1) -end - ---- Compute the value of the 1-st derivative of a Bézier curve at t=1. --- --- This is the same as `curve_derivative_at(c, 1)`, but since it's particularly --- computationally simple and useful in practice, it has its own function. --- --- @tparam {number,...} c The table of control points of the curve. --- @tparam[opt=nil] number|nil zero The value to return if c is empty. --- @treturn[1] number The value of `B'(1)`. --- @treturn[2] number|nil The value of the zero parameter, if c is empty. --- @staticfct gears.math.bezier.curve_derivative_at_one --- @see wibox.widget.graph.step_hook -function bezier.curve_derivative_at_one(c, zero) - local l = #c - if l < 2 then - return get_zero(c[1], zero) - end - return (c[l] - c[l - 1]) * (l - 1) -end - ---- Get the (n+1)-th degree Bézier curve, that has the same shape as --- a given n-th degree Bézier curve. --- --- Given the control points of a curve B of degree n, this function computes --- the control points for the curve Q, such that `Q(t) = B(t)`, and --- Q has the degree n+1, i.e. it has one control point more. --- --- @tparam {number,...} c The table of control points of the curve B. --- @treturn {number,...} The table of control points of the curve Q. --- @staticfct gears.math.bezier.curve_elevate_degree --- @see wibox.widget.graph.step_hook -function bezier.curve_elevate_degree(c) - local ret = { c[1] } - local len = #c - - for i = 1, len - 1 do - ret[i + 1] = (i * c[i] + (len - i) * c[i + 1]) / len - end - - ret[len + 1] = c[len] - return ret -end - ---- Get a cubic Bézier curve that passes through given points (up to 4). --- --- This function takes up to 4 values and returns the 4 control points --- for a cubic curve --- --- `B(t) = c0\*(1-t)^3 + 3\*c1\*t\*(1-t)^2 + 3\*c2\*t^2\*(1-t) + c3\*t^3`, --- that takes on these values at equidistant values of the t parameter. --- --- If only p0 is given, `B(0)=B(1)=B(for all t)=p0`. --- --- If p0 and p1 are given, `B(0)=p0` and `B(1)=p1`. --- --- If p0, p1 and p2 are given, `B(0)=p0`, `B(1/2)=p1` and `B(1)=p2`. --- --- For 4 points given, `B(0)=p0`, `B(1/3)=p1`, `B(2/3)=p2`, `B(1)=p3`. --- --- @tparam number p0 --- @tparam[opt] number p1 --- @tparam[opt] number p2 --- @tparam[opt] number p3 --- @treturn number c0 --- @treturn number c1 --- @treturn number c2 --- @treturn number c3 --- @staticfct gears.math.bezier.cubic_through_points --- @see wibox.widget.graph.step_hook -function bezier.cubic_through_points(p0, p1, p2, p3) - if not p1 then - return p0, p0, p0, p0 - end - if not p2 then - local c1 = (2 * p0 + p1) / 3 - local c2 = (2 * p1 + p0) / 3 - return p0, c1, c2, p1 - end - if not p3 then - local c1 = (4 * p1 - p2) / 3 - local c2 = (4 * p1 - p0) / 3 - return p0, c1, c2, p2 - end - local c1 = (-5 * p0 + 18 * p1 - 9 * p2 + 2 * p3) / 6 - local c2 = (-5 * p3 + 18 * p2 - 9 * p1 + 2 * p0) / 6 - return p0, c1, c2, p3 -end - ---- Get a cubic Bézier curve with given values and derivatives at endpoints. --- --- This function computes the 4 control points for the cubic curve B, such that --- `B(0)=p0`, `B'(0)=d0`, `B(1)=p3`, `B'(1)=d3`. --- --- @tparam number d0 The value of the derivative at t=0. --- @tparam number p0 The value of the curve at t=0. --- @tparam number p3 The value of the curve at t=1. --- @tparam number d3 The value of the derivative at t=1. --- @treturn number c0 --- @treturn number c1 --- @treturn number c2 --- @treturn number c3 --- @staticfct gears.math.bezier.cubic_from_points_and_derivatives --- @see wibox.widget.graph.step_hook -function bezier.cubic_from_points_and_derivatives(d0, p0, p3, d3) - local c1 = p0 + d0 / 3 - local c2 = p3 - d3 / 3 - return p0, c1, c2, p3 -end - ---- Get a cubic Bézier curve with given values at endpoints and starting ---- derivative, while minimizing (an approximation of) the stretch energy. --- --- This function computes the 4 control points for the cubic curve B, such that --- `B(0)=p0`, `B'(0)=d0`, `B(1)=p3`, and --- the integral of `(B'(t))^2` on `t=[0,1]` is minimal. --- (The actual stretch energy is the integral of `|B'(t)|`) --- --- In practical terms this is almost the same as "the curve of shortest length --- connecting given points and having the given starting speed". --- --- @tparam number d0 The value of the derivative at t=0. --- @tparam number p0 The value of the curve at t=0. --- @tparam number p3 The value of the curve at t=1. --- @treturn number c0 --- @treturn number c1 --- @treturn number c2 --- @treturn number c3 --- @staticfct gears.math.bezier.cubic_from_derivative_and_points_min_stretch --- @see wibox.widget.graph.step_hook -function bezier.cubic_from_derivative_and_points_min_stretch(d0, p0, p3) - local c1 = p0 + d0 / 3 - local c2 = (2 * p0 - c1 + 3 * p3) / 4 - return p0, c1, c2, p3 -end - ---- Get a cubic Bézier curve with given values at endpoints and starting ---- derivative, while minimizing (an approximation of) the strain energy. --- --- This function computes the 4 control points for the cubic curve B, such that --- `B(0)=p0`, `B'(0)=d0`, `B(1)=p3`, and --- the integral of `(B''(t))^2` on `t=[0,1]` is minimal. --- --- In practical terms this is almost the same as "the curve of smallest --- speed change connecting given points and having the given starting speed". --- --- @tparam number d0 The value of the derivative at t=0. --- @tparam number p0 The value of the curve at t=0. --- @tparam number p3 The value of the curve at t=1. --- @treturn number c0 --- @treturn number c1 --- @treturn number c2 --- @treturn number c3 --- @staticfct gears.math.bezier.cubic_from_derivative_and_points_min_strain --- @see wibox.widget.graph.step_hook -function bezier.cubic_from_derivative_and_points_min_strain(d, p0, p3) - local c1 = p0 + d / 3 - local c2 = (c1 + p3) / 2 - return p0, c1, c2, p3 -end - ---- Get a cubic Bézier curve with given values at endpoints and starting ---- derivative, while minimizing the jerk energy. --- --- This function computes the 4 control points for the cubic curve B, such that --- `B(0)=p0`, `B'(0)=d0`, `B(1)=p3`, and --- the integral of `(B'''(t))^2` on `t=[0,1]` is minimal. --- --- In practical terms this is almost the same as "the curve of smallest --- acceleration change connecting given points and having the given --- starting speed". --- --- @tparam number d0 The value of the derivative at t=0. --- @tparam number p0 The value of the curve at t=0. --- @tparam number p3 The value of the curve at t=1. --- @treturn number c0 --- @treturn number c1 --- @treturn number c2 --- @treturn number c3 --- @staticfct gears.math.bezier.cubic_from_derivative_and_points_min_jerk --- @see wibox.widget.graph.step_hook -function bezier.cubic_from_derivative_and_points_min_jerk(d, p0, p3) - local c1 = p0 + d / 3 - local c2 = c1 + (p3 - p0) / 3 - return p0, c1, c2, p3 -end - -return bezier - --- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/.config/awesome/quarrel/bind/consts.lua b/.config/awesome/quarrel/bind/consts.lua new file mode 100644 index 0000000..5a91d84 --- /dev/null +++ b/.config/awesome/quarrel/bind/consts.lua @@ -0,0 +1,27 @@ +local awful = require "awful" + +local C = {} + +-- taken from https://github.com/bew/dotfiles/blob/ab9bb1935783f7a31ef777b1d7e26d53f35df864/gui/wezterm/cfg_utils.lua +C.mods = setmetatable({ _SHORT_MAP = { C = "Control", S = "Shift", A = "Mod1", M = "Mod4" } }, { + __index = function(self, key) + local resolved_mods = {} + for i = 1, #key do + resolved_mods[i] = self._SHORT_MAP[key:sub(i, i)] + end + return resolved_mods + end, +}) + +local btns = awful.button.names + +---@enum buttons +C.btns = { + left = btns.LEFT, + right = btns.RIGHT, + middle = btns.MIDDLE, + up = btns.SCROLL_UP, + down = btns.SCROLL_DOWN, +} + +return C diff --git a/.config/awesome/quarrel/bind.lua b/.config/awesome/quarrel/bind/init.lua index a1abf29..a851440 100644 --- a/.config/awesome/quarrel/bind.lua +++ b/.config/awesome/quarrel/bind/init.lua @@ -1,11 +1,10 @@ +local C = require "quarrel.bind.consts" local awful = require "awful" local gtable = require "gears.table" -local qstore = require "quarrel.store" ----@class QuarrelBind -local qbind = {} - -qstore.bindings = {} +local M = gtable.crush({ + bindings = {}, +}, C) ---@alias mouse_button ---| 0 Left mouse button @@ -35,7 +34,7 @@ qstore.bindings = {} ---@field press fun(...) | fun(any, ...) Function to run when the trigger is pressed ---@field desc string? Description ---@field group string? What group the binding will show up in ----@field triggers Trigger[] | bind +---@field triggers Trigger[] | bind[] | bind ---Get the corresponding binding creation function for a trigger ---@param bind bind @@ -83,24 +82,30 @@ end --- Create a new binding ---@param binding Binding ---@return awful.key[] -function qbind:new(binding) +function M:new(binding) if not binding.hidden then - table.insert(qstore.bindings, binding) + table.insert(self.bindings, binding) end binding.mods = binding.mods or {} local awful_bindings = {} if type(binding.triggers) == "table" then - for _, trigger in - ipairs(binding.triggers --[[@as Trigger[]]) + for _, _trigger in + ipairs(binding.triggers --[[@as bind[] | Trigger[]]) do + local trigger + if type(_trigger) == "table" then + trigger = _trigger + elseif type(_trigger) == "string" or type(_trigger) == "number" then + trigger = { _trigger, _trigger } + end table.insert(awful_bindings, translate_binding(binding, trigger, true)) end elseif type(binding.triggers) == "string" or type(binding.triggers) == "number" then return translate_binding(binding, binding.triggers --[[@as bind]], false) else - error "binding.triggers can only be a string or a table" + error("binding.triggers can only be a string or a table (passed in " .. type(binding.triggers) .. ")") end -- for some reason multi-trigger bindings only work if i do this @@ -109,4 +114,6 @@ function qbind:new(binding) return gtable.join(table.unpack(awful_bindings)) end -return qbind +return setmetatable(M, { + __call = M.new, +}) diff --git a/.config/awesome/quarrel/color.lua b/.config/awesome/quarrel/color.lua new file mode 100644 index 0000000..b96ef57 --- /dev/null +++ b/.config/awesome/quarrel/color.lua @@ -0,0 +1,64 @@ +-- autogenerated from palette.json +local M = {} + +local function tone_set(colors) + return setmetatable(colors, { + __call = function(self) + return self[1] + end, + }) +end + +M.palette = { + bg = tone_set { + high = "#171c22", + highest = "#1d2228", + low = "#0c1116", + lowest = "#070c11", + "#12161c", + }, + blue = tone_set { + bright = "#abe4ff", + "#8bc3fc", + }, + border = tone_set { + "#323b46", + variant = "#262f39", + }, + cyan = tone_set { + bright = "#8ef4e2", + "#6dd3c2", + }, + fg = tone_set { + high = "#e8eff8", + low = "#a8afb7", + "#c8ced7", + }, + green = tone_set { + bright = "#abf3b3", + "#8bd294", + }, + orange = tone_set { + bright = "#ffc08e", + "#ff9f6f", + }, + pink = tone_set { + bright = "#ffccd4", + "#e5acb4", + }, + purple = tone_set { + bright = "#e4d1ff", + "#c4b1f6", + }, + red = tone_set { + bright = "#ffb2a9", + "#ff928a", + }, + yellow = tone_set { + bright = "#ffd278", + "#ecb256", + }, + transparent = "#00000000", +} + +return M diff --git a/.config/awesome/quarrel/const.lua b/.config/awesome/quarrel/const.lua new file mode 100644 index 0000000..342ae41 --- /dev/null +++ b/.config/awesome/quarrel/const.lua @@ -0,0 +1,22 @@ +local M = {} + +---@param protected (fun(): table) | table +function M.protect(protected) + ---@type table + local tbl + if type(protected) == "table" then + tbl = protected + elseif type(protected) == "function" then + tbl = protected() + else + error("expected a table or a function that returns a table, got " .. type(protected), 2) + end + return setmetatable({}, { + __index = tbl, + __newindex = function(_, k, v) + error("attempted to change constant " .. tostring(k) .. " to " .. tostring(v), 2) + end, + }) +end + +return M diff --git a/.config/awesome/quarrel/debugger.lua b/.config/awesome/quarrel/debugger.lua new file mode 100644 index 0000000..b1a272a --- /dev/null +++ b/.config/awesome/quarrel/debugger.lua @@ -0,0 +1,865 @@ +-- SPDX-License-Identifier: MIT +-- Copyright (c) 2024 Scott Lembcke and Howling Moon Software + +local dbg + +-- Use ANSI color codes in the prompt by default. +local COLOR_GRAY = "" +local COLOR_RED = "" +local COLOR_BLUE = "" +local COLOR_YELLOW = "" +local COLOR_RESET = "" +local GREEN_CARET = " => " + +local function pretty(obj, max_depth) + if max_depth == nil then + max_depth = dbg.pretty_depth + end + + -- Returns true if a table has a __tostring metamethod. + local function coerceable(tbl) + local meta = getmetatable(tbl) + return (meta and meta.__tostring) + end + + local function recurse(obj, depth) + if type(obj) == "string" then + -- Dump the string so that escape sequences are printed. + return string.format("%q", obj) + elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then + local str = "{" + + for k, v in pairs(obj) do + local pair = pretty(k, 0) .. " = " .. recurse(v, depth + 1) + str = str .. (str == "{" and pair or ", " .. pair) + end + + return str .. "}" + else + -- tostring() can fail if there is an error in a __tostring metamethod. + local success, value = pcall(function() + return tostring(obj) + end) + return (success and value or "<!!error in __tostring metamethod!!>") + end + end + + return recurse(obj, 0) +end + +-- The stack level that cmd_* functions use to access locals or info +-- The structure of the code very carefully ensures this. +local CMD_STACK_LEVEL = 6 + +-- Location of the top of the stack outside of the debugger. +-- Adjusted by some debugger entrypoints. +local stack_top = 0 + +-- The current stack frame index. +-- Changed using the up/down commands +local stack_inspect_offset = 0 + +-- LuaJIT has an off by one bug when setting local variables. +local LUA_JIT_SETLOCAL_WORKAROUND = 0 + +-- Default dbg.read function +local function dbg_read(prompt) + dbg.write(prompt) + io.flush() + return io.read() +end + +-- Default dbg.write function +local function dbg_write(str) + io.write(str) +end + +local function dbg_writeln(str, ...) + if select("#", ...) == 0 then + dbg.write((str or "<NULL>") .. "\n") + else + dbg.write(string.format(str .. "\n", ...)) + end +end + +local function format_loc(file, line) + return COLOR_BLUE .. file .. COLOR_RESET .. ":" .. COLOR_YELLOW .. line .. COLOR_RESET +end +local function format_stack_frame_info(info) + local filename = info.source:match "@(.*)" + local source = filename and dbg.shorten_path(filename) or info.short_src + local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat) + local name = ( + info.name and "'" .. COLOR_BLUE .. info.name .. COLOR_RESET .. "'" or format_loc(source, info.linedefined) + ) + return format_loc(source, info.currentline) .. " in " .. namewhat .. " " .. name +end + +local repl + +-- Return false for stack frames without source, +-- which includes C frames, Lua bytecode, and `loadstring` functions +local function frame_has_line(info) + return info.currentline >= 0 +end + +local function hook_factory(repl_threshold) + return function(offset, reason) + return function(event, _) + -- Skip events that don't have line information. + if not frame_has_line(debug.getinfo(2)) then + return + end + + -- Tail calls are specifically ignored since they also will have tail returns to balance out. + if event == "call" then + offset = offset + 1 + elseif event == "return" and offset > repl_threshold then + offset = offset - 1 + elseif event == "line" and offset <= repl_threshold then + repl(reason) + end + end + end +end + +local hook_step = hook_factory(1) +local hook_next = hook_factory(0) +local hook_finish = hook_factory(-1) + +-- Create a table of all the locally accessible variables. +-- Globals are not included when running the locals command, but are when running the print command. +local function local_bindings(offset, include_globals) + local level = offset + stack_inspect_offset + CMD_STACK_LEVEL + local func = debug.getinfo(level).func + local bindings = {} + + -- Retrieve the upvalues + do + local i = 1 + while true do + local name, value = debug.getupvalue(func, i) + if not name then + break + end + bindings[name] = value + i = i + 1 + end + end + + -- Retrieve the locals (overwriting any upvalues) + do + local i = 1 + while true do + local name, value = debug.getlocal(level, i) + if not name then + break + end + bindings[name] = value + i = i + 1 + end + end + + -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) + local varargs = {} + do + local i = 1 + while true do + local name, value = debug.getlocal(level, -i) + if not name then + break + end + varargs[i] = value + i = i + 1 + end + end + if #varargs > 0 then + bindings["..."] = varargs + end + + if include_globals then + -- In Lua 5.2, you have to get the environment table from the function's locals. + local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV) + return setmetatable(bindings, { __index = env or _G }) + else + return bindings + end +end + +-- Used as a __newindex metamethod to modify variables in cmd_eval(). +local function mutate_bindings(_, name, value) + local FUNC_STACK_OFFSET = 3 -- Stack depth of this function. + local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL + + -- Set a local. + do + local i = 1 + repeat + local var = debug.getlocal(level, i) + if name == var then + dbg_writeln( + COLOR_YELLOW + .. "debugger.lua" + .. GREEN_CARET + .. "Set local variable " + .. COLOR_BLUE + .. name + .. COLOR_RESET + ) + return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value) + end + i = i + 1 + until var == nil + end + + -- Set an upvalue. + local func = debug.getinfo(level).func + do + local i = 1 + repeat + local var = debug.getupvalue(func, i) + if name == var then + dbg_writeln( + COLOR_YELLOW .. "debugger.lua" .. GREEN_CARET .. "Set upvalue " .. COLOR_BLUE .. name .. COLOR_RESET + ) + return debug.setupvalue(func, i, value) + end + i = i + 1 + until var == nil + end + + -- Set a global. + dbg_writeln( + COLOR_YELLOW .. "debugger.lua" .. GREEN_CARET .. "Set global variable " .. COLOR_BLUE .. name .. COLOR_RESET + ) + _G[name] = value +end + +-- Compile an expression with the given variable bindings. +local function compile_chunk(block, env) + local source = "debugger.lua REPL" + local chunk = nil + + if _VERSION <= "Lua 5.1" then + chunk = loadstring(block, source) + if chunk then + setfenv(chunk, env) + end + else + -- The Lua 5.2 way is a bit cleaner + chunk = load(block, source, "t", env) + end + + if not chunk then + dbg_writeln(COLOR_RED .. "Error: Could not compile block:\n" .. COLOR_RESET .. block) + end + return chunk +end + +local SOURCE_CACHE = {} + +local function where(info, context_lines) + local source = SOURCE_CACHE[info.source] + if not source then + source = {} + local filename = info.source:match "@(.*)" + if filename then + pcall(function() + for line in io.lines(filename) do + table.insert(source, line) + end + end) + elseif info.source then + for line in info.source:gmatch "[^\n]+" do + table.insert(source, line) + end + end + SOURCE_CACHE[info.source] = source + end + + if source and source[info.currentline] then + for i = info.currentline - context_lines, info.currentline + context_lines do + local tab_or_caret = (i == info.currentline and GREEN_CARET or " ") + local line = source[i] + if line then + dbg_writeln(COLOR_GRAY .. "% 4d" .. tab_or_caret .. "%s", i, line) + end + end + else + dbg_writeln(COLOR_RED .. "Error: Source not available for " .. COLOR_BLUE .. info.short_src) + end + + return false +end + +-- Wee version differences +local unpack = unpack or table.unpack +local pack = function(...) + return { n = select("#", ...), ... } +end + +local function cmd_step() + stack_inspect_offset = stack_top + return true, hook_step +end + +local function cmd_next() + stack_inspect_offset = stack_top + return true, hook_next +end + +local function cmd_finish() + local offset = stack_top - stack_inspect_offset + stack_inspect_offset = stack_top + return true, offset < 0 and hook_factory(offset - 1) or hook_finish +end + +local function cmd_print(expr) + local env = local_bindings(1, true) + local chunk = compile_chunk("return " .. expr, env) + if chunk == nil then + return false + end + + -- Call the chunk and collect the results. + local results = pack(pcall(chunk, unpack(rawget(env, "...") or {}))) + + -- The first result is the pcall error. + if not results[1] then + dbg_writeln(COLOR_RED .. "Error:" .. COLOR_RESET .. " " .. results[2]) + else + local output = "" + for i = 2, results.n do + output = output .. (i ~= 2 and ", " or "") .. dbg.pretty(results[i]) + end + + if output == "" then + output = "<no result>" + end + dbg_writeln(COLOR_BLUE .. expr .. GREEN_CARET .. output) + end + + return false +end + +local function cmd_eval(code) + local env = local_bindings(1, true) + local mutable_env = setmetatable({}, { + __index = env, + __newindex = mutate_bindings, + }) + + local chunk = compile_chunk(code, mutable_env) + if chunk == nil then + return false + end + + -- Call the chunk and collect the results. + local success, err = pcall(chunk, unpack(rawget(env, "...") or {})) + if not success then + dbg_writeln(COLOR_RED .. "Error:" .. COLOR_RESET .. " " .. tostring(err)) + end + + return false +end + +local function cmd_down() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset + 1 + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until not info or frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: " .. format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then + where(info, dbg.auto_where) + end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln "Already at the bottom of the stack." + end + + return false +end + +local function cmd_up() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset - 1 + if offset < stack_top then + info = nil + break + end + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: " .. format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then + where(info, dbg.auto_where) + end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln "Already at the top of the stack." + end + + return false +end + +local function cmd_inspect(offset) + offset = stack_top + tonumber(offset) + local info = debug.getinfo(offset + CMD_STACK_LEVEL) + if info then + stack_inspect_offset = offset + dbg.writeln("Inspecting frame: " .. format_stack_frame_info(info)) + else + dbg.writeln(COLOR_RED .. "ERROR: " .. COLOR_BLUE .. "Invalid stack frame index." .. COLOR_RESET) + end +end + +local function cmd_where(context_lines) + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + return (info and where(info, tonumber(context_lines) or 5)) +end + +local function cmd_trace() + dbg_writeln("Inspecting frame %d", stack_inspect_offset - stack_top) + local i = 0 + while true do + local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i) + if not info then + break + end + + local is_current_frame = (i + stack_top == stack_inspect_offset) + local tab_or_caret = (is_current_frame and GREEN_CARET or " ") + dbg_writeln(COLOR_GRAY .. "% 4d" .. COLOR_RESET .. tab_or_caret .. "%s", i, format_stack_frame_info(info)) + i = i + 1 + end + + return false +end + +local function cmd_locals() + local bindings = local_bindings(1, false) + + -- Get all the variable binding names and sort them + local keys = {} + for k, _ in pairs(bindings) do + table.insert(keys, k) + end + table.sort(keys) + + for _, k in ipairs(keys) do + local v = bindings[k] + + -- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object. + if not rawequal(v, dbg) and k ~= "_ENV" and not k:match "%(.*%)" then + dbg_writeln(" " .. COLOR_BLUE .. k .. GREEN_CARET .. dbg.pretty(v)) + end + end + + return false +end + +local function cmd_help() + dbg.write( + "" + .. COLOR_BLUE + .. " <return>" + .. GREEN_CARET + .. "re-run last command\n" + .. COLOR_BLUE + .. " c" + .. COLOR_YELLOW + .. "(ontinue)" + .. GREEN_CARET + .. "continue execution\n" + .. COLOR_BLUE + .. " s" + .. COLOR_YELLOW + .. "(tep)" + .. GREEN_CARET + .. "step forward by one line (into functions)\n" + .. COLOR_BLUE + .. " n" + .. COLOR_YELLOW + .. "(ext)" + .. GREEN_CARET + .. "step forward by one line (skipping over functions)\n" + .. COLOR_BLUE + .. " f" + .. COLOR_YELLOW + .. "(inish)" + .. GREEN_CARET + .. "step forward until exiting the current function\n" + .. COLOR_BLUE + .. " u" + .. COLOR_YELLOW + .. "(p)" + .. GREEN_CARET + .. "move up the stack by one frame\n" + .. COLOR_BLUE + .. " d" + .. COLOR_YELLOW + .. "(own)" + .. GREEN_CARET + .. "move down the stack by one frame\n" + .. COLOR_BLUE + .. " i" + .. COLOR_YELLOW + .. "(nspect) " + .. COLOR_BLUE + .. "[index]" + .. GREEN_CARET + .. "move to a specific stack frame\n" + .. COLOR_BLUE + .. " w" + .. COLOR_YELLOW + .. "(here) " + .. COLOR_BLUE + .. "[line count]" + .. GREEN_CARET + .. "print source code around the current line\n" + .. COLOR_BLUE + .. " e" + .. COLOR_YELLOW + .. "(val) " + .. COLOR_BLUE + .. "[statement]" + .. GREEN_CARET + .. "execute the statement\n" + .. COLOR_BLUE + .. " p" + .. COLOR_YELLOW + .. "(rint) " + .. COLOR_BLUE + .. "[expression]" + .. GREEN_CARET + .. "execute the expression and print the result\n" + .. COLOR_BLUE + .. " t" + .. COLOR_YELLOW + .. "(race)" + .. GREEN_CARET + .. "print the stack trace\n" + .. COLOR_BLUE + .. " l" + .. COLOR_YELLOW + .. "(ocals)" + .. GREEN_CARET + .. "print the function arguments, locals and upvalues.\n" + .. COLOR_BLUE + .. " h" + .. COLOR_YELLOW + .. "(elp)" + .. GREEN_CARET + .. "print this message\n" + .. COLOR_BLUE + .. " q" + .. COLOR_YELLOW + .. "(uit)" + .. GREEN_CARET + .. "halt execution\n" + ) + return false +end + +local last_cmd = false + +local commands = { + ["^c$"] = function() + return true + end, + ["^s$"] = cmd_step, + ["^n$"] = cmd_next, + ["^f$"] = cmd_finish, + ["^p%s+(.*)$"] = cmd_print, + ["^e%s+(.*)$"] = cmd_eval, + ["^u$"] = cmd_up, + ["^d$"] = cmd_down, + ["i%s*(%d+)"] = cmd_inspect, + ["^w%s*(%d*)$"] = cmd_where, + ["^t$"] = cmd_trace, + ["^l$"] = cmd_locals, + ["^h$"] = cmd_help, + ["^q$"] = function() + dbg.exit(0) + return true + end, +} + +local function match_command(line) + for pat, func in pairs(commands) do + -- Return the matching command and capture argument. + if line:find(pat) then + return func, line:match(pat) + end + end +end + +-- Run a command line +-- Returns true if the REPL should exit and the hook function factory +local function run_command(line) + -- GDB/LLDB exit on ctrl-d + if line == nil then + dbg.exit(1) + return true + end + + -- Re-execute the last command if you press return. + if line == "" then + line = last_cmd or "h" + end + + local command, command_arg = match_command(line) + if command then + last_cmd = line + -- unpack({...}) prevents tail call elimination so the stack frame indices are predictable. + return unpack { command(command_arg) } + elseif dbg.auto_eval then + return unpack { cmd_eval(line) } + else + dbg_writeln( + COLOR_RED + .. "Error:" + .. COLOR_RESET + .. " command '%s' not recognized.\nType 'h' and press return for a command list.", + line + ) + return false + end +end + +repl = function(reason) + -- Skip frames without source info. + while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do + stack_inspect_offset = stack_inspect_offset + 1 + end + + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3) + reason = reason and (COLOR_YELLOW .. "break via " .. COLOR_RED .. reason .. GREEN_CARET) or "" + dbg_writeln(reason .. format_stack_frame_info(info)) + + if tonumber(dbg.auto_where) then + where(info, dbg.auto_where) + end + + repeat + local success, done, hook = pcall(run_command, dbg.read(COLOR_RED .. "debugger.lua> " .. COLOR_RESET)) + if success then + debug.sethook(hook and hook(0), "crl") + else + local message = COLOR_RED .. "INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:" .. COLOR_RESET .. " " .. done + dbg_writeln(message) + error(message) + end + until done +end + +-- Make the debugger object callable like a function. +dbg = setmetatable({}, { + __call = function(_, condition, top_offset, source) + if condition then + return + end + + top_offset = (top_offset or 0) + stack_inspect_offset = top_offset + stack_top = top_offset + + debug.sethook(hook_next(1, source or "dbg()"), "crl") + return + end, +}) + +-- Expose the debugger's IO functions. +dbg.read = dbg_read +dbg.write = dbg_write +dbg.shorten_path = function(path) + return path +end +dbg.exit = function(err) + os.exit(err) +end + +dbg.writeln = dbg_writeln + +dbg.pretty_depth = 3 +dbg.pretty = pretty +dbg.pp = function(value, depth) + dbg_writeln(dbg.pretty(value, depth)) +end + +dbg.auto_where = false +dbg.auto_eval = false + +local lua_error, lua_assert = error, assert + +-- Works like error(), but invokes the debugger. +function dbg.error(err, level) + level = level or 1 + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. dbg.pretty(err)) + dbg(false, level, "dbg.error()") + + lua_error(err, level) +end + +-- Works like assert(), but invokes the debugger on a failure. +function dbg.assert(condition, message) + message = message or "assertion failed!" + if not condition then + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. message) + dbg(false, 1, "dbg.assert()") + end + + return lua_assert(condition, message) +end + +-- Works like pcall(), but invokes the debugger on an error. +function dbg.call(f, ...) + return xpcall(f, function(err) + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. dbg.pretty(err)) + dbg(false, 1, "dbg.call()") + + return err + end, ...) +end + +-- Error message handler that can be used with lua_pcall(). +function dbg.msgh(...) + if debug.getinfo(2) then + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. dbg.pretty(...)) + dbg(false, 1, "dbg.msgh()") + else + dbg_writeln( + COLOR_RED + .. "debugger.lua: " + .. COLOR_RESET + .. "Error did not occur in Lua code. Execution will continue after dbg_pcall()." + ) + end + + return ... +end + +-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them. +local stdin_isatty = true +local stdout_isatty = true + +-- Conditionally enable the LuaJIT FFI. +local ffi = (jit and require "ffi") +if ffi then + ffi.cdef [[ + int isatty(int); // Unix + int _isatty(int); // Windows + void free(void *ptr); + + char *readline(const char *); + int add_history(const char *); + ]] + + local function get_func_or_nil(sym) + local success, func = pcall(function() + return ffi.C[sym] + end) + return success and func or nil + end + + local isatty = get_func_or_nil "isatty" or get_func_or_nil "_isatty" or (ffi.load "ucrtbase")["_isatty"] + stdin_isatty = isatty(0) + stdout_isatty = isatty(1) +end + +-- Conditionally enable color support. +local color_maybe_supported = (stdout_isatty and os.getenv "TERM" and os.getenv "TERM" ~= "dumb") +if color_maybe_supported and not os.getenv "DBG_NOCOLOR" then + COLOR_GRAY = string.char(27) .. "[90m" + COLOR_RED = string.char(27) .. "[91m" + COLOR_BLUE = string.char(27) .. "[94m" + COLOR_YELLOW = string.char(27) .. "[33m" + COLOR_RESET = string.char(27) .. "[0m" + GREEN_CARET = string.char(27) .. "[92m => " .. COLOR_RESET +end + +if stdin_isatty and not os.getenv "DBG_NOREADLINE" then + pcall(function() + local linenoise = require "linenoise" + + -- Load command history from ~/.lua_history + local hist_path = os.getenv "HOME" .. "/.lua_history" + linenoise.historyload(hist_path) + linenoise.historysetmaxlen(50) + + local function autocomplete(env, input, matches) + for name, _ in pairs(env) do + if name:match("^" .. input .. ".*") then + linenoise.addcompletion(matches, name) + end + end + end + + -- Auto-completion for locals and globals + linenoise.setcompletion(function(matches, input) + -- First, check the locals and upvalues. + local env = local_bindings(1, true) + autocomplete(env, input, matches) + + -- Then, check the implicit environment. + env = getmetatable(env).__index + autocomplete(env, input, matches) + end) + + dbg.read = function(prompt) + local str = linenoise.linenoise(prompt) + if str and not str:match "^%s*$" then + linenoise.historyadd(str) + linenoise.historysave(hist_path) + end + return str + end + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Linenoise support enabled.") + end) + + -- Conditionally enable LuaJIT readline support. + pcall(function() + if dbg.read == dbg_read and ffi then + local readline = ffi.load "readline" + dbg.read = function(prompt) + local cstr = readline.readline(prompt) + if cstr ~= nil then + local str = ffi.string(cstr) + if string.match(str, "[^%s]+") then + readline.add_history(cstr) + end + + ffi.C.free(cstr) + return str + else + return nil + end + end + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Readline support enabled.") + end + end) +end + +-- Detect Lua version. +if jit then -- LuaJIT + LUA_JIT_SETLOCAL_WORKAROUND = -1 + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Loaded for " .. jit.version) +elseif "Lua 5.1" <= _VERSION and _VERSION <= "Lua 5.4" then + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Loaded for " .. _VERSION) +else + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Not tested against " .. _VERSION) + dbg_writeln "Please send me feedback!" +end + +return dbg diff --git a/.config/awesome/quarrel/delegate.lua b/.config/awesome/quarrel/delegate.lua index 54db786..3ea6d75 100644 --- a/.config/awesome/quarrel/delegate.lua +++ b/.config/awesome/quarrel/delegate.lua @@ -1,8 +1,10 @@ +local M = {} + --- Capture `fn`'s upvalues and pass to `delegate` ---@param delegate fun(env: table<string, any>, _: ...): ... ---@param fn function ---@return fun(...): ... -return function(delegate, fn) +function M.new(delegate, fn) local upvalues = {} for i = 1, debug.getinfo(fn, "u").nups do local name, value = debug.getupvalue(fn, i) @@ -12,3 +14,9 @@ return function(delegate, fn) return delegate(upvalues, ...) end end + +return setmetatable(M, { + __call = function(_, ...) + return M.new(...) + end, +}) diff --git a/.config/awesome/quarrel/fs.lua b/.config/awesome/quarrel/fs.lua index 502f189..fa3fc21 100644 --- a/.config/awesome/quarrel/fs.lua +++ b/.config/awesome/quarrel/fs.lua @@ -1,24 +1,35 @@ +local gdebug = require "gears.debug" local GFile = require("lgi").Gio.File ---@class QuarrelFs -local qfs = {} +local M = {} --- Read a file with the specified format (or "a") and close the file ---@param path string ----@param format openmode +---@param format? openmode ---@return any -function qfs.read(path, format) +function M.read(path, format) local f = assert(io.open(path, "r")) local c = f:read(format or "a") f:close() return c end +--- Write a file with the specified format (or "a") and close the file +---@param path string +---@param format? openmode +---@return any +function M.write(content, path, format) + local f = assert(io.open(path, format or "w+")) + f:write(content) + f:close() +end + --- List files in a directory ---@param path string ---@param absolute boolean? ---@return table -function qfs.ls_files(path, absolute) +function M.ls_files(path, absolute) local files = GFile.new_for_path(path):enumerate_children("standard::*", 0) local files_filtered = {} @@ -41,4 +52,4 @@ function qfs.ls_files(path, absolute) return files_filtered end -return qfs +return M diff --git a/.config/awesome/quarrel/iconset.lua b/.config/awesome/quarrel/iconset.lua new file mode 100644 index 0000000..43b7104 --- /dev/null +++ b/.config/awesome/quarrel/iconset.lua @@ -0,0 +1,21 @@ +local gstring = require "gears.string" + +local M = {} + +--- Generate a shim to allow indexing an iconset as a Lua table +---@param path string +---@return table +function M.new(path) + return setmetatable({}, { + ---@param icon string + __index = function(self, icon) + return path .. (gstring.endswith(path, "/") and "" or "/") .. icon:gsub("_", "-") .. ".svg" + end, + }) +end + +return setmetatable(M, { + __call = function(_, ...) + return M.new(...) + end, +}) diff --git a/.config/awesome/quarrel/init.lua b/.config/awesome/quarrel/init.lua index 96f7f5b..5617c1a 100644 --- a/.config/awesome/quarrel/init.lua +++ b/.config/awesome/quarrel/init.lua @@ -9,14 +9,4 @@ function quarrel.debug(message) n { message = tostring(message) } end ---- Check if there was a restart ----@return boolean -function quarrel.is_restart() - awesome.register_xproperty("is_restart", "boolean") - local restart_detected = awesome.get_xproperty "is_restart" ~= nil - awesome.set_xproperty("is_restart", true) - - return restart_detected -end - return quarrel diff --git a/.config/awesome/quarrel/lua-rust.tar.gz b/.config/awesome/quarrel/lua-rust.tar.gz Binary files differdeleted file mode 100644 index 5ff33fd..0000000 --- a/.config/awesome/quarrel/lua-rust.tar.gz +++ /dev/null diff --git a/.config/awesome/quarrel/markup.lua b/.config/awesome/quarrel/markup.lua index d206530..f89368c 100644 --- a/.config/awesome/quarrel/markup.lua +++ b/.config/awesome/quarrel/markup.lua @@ -1,8 +1,10 @@ +local M = {} + --- Apply markup to a file ---@param content string ---@param args { bold: boolean, italic: boolean, fg: string, bg: string } ---@return string -return function(content, args) +function M.convert(content, args) args = args or {} if args.bold then content = "<b>" .. content .. "</b>" @@ -28,3 +30,9 @@ return function(content, args) return content end + +return setmetatable(M, { + __call = function(_, ...) + return M.convert(...) + end, +}) diff --git a/.config/awesome/quarrel/math/consts.lua b/.config/awesome/quarrel/math/consts.lua new file mode 100644 index 0000000..f18a093 --- /dev/null +++ b/.config/awesome/quarrel/math/consts.lua @@ -0,0 +1,5 @@ +local C = {} + +C.EPSILON = 2 ^ -52 + +return C diff --git a/.config/awesome/quarrel/math.lua b/.config/awesome/quarrel/math/init.lua index b16547b..d437654 100644 --- a/.config/awesome/quarrel/math.lua +++ b/.config/awesome/quarrel/math/init.lua @@ -1,11 +1,9 @@ +local M = require "quarrel.math.consts" local gmath = require "gears.math" ----@class QuarrelMath -local qmath = {} - -- TODO: Finish documenting these functions -function qmath.step_value(value, steps) +function M.step_value(value, steps) if value > steps[#steps - 1][1] then return steps[#steps - 1][2] end @@ -16,16 +14,16 @@ function qmath.step_value(value, steps) end end -function qmath.translate_range(value, in_min, in_max, out_min, out_max) +function M.translate_range(value, in_min, in_max, out_min, out_max) return out_min + ((out_max - out_min) / (in_max - in_min)) * (value - in_min) end -function qmath.percentage(value, max) +function M.percentage(value, max) return gmath.round(value / max * 100) end -function qmath.clamp(value, min, max) +function M.clamp(value, min, max) return math.max(math.min(value, max), min) end -return qmath +return M diff --git a/.config/awesome/quarrel/native/Cargo.toml b/.config/awesome/quarrel/native/Cargo.toml index 8d56c9f..2551507 100644 --- a/.config/awesome/quarrel/native/Cargo.toml +++ b/.config/awesome/quarrel/native/Cargo.toml @@ -8,22 +8,21 @@ edition = "2021" [dependencies] freedesktop_entry_parser = "1.3.0" cpc = "1.9.1" -mlua = { version = "0.8.7", features = [ "module", "lua54", "serialize" ] } +mlua = { version = "0.10.3", features = [ "module", "lua54", "serialize" ] } rayon = "1.6.1" serde = { version = "1.0.152", features = [ "derive" ] } url = "2.3.1" -rodio = "0.17.1" -nix = "0.26.2" +rodio = "0.20.1" +nix = { version = "0.29.0", features = [ "ioctl", "socket" ] } chrono = "0.4.24" -itertools = "0.10.5" +itertools = "0.14.0" html-escape = "0.2.13" -mpd = { git = "https://github.com/kstep/rust-mpd", features = [ "serde" ], version = "0.1.0" } -cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core.git", version = "0.18.0" } -gdk-pixbuf = { git = "https://github.com/gtk-rs/gtk-rs-core.git", version = "0.18.0" } -symphonia = "0.5.3" -dirs = "5.0.1" +dirs = "6.0.0" once_cell = "1.18.0" - +ureq = "3.0.9" +anyhow = { version = "1.0.97", features = ["backtrace"] } +mdrop = { git = "https://github.com/frahz/mdrop" } +sysinfo = "0.35.0" [lib] crate-type = ["cdylib"] diff --git a/.config/awesome/quarrel/native/init.lua b/.config/awesome/quarrel/native/init.lua index e5d5aab..6a823ba 100644 --- a/.config/awesome/quarrel/native/init.lua +++ b/.config/awesome/quarrel/native/init.lua @@ -26,16 +26,12 @@ package.cpath = package.cpath .. ";" .. cfg .. "quarrel/native/lib?.so" ---@field decode_html fun(input: string): string ---@field open_file fun(path: string): FileHandle ----@class Mpd ----@field init - ---@class Net ---@field get_essid fun(): string ---@class QuarrelNative ---@field lenses Lenses ---@field util Util ----@field mpd Mpd ---@field net Net local qnative = require "qnative" diff --git a/.config/awesome/quarrel/native/src/http.rs b/.config/awesome/quarrel/native/src/http.rs new file mode 100644 index 0000000..e995556 --- /dev/null +++ b/.config/awesome/quarrel/native/src/http.rs @@ -0,0 +1,22 @@ +use ureq::{Agent, agent}; +use std::{sync::LazyLock, thread}; +use mlua::prelude::*; + +static AGENT: LazyLock<Agent> = LazyLock::new(|| agent()); + +struct Stream(ureq::Body); + +pub fn get(_: &Lua, url: String, callback: LuaFunction, err_callback: LuaFunction) -> LuaResult<()> { + thread::spawn(|| { + match AGENT.get(url).call() { + Ok(body) => { + + }, + Err(err) => { + err_callback.call(err.to_string()); + } + } + // callback.call::<>() + }); + Ok(()) +} diff --git a/.config/awesome/quarrel/native/src/lenses/application.rs b/.config/awesome/quarrel/native/src/lenses/application.rs index 72aba8d..89b7bb4 100644 --- a/.config/awesome/quarrel/native/src/lenses/application.rs +++ b/.config/awesome/quarrel/native/src/lenses/application.rs @@ -1,6 +1,5 @@ use std::{ - fs::read_dir, - path::PathBuf, + fs::read_dir, path::PathBuf }; use freedesktop_entry_parser as fd; @@ -8,12 +7,54 @@ use mlua::prelude::*; use rayon::prelude::*; use url::Url; -use crate::lenses::entry::{ - entries_to_lua_table, +use crate::lenses::{ Entry, + Lense, + Cache }; -fn parse_entry(entry: &fd::Entry, path: &PathBuf) -> Result<Entry, ()> { +#[derive(Default)] +pub struct Application(pub Cache); + +impl Lense for Application { + const NAME: &str = "Application"; + + fn get_cache(&self) -> &Cache { + &self.0 + } + + fn set_cache(&mut self, cache: Cache) { + self.0 = cache; + } + + fn query(_: &Lua, input: String) -> Result<Vec<Entry>, anyhow::Error> { + let applications_dir = "/usr/share/applications"; + let entries = read_dir(applications_dir)? + .map(|result| result.map(|e| e.path())) + .collect::<Result<Vec<_>, std::io::Error>>()?; + + let parsed_entries: Vec<Entry> = entries + .into_par_iter() + .filter(|path| path.extension().is_some_and(|ext| ext == "desktop")) + .filter_map(|path| { + parse_entry(path).ok().flatten() + }) + .collect(); + + Ok( + parsed_entries + .into_iter() + .filter(|entry| entry.message.to_lowercase().contains(&input.to_lowercase())) + .collect(), + ) + } +} + +fn parse_entry(path: PathBuf) -> Result<Option<Entry>, ()> { + let Ok(entry) = fd::parse_entry(&path) else { + return Err(()) + }; + let section = entry.section("Desktop Entry"); let name = section.attr("Name").ok_or(())?.to_string(); @@ -38,14 +79,14 @@ fn parse_entry(entry: &fd::Entry, path: &PathBuf) -> Result<Entry, ()> { } } 'c' => new_exec.replace_range(index..index + 2, &name), - 'k' => new_exec.replace_range(index..index + 2, Url::from_file_path(path)?.as_str()), + 'k' => new_exec.replace_range(index..index + 2, Url::from_file_path(&path)?.as_str()), 'f' | 'u' | 'v' | 'm' | 'd' | 'n' => new_exec.replace_range(index..index + 2, ""), _ => continue, } } - Ok(Entry { + Ok(Some(Entry { message: name, exec: Some(( new_exec, @@ -55,33 +96,5 @@ fn parse_entry(entry: &fd::Entry, path: &PathBuf) -> Result<Entry, ()> { .parse() .map_err(drop)?, )), - provider: "Application".to_string(), - }) -} - -pub fn query(lua: &Lua, input: String) -> LuaResult<LuaTable> { - let applications_dir = "/usr/share/applications"; - let entries = read_dir(applications_dir)? - .map(|result| result.map(|e| e.path())) - .collect::<Result<Vec<_>, std::io::Error>>()?; - - let parsed_entries: Vec<Entry> = entries - .into_par_iter() - .filter(|path| matches!(path.extension(), Some(ext) if ext == "desktop")) - .filter_map(|path| { - let Ok(entry) = fd::parse_entry(&path) else { - return None - }; - - parse_entry(&entry, &path).ok() - }) - .collect(); - - Ok(entries_to_lua_table( - parsed_entries - .into_iter() - .filter(|entry| entry.message.to_lowercase().contains(&input.to_lowercase())) - .collect(), - lua, - )) + })) } diff --git a/.config/awesome/quarrel/native/src/lenses/calculator.rs b/.config/awesome/quarrel/native/src/lenses/calculator.rs index 07f1ee2..640bdeb 100644 --- a/.config/awesome/quarrel/native/src/lenses/calculator.rs +++ b/.config/awesome/quarrel/native/src/lenses/calculator.rs @@ -5,25 +5,36 @@ use cpc::{ }; use mlua::prelude::*; -use crate::lenses::entry::{ - entries_to_lua_table, +use crate::lenses::{ Entry, + Cache, + Lense }; -pub fn query(lua: &Lua, input: String) -> LuaResult<LuaTable> { - let result = match eval(input.trim(), true, Unit::Celsius, false) { - Ok(result) => { - format!("{result}") - } - Err(_) => return lua.create_table(), - }; - - Ok(entries_to_lua_table( - vec![Entry { - message: result, - exec: None, - provider: "Calculator".to_string(), - }], - lua, - )) +pub struct Calculator; + +impl Lense for Calculator { + const NAME: &str = "Calculator"; + + fn get_cache(&self) -> &Cache { + &Cache::Stale + } + + fn set_cache(&mut self, _: Cache) {} + + fn query(_: &Lua, input: String) -> Result<Vec<Entry>, anyhow::Error> { + let result = match eval(input.trim(), true, Unit::Celsius, false) { + Ok(result) => { + format!("{result}") + } + Err(err) => { return Err(anyhow::anyhow!(err)); }, + }; + + Ok(vec![Entry { + message: result, + exec: None, + }; 1] + ) + } } + diff --git a/.config/awesome/quarrel/native/src/lenses/entry.rs b/.config/awesome/quarrel/native/src/lenses/entry.rs deleted file mode 100644 index c7ac09e..0000000 --- a/.config/awesome/quarrel/native/src/lenses/entry.rs +++ /dev/null @@ -1,19 +0,0 @@ -use mlua::{ - prelude::*, - LuaSerdeExt, -}; -use serde::Serialize; - -#[derive(Serialize, Clone)] -pub struct Entry { - pub message: String, - pub exec: Option<(String, bool)>, - pub provider: String, -} - -pub fn entries_to_lua_table(entries: Vec<Entry>, lua: &Lua) -> LuaTable { - match lua.to_value(&entries).unwrap() { - LuaValue::Table(t) => t, - _ => unreachable!(), - } -} diff --git a/.config/awesome/quarrel/native/src/lenses/mod.rs b/.config/awesome/quarrel/native/src/lenses/mod.rs index d0db6f7..6eb5b58 100644 --- a/.config/awesome/quarrel/native/src/lenses/mod.rs +++ b/.config/awesome/quarrel/native/src/lenses/mod.rs @@ -1,3 +1,79 @@ pub mod application; pub mod calculator; -pub mod entry; + +use mlua::{ + prelude::*, + LuaSerdeExt, +}; +use serde::{Serialize, Deserialize}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Entry { + pub message: String, + pub exec: Option<(String, bool)>, +} + +impl IntoLua for Entry { + fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> { + return lua.to_value(&self) + } +} + +impl FromLua for Entry { + fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> { + return lua.from_value(value); + } +} + +#[derive(Default)] +pub enum Cache { + Valid(Vec<Entry>), + #[default] + Stale +} + +pub struct _Lense<T: Lense>(pub T); + +pub trait Lense { + const NAME: &'static str; + + fn set_cache(&mut self, cache: Cache); + fn get_cache(&self) -> &Cache; + + fn query(lua: &Lua, input: String) -> Result<Vec<Entry>, anyhow::Error>; +} + +impl<T: Lense + 'static> LuaUserData for _Lense<T> { + fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) { + // fields.add_field_method_get("cache", |lua, this| { + // let cache = this.0.get_cache(); + // match cache { + // Cache::Valid(cache) => Ok(cache.clone().into_lua(lua)?), + // Cache::Stale => Ok(LuaNil) + // } + // }); + + // fields.add_field_method_set("cache", |_, this, cache: Vec<Entry>| { + // Ok(this.0.set_cache(Cache::Valid(cache))) + // }); + + fields.add_field_method_get("stale", |_, this| Ok(matches!(this.0.get_cache(), Cache::Stale))); + + fields.add_field("name", T::NAME); + } + + fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { + methods.add_method_mut("query", |lua, this, input: String| { + return Ok(match this.0.get_cache() { + Cache::Valid(entries) => entries.clone(), + Cache::Stale => { + let entries = T::query(lua, input).map_err(LuaError::external)?; + this.0.set_cache(Cache::Valid(entries.clone())); + entries + } + }); + }); + + methods.add_method_mut("mark_stale", |_, this, _: ()| Ok(this.0.set_cache(Cache::Stale)) ); + } +} diff --git a/.config/awesome/quarrel/native/src/lib.rs b/.config/awesome/quarrel/native/src/lib.rs index 472313e..4f1780d 100644 --- a/.config/awesome/quarrel/native/src/lib.rs +++ b/.config/awesome/quarrel/native/src/lib.rs @@ -1,29 +1,29 @@ mod lenses; -mod mpd; mod net; +// mod http; mod util; +mod moondrop; use mlua::prelude::*; +use lenses::{ _Lense }; #[mlua::lua_module] fn qnative(lua: &Lua) -> LuaResult<LuaTable> { let lenses = lua.create_table()?; - lenses.set("1", lua.create_function(lenses::calculator::query)?)?; - lenses.set("2", lua.create_function(lenses::application::query)?)?; + lenses.push(_Lense(lenses::calculator::Calculator))?; + lenses.push(_Lense(lenses::application::Application::default()))?; + // lenses.set("1", lua.create_function(lenses::calculator::Application::query)?)?; + // lenses.set("2", lua.create_function(lenses::application::Application::query)?)?; let util = lua.create_table()?; util.set("decode_html", lua.create_function(util::decode_html)?)?; util.set("open_file", lua.create_function(util::FileHandle::new)?)?; - let mpd = lua.create_table()?; - mpd.set("init", lua.create_function(mpd::init)?)?; - let net = lua.create_table()?; net.set("get_essid", lua.create_function(net::get_first_essid)?)?; let exports = lua.create_table()?; exports.set("lenses", lenses)?; - exports.set("mpd", mpd)?; exports.set("net", net)?; exports.set("util", util)?; diff --git a/.config/awesome/quarrel/native/src/moondrop.rs b/.config/awesome/quarrel/native/src/moondrop.rs new file mode 100644 index 0000000..d416ef6 --- /dev/null +++ b/.config/awesome/quarrel/native/src/moondrop.rs @@ -0,0 +1,3 @@ +use mdrop::Moondrop; + + diff --git a/.config/awesome/quarrel/native/src/mpd.rs b/.config/awesome/quarrel/native/src/mpd.rs deleted file mode 100644 index 08c9e42..0000000 --- a/.config/awesome/quarrel/native/src/mpd.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::{ - ffi::c_void, - fs::File, - net::TcpStream, - path::PathBuf, - sync::mpsc::channel, -}; - -use dirs::home_dir; -use gdk_pixbuf::{ - ffi::GdkPixbuf, - glib::translate::IntoGlibPtr, - traits::PixbufLoaderExt, - Pixbuf, - PixbufLoader, -}; -use mlua::{ - prelude::*, - LuaSerdeExt, -}; -use mpd::Client; -use once_cell::sync::Lazy; -use symphonia::{ - core::{ - formats::FormatOptions, - io::{ - MediaSourceStream, - MediaSourceStreamOptions, - }, - meta::MetadataOptions, - probe::Hint, - }, - default::get_probe, -}; - -static MPD_MUSIC_PATH: Lazy<PathBuf> = Lazy::new(|| { - [ - home_dir().expect("home directory should be set"), - "Music".into(), - ] - .iter() - .collect() -}); - -static COVER_FORMATS: [&str; 2] = ["png", "jpg"]; - -pub struct Connection(Client<TcpStream>); - -impl LuaUserData for Connection { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method_mut("status", |lua, this: &mut Connection, (): ()| { - lua.to_value(&this.0.status().map_err(LuaError::external)?) - }); - - methods.add_method_mut("song", |lua, this: &mut Connection, (): ()| { - Ok( - if let Some(song) = this.0.currentsong().map_err(LuaError::external)? { - lua.to_value(&song)? - } else { - LuaNil - }, - ) - }); - - methods.add_method( - "get_cover_pixbuf", - |_, _: &Connection, file: String| { - let song_path = MPD_MUSIC_PATH.join(file); - let mut has_external_cover = false; - let mut cover_path = PathBuf::new(); - - for format in COVER_FORMATS { - let cover = song_path - .parent() - .unwrap() - .to_owned() - .join(format!("cover.{}", format)); - if cover.exists() { - has_external_cover = cover.exists(); - cover_path = cover; - break; - } - } - - let mss = MediaSourceStream::new( - Box::new(File::open(song_path)?), - MediaSourceStreamOptions::default(), - ); - - let mut probed = get_probe() - .format( - &Hint::default(), - mss, - &FormatOptions::default(), - &MetadataOptions::default(), - ) - .map_err(LuaError::external)?; - - let visuals; - - if let Some(metadata) = probed.format.metadata().skip_to_latest() { - visuals = metadata.visuals(); - if visuals.is_empty() && has_external_cover { - let pixbuf = Pixbuf::from_file(cover_path).map_err(LuaError::external)?; - - return Ok(( - Some(LuaLightUserData(unsafe { - <Pixbuf as IntoGlibPtr<*mut GdkPixbuf>>::into_glib_ptr(pixbuf) - .cast::<c_void>() - })), - Some(true), - )); - } - - let loader = PixbufLoader::new(); - loader - .write(visuals.first().unwrap().data.as_ref()) - .map_err(LuaError::external)?; - loader.close().map_err(LuaError::external)?; - - return Ok(( - Some(LuaLightUserData(unsafe { - <Pixbuf as IntoGlibPtr<*mut GdkPixbuf>>::into_glib_ptr( - loader.pixbuf().expect("Pixbuf should be initialized"), - ) - .cast::<c_void>() - })), - Some(false), - )); - } - - Ok((None, None)) - }, - ); - } -} - -pub fn init(_: &Lua, _: ()) -> LuaResult<Connection> { - Ok(Connection( - Client::connect("localhost:6600").map_err(LuaError::external)?, - )) -} diff --git a/.config/awesome/quarrel/native/src/net/mod.rs b/.config/awesome/quarrel/native/src/net/mod.rs index 96c853e..dbd87b9 100644 --- a/.config/awesome/quarrel/native/src/net/mod.rs +++ b/.config/awesome/quarrel/native/src/net/mod.rs @@ -9,7 +9,7 @@ use std::{ c_void, }, mem::size_of, - os::fd::RawFd, + os::fd::{IntoRawFd, RawFd}, }; use mlua::prelude::*; @@ -108,7 +108,7 @@ fn get_first_socket() -> LuaResult<RawFd> { for family in families { if let Ok(socket) = open_socket(family, SockType::Datagram, SockFlag::empty(), None) { - return Ok(socket); + return Ok(socket.into_raw_fd()); } } diff --git a/.config/awesome/quarrel/native/src/util.rs b/.config/awesome/quarrel/native/src/util.rs index 85a2574..41096d8 100644 --- a/.config/awesome/quarrel/native/src/util.rs +++ b/.config/awesome/quarrel/native/src/util.rs @@ -1,7 +1,6 @@ use std::{ cell::RefCell, fs::File, - fs::OpenOptions, io::{ Read, Seek, @@ -49,7 +48,7 @@ enum StringOrNumber { pub struct FileHandle(Rc<RefCell<File>>); impl LuaUserData for FileHandle { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + fn add_methods<'lua, M: LuaUserDataMethods<Self>>(methods: &mut M) { methods.add_method("read", |lua, this: &FileHandle, mode: String| { let content = this.read()?; Ok(lua.to_value(&match ReadMode::from_str(&mode).unwrap() { diff --git a/.config/awesome/quarrel/persistent.lua b/.config/awesome/quarrel/persistent.lua new file mode 100644 index 0000000..2b15c89 --- /dev/null +++ b/.config/awesome/quarrel/persistent.lua @@ -0,0 +1,62 @@ +local gdebug = require "gears.debug" +local qfs = require "quarrel.fs" +local qjson = require "quarrel.json" + +awesome.register_xproperty("is_restart", "boolean") + +---@alias QPersistentValue string|number|table|nil|boolean + +---@class QuarrelPersistent +local qpersistent = {} + +---@type table<string, QPersistentValue> +local inner = {} + +--- Store a key-value pair in a persistent store +---@param key string +---@param value QPersistentValue +function qpersistent.store(key, value) + inner[key] = value +end + +--- Get a key-value pair in a persistent store +---@param key string +---@return QPersistentValue +function qpersistent.get(key) + return inner[key] +end + +--- Check if there was a restart +---@return boolean +function qpersistent.is_restart() + return awesome.get_xproperty "is_restart" ~= nil +end + +do + local ok, content = pcall(qfs.read, "/tmp/qpersistent-store") + + if not ok and not qpersistent.is_restart() then + gdebug.print_error "failed to read persistent store" + inner = {} + else + inner = qjson.decode(content) + end +end + +awesome.connect_signal("exit", function(restart) + if not restart then + return + end -- we probably errored + + local ok = pcall(qfs.write, qjson.encode(inner), "/tmp/qpersistent-store") + if not ok then + gdebug.print_error "failed to open persistent store" + return + end + + if not qpersistent.is_restart() then + awesome.set_xproperty("is_restart", true) + end +end) + +return qpersistent diff --git a/.config/awesome/quarrel/store.lua b/.config/awesome/quarrel/store.lua deleted file mode 100644 index 9422c21..0000000 --- a/.config/awesome/quarrel/store.lua +++ /dev/null @@ -1,4 +0,0 @@ ----@type table -local qstore = {} - -return qstore diff --git a/.config/awesome/quarrel/ui.lua b/.config/awesome/quarrel/ui.lua deleted file mode 100644 index 1db4a70..0000000 --- a/.config/awesome/quarrel/ui.lua +++ /dev/null @@ -1,145 +0,0 @@ -local awful = require "awful" -local gtable = require "gears.table" -local qbind = require "quarrel.bind" -local qvars = require "quarrel.vars" -local rtimed = require "lib.rubato.timed" -local wibox = require "wibox" - ----@class QuarrelUi -local qui = {} - ---- Return qvars.text_font with size scaled by factor ----@param factor number ----@return string ----@see QuarrelVars.text_font -function qui.font(factor) - return qvars.text_font .. " " .. qvars.font_size * (factor or 1) -end - ---- Inject background widget styling into target ----@param target table ----@return table -function qui.styled(target) - return gtable.crush({ - bg = qvars.colors.bg, - border_color = qvars.colors.bright.black, - border_width = qvars.border_width, - shape = qvars.shape, - }, target) -end - ---- Generate a styled popup ----@param target table ----@return table -function qui.popup(target) - target.widget = { - widget = wibox.container.margin, - margins = qvars.big_padding, - target.widget, - } - - return awful.popup(qui.styled(target)) -end - ---- Generate svg recolor string ----@param color string ----@return string -function qui.recolor(color) - return "svg{fill:" .. color .. "}" -end - ---- Generate icon widget ----@param args table ----@return table -function qui.icon(args) - return gtable.crush({ - widget = wibox.widget.imagebox, - image = args.icon, - forced_width = qvars.char_height, - forced_height = qvars.char_height, - stylesheet = qui.recolor(args.color or qvars.colors.fg), - }, args.widget or {}) -end - ---- Generate button widget ----@param args table ----@return table -function qui.button(args) - args.press = args.press or function(_) end - local widget = wibox.widget(gtable.crush({ - widget = wibox.widget.imagebox, - image = args.image, - forced_height = qvars.char_height, - forced_width = qvars.char_height, - stylesheet = qui.recolor(qvars.colors.fg), - press = args.press, - }, args.widget or {})) - - widget.buttons = { - qbind:new { - triggers = qvars.btns.left, - press = function() - widget:press() - end, - hidden = true, - }, - } - - return widget -end - ---- Generate toggle widget ----@param args table ----@return table -function qui.toggle(args) - args.press = args.press or function(_) end - local widget = qui.button { - widget = gtable.crush({ - toggled = false, - silent_press = function(self, state) - if state then - self.toggled = state - else - self.toggled = not self.toggled - end - - if self.toggled then - self.image = args.on - else - self.image = args.off - end - end, - }, args.widget or {}), - image = args.off, - press = function(self) - if not args.manual then - self:silent_press() - end - args.press(self) - end, - } - - return widget -end - ----@param widget wibox.widget.base ----@param cursor string -function qui.hoverable(widget, cursor) - local hovering = false - - widget:connect_signal("mouse::enter", function() - local w = mouse.current_wibox - if w then - w.cursor = cursor - end - end) - - widget:connect_signal("mouse::leave", function() - local w = mouse.current_wibox - if w then - w.cursor = "left_ptr" - end - end) -end - -return qui diff --git a/.config/awesome/quarrel/ui/consts.lua b/.config/awesome/quarrel/ui/consts.lua new file mode 100644 index 0000000..e6e12d0 --- /dev/null +++ b/.config/awesome/quarrel/ui/consts.lua @@ -0,0 +1,24 @@ +local awful = require "awful" +local qconst = require "quarrel.const" +local wibox = require "wibox" + +local C = {} + +C.BORDER_WIDTH = 1 +C.BORDER_RADIUS = 4 +C.PADDING = 4 +C.BIG_PADDING = 8 +C.TEXT_FONT = "Iosevka Comfy Regular" +C.FONT_SIZE = 9 + +local char_width, char_height = wibox + .widget({ + widget = wibox.widget.textbox, + text = "a", + }) + :get_preferred_size_at_dpi(awful.screen.focused().dpi) + +C.CHAR_HEIGHT = char_height +C.CHAR_WIDTH = char_width + +return C diff --git a/.config/awesome/quarrel/ui/init.lua b/.config/awesome/quarrel/ui/init.lua new file mode 100644 index 0000000..5acaa51 --- /dev/null +++ b/.config/awesome/quarrel/ui/init.lua @@ -0,0 +1,167 @@ +local M = require "quarrel.ui.consts" +local awful = require "awful" +local gshape = require "gears.shape" +local gtable = require "gears.table" +local qbind = require "quarrel.bind" +local qcolor = require "quarrel.color" +local wibox = require "wibox" + +--- Clip Cairo context +---@param cr cairo_surface Cairo surface +---@param w integer Widget width +---@param h integer Widget height +---@return nil +function M.shape(cr, w, h) + -- gears.shape.rounded_rect(cr, w, h, dpi(4)) + gshape.rounded_rect(cr, w, h, M.BORDER_RADIUS) + -- gshape.rectangle(cr, w, h) +end + +--- Returns qvars.text_font with size scaled by factor +---@param factor number? +---@return string +function M.font(factor) + return M.TEXT_FONT .. " " .. M.FONT_SIZE * (factor or 1) +end + +--- Injects background widget styling into target +---@param target table +---@return table +function M.styled(target) + return gtable.crush({ + bg = qcolor.palette.bg(), + border_color = qcolor.palette.border(), + border_width = M.BORDER_WIDTH, + shape = M.shape, + }, target) +end + +--- Generates a styled popup +---@param target table +---@return table +function M.popup(target, debug) + target.widget = { + widget = wibox.container.margin, + margins = M.BIG_PADDING, + target.widget, + } + + return awful.popup(M.styled(target)) +end + +--- Generates svg recolor string +---@param color string +---@return string +function M.recolor(color) + return "svg{fill:" .. color .. "}" +end + +--- Generates icon widget +---@param args table +---@return table +function M.icon(args) + return wibox.widget(gtable.crush({ + widget = wibox.widget.imagebox, + image = args.icon, + forced_width = (not args.unsized) and M.CHAR_HEIGHT, + forced_height = (not args.unsized) and M.CHAR_HEIGHT, + stylesheet = M.recolor(args.color or qcolor.palette.fg()), + }, args.widget or {})) +end + +--- Generates button widget +---@param args table +---@return table +function M.button(args) + args.press = args.press or function(_) end + local widget = wibox.widget(gtable.crush({ + widget = wibox.widget.imagebox, + image = args.icon, + forced_height = M.CHAR_HEIGHT, + forced_width = M.CHAR_HEIGHT, + stylesheet = M.recolor(qcolor.palette.fg()), + press = args.press, + }, args.widget or {})) + + widget.buttons = { + qbind { + triggers = qbind.btns.left, + press = function() + widget:press() + end, + hidden = true, + }, + } + + return widget +end + +--- Generates toggle widget +---@param args table +---@return table +function M.toggle(args) + args.press = args.press or function(_) end + local widget = M.button { + widget = gtable.crush({ + toggled = false, + silent_press = function(self, state) + if state then + self.toggled = state + else + self.toggled = not self.toggled + end + + if self.toggled then + self.image = args.on + else + self.image = args.off + end + end, + }, args.widget or {}), + icon = args.off, + press = function(self) + if not args.manual then + self:silent_press() + end + args.press(self) + end, + } + + return widget +end + +---@param widget wibox.widget.base +---@param cursor string +---@return wibox.widget.base +function M.hoverable(widget, cursor) + local last_wibox + + widget:connect_signal("mouse::enter", function() + local w = mouse.current_wibox + last_wibox = w + if w then + w.cursor = cursor + end + + if type(widget.hover) == "function" then -- on enter bind + widget.hover() + elseif type(widget.hover) == "table" and widget.hover.enter then -- { enter: fun(), leave: fun() } + widget.hover.enter() + end + end) + + widget:connect_signal("mouse::leave", function() + local w = last_wibox or mouse.current_wibox + if w then + w.cursor = "left_ptr" + end + + if type(widget.hover) == "table" and widget.hover.leave then -- { enter: fun(), leave: fun() } + widget.hover.leave() + end + end) + + return widget +end + +return M diff --git a/.config/awesome/quarrel/vars.lua b/.config/awesome/quarrel/vars.lua index 1983343..5c3b178 100644 --- a/.config/awesome/quarrel/vars.lua +++ b/.config/awesome/quarrel/vars.lua @@ -1,37 +1,38 @@ local awful = require "awful" local btns = awful.button.names -local gears = require "gears" -local xresources = require "beautiful.xresources" -local x_col = xresources.get_current_theme() -local dpi = xresources.apply_dpi +local gshape = require "gears.shape" local wibox = require "wibox" +-- local qconts = require "quarrel.consts" +local qbezier = require "quarrel.animation.bezier" ----@class QuarrelVars local qvars = {} -qvars.anim_duration = 0.20 -qvars.anim_intro = qvars.anim_duration / 4 - -qvars.notif_timeout = 3 +-- ---@param protected (fun(): table) | table +-- function qconsts.protect(protected) +-- ---@type table +-- local tbl +-- if type(protected) == "table" then +-- tbl = protected +-- elseif type(protected) == "function" then +-- tbl = protected() +-- else +-- error("expected a table or a function that returns a table, got " .. type(protected), 2) +-- end +-- return setmetatable({}, { +-- __index = tbl, +-- __newindex = function(t, k, v) +-- error("attempted to change constant " .. tostring(k) .. " to " .. tostring(v), 2) +-- end +-- }) +-- end + +-- qui.BORDER_WIDTH = 1 +-- qui.BORDER_RADIUS = 4 + +-- qui.PADDING = 4 +-- qui.BIG_PADDING = 8 ---- Clip Cairo context ----@param cr cairo_surface Cairo surface ----@param w integer Widget width ----@param h integer Widget height ----@return nil -function qvars.shape(cr, w, h) - gears.shape.rounded_rect(cr, w, h, dpi(4)) -end - -qvars.border_width = dpi(1.5) - -qvars.padding = dpi(4) -qvars.big_padding = dpi(8) - --- qvars.text_font = "Fira Code Nerd Font Mono Medium" --- qvars.text_font = "Iosevka Comfy SemiBold" qvars.text_font = "Iosevka Comfy Regular" --- qvars.font_size = 8 qvars.font_size = 9 qvars.font = qvars.text_font .. " " .. qvars.font_size @@ -42,63 +43,152 @@ local char_width, char_height = wibox }) :get_preferred_size_at_dpi(awful.screen.focused().dpi) -qvars.char_height = char_height -qvars.char_width = char_width - -qvars.bar_size = dpi(24) + qvars.big_padding * 2 -qvars.element_size = dpi(12) -qvars.expanded_bar_size = qvars.big_padding + (qvars.big_padding * 2 + qvars.element_size * 4) * 3 + qvars.padding * 2 - -qvars.colors = { - fg = x_col.foreground, - bg = x_col.background, - - black = x_col.color0, - red = x_col.color1, - green = x_col.color2, - yellow = x_col.color3, - blue = x_col.color4, - pink = x_col.color5, - cyan = x_col.color6, - white = x_col.color7, - - bright = { - black = x_col.color8, - red = x_col.color9, - green = x_col.color10, - yellow = x_col.color11, - blue = x_col.color12, - pink = x_col.color13, - cyan = x_col.color14, - white = x_col.color15, - }, - - dim = { - fg = "#77828c", - bg = "#161b22", - }, - - transparent = "#00000000", -} +-- qui.CHAR_HEIGHT = char_height +-- qui.CHAR_WIDTH = char_width + +--- Clip Cairo context +---@param cr cairo_surface Cairo surface +---@param w integer Widget width +---@param h integer Widget height +---@return nil +-- function qui.shape(cr, w, h) +-- -- gears.shape.rounded_rect(cr, w, h, dpi(4)) +-- gshape.rounded_rect(cr, w, h, qui.BORDER_RADIUS) +-- end + +qvars.anim_duration = 0.30 +qvars.anim_intro = qvars.anim_duration / 4 + +local easing_bezier = qbezier(0.2, 0, 0, 1) +function qvars.easing(t, b, c, d) + local epsilon = 1000 / d + return c * easing_bezier:solve(t / d, epsilon) + b +end + +qvars.notif_timeout = 3 + +-- qvars.bar_size = dpi(24) + qui.BIG_PADDING * 2 +-- qvars.element_size = dpi(12) +-- qvars.bar_size = 24 + qui.BIG_PADDING * 2 +-- qvars.element_size = 12 +-- qvars.expanded_bar_size = qui.BIG_PADDING + (qui.BIG_PADDING * 2 + qvars.element_size * 4) * 4 + qui.PADDING * 2 + +-- local function palette(colors) +-- return setmetatable(colors, { +-- __call = function(self) +-- return self[1] +-- end, +-- }) +-- end + +-- local colors = require "quarrel.color" +-- qcolor.palette = colors.palette +-- qcolor.palette = { +-- fg = palette { +-- low = "#a8afb7", +-- "#c8ced7", +-- high = "#e8eff8", +-- }, +-- +-- bg = palette { +-- lowest = "#070c11", +-- low = "#0c1116", +-- "#12161c", +-- high = "#171c22", +-- highest = "#1d2228", +-- }, +-- +-- border = palette { +-- "#323b46", +-- variant = "#262f39", +-- }, +-- +-- red = palette { +-- "#ff928a", +-- bright = "#ffb2a9", +-- }, +-- orange = palette { +-- "#ff9f6f", +-- bright = "#ffc08e", +-- }, +-- yellow = palette { +-- "#ecb256", +-- bright = "#ffd278", +-- }, +-- green = palette { +-- "#8bd294", +-- bright = "#abf3b3", +-- }, +-- cyan = palette { +-- "#6dd3c2", +-- bright = "#8ef4e2", +-- }, +-- blue = palette { +-- "#8bc3fc", +-- bright = "#abe4ff", +-- }, +-- purple = palette { +-- "#c4b1f6", +-- bright = "#e4d1ff", +-- }, +-- pink = palette { +-- "#e5acb4", +-- bright = "#ffccd4", +-- }, +-- +-- transparent = "#00000000", +-- } + +-- qcolor.palette = { +-- fg = x_col.foreground, +-- bg = x_col.background, +-- +-- black = x_col.color0, +-- red = x_col.color1, +-- green = x_col.color2, +-- yellow = x_col.color3, +-- blue = x_col.color4, +-- pink = x_col.color5, +-- cyan = x_col.color6, +-- white = x_col.color7, +-- +-- bright = { +-- black = x_col.color8, +-- red = x_col.color9, +-- green = x_col.color10, +-- yellow = x_col.color11, +-- blue = x_col.color12, +-- pink = x_col.color13, +-- cyan = x_col.color14, +-- white = x_col.color15, +-- }, +-- +-- dim = { +-- fg = "#77828c", +-- bg = "#161b22", +-- }, +-- +-- transparent = "#00000000", +-- } -- taken from https://github.com/bew/dotfiles/blob/ab9bb1935783f7a31ef777b1d7e26d53f35df864/gui/wezterm/cfg_utils.lua -qvars.mods = setmetatable({ _SHORT_MAP = { C = "Control", S = "Shift", A = "Mod1", M = "Mod4" } }, { - __index = function(self, key) - local resolved_mods = {} - for i = 1, #key do - resolved_mods[i] = self._SHORT_MAP[key:sub(i, i)] - end - return resolved_mods - end, -}) - ----@enum buttons -qvars.btns = { - left = btns.LEFT, - right = btns.RIGHT, - middle = btns.MIDDLE, - up = btns.SCROLL_UP, - down = btns.SCROLL_DOWN, -} +-- qbind.mods = setmetatable({ _SHORT_MAP = { C = "Control", S = "Shift", A = "Mod1", M = "Mod4" } }, { +-- __index = function(self, key) +-- local resolved_mods = {} +-- for i = 1, #key do +-- resolved_mods[i] = self._SHORT_MAP[key:sub(i, i)] +-- end +-- return resolved_mods +-- end, +-- }) +-- +-- ---@enum buttons +-- qbind.btns = { +-- left = btns.LEFT, +-- right = btns.RIGHT, +-- middle = btns.MIDDLE, +-- up = btns.SCROLL_UP, +-- down = btns.SCROLL_DOWN, +-- } return qvars diff --git a/.config/awesome/rc.lua b/.config/awesome/rc.lua index be5b3f1..32147be 100644 --- a/.config/awesome/rc.lua +++ b/.config/awesome/rc.lua @@ -18,7 +18,6 @@ require "services" require "misc" require "ui" - -- require "curious" -- local gdebug = require "gears.debug" -- diff --git a/.config/awesome/services/backlight.lua b/.config/awesome/services/backlight.lua index 02ed808..bb9bedc 100644 --- a/.config/awesome/services/backlight.lua +++ b/.config/awesome/services/backlight.lua @@ -1,10 +1,8 @@ local backlight = require("lib.lit").backlight.new("amdgpu_bl1", 5) local gobject = require "gears.object" -local gtimer = require "gears.timer" -local naughty = require "naughty" local phosphor = require "assets.phosphor" +local qcolor = require "quarrel.color" local qmath = require "quarrel.math" -local qvars = require "quarrel.vars" local backlight_wrapper = gobject { class = { @@ -27,7 +25,7 @@ backlight:connect_signal("brightness", function(_, brightness) { 255 }, }) - backlight_wrapper:emit_signal("icon", phosphor[icon_data .. "_fill"], qvars.colors.fg) + backlight_wrapper:emit_signal("icon", phosphor[icon_data .. "_fill"], qcolor.palette.fg()) backlight_wrapper:emit_signal("value", brightness) end) diff --git a/.config/awesome/services/battery.lua b/.config/awesome/services/battery.lua index 98bacc9..d10714e 100644 --- a/.config/awesome/services/battery.lua +++ b/.config/awesome/services/battery.lua @@ -2,8 +2,8 @@ local UPower = require("lgi").UPowerGlib local gobject = require "gears.object" local gtimer = require "gears.timer" local phosphor = require "assets.phosphor" +local qcolor = require "quarrel.color" local qmath = require "quarrel.math" -local qvars = require "quarrel.vars" local upower = assert(UPower.Client.new_full()) local inner @@ -51,7 +51,11 @@ local function icon_handler() { 100 }, }) - battery:emit_signal("icon", phosphor["battery_vertical_" .. icon_data[1] .. "_fill"], qvars.colors[icon_data[2]]) + battery:emit_signal( + "icon", + phosphor["battery_vertical_" .. icon_data[1] .. "_fill"], + qcolor.palette[icon_data[2]]() + ) end local function level_handler() diff --git a/.config/awesome/services/common.lua b/.config/awesome/services/common.lua index ad1f998..9657c8a 100644 --- a/.config/awesome/services/common.lua +++ b/.config/awesome/services/common.lua @@ -1,12 +1,10 @@ local gfs = require "gears.filesystem" local gtimer = require "gears.timer" local phosphor = require "assets.phosphor" +local qcolor = require "quarrel.color" local qjson = require "quarrel.json" local qmath = require "quarrel.math" local qnative = require "quarrel.native" -local qstore = require "quarrel.store" -local qvars = require "quarrel.vars" -local UPower = require("lgi").UPowerGlib --- Register a service ---@param name string @@ -39,9 +37,9 @@ if not gfs.file_readable "/tmp/wp_audio_status" then assert(io.open("/tmp/wp_audio_status", "w")):write("{}"):close() end -qstore.audio_file = qnative.util.open_file "/tmp/wp_audio_status" -qstore.brightness_file = qnative.util.open_file "/sys/class/backlight/amdgpu_bl1/actual_brightness" -qstore.wifi_file = qnative.util.open_file "/proc/net/wireless" +local audio_file = qnative.util.open_file "/tmp/wp_audio_status" +-- qstore.brightness_file = qnative.util.open_file "/sys/class/backlight/amdgpu_bl1/actual_brightness" +local wifi_file = qnative.util.open_file "/proc/net/wireless" -- follows the format `service = { provider, icon_provider }` ---@class Service @@ -53,7 +51,7 @@ local services = { audio = { -- volume, muted function() - local audio_status = qjson.decode(read(qstore.audio_file)) + local audio_status = qjson.decode(read(audio_file)) local default_sink = audio_status["G435 Wireless Gaming Headset Analog Stereo"] if not default_sink then @@ -64,7 +62,7 @@ local services = { end, function(volume, muted) if muted or not volume then - return phosphor.speaker_simple_x_fill, qvars.colors.red + return phosphor.speaker_simple_x_fill, qcolor.palette.red() end local icon_data = qmath.step_value(volume, { @@ -75,32 +73,32 @@ local services = { { 100 }, }) - return phosphor["speaker_simple_" .. icon_data .. "_fill"], qvars.colors.fg - end, - }, - brightness = { - -- brightness - function() - return read(qstore.brightness_file, "n") - end, - function(brightness) - local icon_data = qmath.step_value(brightness, { - { 0, "cloud_moon" }, - { 51, "moon" }, - { 102, "sun_horizon" }, - { 153, "sun_dim" }, - { 204, "sun" }, - { 255 }, - }) - - return phosphor[icon_data .. "_fill"], qvars.colors.fg + return phosphor["speaker_simple_" .. icon_data .. "_fill"], qcolor.palette.fg() end, }, + -- brightness = { + -- -- brightness + -- function() + -- return read(qstore.brightness_file, "n") + -- end, + -- function(brightness) + -- local icon_data = qmath.step_value(brightness, { + -- { 0, "cloud_moon" }, + -- { 51, "moon" }, + -- { 102, "sun_horizon" }, + -- { 153, "sun_dim" }, + -- { 204, "sun" }, + -- { 255 }, + -- }) + -- + -- return phosphor[icon_data .. "_fill"], qcolor.palette.fg() + -- end, + -- }, wifi = { -- essid, strength, connected function() - local lines = qstore.wifi_file:lines() - qstore.wifi_file:rewind() + local lines = wifi_file:lines() + wifi_file:rewind() if not lines[3] then return nil, 0, false @@ -112,7 +110,7 @@ local services = { end, function(_, strength, connected) if not connected then - return phosphor.wifi_x_fill, qvars.colors.red + return phosphor.wifi_x_fill, qcolor.palette.red() end local icon_data = qmath.step_value(strength, { @@ -123,7 +121,7 @@ local services = { { 100 }, }) - return phosphor["wifi_" .. icon_data[1] .. "_fill"], qvars.colors[icon_data[2]] + return phosphor["wifi_" .. icon_data[1] .. "_fill"], qcolor.palette[icon_data[2]]() end, }, } diff --git a/.config/awesome/services/mpris/init.lua b/.config/awesome/services/mpris/init.lua new file mode 100644 index 0000000..3659a3a --- /dev/null +++ b/.config/awesome/services/mpris/init.lua @@ -0,0 +1,100 @@ +local gobject = require "gears.object" +local playerctl = require "services.mpris.playerctl" +local qpersistent = require "quarrel.persistent" + +---@class ServiceMpris : gears.object +---@field inner Playerctl +---@field index number the index of the currently active player +local M = {} + +---@param self ServiceMpris +---@param player Playerctl.data +local function update_player(self, player) + self:emit_signal("player::metadata", player) + self:emit_signal("player::position", player) + -- handle_position(nil, player) +end + +---@param diff_player Playerctl.data +local function recalculate_active_player(diff_player, vanished) + if type(diff_player) ~= "table" then + return + end + -- if #layout.children == 0 then + -- M.active_player_index = 1 + -- update_player() + -- return + -- end + + local active_player = players[M.active_player_index] + if not active_player then -- we're recovering from a state with no players + update_player(diff_player) + return + end + + if diff_player.instance == active_player.instance and vanished then -- active player vanished; fall back to previous player + M.previous_player() + else -- non-active player appeared/vanished; try to find active player + for i, p in ipairs(playerctl:list()) do + if p.instance == active_player.instance then + M.active_player_index = i + update_player(p) + return + end + end + + gdebug.print_warning( + "failed to find active player:\n " .. gdebug.dump_return(active_player, nil, 2):gsub("\n", "\n ") + ) + M.active_player_index = 1 + update_player(playerctl:list()[M.active_player_index]) + end +end + +function M:next_player() + local players = self.inner:list() + + if #players == 0 then + return + elseif self.index + 1 > #players then + self.index = 1 + else + self.index = self.index + 1 + end + + -- update_player(playerctl:list()[M.active_player_index]) + local player = players[self.index] + self:emit_signal("player::metadata", player) + self:emit_signal("player::position", player) +end + +function M:previous_player() + local players = self.inner:list() + if #players == 0 then + return + elseif self.index - 1 < #players then + self.index = #players + else + self.index = self.index - 1 + end + + local player = players[self.index] + self:emit_signal("player::metadata", player) + self:emit_signal("player::position", player) +end + +M.inner = playerctl.new { + players = {}, + metadata = { + album = "xesam:album", + title = "xesam:title", + artist = "xesam:artist", + art = "mpris:artUrl", + }, +} + +local instance = gobject { class = M } +instance:connect_signal("property::index", function(self, index) + qpersistent.store("active_player_index", self.index) +end) +return instance diff --git a/.config/awesome/services/playerctl.lua b/.config/awesome/services/playerctl.lua index f6ee71a..c4be74b 100644 --- a/.config/awesome/services/playerctl.lua +++ b/.config/awesome/services/playerctl.lua @@ -1,45 +1,591 @@ -local playerctl = require("lib.bling.signal.playerctl").lib { - player = { "spotify", "%any" }, +-- CREDIT: https://github.com/kosorin/awesome-rice/blob/b6813bd1bbb3fdd697a1b994784b72daaeaf405d/services/media/playerctl.lua + +-- DEPENDENCIES: playerctl + +local type = type +local pairs = pairs +local ipairs = ipairs +local gobject = require "gears.object" +local gtable = require "gears.table" +local gtimer = require "gears.timer" +local lgi_playerctl = require("lgi").Playerctl -- /usr/share/gtk-doc/html/playerctl/index.html + +---@alias Playerctl.selector +---|>nil # Select primary player +---| string # Select player by `instance` +---| "%all" # Select all players + +---@class Playerctl.position_data +---@field position? integer +---@field length? integer + +---@class Playerctl.data +---@field name string +---@field instance string +---@field playback_status lgi.Playerctl.PlaybackStatus +---@field position integer +---@field shuffle boolean +---@field loop_status lgi.Playerctl.LoopStatus +---@field volume number +---@field metadata table<string, any> +---@field package _position_timer? gears.timer + +local playerctl = { + lowest_priority = math.maxinteger, + any = { name = "%any" }, + all = { name = "%all" }, } -playerctl:connect_signal("metadata", function(_, ...) - awesome.emit_signal("services::playerctl::metadata", ...) -end) +---@class Playerctl : gears.object +---@field unit integer # Number of microseconds in a second. +---@field package primary_player_data? Playerctl.data +---@field package player_data table<string, Playerctl.data> +---@field package tracked_metadata table<string, string> +---@field package excluded_players table<string, boolean> +---@field package player_priorities table<string, integer> +---@field package manager lgi.Playerctl.PlayerManager +playerctl.object = { unit = 1000000 } + +---@param player_data? Playerctl.data +---@return Playerctl.position_data|nil +function playerctl.object:get_position_data(player_data) + player_data = player_data or self.primary_player_data + return (player_data and player_data.metadata) + and { + position = player_data.position, + length = player_data.metadata.length, + } +end + +---@param self Playerctl +---@param instance string +---@return lgi.Playerctl.Player|nil +local function find_player_by_instance(self, instance) + for _, player in ipairs(self.manager.players) do + if player.player_instance == instance then + return player + end + end +end + +---@param self Playerctl +---@param player_selector Playerctl.selector +---@param action fun(player: lgi.Playerctl.Player) +local function for_each_player(self, player_selector, action) + local players + if not player_selector then + local player_data = self.primary_player_data + if player_data then + players = { find_player_by_instance(self, player_data.instance) } + end + elseif player_selector == playerctl.all.name then + players = self.manager.players + elseif type(player_selector) == "string" then + players = { find_player_by_instance(self, player_selector) } + end + + if players then + for _, p in ipairs(players) do + action(p) + end + end +end + +---@param player_selector Playerctl.selector +function playerctl.object:play_pause(player_selector) + for_each_player(self, player_selector, function(p) + p:play_pause() + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:play(player_selector) + for_each_player(self, player_selector, function(p) + p:play() + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:pause(player_selector) + for_each_player(self, player_selector, function(p) + p:pause() + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:stop(player_selector) + for_each_player(self, player_selector, function(p) + p:stop() + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:previous(player_selector) + for_each_player(self, player_selector, function(p) + p:previous() + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:next(player_selector) + for_each_player(self, player_selector, function(p) + p:next() + end) +end + +---@param offset integer +---@param player_selector Playerctl.selector +function playerctl.object:skip(offset, player_selector) + if offset > 0 then + self:next(player_selector) + else + self:previous(player_selector) + end +end + +---@param offset integer +---@param player_selector Playerctl.selector +function playerctl.object:rewind(offset, player_selector) + self:seek(-offset, player_selector) +end + +---@param offset integer +---@param player_selector Playerctl.selector +function playerctl.object:fast_forward(offset, player_selector) + self:seek(offset, player_selector) +end + +---@param offset integer +---@param player_selector Playerctl.selector +function playerctl.object:seek(offset, player_selector) + for_each_player(self, player_selector, function(p) + p:seek(offset) + end) +end + +---@param loop_status lgi.Playerctl.LoopStatus +---@param player_selector Playerctl.selector +function playerctl.object:set_loop_status(loop_status, player_selector) + loop_status = loop_status:upper() + for_each_player(self, player_selector, function(p) + p:set_loop_status(loop_status) + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:cycle_loop_status(player_selector) + for_each_player(self, player_selector, function(p) + if p.loop_status == "NONE" then + p:set_loop_status "TRACK" + elseif p.loop_status == "TRACK" then + p:set_loop_status "PLAYLIST" + elseif p.loop_status == "PLAYLIST" then + p:set_loop_status "NONE" + end + end) +end + +---@param position integer +---@param player_selector Playerctl.selector +function playerctl.object:set_position(position, player_selector) + for_each_player(self, player_selector, function(p) + p:set_position(position) + end) +end + +---@param shuffle boolean +---@param player_selector Playerctl.selector +function playerctl.object:set_shuffle(shuffle, player_selector) + for_each_player(self, player_selector, function(p) + p:set_shuffle(shuffle) + end) +end + +---@param player_selector Playerctl.selector +function playerctl.object:toggle_shuffle(player_selector) + for_each_player(self, player_selector, function(p) + p:set_shuffle(not p.shuffle) + end) +end + +---@param volume number +---@param player_selector Playerctl.selector +function playerctl.object:set_volume(volume, player_selector) + for_each_player(self, player_selector, function(p) + p:set_volume(volume) + end) +end + +---@param player_data? Playerctl.data +---@return boolean +function playerctl.object:is_primary_player(player_data) + return self.primary_player_data == player_data +end + +---@return Playerctl.data|nil +function playerctl.object:get_primary_player_data() + return self.primary_player_data +end + +---@param self Playerctl +---@param player? lgi.Playerctl.Player +local function update_primary_player(self, player) + if player then + self.manager:move_player_to_top(player) + end + + local primary_player = self.manager.players[1] + + local old = self.primary_player_data + local new = self.player_data[primary_player and primary_player.player_instance] + if old ~= new then + self.primary_player_data = new + self:emit_signal("player::primary", new, old) + end +end + +---@param player_data Playerctl.data +---@return boolean +function playerctl.object:is_pinned(player_data) + local player_instance = player_data and player_data.instance or nil + return self.pinned_player_instance == player_instance +end + +---@param player_data? Playerctl.data +function playerctl.object:pin(player_data) + local player_instance = player_data and player_data.instance or nil + if self.pinned_player_instance ~= player_instance then + self.pinned_player_instance = player_instance + self:emit_signal("player::pinned", self.pinned_player_instance) + + local player + if player_data then + player = find_player_by_instance(self, player_data.instance) + elseif self.primary_player_data then + player = find_player_by_instance(self, self.primary_player_data.instance) + end + + update_primary_player(self, player) + end +end + +---@return Playerctl.data[] +function playerctl.object:list() + ---@type Playerctl.data[] + local players = {} + for _, p in ipairs(self.manager.players) do + players[#players + 1] = self.player_data[p.player_instance] + end + return players +end + +---@param player_data Playerctl.data +local function refresh_position_timer(player_data) + if player_data.playback_status == "PLAYING" then + player_data._position_timer:again() + else + player_data._position_timer:stop() + end +end + +---@param self Playerctl +---@param player_data Playerctl.data +---@param by_timer? boolean +local function update_position(self, player_data, by_timer) + local player = find_player_by_instance(self, player_data.instance) + if player then + player_data.position = player:get_position() + self:emit_signal("player::position", player_data, by_timer) + end +end -playerctl:connect_signal("position", function(_, ...) - awesome.emit_signal("services::playerctl::position", ...) -end) +---@param self Playerctl +---@param player_data Playerctl.data +---@param metadata lgi.Playerctl.Metadata +---@return boolean +---@return table<string, boolean> +local function update_metadata(self, player_data, metadata) + metadata = metadata and metadata.value or {} -playerctl:connect_signal("playback_status", function(_, ...) - awesome.emit_signal("services::playerctl::playback_status", ...) -end) + local changed = false + local changed_data = {} -playerctl:connect_signal("seeked", function(_, ...) - awesome.emit_signal("services::playerctl::seeked", ...) -end) + local function mark_changed(name) + changed = true + changed_data[name] = true + end -playerctl:connect_signal("volume", function(_, ...) - awesome.emit_signal("services::playerctl::volume", ...) -end) + local target_metadata = player_data.metadata + for name, mpris_name in pairs(self.tracked_metadata) do + local value = metadata[mpris_name] + local value_type = type(value) + if value_type == "nil" or value_type == "boolean" or value_type == "number" or value_type == "string" then + if target_metadata[name] ~= value then + target_metadata[name] = value + mark_changed(name) + end + elseif value_type == "userdata" and value.type == "as" then + local old = target_metadata[name] + if type(old) ~= "table" then + old = {} + end -playerctl:connect_signal("loop_status", function(_, ...) - awesome.emit_signal("services::playerctl::loop_status", ...) -end) + local new = {} + for _, s in value:ipairs() do + new[#new + 1] = s + end -playerctl:connect_signal("shuffle", function(_, ...) - awesome.emit_signal("services::playerctl::shuffle", ...) -end) + target_metadata[name] = new -playerctl:connect_signal("exit", function(_, ...) - awesome.emit_signal("services::playerctl::exit", ...) -end) + if #old ~= #new then + mark_changed(name) + else + for i = 1, #new do + if old[i] ~= new[i] then + mark_changed(name) + break + end + end + end + else + if target_metadata[name] ~= nil then + target_metadata[name] = nil + mark_changed(name) + end + end + end -playerctl:connect_signal("exit", function(_, ...) - awesome.emit_signal("services::playerctl::exit", ...) -end) + return changed, changed_data +end -playerctl:connect_signal("no_players", function() - awesome.emit_signal "services::playerctl::no_players" -end) +---@param self Playerctl +---@param full_name lgi.Playerctl.PlayerName +---@return lgi.Playerctl.Player +local function manage_player(self, full_name) + local new_player = lgi_playerctl.Player.new_from_name(full_name) -return playerctl + function new_player.on_metadata(p, metadata) + local player_data = self.player_data[p.player_instance] + if player_data and update_metadata(self, player_data, metadata) then + self:emit_signal("player::metadata", player_data) + + update_position(self, player_data) + refresh_position_timer(player_data) + end + end + + function new_player.on_playback_status(p, playback_status) + update_primary_player(self, p) + + local player_data = self.player_data[p.player_instance] + if player_data and player_data.playback_status ~= playback_status then + player_data.playback_status = playback_status + self:emit_signal("player::playback_status", player_data) + + update_position(self, player_data) + refresh_position_timer(player_data) + end + end + + function new_player.on_seeked(p, position) + local player_data = self.player_data[p.player_instance] + if player_data and player_data.position ~= position then + player_data.position = position + self:emit_signal("player::position", player_data) + + refresh_position_timer(player_data) + end + end + + function new_player.on_shuffle(p, shuffle) + local player_data = self.player_data[p.player_instance] + if player_data and player_data.shuffle ~= shuffle then + player_data.shuffle = shuffle + self:emit_signal("player::shuffle", player_data) + end + end + + function new_player.on_loop_status(p, loop_status) + local player_data = self.player_data[p.player_instance] + if player_data and player_data.loop_status ~= loop_status then + player_data.loop_status = loop_status + self:emit_signal("player::loop_status", player_data) + end + end + + function new_player.on_volume(p, volume) + local player_data = self.player_data[p.player_instance] + if player_data and player_data.volume ~= volume then + player_data.volume = volume + self:emit_signal("player::volume", player_data) + end + end + + self.manager:manage_player(new_player) + + return new_player +end + +---@param self Playerctl +---@param player_name string +---@return boolean +local function filter_name(self, player_name) + if self.excluded_players[player_name] then + return false + end + if self.player_priorities[playerctl.any] or self.player_priorities[player_name] then + return true + end + return false +end + +---@param self Playerctl +---@param player_a lgi.Playerctl.Player +---@param player_b lgi.Playerctl.Player +---@return sign +local function compare_players(self, player_a, player_b) + if player_a.player_name < player_b.player_name then + return -1 + elseif player_a.player_name > player_b.player_name then + return 1 + else + return 0 + end +end + +---@param self Playerctl +local function initialize_manager(self) + self.player_data = {} + + self.manager = lgi_playerctl.PlayerManager() + self.manager:set_sort_func(function(a, b) + local player_a = lgi_playerctl.Player(a) + local player_b = lgi_playerctl.Player(b) + return compare_players(self, player_a, player_b) + end) + + ---@param full_name lgi.Playerctl.PlayerName + ---@return lgi.Playerctl.Player|nil + local function try_manage(full_name) + if filter_name(self, full_name.name) then + return manage_player(self, full_name) + end + end + + function self.manager.on_name_appeared(_, full_name) + try_manage(full_name) + end + + function self.manager.on_player_appeared(_, player) + ---@type Playerctl.data + local player_data = { + name = player.player_name, + instance = player.player_instance, + playback_status = player.playback_status, + position = player.position, + shuffle = player.shuffle, + loop_status = player.loop_status, + volume = player.volume, + metadata = {}, + } + update_metadata(self, player_data, player.metadata) + + player_data._position_timer = gtimer { + timeout = 1, + callback = function() + update_position(self, player_data, true) + end, + } + refresh_position_timer(player_data) + + self.player_data[player_data.instance] = player_data + self:emit_signal("player::appeared", player_data) + + update_primary_player(self, player) + end + + function self.manager.on_player_vanished(_, player) + update_primary_player(self) + + if self.pinned_player_instance == player.player_instance then + self:pin(nil) + end + + local player_data = self.player_data[player.player_instance] + if player_data then + player_data._position_timer:stop() + self:emit_signal("player::vanished", player_data) + self.player_data[player.player_instance] = nil + end + end + + for _, full_name in ipairs(self.manager.player_names) do + try_manage(full_name) + end + + update_primary_player(self) +end + +---@param self Playerctl +---@param args? Playerctl.new.args +local function parse_args(self, args) + args = args or {} + + self.tracked_metadata = args.metadata or {} + + -- Always track length + self.tracked_metadata.length = "mpris:length" + + local excluded_players = {} + if type(args.excluded_players) == "string" then + excluded_players[args.excluded_players] = true + elseif args.excluded_players then + for _, name in ipairs(args.excluded_players) do + excluded_players[name] = true + end + end + self.excluded_players = excluded_players + + local function get_priority_key(name) + return name == playerctl.any.name and playerctl.any or name + end + local player_priorities + if type(args.players) == "string" then + player_priorities = { [get_priority_key(args.players)] = 1 } + elseif type(args.players) == "table" and #args.players > 0 then + player_priorities = {} + for i, name in ipairs(args.players) do + player_priorities[get_priority_key(name)] = i + end + else + player_priorities = { [playerctl.any] = 1 } + end + self.player_priorities = player_priorities +end + +---@class Playerctl.new.args +---@field players string[] +---@field excluded_players? string[] +---@field metadata table<string, string> + +---@param args? Playerctl.new.args +---@return Playerctl +function playerctl.new(args) + local self = gtable.crush(gobject {}, playerctl.object, true) --[[@as Playerctl]] + + parse_args(self, args) + + initialize_manager(self) + + return self +end + +return playerctl.new { + players = {}, + metadata = { + album = "xesam:album", + title = "xesam:title", + artist = "xesam:artist", + art = "mpris:artUrl", + }, +} diff --git a/.config/awesome/signals/client.lua b/.config/awesome/signals/client.lua index 8788735..f0b5136 100644 --- a/.config/awesome/signals/client.lua +++ b/.config/awesome/signals/client.lua @@ -1,9 +1,9 @@ -local qvars = require "quarrel.vars" +local qui = require "quarrel.ui" local rectangle = require("gears.shape").rectangle -local conductor = require "ui.conductor" +-- local conductor = require "ui.conductor" client.connect_signal("request::manage", function(c) - c.shape = qvars.shape + c.shape = qui.shape if c.maximized then c.maximized = false c.maximized = true @@ -17,8 +17,8 @@ local function handle_corners(c) c.shape = rectangle c.border_width = 0 else - c.shape = qvars.shape - c.border_width = qvars.border_width + c.shape = qui.shape + c.border_width = qui.BORDER_WIDTH end end diff --git a/.config/awesome/ui/conductor/init.lua b/.config/awesome/ui/conductor/init.lua index e6fe552..402945c 100644 --- a/.config/awesome/ui/conductor/init.lua +++ b/.config/awesome/ui/conductor/init.lua @@ -1,8 +1,64 @@ local awful = require "awful" +local gshape = require "gears.shape" +local phosphor = require "assets.phosphor" local q = require "quarrel" local qui = require "quarrel.ui" local qvars = require "quarrel.vars" +local wibox = require "wibox" -local conductor = {} +local M = {} -return conductor +M._popup = qui.popup { + -- visible = false, + ontop = true, + placement = "right", + shape = function(cr, w, h) + gshape.partially_rounded_rect(cr, w, h, true, false, false, true, qui.BORDER_RADIUS) + end, + -- x = awful.screen.focused().geometry.width, + -- minimum_width = width, + -- maximum_width = width, + -- maximum_height = max_height, + widget = awful.widget.tasklist { + screen = awful.screen.focused(), + filter = awful.widget.tasklist.filter.allscreen, + layout = { + spacing = qui.BIG_PADDING, + layout = wibox.layout.fixed.vertical, + }, + widget_template = qui.styled { + widget = wibox.container.background, + { + { + widget = wibox.container.constraint, + strategy = "max", + height = qui.CHAR_HEIGHT, + width = qui.CHAR_HEIGHT, + { + widget = wibox.widget.imagebox, + id = "icon_role", + }, + }, + { + widget = wibox.container.constraint, + strategy = "max", + width = qui.CHAR_WIDTH * 24, + { + widget = wibox.widget.textbox, + id = "client_name", + }, + }, + layout = wibox.layout.fixed.horizontal, + spacing = qui.PADDING, + }, + create_callback = function(self, c) + self:get_children_by_id("client_name")[1].text = c.icon_name + end, + update_callback = function(self, c) + self:get_children_by_id("client_name")[1].text = c.name + end, + }, + }, +} + +return M diff --git a/.config/awesome/ui/decorations/wallpaper.lua b/.config/awesome/ui/decorations/wallpaper.lua index 3a204c2..cb01745 100644 --- a/.config/awesome/ui/decorations/wallpaper.lua +++ b/.config/awesome/ui/decorations/wallpaper.lua @@ -1,12 +1,12 @@ +local qcolor = require "quarrel.color" local qmarkup = require "quarrel.markup" local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" local wallpaper = require "awful.wallpaper" local wibox = require "wibox" screen.connect_signal("request::wallpaper", function(s) wallpaper { - bg = qvars.colors.dim.bg, + bg = qcolor.palette.bg.low, screen = s, widget = { widget = wibox.container.place, @@ -16,12 +16,12 @@ screen.connect_signal("request::wallpaper", function(s) widget = wibox.widget.textbox, font = qui.font(1.5), markup = table.concat({ - qmarkup(" ___", { fg = qvars.colors.red }), - qmarkup(" /\\ \\", { fg = qvars.colors.green }), - qmarkup(" /::\\ \\", { fg = qvars.colors.yellow }), - qmarkup("/::\\:\\__\\", { fg = qvars.colors.blue }), - qmarkup("\\/\\::/ /", { fg = qvars.colors.pink }), - qmarkup(" \\/__/", { fg = qvars.colors.cyan }), + qmarkup(" ___", { fg = qcolor.palette.blue() }), + qmarkup(" /\\ \\", { fg = qcolor.palette.green() }), + qmarkup(" /::\\ \\", { fg = qcolor.palette.yellow() }), + qmarkup("/::\\:\\__\\", { fg = qcolor.palette.orange() }), + qmarkup("\\/\\::/ /", { fg = qcolor.palette.red() }), + qmarkup(" \\/__/", { fg = qcolor.palette.pink() }), }, "\n"), }, }, diff --git a/.config/awesome/ui/fresnel/init.lua b/.config/awesome/ui/fresnel/init.lua index 9905741..5d091b6 100644 --- a/.config/awesome/ui/fresnel/init.lua +++ b/.config/awesome/ui/fresnel/init.lua @@ -1,8 +1,10 @@ local al_prompt = require "lib.bling.widget.app_launcher.prompt" local awful = require "awful" -local beautiful = require "beautiful" local cfg = require "misc.cfg" +local gshape = require "gears.shape" local gtable = require "gears.table" +local qanim = require "quarrel.animation" +local qcolor = require "quarrel.color" local qnative = require "quarrel.native" local qtable = require "quarrel.table" local qui = require "quarrel.ui" @@ -42,8 +44,8 @@ function fresnel:_update(query, scrolled) self._entries_offset = 0 end - local layout = self._widget.widget:get_children_by_id("entry_layout")[1] - local status = self._widget.widget:get_children_by_id("status")[1] + local layout = self._popup.widget:get_children_by_id("entry_layout")[1] + local status = self._popup.widget:get_children_by_id("status")[1] local all_providers = {} local entries_count = 0 @@ -71,18 +73,18 @@ function fresnel:_update(query, scrolled) local entry_widget = wibox.widget { widget = wibox.container.background, - shape = qvars.shape, + shape = qui.shape, { widget = wibox.container.margin, - margins = qvars.padding, + margins = qui.PADDING, { widget = wibox.container.constraint, strategy = "max", - height = qvars.char_height, + height = qui.CHAR_HEIGHT, { { widget = wibox.container.background, - fg = qvars.colors.dim.fg, + fg = qcolor.palette.low, { widget = wibox.widget.textbox, text = entry.provider .. " | ", @@ -92,7 +94,7 @@ function fresnel:_update(query, scrolled) widget = wibox.widget.textbox, text = entry.message, }, - spacing = qvars.padding, + spacing = qui.PADDING, layout = wibox.layout.fixed.horizontal, }, }, @@ -111,21 +113,21 @@ function fresnel:_update(query, scrolled) if self._selected_index + self._entries_offset == i then entry_widget._selected = true - entry_widget.bg = qvars.colors.black + entry_widget.bg = qcolor.palette.bg.high end entry_widget:connect_signal("mouse::enter", function() if entry_widget._selected == true then return end - entry_widget.bg = qvars.colors.black + entry_widget.bg = qcolor.palette.bg.high end) entry_widget:connect_signal("mouse::leave", function() if entry_widget._selected == true then return end - entry_widget.bg = qvars.colors.bg + entry_widget.bg = qcolor.palette.bg() end) layout:add(entry_widget) @@ -143,7 +145,7 @@ fresnel._prompt = al_prompt { prompt = "", reset_on_stop = true, ul_cursor = "low", - bg_cursor = qvars.colors.black, + bg_cursor = qcolor.palette.bg.high, changed_callback = function(text) if fresnel._text == text then return @@ -192,27 +194,31 @@ fresnel._prompt = al_prompt { end, } -fresnel._widget = qui.popup { - visible = false, +local max_height = qui.BIG_PADDING * 2 + (qui.BIG_PADDING * 2 + qui.CHAR_HEIGHT) * 10 +-- + qui.PADDING +-- + qui.PADDING * 9 + +local width = awful.screen.focused().geometry.width / 2 + +fresnel._popup = qui.popup { + -- visible = false, ontop = true, - placement = function(d) - awful.placement.top(d, { - margins = { - top = beautiful.useless_gap * 2, - }, - }) + placement = false, + shape = function(cr, w) + gshape.partially_rounded_rect(cr, w, 0, false, false, true, true, qui.BORDER_RADIUS) end, - minimum_width = awful.screen.focused().geometry.width / 2, - maximum_width = awful.screen.focused().geometry.width / 2, + x = width / 2, + minimum_width = width, + maximum_width = width, + -- maximum_height = max_height, widget = { - { + qui.styled { widget = wibox.container.background, - bg = qvars.colors.black, - fg = qvars.colors.dim.fg, - shape = qvars.shape, + fg = qcolor.palette.fg.low, + bg = qcolor.palette.bg.high, { widget = wibox.container.margin, - margins = qvars.padding, + margins = qui.BIG_PADDING, { { widget = wibox.widget.textbox, @@ -221,16 +227,16 @@ fresnel._widget = qui.popup { { widget = wibox.container.margin, margins = { - left = qvars.padding, - right = qvars.padding, + left = qui.PADDING, + right = qui.PADDING, }, { widget = wibox.container.constraint, strategy = "max", - height = qvars.char_height, + height = qui.CHAR_HEIGHT, { widget = wibox.container.background, - fg = qvars.colors.fg, + fg = qcolor.palette.fg(), fresnel._prompt.textbox, }, }, @@ -247,10 +253,10 @@ fresnel._widget = qui.popup { { widget = wibox.container.margin, margins = { - top = qvars.padding, + top = qui.PADDING, }, { - spacing = qvars.padding, + spacing = qui.PADDING, layout = wibox.layout.fixed.vertical, id = "entry_layout", }, @@ -258,35 +264,43 @@ fresnel._widget = qui.popup { layout = wibox.layout.align.vertical, }, } -fresnel._widget.maximum_height = qvars.big_padding * 2 - + (qvars.padding * 2 + qvars.char_height) * (1 + 10) - + qvars.padding - + qvars.padding * 9 - function fresnel:show() self._toggled = true - self._timed.target = 1 + self._opacity_timed.target = 1 + self._height_timed:set(max_height) self:_update() self._prompt:start() end function fresnel:hide() self._toggled = false - self._timed.target = 0 + self._opacity_timed.target = 0 + self._height_timed:set(0) self._prompt:stop() end -fresnel._timed = rubato.timed { +fresnel._height_timed = qanim:new { + duration = qvars.anim_duration, + pos = 0, + easing = qvars.easing, + subscribed = function(pos) + fresnel._popup.shape = function(cr, w) + gshape.partially_rounded_rect(cr, w, pos, false, false, true, true, qui.BORDER_RADIUS) + end + end, +} + +-- TODO: optimize the search algo to be more efficient and not require making fresnel invisible +fresnel._opacity_timed = rubato.timed { duration = qvars.anim_duration, - intro = qvars.anim_intro, pos = 0, subscribed = function(pos) - fresnel._widget.opacity = pos + fresnel._popup.opacity = pos if pos == 0 then - fresnel._widget.visible = false + fresnel._popup.visible = false else - fresnel._widget.visible = true + fresnel._popup.visible = true end end, } diff --git a/.config/awesome/ui/init.lua b/.config/awesome/ui/init.lua index dd93ad1..6ad1c7b 100644 --- a/.config/awesome/ui/init.lua +++ b/.config/awesome/ui/init.lua @@ -1,4 +1,4 @@ require "ui.statusbar" require "ui.decorations" -require "ui.conductor" -require "ui.osd" +-- require "ui.conductor" +-- require "ui.osd" diff --git a/.config/awesome/ui/insightful/init.lua b/.config/awesome/ui/insightful/init.lua index 3dcbbea..be072d5 100644 --- a/.config/awesome/ui/insightful/init.lua +++ b/.config/awesome/ui/insightful/init.lua @@ -1,6 +1,6 @@ local awful = require "awful" local beautiful = require "beautiful" -local qstore = require "quarrel.store" +local qbind = require "quarrel.bind" local qtable = require "quarrel.table" local qui = require "quarrel.ui" local qvars = require "quarrel.vars" @@ -81,7 +81,7 @@ insightful._widget = qui.popup { minimum_width = awful.screen.focused().geometry.width / 2, widget = { layout = wibox.layout.fixed.vertical, - spacing = qvars.big_padding, + spacing = qui.BIG_PADDING, id = "layout_container", }, } @@ -90,7 +90,7 @@ function insightful:_generate() local grouped_binds = {} local layout_container = insightful._widget.widget.layout_container - for _, keybind in ipairs(qstore.bindings) do + for _, keybind in ipairs(qbind.bindings) do local group = keybind.group or "general" local group_exists = grouped_binds[group] ~= nil @@ -107,7 +107,7 @@ function insightful:_generate() for group, keybinds in qtable.opairs(grouped_binds) do local group_layout = { - spacing = qvars.padding, + spacing = qui.PADDING, layout = wibox.layout.fixed.vertical, } @@ -129,11 +129,11 @@ function insightful:_generate() 1, qui.styled { widget = wibox.container.background, - bg = qvars.colors.bright.black, + bg = qcolor.palette.bright.black, border_width = 0, { widget = wibox.container.margin, - margins = qvars.padding, + margins = qui.PADDING, { widget = wibox.widget.textbox, text = insightful._keymap[mod] or mod, @@ -166,7 +166,7 @@ function insightful:_generate() if keybind.desc then key_and_desc_layout[3] = { widget = wibox.container.background, - fg = qvars.colors.dim.fg, + fg = qcolor.palette.dim.fg, { widget = wibox.widget.textbox, text = keybind.desc, @@ -183,12 +183,12 @@ function insightful:_generate() { { widget = wibox.container.background, - bg = qvars.colors.yellow, - fg = qvars.colors.bg, - shape = qvars.shape, + bg = qcolor.palette.yellow, + fg = qcolor.palette.bg, + shape = qui.shape, { widget = wibox.container.margin, - margins = qvars.padding, + margins = qui.PADDING, { widget = wibox.widget.textbox, text = group, @@ -199,7 +199,7 @@ function insightful:_generate() layout = wibox.layout.align.horizontal, }, group_layout, - spacing = qvars.padding, + spacing = qui.PADDING, layout = wibox.layout.fixed.vertical, } end diff --git a/.config/awesome/ui/osd.lua b/.config/awesome/ui/osd.lua deleted file mode 100644 index fc74ed9..0000000 --- a/.config/awesome/ui/osd.lua +++ /dev/null @@ -1,102 +0,0 @@ ---- Heavy inspiration from a design made by https://github.com/tsukki9696 - -local awful = require "awful" -local beautiful = require "beautiful" -local gtimer = require "gears.timer" -local phosphor = require "assets.phosphor" -local qmath = require "quarrel.math" -local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" -local rubato = require "lib.rubato" -local wibox = require "wibox" - -local osd = {} - -local widget = qui.popup { - ontop = true, - visible = false, - placement = function(d) - awful.placement.top(d, { - margins = { - top = beautiful.useless_gap * 2, - }, - }) - end, - minimum_height = qvars.char_height * 2, - -- minimum_width = awful.screen.focused().geometry.width / 2, - widget = { - { - widget = wibox.container.place, - - qui.icon { - icon = phosphor.speaker_simple_none_fill, - widget = { - forced_height = qvars.char_height * 1.2, - forced_width = qvars.char_height * 1.2, - id = "icon", - }, - }, - }, - { - widget = wibox.container.place, - { - widget = wibox.widget.progressbar, - max_value = 100, - value = 20, - forced_height = qvars.char_height / 1.5, - forced_width = qvars.expanded_bar_size - - (qvars.big_padding + qvars.big_padding * 2 + qvars.padding * 2) - - (qvars.char_height / 1.25 + qvars.padding) * 3, - color = qvars.colors.yellow, - background_color = qvars.colors.black, - shape = qvars.shape, - id = "progress", - }, - }, - - { - widget = wibox.widget.textbox, - text = "20%", - font = qui.font(1.2), - id = "percentage", - }, - layout = wibox.layout.fixed.horizontal, - spacing = qvars.big_padding, - }, -} - -local timer - -local anim = rubato.timed { - duration = qvars.anim_duration, - intro = qvars.anim_intro, - pos = 0, - subscribed = function(pos) - widget.opacity = pos - if pos == 0 then - widget.visible = false - elseif not widget.visible then - widget.visible = true - elseif pos == 1 then - timer:start() - end - end, -} - -timer = gtimer { - timeout = 1, - callback = function() - anim.target = 0 - end, - single_shot = true, -} - -function osd.notify(icon, value, max) - anim.target = 1 - widget:get_children_by_id("icon")[1].image = icon - widget:get_children_by_id("progress")[1].value = value - widget:get_children_by_id("progress")[1].max_value = max - widget:get_children_by_id("percentage")[1].text = tostring(qmath.percentage(value, max)) .. "%" -end - -return osd diff --git a/.config/awesome/ui/osd/init.lua b/.config/awesome/ui/osd/init.lua new file mode 100644 index 0000000..4310f17 --- /dev/null +++ b/.config/awesome/ui/osd/init.lua @@ -0,0 +1,102 @@ +local awful = require "awful" +local beautiful = require "beautiful" +local gtimer = require "gears.timer" +local phosphor = require "assets.phosphor" +local qmath = require "quarrel.math" +local qui = require "quarrel.ui" +local qvars = require "quarrel.vars" +local rubato = require "lib.rubato" +local wibox = require "wibox" +-- local mpris_widget = require "ui.osd.mpris" + +local osd = {} + +local widget = awful.popup(qui.styled { + ontop = true, + visible = true, + placement = function(d) + awful.placement.top(d, { + margins = { + top = beautiful.useless_gap * 2, + }, + }) + end, + minimum_height = qui.CHAR_HEIGHT * 3, + -- widget = mpris_widget + -- minimum_width = awful.screen.focused().geometry.width / 2, + -- widget = { + -- { + -- widget = wibox.container.place, + -- + -- qui.icon { + -- icon = phosphor.speaker_simple_none_fill, + -- widget = { + -- forced_height = qui.CHAR_HEIGHT * 1.2, + -- forced_width = qui.CHAR_HEIGHT * 1.2, + -- id = "icon", + -- }, + -- }, + -- }, + -- { + -- widget = wibox.container.place, + -- { + -- widget = wibox.widget.progressbar, + -- max_value = 100, + -- value = 20, + -- forced_height = qui.CHAR_HEIGHT / 1.5, + -- forced_width = qvars.expanded_bar_size + -- - (qui.BIG_PADDING + qui.BIG_PADDING * 2 + qui.PADDING * 2) + -- - (qui.CHAR_HEIGHT / 1.25 + qui.PADDING) * 3, + -- color = qcolor.palette.yellow(), + -- background_color = qcolor.palette.border.variant, + -- shape = qui.shape, + -- id = "progress", + -- }, + -- }, + -- + -- { + -- widget = wibox.widget.textbox, + -- text = "20%", + -- font = qui.font(1.2), + -- id = "percentage", + -- }, + -- layout = wibox.layout.fixed.horizontal, + -- spacing = qui.BIG_PADDING, + -- }, +}) + +local timer + +local anim = rubato.timed { + duration = qvars.anim_duration, + intro = qvars.anim_intro, + pos = 1, + subscribed = function(pos) + widget.opacity = pos + if pos == 0 then + widget.visible = false + elseif not widget.visible then + widget.visible = true + elseif pos == 1 then + -- timer:start() + end + end, +} + +timer = gtimer { + timeout = 1, + callback = function() + -- anim.target = 0 + end, + single_shot = true, +} + +function osd.notify(icon, value, max) + anim.target = 1 + widget:get_children_by_id("icon")[1].image = icon + widget:get_children_by_id("progress")[1].value = value + widget:get_children_by_id("progress")[1].max_value = max + widget:get_children_by_id("percentage")[1].text = tostring(qmath.percentage(value, max)) .. "%" +end + +return osd diff --git a/.config/awesome/ui/powermenu/init.lua b/.config/awesome/ui/powermenu/init.lua index 81d2ea3..2e2b4af 100644 --- a/.config/awesome/ui/powermenu/init.lua +++ b/.config/awesome/ui/powermenu/init.lua @@ -1,5 +1,6 @@ local awful = require "awful" local phosphor = require "assets.phosphor" +local qcolor = require "quarrel.color" local qui = require "quarrel.ui" local qvars = require "quarrel.vars" local rubato = require "lib.rubato" @@ -8,21 +9,21 @@ local wibox = require "wibox" local function create_button(title, icon, color, exec) return wibox.widget { widget = wibox.container.background, - shape = qvars.shape, + shape = qui.shape, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { qui.styled { widget = wibox.container.background, border_color = color, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, qui.icon { widget = { - forced_height = qvars.char_height * 4 - qvars.big_padding * 2, - forced_width = qvars.char_height * 4 - qvars.big_padding * 2, + forced_height = qui.CHAR_HEIGHT * 4 - qui.BIG_PADDING * 2, + forced_width = qui.CHAR_HEIGHT * 4 - qui.BIG_PADDING * 2, }, icon = icon, color = color, @@ -37,12 +38,12 @@ local function create_button(title, icon, color, exec) }, }, layout = wibox.layout.fixed.vertical, - spacing = qvars.big_padding, + spacing = qui.BIG_PADDING, exec = exec, }, }, select = function(self) - self.bg = qvars.colors.black + self.bg = qcolor.palette.bg.high end, } end @@ -56,7 +57,7 @@ screen.connect_signal("request::desktop_decoration", function(s) visible = false, minimum_width = s.geometry.width, minimum_height = s.geometry.height, - bg = qvars.colors.bg .. "ee", + bg = qcolor.palette.bg() .. "ee", border_width = 0, widget = { widget = wibox.container.place, @@ -64,10 +65,10 @@ screen.connect_signal("request::desktop_decoration", function(s) widget = wibox.container.background, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { layout = wibox.layout.fixed.horizontal, - spacing = qvars.big_padding * 2, + spacing = qui.BIG_PADDING * 2, id = "list", }, }, @@ -77,16 +78,16 @@ screen.connect_signal("request::desktop_decoration", function(s) local layout = powermenu._widget.widget:get_children_by_id("list")[1] - layout:add(create_button("Shutdown", phosphor.power_bold, qvars.colors.red, function() + layout:add(create_button("Shutdown", phosphor.power_bold, qcolor.palette.red(), function() awful.spawn "poweroff" end)) - layout:add(create_button("Log out", phosphor.sign_out_bold, qvars.colors.green, function() + layout:add(create_button("Log out", phosphor.sign_out_bold, qcolor.palette.green(), function() awesome.quit() end)) - layout:add(create_button("Lock", phosphor.lock_simple_fill, qvars.colors.blue, function() + layout:add(create_button("Lock", phosphor.lock_simple_fill, qcolor.palette.blue(), function() require("quarrel").debug "locked!l" end)) - layout:add(create_button("Restart", phosphor.arrow_clockwise_bold, qvars.colors.pink, function() + layout:add(create_button("Restart", phosphor.arrow_clockwise_bold, qcolor.palette.pink(), function() awful.spawn "reboot" end)) end) diff --git a/.config/awesome/ui/statusbar/consts.lua b/.config/awesome/ui/statusbar/consts.lua new file mode 100644 index 0000000..00d01bf --- /dev/null +++ b/.config/awesome/ui/statusbar/consts.lua @@ -0,0 +1,8 @@ +local qui = require "quarrel.ui" +local C = {} + +C.BAR_SIZE = 24 + qui.BIG_PADDING * 2 +C.ELEMENT_SIZE = 12 +C.EXPANDED_BAR_SIZE = qui.BIG_PADDING + (qui.BIG_PADDING * 2 + C.ELEMENT_SIZE * 4) * 4 + qui.PADDING * 2 + +return C diff --git a/.config/awesome/ui/statusbar/init.lua b/.config/awesome/ui/statusbar/init.lua index 77a28f7..137ffb8 100644 --- a/.config/awesome/ui/statusbar/init.lua +++ b/.config/awesome/ui/statusbar/init.lua @@ -1,18 +1,17 @@ local awful = require "awful" local beautiful = require "beautiful" local panel = require "ui.statusbar.panel" -local phosphor = require "assets.phosphor" -local qstore = require "quarrel.store" +local qanim = require "quarrel.animation" local qui = require "quarrel.ui" local qvars = require "quarrel.vars" -local rubato = require "lib.rubato" local wibox = require "wibox" +local M = require "ui.statusbar.consts" + local clock = require "ui.statusbar.widgets.clock" local displays = require "ui.statusbar.widgets.displays" local keyboardlayout = require "ui.statusbar.widgets.keyboardlayout" -local taglist = require "ui.statusbar.widgets.taglist" -local tasklist = require "ui.statusbar.widgets.tasklist" +local taglist = require "ui.statusbar.widgets.taglist_new" screen.connect_signal("request::desktop_decoration", function(s) local bar = qui.popup { @@ -21,17 +20,16 @@ screen.connect_signal("request::desktop_decoration", function(s) margins = beautiful.useless_gap * 2, }) end, - minimum_height = s.geometry.height - (beautiful.useless_gap * 4 + qvars.border_width * 2), + minimum_height = s.geometry.height - (beautiful.useless_gap * 4 + qui.BORDER_WIDTH * 2), widget = { { { taglist(s), layout = wibox.layout.fixed.vertical, - spacing = qvars.padding * 2, + spacing = qui.PADDING * 2, id = "top", }, nil, - -- tasklist(s), { widget = wibox.container.place, valign = "bottom", @@ -44,14 +42,14 @@ screen.connect_signal("request::desktop_decoration", function(s) widget = wibox.container.place, { widget = wibox.container.constraint, - height = qvars.char_height, - width = qvars.char_height, + height = qui.CHAR_HEIGHT, + width = qui.CHAR_HEIGHT, keyboardlayout, }, }, clock, layout = wibox.layout.fixed.vertical, - spacing = qvars.padding * 2, + spacing = qui.PADDING * 2, }, }, layout = wibox.layout.align.vertical, @@ -61,51 +59,47 @@ screen.connect_signal("request::desktop_decoration", function(s) nil, layout = wibox.layout.align.horizontal, }, + + toggled = false, } - local bar_width = bar.width + qvars.border_width * 2 + local bar_width = bar.width + qui.BORDER_WIDTH * 2 + + bar.shape = function(cr, _, h) + qui.shape(cr, bar_width, h) + end bar:struts { left = bar_width + beautiful.useless_gap * 4, } - local panel_width - local panel_toggle = { toggled = false } -- hacky but it works + bar.widget.widget.third = panel - local timed = rubato.timed { + local timed = qanim:new { duration = qvars.anim_duration, - intro = qvars.anim_intro, pos = bar_width, + easing = qvars.easing, subscribed = function(pos) - if pos ~= bar_width and panel_toggle.toggled then - bar.widget.widget.third = panel - if panel_width == nil then - panel_width = bar.widget.widget.third.width - end + if pos ~= bar_width and bar.toggled then bar.ontop = true - elseif pos == bar_width and not panel_toggle.toggled then - bar.widget.widget.third = nil + elseif pos == bar_width and not bar.toggled then bar.ontop = false end bar.shape = function(cr, _, h) - qvars.shape(cr, pos, h) + qui.shape(cr, pos, h) end end, } - panel_toggle = qui.toggle { - off = phosphor.arrows_out_simple_fill, - on = phosphor.arrows_in_simple_fill, - press = function(self) - if not self.toggled then - timed.target = bar_width - else - timed.target = bar_width + qvars.expanded_bar_size - end - end, - } + function bar:toggle() + self.toggled = not self.toggled + if self.toggled then + timed:set(bar_width + M.EXPANDED_BAR_SIZE) + else + timed:set(bar_width) + end + end - bar.widget:get_children_by_id("top")[1]:insert(1, panel_toggle) - qstore.panel_toggle = panel_toggle + s.bar = bar end) diff --git a/.config/awesome/ui/statusbar/panel/init.lua b/.config/awesome/ui/statusbar/panel/init.lua index 31c2860..814a5a1 100644 --- a/.config/awesome/ui/statusbar/panel/init.lua +++ b/.config/awesome/ui/statusbar/panel/init.lua @@ -1,51 +1,61 @@ -local qvars = require "quarrel.vars" +local C = require "ui.statusbar.consts" +local qui = require "quarrel.ui" local wibox = require "wibox" local battery = require "ui.statusbar.panel.widgets.battery" local calendar = require "ui.statusbar.panel.widgets.calendar" local displays = require "ui.statusbar.panel.widgets.displays" -local music = require "ui.statusbar.panel.widgets.music" -local power_menu = require "ui.statusbar.panel.widgets.power_menu" +local music = require "ui.statusbar.panel.widgets.mpris" local wifi = require "ui.statusbar.panel.widgets.wifi" local panel = wibox.widget { - widget = wibox.container.margin, - margins = { - left = qvars.big_padding, - }, + widget = wibox.container.constraint, + strategy = "max", + width = C.EXPANDED_BAR_SIZE, + -- visible = false, { + widget = wibox.container.background, { - widget = wibox.container.place, - valign = "top", + widget = wibox.container.margin, + margins = { + left = qui.BIG_PADDING, + }, { { - displays.battery, - displays.audio, - displays.brightness, - layout = wibox.layout.fixed.horizontal, - spacing = qvars.padding, + widget = wibox.container.place, + valign = "top", + { + { + displays.battery, + displays.audio, + -- displays.brightness, + -- displays.brightness, + layout = wibox.layout.flex.horizontal, + spacing = qui.PADDING, + }, + wifi, + -- battery, + music.widget, + calendar, + layout = wibox.layout.fixed.vertical, + spacing = qui.PADDING, + }, }, - wifi, - -- battery, - -- music, - calendar, - layout = wibox.layout.fixed.vertical, - spacing = qvars.padding, - }, - }, - { - widget = wibox.container.background, - { - widget = wibox.widget.textbox, - text = ":)", + -- { + -- widget = wibox.container.background, + -- { + -- widget = wibox.widget.textbox, + -- text = ":)", + -- }, + -- }, + -- { + -- widget = wibox.container.place, + -- valign = "bottom", + -- power_menu, + -- }, + layout = wibox.layout.align.vertical, }, }, - { - widget = wibox.container.place, - valign = "bottom", - power_menu, - }, - layout = wibox.layout.align.vertical, }, } diff --git a/.config/awesome/ui/statusbar/panel/widgets/battery.lua b/.config/awesome/ui/statusbar/panel/widgets/battery.lua index 52685b7..52cf5da 100644 --- a/.config/awesome/ui/statusbar/panel/widgets/battery.lua +++ b/.config/awesome/ui/statusbar/panel/widgets/battery.lua @@ -1,6 +1,5 @@ local gears = require "gears" local lit = require "lib.lit" -local qbezier = require "quarrel.bezier" local qui = require "quarrel.ui" local wibox = require "wibox" diff --git a/.config/awesome/ui/statusbar/panel/widgets/calendar.lua b/.config/awesome/ui/statusbar/panel/widgets/calendar.lua index 8933543..ddbffc0 100644 --- a/.config/awesome/ui/statusbar/panel/widgets/calendar.lua +++ b/.config/awesome/ui/statusbar/panel/widgets/calendar.lua @@ -1,58 +1,123 @@ local awful = require "awful" local phosphor = require "assets.phosphor" local qbind = require "quarrel.bind" +local qcolor = require "quarrel.color" local qmarkup = require "quarrel.markup" local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" local wibox = require "wibox" local weekday_map = { 7, 1, 2, 3, 4, 5, 6 } local calendar = wibox.widget(qui.styled { widget = wibox.container.background, + bg = qcolor.palette.bg.high, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BORDER_WIDTH, { { { - widget = wibox.container.place, - qui.icon { - icon = phosphor.caret_left_bold, + layout = wibox.layout.grid, + forced_num_rows = 7, + forced_num_cols = 7, + -- spacing = (qui.PADDING) / 2, + border_width = { + inner = qui.BORDER_WIDTH, + outer = 0, }, + border_color = qcolor.palette.border.variant, + id = "grid", }, { - widget = wibox.container.place, - { - widget = wibox.widget.textbox, - text = "January 2024", - }, + widget = wibox.container.background, + bg = qcolor.palette.border(), + forced_width = qui.BORDER_WIDTH, }, + layout = wibox.layout.fixed.horizontal, + }, + { + widget = wibox.container.margin, + margins = qui.BIG_PADDING, { - widget = wibox.container.place, - qui.icon { - icon = phosphor.caret_right_bold, + { + widget = wibox.container.place, + fill_horizontal = true, + { + widget = wibox.widget.textbox, + text = "05\n25", + }, }, + nil, + qui.styled { + widget = wibox.container.background, + bg = qcolor.palette.bg.highest, + { + { + widget = wibox.container.margin, + margins = qui.PADDING, + { + widget = wibox.container.place, + qui.icon { + icon = phosphor.caret_up_bold, + widget = { + forced_height = qui.CHAR_HEIGHT / 2, + forced_width = qui.CHAR_HEIGHT / 2, + }, + }, + }, + }, + { + widget = wibox.container.background, + bg = qcolor.palette.border.variant, + forced_height = qui.BORDER_WIDTH, + }, + { + widget = wibox.container.margin, + margins = qui.PADDING, + { + widget = wibox.container.place, + qui.icon { + icon = phosphor.caret_down_bold, + widget = { + forced_height = qui.CHAR_HEIGHT / 2, + forced_width = qui.CHAR_HEIGHT / 2, + }, + }, + }, + }, + layout = wibox.layout.fixed.vertical, + -- spacing = qui.PADDING + }, + }, + layout = wibox.layout.align.vertical, }, - layout = wibox.layout.align.horizontal, - expand = "outside", - }, - { - layout = wibox.layout.grid, - forced_num_rows = 7, - forced_num_cols = 7, - spacing = qvars.padding, - id = "grid", }, - layout = wibox.layout.fixed.vertical, - spacing = qvars.big_padding, + layout = wibox.layout.fixed.horizontal, }, }, }) +local grid = calendar:get_children_by_id("grid")[1] + -- Logic heavily inspired by https://github.com/Sinomor/dotfiles/blob/e409f9a84bf40daf1e39c0179ec749232ed827c9/home/.config/awesome/ui/control/moment/calendar.lua#L134-L173 function calendar:compute_grid(year, month) - local calendar_table = { {}, {}, {}, {}, {}, {} } + local calendar_table = { + { + { "Mo", false, true }, + { "Tu", false, true }, + { "We", false, true }, + { "Th", false, true }, + { "Fr", false, true }, + { "Sa", false, true }, + { "Su", false, true }, + }, + {}, + {}, + {}, + {}, + {}, + {}, + } local current = os.date("*t", os.time()) local first_day = os.date("*t", os.time { year = year, month = month, day = 1 }) local last_day = os.date("*t", os.time { year = year, month = month + 1, day = 0 }) @@ -68,14 +133,14 @@ function calendar:compute_grid(year, month) for offset = prev_offset, 1, -1 do local day = prev_days.day - offset + 1 table.insert( - calendar_table[1], + calendar_table[2], { day, day == current.day and prev_month == current.month and prev_year == current.year, false } ) end end do - local row = 1 + local row = 2 local weekday = weekday_map[first_day.wday] for day = 1, days do table.insert( @@ -108,14 +173,22 @@ function calendar:compute_grid(year, month) for i, row in ipairs(calendar_table) do for j, col in ipairs(row) do - local widget = self:get_children_by_id("grid")[1]:get_widgets_at(i + 1, j)[1] - widget.widget.widget.text = col[1] + local widget = grid:get_widgets_at(i, j)[1] + widget.widget.widget.widget.text = col[1] if col[2] then - widget.bg = qvars.colors.yellow - widget.fg = qvars.colors.bg + widget.bg = qcolor.palette.yellow() + widget.fg = qcolor.palette.bg() + elseif j % 6 == 0 or j % 7 == 0 then + widget.bg = col[3] and qcolor.palette.bg.highest or qcolor.palette.bg.high else - widget.bg = qvars.colors.bg - widget.fg = col[3] and qvars.colors.fg or qvars.colors.dim.fg + widget.bg = col[3] and qcolor.palette.bg.high or qcolor.palette.bg() + end + + if i == 1 then + widget.fg = qcolor.palette.fg() + elseif not col[2] then + -- widget.fg = col[3] and qcolor.palette.fg() or qcolor.palette.fg.low + widget.fg = qcolor.palette.fg.low end end end @@ -125,32 +198,31 @@ local cells = {} local function cell(content) local widget = wibox.widget { widget = wibox.container.background, - shape = qvars.shape, + bg = qcolor.palette.bg.high, + -- shape = qui.shape, { - widget = wibox.container.place, - forced_height = qvars.char_height * 1.5, - forced_width = qvars.char_height * 1.5, + widget = wibox.container.margin, + margins = qui.PADDING * 1.5, { - widget = wibox.widget.textbox, - markup = content, + widget = wibox.container.place, + forced_height = qui.CHAR_HEIGHT, + forced_width = qui.CHAR_HEIGHT, + { + widget = wibox.widget.textbox, + markup = content, + }, }, }, } table.insert(cells, widget) end -cell "Mo" -cell "Tu" -cell "We" -cell "Th" -cell "Fr" -cell(qmarkup("Sa", { bold = true })) -cell(qmarkup("Su", { bold = true })) - -for _ = 1, 42 do +for _ = 1, 49 do cell() end -calendar:get_children_by_id("grid")[1]:add(table.unpack(cells)) -calendar:compute_grid(2024, 1) +grid:add(table.unpack(cells)) +local current_time = os.date "*t" +calendar:compute_grid(current_time.year, current_time.month) +grid:add_row_border(2, qui.BORDER_WIDTH, { color = qcolor.palette.border() }) return calendar diff --git a/.config/awesome/ui/statusbar/panel/widgets/displays.lua b/.config/awesome/ui/statusbar/panel/widgets/displays.lua index b52c986..bfb9113 100644 --- a/.config/awesome/ui/statusbar/panel/widgets/displays.lua +++ b/.config/awesome/ui/statusbar/panel/widgets/displays.lua @@ -1,37 +1,43 @@ -local backlight = require "services.backlight" +-- local backlight = require "services.backlight" local battery = require "services.battery" local phosphor = require "assets.phosphor" -local qmath = require "quarrel.math" +-- local qmath = require "quarrel.math" +local C = require "ui.statusbar.consts" +local qcolor = require "quarrel.color" local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" local wibox = require "wibox" local function create_display(icon, color) + -- the reason this is done is because ids can't be found with `get_children_by_id` across widget boundaries, + -- so we return it as a second value in order to manipulate the icon + local d_icon = qui.icon { + icon = icon, + color = color, + widget = { + id = "icon", + }, + } + return wibox.widget(qui.styled { widget = wibox.container.background, + bg = qcolor.palette.bg.high, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { widget = wibox.container.place, { { widget = wibox.container.radialprogressbar, max_value = 100, - border_color = qvars.colors.black, + border_color = qcolor.palette.bg.lowest, color = color, - border_width = qvars.border_width * 2, - forced_height = qvars.element_size * 4, - forced_width = qvars.element_size * 4, + border_width = qui.BORDER_WIDTH * 2, + forced_height = C.ELEMENT_SIZE * 4, + forced_width = C.ELEMENT_SIZE * 4, { widget = wibox.container.place, - qui.icon { - icon = icon, - color = color, - widget = { - id = "icon", - }, - }, + d_icon, }, id = "indicator", }, @@ -44,14 +50,15 @@ local function create_display(icon, color) }, }, layout = wibox.layout.fixed.vertical, - spacing = qvars.big_padding, + spacing = qui.BIG_PADDING, }, }, }, - }) + }), + d_icon end -local d_audio = create_display(phosphor.speaker_simple_high_fill, qvars.colors.fg) +local d_audio, d_audio_icon = create_display(phosphor.speaker_simple_high_fill, qcolor.palette.fg()) awesome.connect_signal("services::audio", function(volume) if not volume then return @@ -61,30 +68,33 @@ awesome.connect_signal("services::audio", function(volume) end) awesome.connect_signal("services::audio::icon", function(icon, color) d_audio:get_children_by_id("indicator")[1].color = color - d_audio:get_children_by_id("icon")[1].image = icon - d_audio:get_children_by_id("icon")[1].stylesheet = qui.recolor(color) + d_audio_icon.image = icon + d_audio_icon.stylesheet = qui.recolor(color) end) -local d_battery = create_display(phosphor.battery_vertical_warning_fill, qvars.colors.red) +local d_battery, d_battery_icon = create_display(phosphor.battery_vertical_warning_fill, qcolor.palette.red()) battery:connect_signal("level", function(_, level) d_battery:get_children_by_id("indicator")[1].value = level d_battery:get_children_by_id("text")[1].text = level .. "%" end) battery:connect_signal("icon", function(_, icon, color) d_battery:get_children_by_id("indicator")[1].color = color - d_battery:get_children_by_id("icon")[1].image = icon - d_battery:get_children_by_id("icon")[1].stylesheet = qui.recolor(color) + d_battery_icon.image = icon + d_battery_icon.stylesheet = qui.recolor(color) end) -local d_brightness = create_display(phosphor.sun_fill, qvars.colors.fg) -backlight:connect_signal("value", function(_, brightness) - brightness = math.floor(qmath.translate_range(brightness, 0, 255, 0, 100)) - d_brightness:get_children_by_id("indicator")[1].value = brightness - d_brightness:get_children_by_id("text")[1].text = brightness .. "%" -end) -backlight:connect_signal("icon", function(_, icon, color) - d_brightness:get_children_by_id("icon")[1].image = icon - d_brightness:get_children_by_id("icon")[1].stylesheet = qui.recolor(color) -end) +-- local d_brightness, d_brightness_icon = create_display(phosphor.sun_fill, qcolor.palette.fg()) +-- backlight:connect_signal("value", function(_, brightness) +-- brightness = math.floor(qmath.translate_range(brightness, 0, 255, 0, 100)) +-- d_brightness:get_children_by_id("indicator")[1].value = brightness +-- d_brightness:get_children_by_id("text")[1].text = brightness .. "%" +-- end) +-- backlight:connect_signal("icon", function(_, icon, color) +-- d_brightness_icon.image = icon +-- d_brightness_icon.stylesheet = qui.recolor(color) +-- end) -return { audio = d_audio, battery = d_battery, brightness = d_brightness } +return { + audio = d_audio, + battery = d_battery, --[[brightness = d_brightness]] +} diff --git a/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua b/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua deleted file mode 100644 index 5caadc5..0000000 --- a/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua +++ /dev/null @@ -1,720 +0,0 @@ ---------------------------------------------------------------------------- --- A widget to display an image. --- --- The `wibox.widget.imagebox` is part of the Awesome WM's widget system --- (see @{03-declarative-layout.md}). --- --- This widget displays an image. The image can be a file, --- a cairo image surface, or an rsvg handle object (see the --- [image property](#image)). --- --- Examples using a `wibox.widget.imagebox`: --- --- --- --- @DOC_wibox_widget_defaults_imagebox_EXAMPLE@ --- --- Alternatively, you can declare the `imagebox` widget using the --- declarative pattern (both variants are strictly equivalent): --- --- @DOC_wibox_widget_declarative-pattern_imagebox_EXAMPLE@ --- --- @author Uli Schlachter --- @copyright 2010 Uli Schlachter --- @widgetmod wibox.widget.imagebox --- @supermodule wibox.widget.base ---------------------------------------------------------------------------- - -local lgi = require "lgi" -local cairo = lgi.cairo - -local base = require "wibox.widget.base" -local gdebug = require "gears.debug" -local gtable = require "gears.table" -local surface = require "gears.surface" -local setmetatable = setmetatable -local type = type -local math = math - -local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) - --- Safe load for optional Rsvg module -local Rsvg = nil -do - local success, err = pcall(function() - Rsvg = lgi.Rsvg - end) - if not success then - gdebug.print_warning(debug.traceback("Could not load Rsvg: " .. tostring(err))) - end -end - -local imagebox = { mt = {} } - -local rsvg_handle_cache = setmetatable({}, { __mode = "k" }) - ----Load rsvg handle form image file --- @tparam string file Path to svg file. --- @return Rsvg handle --- @treturn table A table where cached data can be stored. -local function load_rsvg_handle(file) - if not Rsvg then - return - end - - local cache = (rsvg_handle_cache[file] or {})["handle"] - - if cache then - return cache, rsvg_handle_cache[file] - end - - local handle, err - - if file:match "<[?]?xml" or file:match "<svg" then - handle, err = Rsvg.Handle.new_from_data(file) - else - handle, err = Rsvg.Handle.new_from_file(file) - end - - if not err then - rsvg_handle_cache[file] = rsvg_handle_cache[file] or {} - rsvg_handle_cache[file]["handle"] = handle - return handle, rsvg_handle_cache[file] - end -end - ----Apply cairo surface for given imagebox widget -local function set_surface(ib, surf) - local is_surf_valid = surf.width > 0 and surf.height > 0 - if not is_surf_valid then - return false - end - - ib._private.default = { width = surf.width, height = surf.height } - ib._private.handle = nil - ib._private.image = surf - return true -end - ----Apply RsvgHandle for given imagebox widget -local function set_handle(ib, handle, cache) - local dim = handle:get_dimensions() - local is_handle_valid = dim.width > 0 and dim.height > 0 - if not is_handle_valid then - return false - end - - ib._private.default = { width = dim.width, height = dim.height } - ib._private.handle = handle - ib._private.cache = cache - ib._private.image = nil - - return true -end - ----Try to load some image object from file then apply it to imagebox. ----@tparam table ib Imagebox ----@tparam string file Image file name ----@tparam function image_loader Function to load image object from file ----@tparam function image_setter Function to set image object to imagebox ----@treturn boolean True if image was successfully applied -local function load_and_apply(ib, file, image_loader, image_setter) - local image_applied - local object, cache = image_loader(file) - - if object then - image_applied = image_setter(ib, object, cache) - end - return image_applied -end - ----Update the cached size depending on the stylesheet and dpi. --- --- It's necessary because a single RSVG handle can be used by --- many imageboxes. So DPI and Stylesheet need to be set each time. -local function update_dpi(self, ctx) - if not self._private.handle then - return - end - - local dpi = self._private.auto_dpi and ctx.dpi or self._private.dpi or nil - - local need_dpi = dpi and self._private.last_dpi ~= dpi - - local need_style = self._private.handle.set_stylesheet and self._private.stylesheet - - local old_size = self._private.default and self._private.default.width - - if dpi and dpi ~= self._private.cache.dpi then - if type(dpi) == "table" then - self._private.handle:set_dpi_x_y(dpi.x, dpi.y) - else - self._private.handle:set_dpi(dpi) - end - end - - if need_style and self._private.cache.stylesheet ~= self._private.stylesheet then - self._private.handle:set_stylesheet(self._private.stylesheet) - end - - -- Reload the size. - if need_dpi or (need_style and self._private.stylesheet ~= self._private.last_stylesheet) then - set_handle(self, self._private.handle, self._private.cache) - end - - self._private.last_dpi = dpi - self._private.cache.dpi = dpi - self._private.last_stylesheet = self._private.stylesheet - self._private.cache.stylesheet = self._private.stylesheet - - -- This can happen in the constructor when `dpi` is set after `image`. - if old_size and old_size ~= self._private.default.width then - self:emit_signal "widget::redraw_needed" - self:emit_signal "widget::layout_changed" - end -end - --- Draw an imagebox with the given cairo context in the given geometry. -function imagebox:draw(ctx, cr, width, height) - if width == 0 or height == 0 or not self._private.default then - return - end - - -- For valign = "top" and halign = "left" - local translate = { - x = 0, - y = 0, - } - - update_dpi(self, ctx) - - local w, h = self._private.default.width, self._private.default.height - - if self._private.resize then - -- That's for the "fit" policy. - local aspects = { - w = width / w, - h = height / h, - } - - local policy = { - w = self._private.horizontal_fit_policy or "auto", - h = self._private.vertical_fit_policy or "auto", - } - - for _, aspect in ipairs { "w", "h" } do - if self._private.upscale == false and (w < width and h < height) then - aspects[aspect] = 1 - elseif self._private.downscale == false and (w >= width and h >= height) then - aspects[aspect] = 1 - elseif policy[aspect] == "none" then - aspects[aspect] = 1 - elseif policy[aspect] == "auto" then - aspects[aspect] = math.min(width / w, height / h) - elseif policy[aspect] == "cover" then - aspects[aspect] = math.max(width / w, height / h) - end - end - - if self._private.halign == "center" then - translate.x = math.floor((width - w * aspects.w) / 2) - elseif self._private.halign == "right" then - translate.x = math.floor(width - (w * aspects.w)) - end - - if self._private.valign == "center" then - translate.y = math.floor((height - h * aspects.h) / 2) - elseif self._private.valign == "bottom" then - translate.y = math.floor(height - (h * aspects.h)) - end - - cr:translate(translate.x, translate.y) - - -- Before using the scale, make sure it is below the threshold. - local threshold, max_factor = self._private.max_scaling_factor, math.max(aspects.w, aspects.h) - - if threshold and threshold > 0 and threshold < max_factor then - aspects.w = (aspects.w * threshold) / max_factor - aspects.h = (aspects.h * threshold) / max_factor - end - - -- Set the clip - if self._private.clip_shape then - cr:clip(self._private.clip_shape(cr, w * aspects.w, h * aspects.h, unpack(self._private.clip_args))) - end - - cr:scale(aspects.w, aspects.h) - else - if self._private.halign == "center" then - translate.x = math.floor((width - w) / 2) - elseif self._private.halign == "right" then - translate.x = math.floor(width - w) - end - - if self._private.valign == "center" then - translate.y = math.floor((height - h) / 2) - elseif self._private.valign == "bottom" then - translate.y = math.floor(height - h) - end - - cr:translate(translate.x, translate.y) - - -- Set the clip - if self._private.clip_shape then - cr:clip(self._private.clip_shape(cr, w, h, unpack(self._private.clip_args))) - end - end - - if self._private.handle then - self._private.handle:render_cairo(cr) - else - cr:set_source_surface(self._private.image, 0, 0) - - local filter = self._private.scaling_quality - - if filter then - cr:get_source():set_filter(cairo.Filter[filter:upper()]) - end - - cr:paint() - end -end - --- Fit the imagebox into the given geometry -function imagebox:fit(ctx, width, height) - if not self._private.default then - return 0, 0 - end - - update_dpi(self, ctx) - - local w, h = self._private.default.width, self._private.default.height - - if w <= width and h <= height and self._private.upscale == false then - return w, h - end - - if (w < width or h < height) and self._private.downscale == false then - return w, h - end - - if self._private.resize or w > width or h > height then - local aspect = math.min(width / w, height / h) - return w * aspect, h * aspect - end - - return w, h -end - ---- The image rendered by the `imagebox`. --- --- @property image --- @tparam[opt=nil] image|nil image --- @propemits false false - ---- Set the `imagebox` image. --- --- The image can be a file, a cairo image surface, or an rsvg handle object --- (see the [image property](#image)). --- @method set_image --- @hidden --- @tparam image image The image to render. --- @treturn boolean `true` on success, `false` if the image cannot be used. --- @usage my_imagebox:set_image(beautiful.awesome_icon) --- @usage my_imagebox:set_image('/usr/share/icons/theme/my_icon.png') --- @see image -function imagebox:set_image(image) - local setup_succeed - - -- Keep the original to prevent the cache from being GCed. - self._private.original_image = image - - if type(image) == "userdata" and not (Rsvg and Rsvg.Handle:is_type_of(image)) then - -- This function is not documented to handle userdata objects, but - -- historically it did, and it did by just assuming they refer to a - -- cairo surface. - image = surface.load(image) - end - - if type(image) == "string" then - -- try to load rsvg handle from file - setup_succeed = load_and_apply(self, image, load_rsvg_handle, set_handle) - - if not setup_succeed then - -- rsvg handle failed, try to load cairo surface with pixbuf - setup_succeed = load_and_apply(self, image, surface.load, set_surface) - end - elseif Rsvg and Rsvg.Handle:is_type_of(image) then - -- try to apply given rsvg handle - rsvg_handle_cache[image] = rsvg_handle_cache[image] or {} - setup_succeed = set_handle(self, image, rsvg_handle_cache[image]) - elseif cairo.Surface:is_type_of(image) then - -- try to apply given cairo surface - setup_succeed = set_surface(self, image) - elseif not image then - -- nil as argument mean full imagebox reset - setup_succeed = true - self._private.handle = nil - self._private.image = nil - self._private.default = nil - end - - if not setup_succeed then - return false - end - - self:emit_signal "widget::redraw_needed" - self:emit_signal "widget::layout_changed" - self:emit_signal "property::image" - return true -end - ---- Set a clip shape for this imagebox. --- --- A clip shape defines an area and dimension to which the content should be --- trimmed. --- --- @DOC_wibox_widget_imagebox_clip_shape_EXAMPLE@ --- --- @property clip_shape --- @tparam[opt=gears.shape.rectangle] shape clip_shape A `gears.shape` compatible shape function. --- @propemits true false --- @see gears.shape - ---- Set a clip shape for this imagebox. --- --- A clip shape defines an area and dimensions to which the content should be --- trimmed. --- --- Additional parameters will be passed to the clip shape function. --- --- @tparam function|gears.shape clip_shape A `gears_shape` compatible shape function. --- @method set_clip_shape --- @hidden --- @see gears.shape --- @see clip_shape -function imagebox:set_clip_shape(clip_shape, ...) - self._private.clip_shape = clip_shape - self._private.clip_args = { ... } - self:emit_signal "widget::redraw_needed" - self:emit_signal("property::clip_shape", clip_shape) -end - ---- Should the image be resized to fit into the available space? --- --- Note that `upscale` and `downscale` can affect the value of `resize`. --- If conflicting values are passed to the constructor, then the result --- is undefined. --- --- @DOC_wibox_widget_imagebox_resize_EXAMPLE@ --- @property resize --- @propemits true false --- @tparam[opt=true] boolean resize - ---- Allow the image to be upscaled (made bigger). --- --- Note that `upscale` and `downscale` can affect the value of `resize`. --- If conflicting values are passed to the constructor, then the result --- is undefined. --- --- @DOC_wibox_widget_imagebox_upscale_EXAMPLE@ --- @property upscale --- @tparam[opt=self.resize] boolean upscale --- @see downscale --- @see resize - ---- Allow the image to be downscaled (made smaller). --- --- Note that `upscale` and `downscale` can affect the value of `resize`. --- If conflicting values are passed to the constructor, then the result --- is undefined. --- --- @DOC_wibox_widget_imagebox_downscale_EXAMPLE@ --- @property downscale --- @tparam[opt=self.resize] boolean downscale --- @see upscale --- @see resize - ---- Set the SVG CSS stylesheet. --- --- If the image is an SVG (vector graphics), this property allows to set --- a CSS stylesheet. It can be used to set colors and much more. --- --- Note that this property is a string, not a path. If the stylesheet is --- stored on disk, read the content first. --- ---@DOC_wibox_widget_imagebox_stylesheet_EXAMPLE@ --- --- @property stylesheet --- @tparam[opt=""] string stylesheet --- @propemits true false - ---- Set the SVG DPI (dot per inch). --- --- Force a specific DPI when rendering the `.svg`. For other file formats, --- this does nothing. --- --- It can either be a number of a table containing the `x` and `y` keys. --- --- Please note that DPI and `resize` can "fight" each other and end up --- making the image smaller instead of bigger. --- ---@DOC_wibox_widget_imagebox_dpi_EXAMPLE@ --- --- @property dpi --- @tparam[opt=96] number|table dpi --- @negativeallowed false --- @propemits true false --- @see auto_dpi - ---- Use the object DPI when rendering the SVG. --- --- By default, the SVG are interpreted as-is. When this property is set, --- the screen DPI will be passed to the SVG renderer. Depending on which --- tool was used to create the `.svg`, this may do nothing at all. However, --- for example, if the `.svg` uses `<text>` elements and doesn't have an --- hardcoded stylesheet, the result will differ. --- --- @property auto_dpi --- @tparam[opt=false] boolean auto_dpi --- @propemits true false --- @see dpi - -for _, prop in ipairs { "stylesheet", "dpi", "auto_dpi" } do - imagebox["set_" .. prop] = function(self, value) - -- It will be set in :fit and :draw. The handle is shared - -- by multiple imagebox, so it cannot be set just once. - self._private[prop] = value - - self:emit_signal "widget::redraw_needed" - self:emit_signal "widget::layout_changed" - self:emit_signal("property::" .. prop) - end -end - -function imagebox:set_resize(allowed) - self._private.resize = allowed - - if allowed then - self._private.downscale = true - self._private.upscale = true - self:emit_signal("property::downscale", allowed) - self:emit_signal("property::upscale", allowed) - end - - self:emit_signal "widget::redraw_needed" - self:emit_signal "widget::layout_changed" - self:emit_signal("property::resize", allowed) -end - -for _, prop in ipairs { "downscale", "upscale" } do - imagebox["set_" .. prop] = function(self, allowed) - self._private[prop] = allowed - - if self._private.resize ~= (self._private.upscale or self._private.downscale) then - self._private.resize = self._private.upscale or self._private.downscale - self:emit_signal("property::resize", self._private.resize) - end - - self:emit_signal "widget::redraw_needed" - self:emit_signal "widget::layout_changed" - self:emit_signal("property::" .. prop, allowed) - end -end - ---- Set the horizontal fit policy. --- --- Here is the result for a 22x32 image: --- --- @DOC_wibox_widget_imagebox_horizontal_fit_policy_EXAMPLE@ --- --- @property horizontal_fit_policy --- @tparam[opt="auto"] string horizontal_fit_policy --- @propertyvalue "auto" Honor the `resize` variable and preserve the aspect ratio. --- @propertyvalue "none" Do not resize at all. --- @propertyvalue "fit" Resize to the widget width. --- @propertyvalue "cover" Resize to fill widget and preserve the aspect ratio. --- @propemits true false --- @see vertical_fit_policy --- @see resize - ---- Set the vertical fit policy. --- --- Here is the result for a 32x22 image: --- --- @DOC_wibox_widget_imagebox_vertical_fit_policy_EXAMPLE@ --- --- @property vertical_fit_policy --- @tparam[opt="auto"] string vertical_fit_policy --- @propertyvalue "auto" Honor the `resize` variable and preserve the aspect ratio. --- @propertyvalue "none" Do not resize at all. --- @propertyvalue "fit" Resize to the widget height. --- @propertyvalue "cover" Resize to fill widget and preserve the aspect ratio. --- @propemits true false --- @see horizontal_fit_policy --- @see resize - ---- The vertical alignment. --- --- @DOC_wibox_widget_imagebox_valign_EXAMPLE@ --- --- @property valign --- @tparam[opt="center"] string valign --- @propertyvalue "top" --- @propertyvalue "center" --- @propertyvalue "bottom" --- @propemits true false --- @see wibox.container.place --- @see halign - ---- The horizontal alignment. --- --- @DOC_wibox_widget_imagebox_halign_EXAMPLE@ --- --- @property halign --- @tparam[opt="center"] string halign --- @propertyvalue "left" --- @propertyvalue "center" --- @propertyvalue "right" --- @propemits true false --- @see wibox.container.place --- @see valign - ---- The maximum scaling factor. --- --- If an image is scaled too much, it gets very blurry. This --- property allows to limit the scaling. --- Use the properties `valign` and `halign` to control how the image will be --- aligned. --- --- In the example below, the original size is 22x22 --- --- @DOC_wibox_widget_imagebox_max_scaling_factor_EXAMPLE@ --- --- @property max_scaling_factor --- @tparam[opt=0] number max_scaling_factor Use `0` for "no limit". --- @negativeallowed false --- @propemits true false --- @see valign --- @see halign --- @see scaling_quality - ---- Set the scaling aligorithm. --- --- Depending on how the image is used, what is the "correct" way to --- scale can change. For example, upscaling a pixel art image should --- not make it blurry. However, scaling up a photo should not make it --- blocky. --- ---<table class='widget_list' border=1> --- <tr style='font-weight: bold;'> --- <th align='center'>Value</th> --- <th align='center'>Description</th> --- </tr> --- <tr><td>fast</td><td>A high-performance filter</td></tr> --- <tr><td>good</td><td>A reasonable-performance filter</td></tr> --- <tr><td>best</td><td>The highest-quality available</td></tr> --- <tr><td>nearest</td><td>Nearest-neighbor filtering (blocky)</td></tr> --- <tr><td>bilinear</td><td>Linear interpolation in two dimensions</td></tr> ---</table> --- --- The image used in the example below has a resolution of 32x22 and is --- intentionally blocky to highlight the difference. --- It is zoomed by a factor of 3. --- --- @DOC_wibox_widget_imagebox_scaling_quality_EXAMPLE@ --- --- @property scaling_quality --- @tparam[opt="good"] string scaling_quality --- @propertyvalue "fast" A high-performance filter. --- @propertyvalue "good" A reasonable-performance filter. --- @propertyvalue "best" The highest-quality available. --- @propertyvalue "nearest" Nearest-neighbor filtering (blocky). --- @propertyvalue "bilinear" Linear interpolation in two dimensions. --- @propemits true false --- @see resize --- @see horizontal_fit_policy --- @see vertical_fit_policy --- @see max_scaling_factor - -local defaults = { - halign = "left", - valign = "top", - horizontal_fit_policy = "auto", - vertical_fit_policy = "auto", - max_scaling_factor = 0, - scaling_quality = "good", -} - -local function get_default(prop, value) - if value == nil then - return defaults[prop] - end - - return value -end - -for prop in pairs(defaults) do - imagebox["set_" .. prop] = function(self, value) - if value == self._private[prop] then - return - end - - self._private[prop] = get_default(prop, value) - self:emit_signal "widget::redraw_needed" - self:emit_signal("property::" .. prop, self._private[prop]) - end - - imagebox["get_" .. prop] = function(self) - if self._private[prop] == nil then - return defaults[prop] - end - - return self._private[prop] - end -end - ---- Returns a new `wibox.widget.imagebox` instance. --- --- This is the constructor of `wibox.widget.imagebox`. It creates a new --- instance of imagebox widget. --- --- Alternatively, the declarative layout syntax can handle --- `wibox.widget.imagebox` instanciation. --- --- The image can be a file, a cairo image surface, or an rsvg handle object --- (see the [image property](#image)). --- --- Any additional arguments will be passed to the clip shape function. --- @tparam[opt] image image The image to display (may be `nil`). --- @tparam[opt] boolean resize_allowed If `false`, the image will be --- clipped, else it will be resized to fit into the available space. --- @tparam[opt] function clip_shape A `gears.shape` compatible function. --- @treturn wibox.widget.imagebox A new `wibox.widget.imagebox` widget instance. --- @constructorfct wibox.widget.imagebox -local function new(image, resize_allowed, clip_shape, ...) - local ret = base.make_widget(nil, nil, { enable_properties = true }) - - gtable.crush(ret, imagebox, true) - ret._private.resize = true - - if image then - ret:set_image(image) - end - - if resize_allowed ~= nil then - ret.resize = resize_allowed - end - - ret._private.clip_shape = clip_shape - ret._private.clip_args = { ... } - - return ret -end - -function imagebox.mt:__call(...) - return new(...) -end - -return setmetatable(imagebox, imagebox.mt) - --- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/.config/awesome/ui/statusbar/panel/widgets/mpris.lua b/.config/awesome/ui/statusbar/panel/widgets/mpris.lua new file mode 100644 index 0000000..c27bf66 --- /dev/null +++ b/.config/awesome/ui/statusbar/panel/widgets/mpris.lua @@ -0,0 +1,429 @@ +local custom = require "assets.custom" +local gdebug = require "gears.debug" +local phosphor = require "assets.phosphor" +local playerctl = require "services.playerctl" +local qcolor = require "quarrel.color" +local qpersistent = require "quarrel.persistent" +local qui = require "quarrel.ui" +local qvars = require "quarrel.vars" +local simpleicons = require "assets.simpleicons" +local wibox = require "wibox" + +local M = {} + +local client_icons = { -- this is used to map the mpris clients to icons + firefox = simpleicons.librewolf, -- librewolf uses the same player id as firefox + spotify = simpleicons.spotify, + mpd = custom.vinyl_record_fill, + __generic = phosphor.waveform_fill, -- used for any client not in the map +} + +local DEFAULTS = { + ---@type string + title = "Nothing's playing", + artist = {}, + ---@type string + album = "", + progresstext = { + ---@type string + position = "~", + ---@type string + length = "~", + }, + ---@type number + position = 0, + ---@type number + length = math.huge, + client_icon = client_icons.__generic, + art = phosphor.music_notes_fill, +} + +local bg_icon = qui.icon { + icon = DEFAULTS.client_icon, + widget = { + forced_width = 0, + forced_height = 0, + }, + color = qcolor.palette.bg.highest, +} + +local function to_hms(time) + local format = "%i:%02i" + + local h = math.floor(time / 60 ^ 2) + local m = math.floor((time / 60) % 60) + local s = math.floor(time % 60) + + if h > 0 then + format = "%i:" .. format + return string.format(format, h, m, s) + end + + return string.format(format, m, s) +end + +local function is_empty(str) + return (str == nil or #str == 0) +end + +--- mirror of playerctl:list() +--- why bother mirroring? simple: we need to track changes to the player list +--- but vanished and appeared simply don't give us an old version to compare with +---@type Playerctl.data[] +local players = {} + +---@type number +M.active_player_index = qpersistent.get "active_player_index" --[[@as number]] +if not M.active_player_index then + M.active_player_index = 1 + gdebug.print_error "failed to get active_player_index from qpersistent, falling back..." + qpersistent.store("active_player_index", M.active_player_index) +end + +--- the reason we do this instead of some other hack is this is the only way to draw the icons *without* resizing the mpris container +--- this happens because in the panel, height is unlimited, and imagebox grows until a hard limit +---@class wibox.widget.base +local client_background = wibox.container.background() + +function client_background:before_draw_children(_, cr, width, height) + cr:save() + cr:translate(width - (height / 1.25), -(height * 0.125)) + ---@diagnostic disable-next-line: missing-parameter + wibox.widget.draw_to_cairo_context(bg_icon, cr, height * 1.25, height * 1.25) + cr:restore() +end + +M.widget = wibox.widget(qui.styled { + widget = wibox.container.background, + bg = qcolor.palette.bg.high, + { + widget = client_background, + { + widget = wibox.container.margin, + margins = qui.BIG_PADDING, + { + nil, + { + { + { + widget = wibox.container.background, + bg = qcolor.palette.bg(), + shape = qui.shape, + { + widget = wibox.widget.imagebox, + image = DEFAULTS.art, + forced_height = qui.CHAR_HEIGHT * 5, + forced_width = qui.CHAR_HEIGHT * 5, + valign = "center", + halign = "center", + stylesheet = qui.recolor(qcolor.palette.bg.highest), + id = "cover", + }, + }, + { + widget = wibox.container.margin, + right = qui.BIG_PADDING, + left = qui.BIG_PADDING, + { + { + widget = wibox.container.constraint, + height = qui.CHAR_HEIGHT * 2.5, + strategy = "max", + { + widget = wibox.widget.textbox, + text = DEFAULTS.title, -- Song + id = "song", + valign = "top", + }, + }, + { + widget = wibox.container.constraint, + height = qui.CHAR_HEIGHT * 2.5, + strategy = "max", + { + widget = wibox.container.background, + fg = qcolor.palette.fg.low, + { + widget = wibox.widget.textbox, + text = DEFAULTS.artist_album, -- Artist - Album Name + id = "artist_album", + valign = "top", + }, + }, + }, + layout = wibox.layout.fixed.vertical, + }, + }, + layout = wibox.layout.fixed.horizontal, + }, + nil, + { + widget = wibox.container.margin, + top = qui.BIG_PADDING, + right = qui.BIG_PADDING, + { + { + widget = wibox.widget.textbox, + text = DEFAULTS.progresstext.position .. " / " .. DEFAULTS.progresstext.length, -- position / length + id = "progresstext", + }, + { + widget = wibox.container.place, + { + widget = wibox.widget.progressbar, + forced_height = qui.PADDING, + color = qcolor.palette.yellow(), + value = DEFAULTS.position, + max_value = DEFAULTS.length, + background_color = qcolor.palette.bg.lowest, + bar_shape = qui.shape, + shape = qui.shape, + id = "progressbar", + }, + }, + layout = wibox.layout.fixed.horizontal, + spacing = qui.BIG_PADDING, + }, + }, + layout = wibox.layout.align.vertical, + }, + { + layout = wibox.layout.flex.vertical, + spacing = qui.BIG_PADDING, + id = "client_list", + }, + layout = wibox.layout.align.horizontal, + }, + }, + }, +}) + +local layout = M.widget:get_children_by_id("client_list")[1] --[[@as wibox.layout.flex]] +local progressbar = M.widget:get_children_by_id("progressbar")[1] --[[@as wibox.widget.progressbar]] +local progresstext = M.widget:get_children_by_id("progresstext")[1] --[[@as wibox.widget.textbox]] +local song = M.widget:get_children_by_id("song")[1] --[[@as wibox.widget.textbox]] +local artist_album = M.widget:get_children_by_id("artist_album")[1] --[[@as wibox.widget.textbox]] +local cover = M.widget:get_children_by_id("cover")[1] --[[@as wibox.widget.imagebox]] + +local function mirror_player_list() + players = playerctl:list() +end + +mirror_player_list() + +local function handle_metadata(_, player) + local title, album, artist, art + if player ~= playerctl:list()[M.active_player_index] then + return + end + if player then + if player.metadata.title then + title = player.metadata.title + else + title = DEFAULTS.title + end + + if player.metadata.album then + album = player.metadata.album + else + album = DEFAULTS.album + end + + if player.metadata.artist then + artist = player.metadata.artist + else + artist = DEFAULTS.artist + end + + if player.metadata.art and player.metadata.art:match "^file://" then + art = player.metadata.art:gsub("^file://", "") + else + art = DEFAULTS.art + end + else + title = DEFAULTS.title + album = DEFAULTS.album + artist = DEFAULTS.artist + art = DEFAULTS.art + end + + artist_album.text = table.concat(artist, ", ") .. ((is_empty(artist) or is_empty(album)) and "" or " - ") .. album + song.text = title + ---@diagnostic disable-next-line:inject-field + cover.image = art +end + +local function handle_position(_, player) + if player ~= playerctl:list()[M.active_player_index] then + return + end + local position, length + local content = "" + + if player then + if player.position then + position = player.position / playerctl.unit + content = content .. to_hms(position) + else + position = DEFAULTS.position + content = content .. DEFAULTS.progresstext.position + end + + content = content .. " / " + + if player.metadata.length then + length = player.metadata.length / playerctl.unit + content = content .. to_hms(length) + else + length = DEFAULTS.length + content = content .. DEFAULTS.progresstext.length + end + else + position = DEFAULTS.position + length = DEFAULTS.length + content = DEFAULTS.progresstext.position .. " / " .. DEFAULTS.progresstext.length + end + + progresstext.text = content + ---@diagnostic disable-next-line:inject-field + progressbar.value = position + ---@diagnostic disable-next-line:inject-field + progressbar.max_value = length +end + +playerctl:connect_signal("player::metadata", handle_metadata) +playerctl:connect_signal("player::position", handle_position) + +local function update_player(player) + handle_metadata(nil, player) + handle_position(nil, player) + qpersistent.store("active_player_index", M.active_player_index) + + local client_icon = client_icons[(player or { name = "__generic" }).name] + bg_icon.image = client_icon or client_icons.__generic + client_background:emit_signal "widget::redraw_needed" + + for i, child in ipairs(layout.children) do + ---@diagnostic disable-next-line:undefined-field + child:index_handler(i) + end +end + +function M.next_player() + local players_length = #layout.children + if players_length == 0 then + return + end + + if M.active_player_index + 1 > players_length then + M.active_player_index = 1 + else + M.active_player_index = M.active_player_index + 1 + end + + update_player(playerctl:list()[M.active_player_index]) +end + +function M.previous_player() + local players_length = #layout.children + if players_length == 0 then + return + end + + if M.active_player_index - 1 < 1 then + M.active_player_index = players_length + else + M.active_player_index = M.active_player_index - 1 + end + + update_player(playerctl:list()[M.active_player_index]) +end + +---@param diff_player Playerctl.data +local function recalculate_active_player(diff_player, vanished) + if type(diff_player) ~= "table" then + return + end + if #layout.children == 0 then + M.active_player_index = 1 + update_player() + return + end + + local active_player = players[M.active_player_index] + if not active_player then -- we're recovering from a state with no players + update_player(diff_player) + return + end + + if diff_player.instance == active_player.instance and vanished then -- active player vanished; fall back to previous player + M.previous_player() + else -- non-active player appeared/vanished; try to find active player + for i, p in ipairs(playerctl:list()) do + if p.instance == active_player.instance then + M.active_player_index = i + update_player(p) + return + end + end + + gdebug.print_warning( + "failed to find active player:\n " .. gdebug.dump_return(active_player, nil, 2):gsub("\n", "\n ") + ) + M.active_player_index = 1 + update_player(playerctl:list()[M.active_player_index]) + end +end + +local function register_player(player) + local widget = wibox.widget { + widget = wibox.container.constraint, + width = qui.PADDING, + strategy = "min", + { + widget = wibox.container.background, + shape = qui.shape, + bg = qcolor.palette.bg.lowest, + }, + index_handler = function(self, index) + if M.active_player_index == index then + self.widget.bg = qcolor.palette.yellow() + else + self.widget.bg = qcolor.palette.bg.lowest + end + end, + } + + ---@diagnostic disable-next-line:undefined-field + layout:add(widget) + + recalculate_active_player(player, false) + mirror_player_list() +end + +local function unregister_player(player) + ---@diagnostic disable-next-line:undefined-field + layout:remove(#layout.children) + recalculate_active_player(player, true) + mirror_player_list() +end + +for _, player in ipairs(playerctl:list()) do + register_player(player) +end + +-- recover state +---@diagnostic disable-next-line:undefined-field +local last_active_player = playerctl:list()[M.active_player_index] + +update_player(last_active_player) + +playerctl:connect_signal("player::appeared", function(_, player) + register_player(player) +end) + +playerctl:connect_signal("player::vanished", function(_, player) + unregister_player(player) +end) + +return M diff --git a/.config/awesome/ui/statusbar/panel/widgets/music.lua b/.config/awesome/ui/statusbar/panel/widgets/music.old.lua index eea7335..5631f66 100644 --- a/.config/awesome/ui/statusbar/panel/widgets/music.lua +++ b/.config/awesome/ui/statusbar/panel/widgets/music.old.lua @@ -1,3 +1,4 @@ +-- TODO: update to the new color format local qnative = require "quarrel.native" local qui = require "quarrel.ui" local qvars = require "quarrel.vars" @@ -12,7 +13,7 @@ local w_title = wibox.widget { local w_artist = wibox.widget { widget = wibox.container.background, - fg = qvars.colors.dim.fg, + fg = qcolor.palette.dim.fg, { widget = wibox.widget.textbox, text = "", @@ -23,35 +24,35 @@ local w_progress_bar = wibox.widget { widget = wibox.widget.progressbar, max_value = 0, value = 0, - forced_height = qvars.char_height / 2, + forced_height = qui.CHAR_HEIGHT / 2, forced_width = qvars.expanded_bar_size - - (qvars.big_padding + qvars.big_padding * 2 + qvars.padding * 2) - - (qvars.char_height / 1.25 + qvars.padding) * 3, - color = qvars.colors.yellow, - background_color = qvars.colors.black, - shape = qvars.shape, + - (qui.BIG_PADDING + qui.BIG_PADDING * 2 + qui.PADDING * 2) + - (qui.CHAR_HEIGHT / 1.25 + qui.PADDING) * 3, + color = qcolor.palette.yellow, + background_color = qcolor.palette.black, + shape = qui.shape, } local music = wibox.widget(qui.styled { widget = wibox.container.background, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { { { widget = wibox.container.background, - bg = qvars.colors.bg, - shape = qvars.shape, + bg = qcolor.palette.bg, + shape = qui.shape, { widget = wibox.container.margin, - margins = qvars.padding, + margins = qui.PADDING, { { widget = wibox.container.constraint, width = qvars.expanded_bar_size - - (qvars.big_padding + qvars.big_padding * 2 + qvars.padding * 2), - height = qvars.char_height, + - (qui.BIG_PADDING + qui.BIG_PADDING * 2 + qui.PADDING * 2), + height = qui.CHAR_HEIGHT, { widget = wibox.container.scroll.horizontal, speed = 50, @@ -62,8 +63,8 @@ local music = wibox.widget(qui.styled { { widget = wibox.container.constraint, width = qvars.expanded_bar_size - - (qvars.big_padding + qvars.big_padding * 2 + qvars.padding * 2), - height = qvars.char_height, + - (qui.BIG_PADDING + qui.BIG_PADDING * 2 + qui.PADDING * 2), + height = qui.CHAR_HEIGHT, { widget = wibox.container.scroll.horizontal, speed = 50, @@ -82,8 +83,8 @@ local music = wibox.widget(qui.styled { nil, { widget = wibox.container.background, - bg = qvars.colors.bg, - shape = qvars.shape, + bg = qcolor.palette.bg, + shape = qui.shape, w_progress_bar, }, layout = wibox.layout.align.vertical, diff --git a/.config/awesome/ui/statusbar/panel/widgets/power_menu.lua b/.config/awesome/ui/statusbar/panel/widgets/power_menu.lua index feea829..01171df 100644 --- a/.config/awesome/ui/statusbar/panel/widgets/power_menu.lua +++ b/.config/awesome/ui/statusbar/panel/widgets/power_menu.lua @@ -1,19 +1,17 @@ local q = require "quarrel" local qbind = require "quarrel.bind" local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" local wibox = require "wibox" return wibox.widget { qui.styled { widget = wibox.container.background, - bg = qvars.colors.black, { widget = wibox.widget.textbox, text = "1", buttons = { - qbind:new { - triggers = qvars.btns.left, + qbind { + triggers = qbind.btns.left, press = function() q.debug "from 1" end, @@ -24,13 +22,12 @@ return wibox.widget { }, qui.styled { widget = wibox.container.background, - bg = qvars.colors.black, { widget = wibox.widget.textbox, text = "2", buttons = { - qbind:new { - triggers = qvars.btns.left, + qbind { + triggers = qbind.btns.left, press = function() q.debug "from 2" end, @@ -41,13 +38,12 @@ return wibox.widget { }, qui.styled { widget = wibox.container.background, - bg = qvars.colors.black, { widget = wibox.widget.textbox, text = "3", buttons = { - qbind:new { - triggers = qvars.btns.left, + qbind { + triggers = qbind.btns.left, press = function() q.debug "from 3" end, diff --git a/.config/awesome/ui/statusbar/panel/widgets/weather.lua b/.config/awesome/ui/statusbar/panel/widgets/weather.lua new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/.config/awesome/ui/statusbar/panel/widgets/weather.lua diff --git a/.config/awesome/ui/statusbar/panel/widgets/wifi.lua b/.config/awesome/ui/statusbar/panel/widgets/wifi.lua index ad2234f..8ccff95 100644 --- a/.config/awesome/ui/statusbar/panel/widgets/wifi.lua +++ b/.config/awesome/ui/statusbar/panel/widgets/wifi.lua @@ -1,27 +1,30 @@ local lgi = require "lgi" local phosphor = require "assets.phosphor" +local qcolor = require "quarrel.color" local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" local wibox = require "wibox" local glib = lgi.GLib +local wifi_icon = qui.icon { + icon = phosphor.wifi_x_fill, + color = qcolor.palette.red(), + widget = { + id = "icon", + }, +} + local wifi = wibox.widget(qui.styled { widget = wibox.container.background, + bg = qcolor.palette.bg.high, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { { widget = wibox.container.place, valign = "center", halign = "center", - qui.icon { - icon = phosphor.wifi_x_fill, - color = qvars.colors.red, - widget = { - id = "icon", - }, - }, + wifi_icon, }, { widget = wibox.widget.textbox, @@ -29,7 +32,7 @@ local wifi = wibox.widget(qui.styled { id = "essid", }, layout = wibox.layout.fixed.horizontal, - spacing = qvars.padding, + spacing = qui.PADDING, }, }, }) @@ -43,8 +46,8 @@ awesome.connect_signal("services::wifi", function(essid, _, connected) end) awesome.connect_signal("services::wifi::icon", function(icon, color) - wifi:get_children_by_id("icon")[1].image = icon - wifi:get_children_by_id("icon")[1].stylesheet = qui.recolor(color) + wifi_icon.image = icon + wifi_icon.stylesheet = qui.recolor(color) end) return wifi diff --git a/.config/awesome/ui/statusbar/widgets/displays.lua b/.config/awesome/ui/statusbar/widgets/displays.lua index 62e4398..96cf84c 100644 --- a/.config/awesome/ui/statusbar/widgets/displays.lua +++ b/.config/awesome/ui/statusbar/widgets/displays.lua @@ -1,7 +1,7 @@ local battery = require "services.battery" local phosphor = require "assets.phosphor" +local qcolor = require "quarrel.color" local qui = require "quarrel.ui" -local qvars = require "quarrel.vars" local wibox = require "wibox" local function create_display(icon, color) @@ -16,28 +16,32 @@ local function create_display(icon, color) } end -local d_battery = create_display(phosphor.battery_vertical_warning_fill, qvars.colors.red) +local d_battery = create_display(phosphor.battery_vertical_warning_fill, qcolor.palette.red()) battery:connect_signal("icon", function(_, icon, color) d_battery.widget.image = icon d_battery.widget.stylesheet = qui.recolor(color) end) -local d_brightness = create_display(phosphor.moon_fill, qvars.colors.fg) -awesome.connect_signal("services::brightness::icon", function(icon, color) - d_brightness.widget.image = icon - d_brightness.widget.stylesheet = qui.recolor(color) -end) +-- local d_brightness = create_display(phosphor.moon_fill, qcolor.palette.fg()) +-- awesome.connect_signal("services::brightness::icon", function(icon, color) +-- d_brightness.widget.image = icon +-- d_brightness.widget.stylesheet = qui.recolor(color) +-- end) -local d_audio = create_display(phosphor.speaker_simple_slash_fill, qvars.colors.red) +local d_audio = create_display(phosphor.speaker_simple_slash_fill, qcolor.palette.red()) awesome.connect_signal("services::audio::icon", function(icon, color) d_audio.widget.image = icon d_audio.widget.stylesheet = qui.recolor(color) end) -local d_wifi = create_display(phosphor.wifi_x_fill, qvars.colors.red) +local d_wifi = create_display(phosphor.wifi_x_fill, qcolor.palette.red()) awesome.connect_signal("services::wifi::icon", function(icon, color) d_wifi.widget.image = icon d_wifi.widget.stylesheet = qui.recolor(color) end) -return { audio = d_audio, battery = d_battery, brightness = d_brightness, wifi = d_wifi } +return { + audio = d_audio, + battery = d_battery, --[[brightness = d_brightness,]] + wifi = d_wifi, +} diff --git a/.config/awesome/ui/statusbar/widgets/taglist.lua b/.config/awesome/ui/statusbar/widgets/taglist.lua index df114df..3b81173 100644 --- a/.config/awesome/ui/statusbar/widgets/taglist.lua +++ b/.config/awesome/ui/statusbar/widgets/taglist.lua @@ -10,7 +10,7 @@ return function(s) screen = s, filter = awful.widget.taglist.filter.all, layout = { - spacing = qvars.padding, + spacing = qui.PADDING, layout = wibox.layout.fixed.vertical, }, widget_template = { @@ -21,8 +21,8 @@ return function(s) self.widget = qui.icon { icon = phosphor[next(tag:clients()) and "circle_fill" or "circle_bold"], widget = { - forced_height = qvars.char_height / 1.5, - forced_width = qvars.char_height / 1.5, + forced_height = qui.CHAR_HEIGHT / 1.5, + forced_width = qui.CHAR_HEIGHT / 1.5, }, } -- self.widget = wibox.widget { @@ -33,44 +33,44 @@ return function(s) if tag.selected then return end - self.widget.stylesheet = qui.recolor(qvars.colors.yellow) + self.widget.stylesheet = qui.recolor(qcolor.palette.yellow()) end) self:connect_signal("mouse::leave", function() if tag.selected then return end - self.widget.stylesheet = qui.recolor(qvars.colors.fg) + self.widget.stylesheet = qui.recolor(qcolor.palette.fg()) end) if tag.selected then - self.widget.stylesheet = qui.recolor(qvars.colors.yellow) + self.widget.stylesheet = qui.recolor(qcolor.palette.yellow()) return end - self.widget.stylesheet = qui.recolor(qvars.colors.fg) + self.widget.stylesheet = qui.recolor(qcolor.palette.fg()) end, update_callback = function(self, tag) self.widget.image = phosphor[next(tag:clients()) and "circle_fill" or "circle_bold"] if tag.selected then - self.widget.stylesheet = qui.recolor(qvars.colors.yellow) + self.widget.stylesheet = qui.recolor(qcolor.palette.yellow()) else - self.widget.stylesheet = qui.recolor(qvars.colors.fg) + self.widget.stylesheet = qui.recolor(qcolor.palette.fg()) end end, }, buttons = { - qbind:new { - triggers = qvars.btns.left, + qbind { + triggers = qbind.btns.left, press = function(t) t:view_only() end, hidden = true, }, - qbind:new { - mods = qvars.mods.M, - triggers = qvars.btns.left, + qbind { + mods = qbind.mods.M, + triggers = qbind.btns.left, press = function(t) if client.focus then client.focus:move_to_tag(t) @@ -78,8 +78,8 @@ return function(s) end, hidden = true, }, - qbind:new { - triggers = qvars.btns.right, + qbind { + triggers = qbind.btns.right, press = awful.tag.viewtoggle, hidden = true, }, diff --git a/.config/awesome/ui/statusbar/widgets/taglist_new.lua b/.config/awesome/ui/statusbar/widgets/taglist_new.lua new file mode 100644 index 0000000..79b8a96 --- /dev/null +++ b/.config/awesome/ui/statusbar/widgets/taglist_new.lua @@ -0,0 +1,147 @@ +local awful = require "awful" +local qbind = require "quarrel.bind" +local qcolor = require "quarrel.color" +local qui = require "quarrel.ui" +local qvars = require "quarrel.vars" +local wibox = require "wibox" + +local empty_indicator = wibox.widget { + widget = wibox.container.place, + { + widget = wibox.container.constraint, + strategy = "exact", + height = qui.PADDING, + width = qui.PADDING, + { + widget = wibox.container.background, + bg = qcolor.palette.border.variant, + shape = qui.shape, + }, + }, +} + +local client_indicator = wibox.widget { + widget = wibox.container.place, + { + widget = wibox.container.constraint, + height = qui.CHAR_HEIGHT, + width = qui.PADDING, + strategy = "exact", + { + widget = wibox.container.background, + shape = qui.shape, + bg = qcolor.palette.fg(), + }, + }, +} + +local function update_tasklist(layout, tag, indicated_empty) + local client_delta = #tag:clients() - #layout.children + if client_delta < 0 then + for _ = 1, -(client_delta + (indicated_empty and 1 or 0)) do + layout:remove(1) + end + elseif client_delta > 0 then + for _ = 1, client_delta do + layout:add(client_indicator) + end + end +end + +return function(s) + return awful.widget.taglist { + screen = s, + filter = awful.widget.taglist.filter.all, + layout = { + spacing = qui.PADDING, + layout = wibox.layout.fixed.vertical, + }, + widget_template = { + widget = wibox.container.constraint, + strategy = "min", + height = qui.CHAR_HEIGHT, + qui.styled { + widget = wibox.container.background, + bg = qcolor.palette.bg.high, + { + widget = wibox.container.margin, + margins = qui.PADDING, + { + layout = wibox.layout.flex.vertical, + spacing = qui.PADDING, + }, + }, + }, + create_callback = function( + self, + tag --[[@as tag]] + ) + if tag.selected then + self.widget.border_color = qcolor.palette.yellow() + end + + if #tag:clients() ~= 0 then + self.widget.widget.spacing = qui.PADDING + if self.indicated_empty then + self.widget.widget.widget:remove(1) + self.indicated_empty = false + end + else + if not self.indicated_empty then + self.widget.widget.widget:add(empty_indicator) + self.indicated_empty = true + end + end + + update_tasklist(self.widget.widget.widget, tag, self.indicated_empty) + end, + update_callback = function(self, tag) + if tag.selected then + self.widget.border_color = qcolor.palette.yellow() + else + self.widget.border_color = qcolor.palette.border() + end + + if #tag:clients() ~= 0 then + self.widget.widget.spacing = qui.PADDING + if self.indicated_empty then + self.widget.widget.widget:remove(1) + self.indicated_empty = false + end + else + self.widget.widget.spacing = nil + if not self.indicated_empty then + self.widget.widget.widget:add(empty_indicator) + self.indicated_empty = true + end + end + + update_tasklist(self.widget.widget.widget, tag, self.indicated_empty) + end, + }, + buttons = { + qbind { + triggers = qbind.btns.left, + press = function(t) + t:view_only() + end, + hidden = true, + }, + qbind { + mods = qbind.mods.M, + triggers = qbind.btns.left, + press = function(t) + if client.focus then + client.focus:move_to_tag(t) + end + end, + hidden = true, + }, + qbind { + triggers = qbind.btns.right, + press = awful.tag.viewtoggle, + hidden = true, + }, + }, + } +end diff --git a/.config/awesome/ui/statusbar/widgets/tasklist.lua b/.config/awesome/ui/statusbar/widgets/tasklist.lua deleted file mode 100644 index 9656185..0000000 --- a/.config/awesome/ui/statusbar/widgets/tasklist.lua +++ /dev/null @@ -1,30 +0,0 @@ -local awful = require "awful" -local gears = require "gears" -local qvars = require "quarrel.vars" -local wibox = require "wibox" - -return function(s) - return awful.widget.tasklist { - screen = s, - filter = awful.widget.tasklist.filter.currenttags, - -- buttons = tasklist_buttons, - layout = { - spacing = qvars.padding, - layout = wibox.layout.flex.vertical, - }, - widget_template = { - widget = wibox.container.place, - valign = "center", - halign = "center", - -- { - -- widget = awful.widget.clienticon, - - -- } - { - widget = awful.widget.clienticon, - forced_width = qvars.char_height, - forced_height = qvars.char_height, - }, - }, - } -end diff --git a/.config/awesome/ui/wicked/consts.lua b/.config/awesome/ui/wicked/consts.lua new file mode 100644 index 0000000..e00cae5 --- /dev/null +++ b/.config/awesome/ui/wicked/consts.lua @@ -0,0 +1,5 @@ +local C = {} + +C.NOTIF_TIMEOUT = 3 + +return C diff --git a/.config/awesome/ui/wicked/init.lua b/.config/awesome/ui/wicked/init.lua index ddbf912..dea28d5 100644 --- a/.config/awesome/ui/wicked/init.lua +++ b/.config/awesome/ui/wicked/init.lua @@ -2,19 +2,22 @@ local awful = require "awful" local beautiful = require "beautiful" local gshape = require "gears.shape" local naughty = require "naughty" +local qanim = require "quarrel.animation" local qui = require "quarrel.ui" local qvars = require "quarrel.vars" local wibox = require "wibox" local rtimed = require("lib.rubato").timed local easing = require("lib.rubato").easing local gtimer = require "gears.timer" +local qcolor = require "quarrel.color" local qmarkup = require "quarrel.markup" -return function(n) - local intertext_margin = (n.title ~= "" or n.message ~= "") and qvars.padding or 0 - local title_height = n.title ~= "" and qvars.char_height or 0 - local message_height = n.message ~= "" and qvars.char_height or 0 - -- local app_name_height = n.app_name ~= "" and +local M = require "ui.wicked.consts" + +function M.new(n) + local intertext_margin = (n.title ~= "" or n.message ~= "") and qui.PADDING or 0 + local title_height = n.title ~= "" and qui.CHAR_HEIGHT or 0 + local message_height = n.message ~= "" and qui.CHAR_HEIGHT or 0 local app_name if n.app_name == "" then app_name = n._private._foreign and "Unknown" or "Awesome" @@ -29,22 +32,19 @@ return function(n) margins = beautiful.useless_gap * 2, }) end, - bg = qvars.colors.transparent, + bg = qcolor.palette.transparent, border_width = 0, shape = gshape.rectangle, widget_template = { widget = wibox.container.constraint, - height = qvars.big_padding * 2 - + qvars.char_height - + qvars.border_width - + qvars.big_padding * 2 + height = qui.BIG_PADDING * 2 + + qui.CHAR_HEIGHT + + qui.BORDER_WIDTH + + qui.BIG_PADDING * 2 -- + title_height -- + message_height -- + intertext_margin, - + ( - n.icon and qvars.char_height * 2 + qvars.padding - or (title_height + message_height + intertext_margin) - ), + + (n.icon and qui.CHAR_HEIGHT * 2 + qui.PADDING or (title_height + message_height + intertext_margin)), strategy = "exact", { @@ -59,7 +59,7 @@ return function(n) { { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { { widget = wibox.widget.textbox, @@ -69,12 +69,12 @@ return function(n) { widget = wibox.container.radialprogressbar, - max_value = qvars.notif_timeout, - border_color = qvars.colors.black, - color = qvars.colors.yellow, - border_width = qvars.border_width, - forced_height = qvars.char_height, - forced_width = qvars.char_height, + max_value = M.NOTIF_TIMEOUT, + border_color = qcolor.palette.bg.lowest, + color = qcolor.palette.yellow(), + border_width = qui.BORDER_WIDTH, + forced_height = qui.CHAR_HEIGHT, + forced_width = qui.CHAR_HEIGHT, id = "progress", }, layout = wibox.layout.align.horizontal, @@ -82,20 +82,20 @@ return function(n) }, { widget = wibox.container.constraint, - height = qvars.border_width, + height = qui.BORDER_WIDTH, width = beautiful.notification_max_width, strategy = "exact", { widget = wibox.container.background, - bg = qvars.colors.bright.black, + bg = qcolor.palette.border(), }, }, { widget = wibox.container.margin, - margins = qvars.big_padding, + margins = qui.BIG_PADDING, { widget = wibox.container.constraint, - height = n.icon and qvars.char_height * 2 + qvars.padding + height = n.icon and qui.CHAR_HEIGHT * 2 + qui.PADDING or (title_height + message_height + intertext_margin), strategy = "exact", { @@ -103,7 +103,7 @@ return function(n) widget = wibox.container.background, { widget = naughty.widget.icon, - shape = qvars.shape, + shape = qui.shape, notification = n, }, }, @@ -130,7 +130,7 @@ return function(n) layout = wibox.layout.fixed.vertical, }, fill_space = true, - spacing = n.icon and qvars.big_padding or nil, + spacing = n.icon and qui.BIG_PADDING or nil, layout = wibox.layout.fixed.horizontal, }, }, @@ -158,12 +158,10 @@ return function(n) end, } - local position = rtimed { - duration = qvars.anim_duration * 2, - intro = qvars.anim_intro * 2, + local position = qanim:new { + duration = qvars.anim_duration, pos = 0, - easing = easing.quadratic, - clamp_position = true, + easing = qvars.easing, subscribed = function(pos) gtimer.delayed_call(function() notif.widget.widget:move(1, function(geo, args) @@ -179,8 +177,8 @@ return function(n) end, } local opacity = rtimed { - duration = qvars.anim_duration * 2, - intro = qvars.anim_intro * 2, + duration = qvars.anim_duration, + intro = qvars.anim_intro, easing = easing.quadratic, pos = 0, clamp_position = true, @@ -192,12 +190,20 @@ return function(n) n:disconnect_signal("destroyed", notif._private.destroy_callback) function notif._private.destroy_callback() opacity.target = 0 - position.target = 0 + position:set(0) hiding = true end n:weak_connect_signal("destroyed", notif._private.destroy_callback) + --- for some reason when urgency is critical, it somehow makes it not disappear + --- why? dunno, FIXME opacity.target = 1 - position.target = qvars.char_width * 48 - progress.target = qvars.notif_timeout + position:set(beautiful.notification_max_width) + progress.target = M.NOTIF_TIMEOUT end + +return setmetatable(M, { + __call = function(_, ...) + return M.new(...) + end, +}) |