summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--battery-widget/battery.lua23
-rw-r--r--pactl-widget/README.md54
-rw-r--r--pactl-widget/pactl.lua124
-rw-r--r--pactl-widget/utils.lua28
-rw-r--r--pactl-widget/volume.lua233
-rw-r--r--spotify-widget/spotify.lua9
7 files changed, 462 insertions, 11 deletions
diff --git a/README.md b/README.md
index 1617fae..9a5819f 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
<img src="https://img.shields.io/github/stars/streetturtle/awesome-wm-widgets.svg">
<img src="https://img.shields.io/github/forks/streetturtle/awesome-wm-widgets.svg">
<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/streetturtle/awesome-wm-widgets">
- <img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/streetturtle/awesome-wm-widgets/luacheck">
+ <img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/streetturtle/awesome-wm-widgets/build.yml?branch=main">
<a href="https://www.buymeacoffee.com/streetturtle"><img src="https://img.shields.io/badge/-buy%20me%20a%20coffee-3B4252?style=flat&logo=Buy-Me-A-Coffee"></a>
<a href="https://twitter.com/intent/tweet?text=Check%20out%20these%20awesome%20widgets%20for%20Awesome Window Manager%20&url=https://github.com/streetturtle/awesome-wm-widgets">
<img src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social">
diff --git a/battery-widget/battery.lua b/battery-widget/battery.lua
index 452d7ef..4b02a7c 100644
--- a/battery-widget/battery.lua
+++ b/battery-widget/battery.lua
@@ -125,20 +125,25 @@ local function worker(user_args)
local battery_info = {}
local capacities = {}
for s in stdout:gmatch("[^\r\n]+") do
+ -- Match a line with status and charge level
local status, charge_str, _ = string.match(s, '.+: ([%a%s]+), (%d?%d?%d)%%,?(.*)')
if status ~= nil then
+ -- Enforce that for each entry in battery_info there is an
+ -- entry in capacities of zero. If a battery has status
+ -- "Unknown" then there is no capacity reported and we treat it
+ -- as zero capactiy for later calculations.
table.insert(battery_info, {status = status, charge = tonumber(charge_str)})
- else
- local cap_str = string.match(s, '.+:.+last full capacity (%d+)')
- table.insert(capacities, tonumber(cap_str))
+ table.insert(capacities, 0)
end
- end
- local capacity = 0
- for _, cap in ipairs(capacities) do
- capacity = capacity + cap
+ -- Match a line where capacity is reported
+ local cap_str = string.match(s, '.+:.+last full capacity (%d+)')
+ if cap_str ~= nil then
+ capacities[#capacities] = tonumber(cap_str) or 0
+ end
end
+ local capacity = 0
local charge = 0
local status
for i, batt in ipairs(battery_info) do
@@ -148,7 +153,11 @@ local function worker(user_args)
-- this is arbitrary, and maybe another metric should be used
end
+ -- Adds up total (capacity-weighted) charge and total capacity.
+ -- It effectively ignores batteries with status "Unknown" as we
+ -- treat them with capacity zero.
charge = charge + batt.charge * capacities[i]
+ capacity = capacity + capacities[i]
end
end
charge = charge / capacity
diff --git a/pactl-widget/README.md b/pactl-widget/README.md
new file mode 100644
index 0000000..24e4471
--- /dev/null
+++ b/pactl-widget/README.md
@@ -0,0 +1,54 @@
+# Pactl volume widget
+
+This is a volume widget that uses `pactl` only for controlling volume and
+selecting sinks and sources. Hence, it can be used with PulseAudio or PipeWire
+likewise, unlike the original Volume widget.
+
+Other than that it is heavily based on the original widget, including its
+customization and icon options. For screenshots, see the original widget.
+
+## Installation
+
+Clone the repo under **~/.config/awesome/** and add widget in **rc.lua**:
+
+```lua
+local volume_widget = require('awesome-wm-widgets.pactl-widget.volume')
+...
+s.mytasklist, -- Middle widget
+ { -- Right widgets
+ layout = wibox.layout.fixed.horizontal,
+ ...
+ -- default
+ volume_widget(),
+ -- customized
+ volume_widget{
+ widget_type = 'arc'
+ },
+```
+
+### Shortcuts
+
+To improve responsiveness of the widget when volume level is changed by a shortcut use corresponding methods of the widget:
+
+```lua
+awful.key({}, "XF86AudioRaiseVolume", function () volume_widget:inc(5) end),
+awful.key({}, "XF86AudioLowerVolume", function () volume_widget:dec(5) end),
+awful.key({}, "XF86AudioMute", function () volume_widget:toggle() end),
+```
+
+## Customization
+
+It is possible to customize the widget by providing a table with all or some of
+the following config parameters:
+
+### Generic parameter
+
+| Name | Default | Description |
+|---|---|---|
+| `mixer_cmd` | `pavucontrol` | command to run on middle click (e.g. a mixer program) |
+| `step` | `5` | How much the volume is raised or lowered at once (in %) |
+| `widget_type`| `icon_and_text`| Widget type, one of `horizontal_bar`, `vertical_bar`, `icon`, `icon_and_text`, `arc` |
+| `device` | `@DEFAULT_SINK@` | Select the device name to control |
+
+For more details on parameters depending on the chosen widget type, please
+refer to the original Volume widget.
diff --git a/pactl-widget/pactl.lua b/pactl-widget/pactl.lua
new file mode 100644
index 0000000..638dc7e
--- /dev/null
+++ b/pactl-widget/pactl.lua
@@ -0,0 +1,124 @@
+local spawn = require("awful.spawn")
+local utils = require("awesome-wm-widgets.pactl-widget.utils")
+
+local pactl = {}
+
+
+function pactl.volume_increase(device, step)
+ spawn('pactl set-sink-volume ' .. device .. ' +' .. step .. '%', false)
+end
+
+function pactl.volume_decrease(device, step)
+ spawn('pactl set-sink-volume ' .. device .. ' -' .. step .. '%', false)
+end
+
+function pactl.mute_toggle(device)
+ spawn('pactl set-sink-mute ' .. device .. ' toggle', false)
+end
+
+function pactl.get_volume(device)
+ local stdout = utils.popen_and_return('pactl get-sink-volume ' .. device)
+
+ local volsum, volcnt = 0, 0
+ for vol in string.gmatch(stdout, "(%d?%d?%d)%%") do
+ vol = tonumber(vol)
+ if vol ~= nil then
+ volsum = volsum + vol
+ volcnt = volcnt + 1
+ end
+ end
+
+ if volcnt == 0 then
+ return nil
+ end
+
+ return volsum / volcnt
+end
+
+function pactl.get_mute(device)
+ local stdout = utils.popen_and_return('pactl get-sink-mute ' .. device)
+ if string.find(stdout, "yes") then
+ return true
+ else
+ return false
+ end
+end
+
+function pactl.get_sinks_and_sources()
+ local default_sink = utils.trim(utils.popen_and_return('pactl get-default-sink'))
+ local default_source = utils.trim(utils.popen_and_return('pactl get-default-source'))
+
+ local sinks = {}
+ local sources = {}
+
+ local device
+ local ports
+ local key
+ local value
+ local in_section
+
+ for line in utils.popen_and_return('pactl list'):gmatch('[^\r\n]*') do
+
+ if string.match(line, '^%a+ #') then
+ in_section = nil
+ end
+
+ local is_sink_line = string.match(line, '^Sink #')
+ local is_source_line = string.match(line, '^Source #')
+
+ if is_sink_line or is_source_line then
+ in_section = "main"
+
+ device = {
+ id = line:match('#(%d+)'),
+ is_default = false
+ }
+ if is_sink_line then
+ table.insert(sinks, device)
+ else
+ table.insert(sources, device)
+ end
+ end
+
+ -- Found a new subsection
+ if in_section ~= nil and string.match(line, '^\t%a+:$') then
+ in_section = utils.trim(line):lower()
+ in_section = string.sub(in_section, 1, #in_section-1)
+
+ if in_section == 'ports' then
+ ports = {}
+ device['ports'] = ports
+ end
+ end
+
+ -- Found a key-value pair
+ if string.match(line, "^\t*[^\t]+: ") then
+ local t = utils.split(line, ':')
+ key = utils.trim(t[1]):lower():gsub(' ', '_')
+ value = utils.trim(t[2])
+ end
+
+ -- Key value pair on 1st level
+ if in_section ~= nil and string.match(line, "^\t[^\t]+: ") then
+ device[key] = value
+
+ if key == "name" and (value == default_sink or value == default_source) then
+ device['is_default'] = true
+ end
+ end
+
+ -- Key value pair in ports section
+ if in_section == "ports" and string.match(line, "^\t\t[^\t]+: ") then
+ ports[key] = value
+ end
+ end
+
+ return sinks, sources
+end
+
+function pactl.set_default(type, name)
+ spawn('pactl set-default-' .. type .. ' "' .. name .. '"', false)
+end
+
+
+return pactl
diff --git a/pactl-widget/utils.lua b/pactl-widget/utils.lua
new file mode 100644
index 0000000..52e7869
--- /dev/null
+++ b/pactl-widget/utils.lua
@@ -0,0 +1,28 @@
+local utils = {}
+
+
+function utils.trim(str)
+ return string.match(str, "^%s*(.-)%s*$")
+end
+
+function utils.split(string_to_split, separator)
+ if separator == nil then separator = "%s" end
+ local t = {}
+
+ for str in string.gmatch(string_to_split, "([^".. separator .."]+)") do
+ table.insert(t, str)
+ end
+
+ return t
+end
+
+function utils.popen_and_return(cmd)
+ local handle = io.popen(cmd)
+ local result = handle:read("*a")
+ handle:close()
+
+ return result
+end
+
+
+return utils
diff --git a/pactl-widget/volume.lua b/pactl-widget/volume.lua
new file mode 100644
index 0000000..53441fd
--- /dev/null
+++ b/pactl-widget/volume.lua
@@ -0,0 +1,233 @@
+-------------------------------------------------
+-- A purely pactl-based volume widget based on the original Volume widget
+-- More details could be found here:
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/pactl-widget
+
+-- @author Stefan Huber
+-- @copyright 2023 Stefan Huber
+-------------------------------------------------
+
+local awful = require("awful")
+local wibox = require("wibox")
+local spawn = require("awful.spawn")
+local gears = require("gears")
+local beautiful = require("beautiful")
+
+local pactl = require("awesome-wm-widgets.pactl-widget.pactl")
+local utils = require("awesome-wm-widgets.pactl-widget.utils")
+
+
+local widget_types = {
+ icon_and_text = require("awesome-wm-widgets.volume-widget.widgets.icon-and-text-widget"),
+ icon = require("awesome-wm-widgets.volume-widget.widgets.icon-widget"),
+ arc = require("awesome-wm-widgets.volume-widget.widgets.arc-widget"),
+ horizontal_bar = require("awesome-wm-widgets.volume-widget.widgets.horizontal-bar-widget"),
+ vertical_bar = require("awesome-wm-widgets.volume-widget.widgets.vertical-bar-widget")
+}
+local volume = {}
+
+local rows = { layout = wibox.layout.fixed.vertical }
+
+local popup = awful.popup{
+ bg = beautiful.bg_normal,
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = { y = 5 },
+ widget = {}
+}
+
+local function build_main_line(device)
+ if device.active_port ~= nil and device.ports[device.active_port] ~= nil then
+ return device.description .. ' ยท ' .. utils.split(device.ports[device.active_port], " ")[1]
+ else
+ return device.description
+ end
+end
+
+local function build_rows(devices, on_checkbox_click, device_type)
+ local device_rows = { layout = wibox.layout.fixed.vertical }
+ for _, device in pairs(devices) do
+
+ local checkbox = wibox.widget {
+ checked = device.is_default,
+ color = beautiful.bg_normal,
+ paddings = 2,
+ shape = gears.shape.circle,
+ forced_width = 20,
+ forced_height = 20,
+ check_color = beautiful.fg_urgent,
+ widget = wibox.widget.checkbox
+ }
+
+ checkbox:connect_signal("button::press", function()
+ pactl.set_default(device_type, device.name)
+ on_checkbox_click()
+ end)
+
+ local row = wibox.widget {
+ {
+ {
+ {
+ checkbox,
+ valign = 'center',
+ layout = wibox.container.place,
+ },
+ {
+ {
+ text = build_main_line(device),
+ align = 'left',
+ widget = wibox.widget.textbox
+ },
+ left = 10,
+ layout = wibox.container.margin
+ },
+ spacing = 8,
+ layout = wibox.layout.align.horizontal
+ },
+ margins = 4,
+ layout = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
+ row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
+
+ local old_cursor, old_wibox
+ row:connect_signal("mouse::enter", function()
+ local wb = mouse.current_wibox
+ old_cursor, old_wibox = wb.cursor, wb
+ wb.cursor = "hand1"
+ end)
+ row:connect_signal("mouse::leave", function()
+ if old_wibox then
+ old_wibox.cursor = old_cursor
+ old_wibox = nil
+ end
+ end)
+
+ row:connect_signal("button::press", function()
+ pactl.set_default(device_type, device.name)
+ on_checkbox_click()
+ end)
+
+ table.insert(device_rows, row)
+ end
+
+ return device_rows
+end
+
+local function build_header_row(text)
+ return wibox.widget{
+ {
+ markup = "<b>" .. text .. "</b>",
+ align = 'center',
+ widget = wibox.widget.textbox
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+end
+
+local function rebuild_popup()
+ for i = 0, #rows do
+ rows[i]=nil
+ end
+
+ local sinks, sources = pactl.get_sinks_and_sources()
+ table.insert(rows, build_header_row("SINKS"))
+ table.insert(rows, build_rows(sinks, function() rebuild_popup() end, "sink"))
+ table.insert(rows, build_header_row("SOURCES"))
+ table.insert(rows, build_rows(sources, function() rebuild_popup() end, "source"))
+
+ popup:setup(rows)
+end
+
+local function worker(user_args)
+
+ local args = user_args or {}
+
+ local mixer_cmd = args.mixer_cmd or 'pavucontrol'
+ local widget_type = args.widget_type
+ local refresh_rate = args.refresh_rate or 1
+ local step = args.step or 5
+ local device = args.device or '@DEFAULT_SINK@'
+
+ if widget_types[widget_type] == nil then
+ volume.widget = widget_types['icon_and_text'].get_widget(args.icon_and_text_args)
+ else
+ volume.widget = widget_types[widget_type].get_widget(args)
+ end
+
+ local function update_graphic(widget)
+ local vol = pactl.get_volume(device)
+ if vol ~= nil then
+ widget:set_volume_level(vol)
+ end
+
+ if pactl.get_mute(device) then
+ widget:mute()
+ else
+ widget:unmute()
+ end
+ end
+
+ function volume:inc(s)
+ pactl.volume_increase(device, s or step)
+ update_graphic(volume.widget)
+ end
+
+ function volume:dec(s)
+ pactl.volume_decrease(device, s or step)
+ update_graphic(volume.widget)
+ end
+
+ function volume:toggle()
+ pactl.mute_toggle(device)
+ update_graphic(volume.widget)
+ end
+
+ function volume:popup()
+ if popup.visible then
+ popup.visible = not popup.visible
+ else
+ rebuild_popup()
+ popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end
+
+ function volume:mixer()
+ if mixer_cmd then
+ spawn(mixer_cmd)
+ end
+ end
+
+ volume.widget:buttons(
+ awful.util.table.join(
+ awful.button({}, 1, function() volume:toggle() end),
+ awful.button({}, 2, function() volume:mixer() end),
+ awful.button({}, 3, function() volume:popup() end),
+ awful.button({}, 4, function() volume:inc() end),
+ awful.button({}, 5, function() volume:dec() end)
+ )
+ )
+
+ gears.timer {
+ timeout = refresh_rate,
+ call_now = true,
+ autostart = true,
+ callback = function()
+ update_graphic(volume.widget)
+ end
+ }
+
+ return volume.widget
+end
+
+
+return setmetatable(volume, { __call = function(_, ...) return worker(...) end })
diff --git a/spotify-widget/spotify.lua b/spotify-widget/spotify.lua
index 2c30685..0ea7cfd 100644
--- a/spotify-widget/spotify.lua
+++ b/spotify-widget/spotify.lua
@@ -40,6 +40,9 @@ local function worker(user_args)
local GET_SPOTIFY_STATUS_CMD = sp_bin .. ' status'
local GET_CURRENT_SONG_CMD = sp_bin .. ' current'
+ local PLAY_PAUSE_CMD = sp_bin .. ' play'
+ local NEXT_SONG_CMD = sp_bin .. ' next'
+ local PREVIOUS_SONG_CMD = sp_bin .. ' prev'
local cur_artist = ''
local cur_title = ''
@@ -135,11 +138,11 @@ local function worker(user_args)
-- - scroll down - play previous song
spotify_widget:connect_signal("button::press", function(_, _, _, button)
if (button == 1) then
- awful.spawn("sp play", false) -- left click
+ awful.spawn(PLAY_PAUSE_CMD, false) -- left click
elseif (button == 4) then
- awful.spawn("sp next", false) -- scroll up
+ awful.spawn(NEXT_SONG_CMD, false) -- scroll up
elseif (button == 5) then
- awful.spawn("sp prev", false) -- scroll down
+ awful.spawn(PREVIOUS_SONG_CMD, false) -- scroll down
end
awful.spawn.easy_async(GET_SPOTIFY_STATUS_CMD, function(stdout, stderr, exitreason, exitcode)
update_widget_icon(spotify_widget, stdout, stderr, exitreason, exitcode)