A => .gitignore +1 -0
A => .stylua.toml +6 -0
@@ 1,6 @@
+column_width = 120
+line_endings = "Unix"
+indent_type = "Tabs"
+indent_width = 4
+quote_style = "AutoPreferSingle"
+no_call_parentheses = false
A => CONTRIBUTING.md +53 -0
@@ 1,53 @@
+# Contribution Quick Start
+
+I do not accept pull requests on the github mirror, please use patches instead.
+
+Anyone can contribute to musicctl. First you need to clone the repository:
+
+ git clone https://git.sr.ht/~theorytoe/musicctl
+ cd musicctl
+
+When making changes, ensure the following:
+
+- Ensure that your code is properly formatted properly with stylua.
+- Ensure that everything works as expected.
+- Do not forget to update the docs.
+
+Once you are happy with your work, you can create a commit (or several
+commits). Follow these general rules:
+
+- Limit the first line (title) of the commit message to 60 characters.
+- Use a short prefix for the commit title for readability with `git log --oneline`.
+- Use the body of the commit message to actually explain what your patch does
+ and why it is useful.
+- Address only one issue/topic per commit.
+- If you are fixing a ticket, use appropriate
+ [commit trailers](https://man.sr.ht/git.sr.ht/#referencing-tickets-in-git-commit-messages).
+- If you are fixing a regression introduced by another commit, add a `fix:`
+ trailer with the commit id and its title.
+
+There is a great reference for commit messages in the
+[Linux kernel documentation](https://www.kernel.org/doc/html/latest/process/submitting-patches.html#describe-your-changes).
+
+Before sending the patch, you should configure your local clone with sane
+defaults:
+
+ git config format.subjectPrefix "[PATCH] musicctl.lua"
+ git config sendemail.to "~theorytoe/musicctl-devel@lists.sr.ht"
+
+And send the patch to the mailing list:
+
+ git send-email --annotate -1
+
+Before your patch can be applied, it needs to be reviewed and approved. Approval
+will be indicated by a reply to your patch with a Reviewed-by trailer.
+
+You can follow the review process via email and on the
+[web ui](https://lists.sr.ht/~theorytoe/musicctl-devel/patches).
+
+Wait for feedback. Address comments and amend changes to your original commit.
+Then you should send a v2 (and maybe a v3, v4, etc.):
+
+ git send-email --annotate -v2 -1
+
+Once your patch has been reviewed and approved it will be applied and pushed.
A => LICENSE +17 -0
@@ 1,17 @@
+MIT License Copyright (c) Evan Sarris
+
+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.
A => README.md +63 -0
@@ 1,63 @@
+# musicctl.lua
+
+musicctl.lua is a libaray for [AweseomWM](https://awesomewm.org) that gives
+functions to control MPRIS-capable media players.
+
+- [Docs](https://docs.theoryware.net/musicctl/index.html)
+
+## Installation
+
+[playerctl](https://github.com/altdesktop/playerctl) In order for this module to function
+properly.
+
+```bash
+# clone repo
+git clone https://git.sr.ht/~theorytoe/musicctl.lua
+
+# copy file to config dir
+cp ./musicctl.lua ~/.config/awesome
+```
+
+### Patching musicctl
+
+musicctl was designed to not depend on anything other than the core AweseomWM
+library and playerctl. However, there are patches in this repository that add
+the following features:
+
+- "floating", top-aligned widgets (needs compositor for transparency) `top_align.patch`
+- Animations via [rubato](https://github.com/andOrlando/rubato) `rubato.patch`
+
+You can apply/remove a patch as follows:
+
+```bash
+# -b creates a backup file
+patch -b < patches/rubato.patch
+
+# -R reverses the above
+patch -R < patches/rubato.patch
+```
+
+### Generating docs
+
+Docs are generated using [LDoc](https://stevedonovan.github.io/ldoc) in HTML format.
+
+```bash
+ldoc musicctl.lua -f markdown
+```
+
+## Usage
+
+Example usages are found in `example/`
+
+```lua
+-- in rc.lua
+local musicctl = require('musicctl')
+musicctl.vol_up()
+```
+
+## Contributing
+
+See:
+
+- CONTRIBUTING.md
+- [The docs page](https://docs.theoryware.net/musicctl/topics/CONTRIBUTING.md.html)
A => config.ld +11 -0
@@ 1,11 @@
+project = "musicctl.lua"
+title = "docs - musicctl.lua"
+description = "MPRIS media control module for awesomewm with fine-grain control."
+
+style = "./ldoc-gen"
+template = "./ldoc-gen"
+output = "index"
+
+examples = {"examples"}
+topics = { "README.md", "CONTRIBUTING.md"}
+new_type("theme", "Theme variables", false, "Type")
A => examples/beautiful.lua +26 -0
@@ 1,26 @@
+-- sample theme file with musicctl theme vars
+-- define vars
+local xresources = require("beautiful.xresources")
+local dpi = xresources.apply_dpi
+
+local theme = {}
+
+-- some theme vars
+theme.font = "sans 8"
+theme.bg_normal = "#222222"
+theme.bg_focus = "#535d6c"
+theme.bg_urgent = "#ff0000"
+theme.bg_minimize = "#444444"
+theme.bg_systray = theme.bg_normal
+
+-- ... continue contents
+
+-- volume and status widget theme varibles
+theme.musicctl_col = "#00ffa2"
+theme.musicctl_col_alt "#011901"
+theme.musicctl_fg = "#ffffff"
+theme.musicctl_bg = "#121212"
+
+-- ... continue contents
+
+return theme
A => examples/keybinds.lua +65 -0
@@ 1,65 @@
+-- import needed libraries
+local gears = require('gears')
+local awful = require('awful')
+local musicctl = require('musicctl')
+
+-- define modifier key
+local modkey = 'Mod4'
+
+-- table of keybinds
+-- you will probably want to append this to whatever you are
+-- using for keybinds. In this case, a gears.table holds the binds
+local keys = gears.table.join(
+ awful.key({ modkey }, 'F5', function()
+ musicctl.playback_previous()
+ end, {
+ description = 'play previous song (playerctl)',
+ group = 'music',
+ }),
+ awful.key({ modkey }, 'F6', function()
+ musicctl.playback_toggle()
+ end, {
+ description = 'toggle music playback (playerctl)',
+ group = 'music',
+ }),
+ awful.key({ modkey }, 'F7', function()
+ musicctl.playback_next()
+ end, {
+ description = 'play next song (playerctl)',
+ group = 'music',
+ }),
+ awful.key({ modkey }, 'F8', function()
+ musicctl.switch_mode()
+ end, {
+ description = 'switch playerctl mode',
+ group = 'music',
+ }),
+
+ awful.key({ modkey, 'Control' }, 'F5', function()
+ musicctl.vol_down()
+ end, {
+ description = 'decrease volume (playerctl)',
+ group = 'music',
+ }),
+ awful.key({ modkey, 'Control' }, 'F6', function()
+ musicctl.vol_mute()
+ end, {
+ description = 'mute volume (playerctl)',
+ group = 'music',
+ }),
+ awful.key({ modkey, 'Control' }, 'F7', function()
+ musicctl.vol_up()
+ end, {
+ description = 'increase volume (playerctl)',
+ group = 'music',
+ }),
+ awful.key({ modkey, 'Control' }, 'F6', function()
+ musicctl.vol_blast()
+ end, {
+ description = 'blast volume (playerctl)',
+ group = 'music',
+ }),
+)
+
+-- append to the global keymap
+root.keys(keys)
A => examples/set-vars.lua +15 -0
@@ 1,15 @@
+local musicctl = require('musicctl')
+
+-- table of player names
+local modes = { 'vlc', 'firefox' }
+
+-- set the modes
+musicctl.set_modes(modes)
+
+-- set the current player to 'firefox'
+-- instead of musicctl.modes[1]
+-- from the call to set_modes()
+musicctl.current_mode = 'firefox'
+
+-- modes can also be set like this
+musicctl.set_modes({ 'mpd', 'ncspot' })
A => ldoc-gen/ldoc.css +311 -0
@@ 1,311 @@
+html {
+ color: #d3c6aa;
+ background: #2b3339;
+}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td {
+ margin: 0;
+ padding: 0;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+fieldset,img {
+ border: 0;
+}
+address,caption,cite,code,dfn,em,strong,th,var,optgroup {
+ font-style: inherit;
+ font-weight: inherit;
+}
+del,ins {
+ text-decoration: none;
+}
+li {
+ margin-left: 20px;
+}
+caption,th {
+ text-align: left;
+}
+h1,h2,h3,h4,h5,h6 {
+ font-size: 100%;
+ font-weight: bold;
+}
+q:before,q:after {
+ content: '';
+}
+abbr,acronym {
+ border: 0;
+ font-variant: normal;
+}
+sup {
+ vertical-align: baseline;
+}
+sub {
+ vertical-align: baseline;
+}
+legend {
+ color: #d3c6aa;
+}
+input,button,textarea,select,optgroup,option {
+ font-family: inherit;
+ font-size: inherit;
+ font-style: inherit;
+ font-weight: inherit;
+}
+input,button,textarea,select {*font-size:100%;
+}
+body {
+ margin-left: 1em;
+ margin-right: 1em;
+ font-family: arial, helvetica, geneva, sans-serif;
+ background-color: #2b3339; margin: 0px;
+}
+
+code, tt { font-family: monospace; font-size: 1.1em; }
+span.parameter { font-family:monospace; }
+span.parameter:after { content:":"; }
+span.types:before { content:"("; }
+span.types:after { content:")"; }
+.type { font-weight: bold; font-style:italic }
+
+body, p, td, th { font-size: .95em; line-height: 1.2em;}
+
+p, ul { margin: 10px 0 0 0px;}
+
+strong { font-weight: bold;}
+
+em { font-style: italic;}
+
+h1 {
+ font-size: 1.5em;
+ margin: 20px 0 20px 0;
+}
+h2, h3, h4 { margin: 15px 0 10px 0; }
+h2 { font-size: 1.25em; }
+h3 { font-size: 1.15em; }
+h4 { font-size: 1.06em; }
+
+a:link { font-weight: bold; color: #7fbbb3; text-decoration: none; }
+a:visited { font-weight: bold; color: #d699b6; text-decoration: none; }
+a:link:hover { text-decoration: underline; }
+
+hr {
+ color:#3a454a;
+ background: #00007f;
+ height: 1px;
+}
+
+blockquote { margin-left: 3em; }
+
+ul { list-style-type: disc; }
+
+p.name {
+ font-family: "Andale Mono", monospace;
+ padding-top: 1em;
+}
+
+pre {
+ background-color: #323c41;
+ border: 1px solid #4c555b; /* silver */
+ padding: 10px;
+ margin: 10px 0 10px 0;
+ overflow: auto;
+ font-family: "Andale Mono", monospace;
+}
+
+pre.example {
+ font-size: .85em;
+}
+
+table.index { border: 1px #00007f; }
+table.index td { text-align: left; vertical-align: top; }
+
+#container {
+ margin-left: 1em;
+ margin-right: 1em;
+ background-color: #2f383e;
+}
+
+#product {
+ text-align: center;
+ border-bottom: 1px solid #3a454a;
+ background-color: #2b3339;
+}
+
+#product big {
+ font-size: 2em;
+}
+
+#main {
+ background-color: #2f383e;
+ border-left: 2px solid #3a454a;
+}
+
+#navigation {
+ float: left;
+ width: 14em;
+ vertical-align: top;
+ background-color: #2f383e;
+ overflow: visible;
+}
+
+#navigation h2 {
+ background-color: #445055;
+ font-size:1.1em;
+ color:#d3c6aa;
+ text-align: left;
+ padding:0.2em;
+ border-top:1px solid #4c555b;
+ border-bottom:1px solid #4c555b;
+}
+
+#navigation ul
+{
+ font-size:1em;
+ list-style-type: none;
+ margin: 1px 1px 10px 1px;
+}
+
+#navigation li {
+ text-indent: -1em;
+ display: block;
+ margin: 3px 0px 0px 22px;
+}
+
+#navigation li li a {
+ margin: 0px 3px 0px -1em;
+}
+
+#content {
+ margin-left: 14em;
+ margin-right: 14em;
+ padding: 1em;
+ border-left: 2px solid #3a454a;
+ border-right: 2px solid #3a454a;
+ background-color: #2b3339;
+}
+
+#about {
+ clear: both;
+ padding: 5px;
+ border-top: 2px solid #3a454a;
+ background-color: #2b3339;
+}
+
+#icon {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: 95%;
+}
+
+#title-content {
+ text-align: center;
+}
+
+
+.section-header {
+ color: #7fbbb3;
+ text-decoration: underline;
+}
+
+@media print {
+ body {
+ font: 12pt "Times New Roman", "TimeNR", Times, serif;
+ }
+ a { font-weight: bold; color: #004080; text-decoration: underline; }
+
+ #main {
+ background-color: #2b3339;
+ border-left: 0px;
+ }
+
+ #container {
+ margin-left: 2%;
+ margin-right: 2%;
+ background-color: #2b3339;
+ }
+
+ #content {
+ padding: 1em;
+ background-color: #2b3339;
+ }
+
+ #navigation {
+ display: none;
+ }
+ pre.example {
+ font-family: "Andale Mono", monospace;
+ font-size: 10pt;
+ page-break-inside: avoid;
+ }
+}
+
+table.module_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #3a454a;
+ border-collapse: collapse;
+}
+table.module_list td {
+ border-width: 1px;
+ padding: 3px;
+ border-style: solid;
+ border-color: #3a454a;
+}
+table.module_list td.name { background-color: #2f383e; min-width: 200px; }
+table.module_list td.summary { width: 100%; }
+
+
+table.function_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #3a454a;
+ border-collapse: collapse;
+}
+table.function_list td {
+ border-width: 1px;
+ padding: 3px;
+ border-style: solid;
+ border-color: #3a454a;
+}
+table.function_list td.name { background-color: #2f383e; min-width: 200px; }
+table.function_list td.summary { width: 100%; }
+
+ul.nowrap {
+ overflow:auto;
+ white-space:nowrap;
+}
+
+dl.table dt, dl.function dt {border-top: 1px solid #4c555b; padding-top: 1em;}
+dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
+dl.table h3, dl.function h3 {font-size: .95em;}
+
+/* stop sublists from having initial vertical space */
+ul ul { margin-top: 0px; }
+ol ul { margin-top: 0px; }
+ol ol { margin-top: 0px; }
+ul ol { margin-top: 0px; }
+
+/* make the target distinct; helps when we're navigating to a function */
+a:target + * {
+ background-color: #445349;
+}
+
+
+/* styles for prettification of source */
+pre .comment { color: #859289; }
+pre .constant { color: #e69875; }
+pre .escape { color: #83c092; }
+pre .keyword { color: #e67e80; font-weight: bold; }
+pre .library { color: #d699b6; }
+pre .marker { color: #e69875; background: #fedc56; font-weight: bold; }
+pre .string { color: #a7c080; }
+pre .number { color: #d699b6; }
+pre .operator { color: #a7c080; font-weight: bold; }
+pre .preprocessor, pre .prepro { color: #a33243; }
+pre .global { color: #a7c080; }
+pre .user-keyword { color: #d699b6; }
+pre .prompt { color: #558817; }
+pre .url { color: #7fbbb3; text-decoration: underline; }
+
A => ldoc-gen/ldoc.ltp +319 -0
@@ 1,319 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta content="musicctl.lua docs" property="og:title" />
+<meta content="Documentation page for the awesomewm library musicctl.lua" property="og:description" />
+<meta http-equiv="Content-Type" content="text/html; charset=$(ldoc.doc_charset)"/>
+<head>
+ <title>$(ldoc.title)</title>
+ <link rel="stylesheet" href="$(ldoc.css)" type="text/css" />
+# if ldoc.custom_css then -- add custom CSS file if configured.
+ <link rel="stylesheet" href="$(ldoc.custom_css)" type="text/css" />
+# end
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+# local no_spaces = ldoc.no_spaces
+# local use_li = ldoc.use_li
+# local display_name = ldoc.display_name
+# local iter = ldoc.modules.iter
+# local function M(txt,item) return ldoc.markup(txt,item,ldoc.plain) end
+# local nowrap = ldoc.wrap and '' or 'nowrap'
+
+<!-- Menu -->
+
+<div id="navigation">
+<br/>
+<h1 id=title-content>$(ldoc.project)</h1>
+
+<img src="https://cdn.theoryware.net/musicctl-icon.png" />
+
+# if not ldoc.single and module then -- reference back to project index
+<ul>
+ <li><a href="../$(ldoc.output).html">Index</a></li>
+</ul>
+# end
+
+# --------- contents of module -------------
+# if module and not ldoc.no_summary and #module.items > 0 then
+<h2>Contents</h2>
+<ul>
+# for kind,items in module.kinds() do
+<li><a href="#$(no_spaces(kind))">$(kind)</a></li>
+# end
+</ul>
+# end
+
+
+# if ldoc.no_summary and module and not ldoc.one then -- bang out the functions on the side
+# for kind, items in module.kinds() do
+<h2>$(kind)</h2>
+<ul class="nowrap">
+# for item in items() do
+ <li><a href="#$(item.name)">$(display_name(item))</a></li>
+# end
+</ul>
+# end
+# end
+# -------- contents of project ----------
+# local this_mod = module and module.name
+# for kind, mods, type in ldoc.kinds() do
+# if ldoc.allowed_in_contents(type,module) then
+<h2>$(kind)</h2>
+<ul class="$(kind=='Topics' and '' or 'nowrap')">
+# for mod in mods() do local name = display_name(mod)
+# if mod.name == this_mod then
+ <li><strong>$(name)</strong></li>
+# else
+ <li><a href="$(ldoc.ref_to_module(mod))">$(name)</a></li>
+# end
+# end
+# end
+</ul>
+# end
+
+</div>
+
+<div id="content">
+
+# if ldoc.body then -- verbatim HTML as contents; 'non-code' entries
+ $(ldoc.body)
+# elseif module then -- module documentation
+<h1>$(ldoc.module_typename(module)) <code>$(module.name)</code></h1>
+<p>$(M(module.summary,module))</p>
+<p>$(M(module.description,module))</p>
+# if module.tags.include then
+ $(M(ldoc.include_file(module.tags.include)))
+# end
+# if module.see then
+# local li,il = use_li(module.see)
+ <h3>See also:</h3>
+ <ul>
+# for see in iter(module.see) do
+ $(li)<a href="$(ldoc.href(see))">$(see.label)</a>$(il)
+# end -- for
+ </ul>
+# end -- if see
+# if module.usage then
+# local li,il = use_li(module.usage)
+ <h3>Usage:</h3>
+ <ul>
+# for usage in iter(module.usage) do
+ $(li)<pre class="example">$(ldoc.escape(usage))</pre>$(il)
+# end -- for
+ </ul>
+# end -- if usage
+# if module.info then
+ <h3>Info:</h3>
+ <ul>
+# for tag, value in module.info:iter() do
+ <li><strong>$(tag)</strong>: $(M(value,module))</li>
+# end
+ </ul>
+# end -- if module.info
+
+
+# if not ldoc.no_summary then
+# -- bang out the tables of item types for this module (e.g Functions, Tables, etc)
+# for kind,items in module.kinds() do
+<h2><a href="#$(no_spaces(kind))">$(kind)</a></h2>
+<table class="function_list">
+# for item in items() do
+ <tr>
+ <td class="name" $(nowrap)><a href="#$(item.name)">$(display_name(item))</a></td>
+ <td class="summary">$(M(item.summary,item))</td>
+ </tr>
+# end -- for items
+</table>
+#end -- for kinds
+
+<br/>
+<br/>
+
+#end -- if not no_summary
+
+# --- currently works for both Functions and Tables. The params field either contains
+# --- function parameters or table fields.
+# local show_return = not ldoc.no_return_or_parms
+# local show_parms = show_return
+# for kind, items in module.kinds() do
+# local kitem = module.kinds:get_item(kind)
+# local has_description = kitem and ldoc.descript(kitem) ~= ""
+ <h2 class="section-header $(has_description and 'has-description')"><a id="section-header" name="$(no_spaces(kind))" href="#$(no_spaces(kind))">$(kind)</a></h2>
+ $(M(module.kinds:get_section_description(kind),nil))
+# if kitem then
+# if has_description then
+ <div class="section-description">
+ $(M(ldoc.descript(kitem),kitem))
+ </div>
+# end
+# if kitem.usage then
+ <h3>Usage:</h3>
+ <pre class="example">$(ldoc.prettify(kitem.usage[1]))</pre>
+# end
+# end
+ <dl class="function">
+# for item in items() do
+ <dt>
+ <a name = "$(item.name)"></a>
+ <strong>
+ $(display_name(item))
+ </strong>
+# if ldoc.prettify_files and ldoc.is_file_prettified[item.module.file.filename] then
+ <a style="float:right;" href="$(ldoc.source_ref(item))">line $(item.lineno)</a>
+# end
+ </dt>
+ <dd>
+ $(M(ldoc.descript(item),item))
+
+# if ldoc.custom_tags then
+# for custom in iter(ldoc.custom_tags) do
+# local tag = item.tags[custom[1]]
+# if tag and not custom.hidden then
+# local li,il = use_li(tag)
+ <h3>$(custom.title or custom[1]):</h3>
+ <ul>
+# for value in iter(tag) do
+ $(li)$(custom.format and custom.format(value) or M(value))$(il)
+# end -- for
+# end -- if tag
+ </ul>
+# end -- iter tags
+# end
+
+# if show_parms and item.params and #item.params > 0 then
+# local subnames = module.kinds:type_of(item).subnames
+# if subnames then
+ <h3>$(subnames):</h3>
+# end
+ <ul>
+# for parm in iter(item.params) do
+# local param,sublist = item:subparam(parm)
+# if sublist then
+ <li><span class="parameter">$(sublist)</span>$(M(item.params.map[sublist],item))
+ <ul>
+# end
+# for p in iter(param) do
+# local name,tp,def = item:display_name_of(p), ldoc.typename(item:type_of_param(p)), item:default_of_param(p)
+ <li><span class="parameter">$(name)</span>
+# if tp ~= '' then
+ <span class="types">$(tp)</span>
+# end
+ $(M(item.params.map[p],item))
+# if def == true then
+ (<em>optional</em>)
+# elseif def then
+ (<em>default</em> $(def))
+# end
+# if item:readonly(p) then
+ <em>readonly</em>
+# end
+ </li>
+# end
+# if sublist then
+ </li></ul>
+# end
+# end -- for
+ </ul>
+# end -- if params
+
+# if show_return and item.retgroups then local groups = item.retgroups
+ <h3>Returns:</h3>
+# for i,group in ldoc.ipairs(groups) do local li,il = use_li(group)
+ <ol>
+# for r in group:iter() do local type, ctypes = item:return_type(r); local rt = ldoc.typename(type)
+ $(li)
+# if rt ~= '' then
+ <span class="types">$(rt)</span>
+# end
+ $(M(r.text,item))$(il)
+# if ctypes then
+ <ul>
+# for c in ctypes:iter() do
+ <li><span class="parameter">$(c.name)</span>
+ <span class="types">$(ldoc.typename(c.type))</span>
+ $(M(c.comment,item))</li>
+# end
+ </ul>
+# end -- if ctypes
+# end -- for r
+ </ol>
+# if i < #groups then
+ <h3>Or</h3>
+# end
+# end -- for group
+# end -- if returns
+
+# if show_return and item.raise then
+ <h3>Raises:</h3>
+ $(M(item.raise,item))
+# end
+
+# if item.see then
+# local li,il = use_li(item.see)
+ <h3>See also:</h3>
+ <ul>
+# for see in iter(item.see) do
+ $(li)<a href="$(ldoc.href(see))">$(see.label)</a>$(il)
+# end -- for
+ </ul>
+# end -- if see
+
+# if item.usage then
+# local li,il = use_li(item.usage)
+ <h3>Usage:</h3>
+ <ul>
+# for usage in iter(item.usage) do
+ $(li)<pre class="example">$(ldoc.prettify(usage))</pre>$(il)
+# end -- for
+ </ul>
+# end -- if usage
+
+</dd>
+# end -- for items
+</dl>
+# end -- for kinds
+
+# else -- if module; project-level contents
+
+# if ldoc.description then
+ <h2>$(M(ldoc.description,nil))</h2>
+# end
+# if ldoc.full_description then
+ <p>$(M(ldoc.full_description,nil))</p>
+# end
+
+# for kind, mods in ldoc.kinds() do
+<h2>$(kind)</h2>
+# kind = kind:lower()
+<table class="module_list">
+# for m in mods() do
+ <tr>
+ <td class="name" $(nowrap)><a href="$(no_spaces(kind))/$(m.name).html">$(m.name)</a></td>
+ <td class="summary">$(M(ldoc.strip_header(m.summary),m))</td>
+ </tr>
+# end -- for modules
+</table>
+# end -- for kinds
+# end -- if module
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/stevedonovan/LDoc">LDoc $(ldoc.version)</a></i>
+<i style="float:right;">Last updated $(ldoc.updatetime) </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
A => musicctl.lua +285 -0
@@ 1,285 @@
+--- Fine-grained media player control for awesomewm.
+-- Controls playback and volume of MPRIS-compatable media
+-- sources.
+-- @module musicctl.lua
+-- @author TheoryToE
+-- @alias M
+
+local awful = require('awful')
+local beautiful = require('beautiful')
+local gears = require('gears')
+local wibox = require('wibox')
+
+local M = {}
+
+local modes = { 'mpd', 'ncspot' }
+M.current_mode = modes[1]
+
+-- local vars
+local modeindx = 1
+local vol_inc_amt = 0.02 -- going lower than this number will produce odd bugs
+
+--- Primary accent color of widgets
+-- @theme beautiful.musicctl_col
+-- @tparam string color hex color code (default: '#a7c080')
+
+--- Secondary accent color of widgets
+-- @theme beautiful.musicctl_col_alt
+-- @tparam string color hex color code (default: '#404d44')
+
+--- Foreground color of widgets
+-- @theme beautiful.musicctl_fg
+-- @tparam string color hex color code (default: '#d3c6aa')
+
+--- Background color of widgets
+-- @theme beautiful.musicctl_bg
+-- @tparam string color hex color code (default: '#2f383e')
+
+local thm_col = beautiful.musicctl_col or '#a7c080'
+local thm_col_alt = beautiful.musicctl_col_alt or '#404d44'
+local thm_fg = beautiful.musicctl_fg or '#d3c6aa'
+local thm_bg = beautiful.musicctl_bg or '#2f383e'
+
+-- {{{ helper functions
+
+-- rounded rectangle wrapper
+local function rrect(radius)
+ return function(cr, width, height)
+ gears.shape.rounded_rect(cr, width, height, radius)
+ end
+end
+
+-- trigger and gears.timer objects
+local function trigger(timer)
+ if timer.started == true then
+ timer:again()
+ else
+ timer:start()
+ end
+end
+-- }}}
+
+-- {{{ widgets
+
+-- {{{ misc
+
+-- volume text for volume widget
+local textwid = wibox.widget({
+ text = 'vol: nil',
+ widget = wibox.widget.textbox,
+})
+
+-- title of status widget
+local ststitlewid = wibox.widget({
+ widget = wibox.widget.textbox,
+ forced_height = 20,
+ forced_width = 300,
+})
+
+-- text area of status widget
+local stswid = wibox.widget({
+ text = 'nil',
+ widget = wibox.widget.textbox,
+})
+-- }}}
+
+-- {{{ volume indicator widget
+-- progressbar for indicating player volume
+local volbar = wibox.widget({
+ color = thm_col,
+ background_color = thm_col_alt,
+ max_value = 100,
+ value = 10,
+ forced_height = 20,
+ forced_width = 200,
+ shape = rrect(5),
+ bar_shape = rrect(5),
+ widget = wibox.widget.progressbar,
+})
+
+-- container for volbar
+local volwid = awful.popup({
+ widget = {
+ {
+ volbar,
+ textwid,
+ text = M.current_mode,
+ layout = wibox.layout.flex.vertical,
+ },
+ margins = 20,
+ widget = wibox.container.margin,
+ },
+ fg = thm_fg,
+ bg = thm_bg,
+ ontop = true,
+ visible = false,
+ shape = rrect(10),
+ placement = awful.placement.top,
+})
+-- }}}
+
+-- {{{ player status widget
+-- current player indicator widget
+local statuswid = awful.popup({
+ widget = {
+ {
+
+ ststitlewid,
+ stswid,
+ layout = wibox.layout.flex.vertical,
+ },
+ margins = 20,
+ widget = wibox.container.margin,
+ },
+ fg = thm_fg,
+ bg = thm_bg,
+ ontop = true,
+ visible = false,
+ shape = rrect(10),
+ placement = awful.placement.top,
+})
+-- }}}
+
+-- {{{ fade timers
+-- volume widget fade timer
+local vol_timer = gears.timer({
+ timeout = 2,
+ callback = function()
+ volwid.visible = false
+ end,
+})
+
+-- status widget fade timer
+local sts_timer = gears.timer({
+ timeout = 2,
+ callback = function()
+ statuswid.visible = false
+ end,
+})
+-- }}}
+
+-- async func to get shell output and print volume to widget
+local function spwn_volwid()
+ awful.spawn.easy_async_with_shell('playerctl --player ' .. M.current_mode .. ' volume', function(out)
+ vol_timed.target = tonumber(out:gsub('[%p%c%s]', ''):sub(1, -5))
+ textwid.text = 'vol: ' .. tonumber(out:gsub('[%p%c%s]', ''):sub(1, -5)) .. '%'
+ end)
+ volwid.screen = mouse.screen
+ volwid.visible = true
+ trigger(vol_timer)
+end
+
+-- }}}
+
+-- {{{ Mode control
+
+--- Mode control
+-- @section modes
+
+--- Iterate through table 'modes', taking control of player
+-- @function switch_mode
+M.switch_mode = function()
+ modeindx = modeindx + 1
+ if modeindx > #modes then
+ modeindx = 1
+ end
+ M.current_mode = modes[modeindx]
+ stswid.text = M.current_mode
+ ststitlewid.text = 'playerctl source:'
+ statuswid.screen = mouse.screen
+ statuswid.visible = true
+ trigger(sts_timer)
+end
+
+--- Set the table of controlled modes
+-- @function set_modes
+-- @tparam table newmodes string array of modes to control
+-- @usage
+-- M.set_modes({ 'vlc', 'spotify'})
+M.set_modes = function(newmodes)
+ modes = newmodes
+ M.current_mode = newmodes[1]
+end
+-- }}}
+
+-- {{{ Playback control
+
+--- Playback control
+-- @section playback
+
+--- Toggle playback of selected player
+-- @function playback_toggle
+M.playback_toggle = function()
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' play-pause')
+end
+
+--- Next song of controlled player
+-- @function playback_next
+M.playback_next = function()
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' next')
+end
+
+--- Previous song of controlled player
+-- @function playback_previous
+M.playback_previous = function()
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' previous')
+end
+-- }}}
+
+-- {{{ Volume Control
+
+--- Volume control
+-- @section volume
+
+--- Granular volume increment control
+-- @function vol_increment
+-- @tparam string amount
+-- @usage
+-- -- incremental control (increment polarity (+/-) must follow the number with no space)
+-- -- don't use this unless you *really* need too
+-- vol_increment('0.1-') -- vol down
+-- vol_increment('0.1+') -- vol up
+M.vol_increment = function(inc)
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' volume ' .. inc)
+ spwn_volwid()
+end
+
+--- Increase volume of current player
+-- @function vol_up
+M.vol_up = function()
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' volume ' .. vol_inc_amt .. '+')
+ spwn_volwid()
+end
+
+--- Decrease volume of current player
+-- @function vol_down
+M.vol_down = function()
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' volume ' .. vol_inc_amt .. '-')
+ spwn_volwid()
+end
+
+--- Mute current player
+-- @function vol_mute
+M.vol_mute = function()
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' volume 0')
+ spwn_volwid()
+end
+
+--- Set volume to a constant value
+-- @function vol_const
+-- @int num volume to set (0.01 - 1), if ooutside these bounds, defaults to 0.02
+M.vol_const = function(num)
+ if num > 1 then
+ num = 0.02
+ elseif num < 0.01 then
+ num = 0.02
+ end
+ awful.spawn.with_shell('playerctl --player ' .. M.current_mode .. ' volume ' .. num)
+ spwn_volwid()
+end
+
+-- }}}
+
+return M
+
+-- vim:ft=lua:fdm=marker
A => patches/rubato.patch +28 -0
@@ 1,28 @@
+--- musicctl.lua 2022-04-14 14:02:10.894923198 -0600
++++ rubato.musicctl.lua 2022-04-14 14:02:05.850922794 -0600
+@@ -9,6 +9,7 @@
+ local beautiful = require('beautiful')
+ local gears = require('gears')
+ local wibox = require('wibox')
++local rubato = require('rubato')
+
+ local M = {}
+
+@@ -157,6 +158,17 @@
+ end,
+ })
+ -- }}}
++
++-- {{{ animation objects
++-- eased bar shifting
++local vol_timed = rubato.timed({
++ intro = 0.1,
++ duration = 0.3,
++ subscribed = function(num)
++ volbar.value = num
++ end,
++})
++-- }}}
+
+ -- async func to get shell output and print volume to widget
+ local function spwn_volwid()
A => patches/top_align.patch +98 -0
@@ 1,98 @@
+--- musicctl.lua 2022-04-14 20:26:39.583774508 -0600
++++ top_align.musicctl.lua 2022-04-14 20:25:54.341770877 -0600
+@@ -35,10 +35,15 @@
+ -- @theme beautiful.musicctl_bg
+ -- @tparam string color hex color code (default: '#2f383e')
+
++--- Transparent color of widgets (if patched in)
++-- @theme beautiful.musicctl_trs
++-- @tparam string color hex color code (default: '#00000000')
++
+ local thm_col = beautiful.musicctl_col or '#a7c080'
+ local thm_col_alt = beautiful.musicctl_col_alt or '#404d44'
+ local thm_fg = beautiful.musicctl_fg or '#d3c6aa'
+ local thm_bg = beautiful.musicctl_bg or '#2f383e'
++local thm_trs = beautiful.musicctl_trs or '#00000000'
+
+ -- {{{ helper functions
+
+@@ -101,16 +106,28 @@
+ local volwid = awful.popup({
+ widget = {
+ {
+- volbar,
+- textwid,
+- text = M.current_mode,
+- layout = wibox.layout.flex.vertical,
++ {
++ {
++ volbar,
++ textwid,
++ text = M.current_mode,
++ layout = wibox.layout.flex.vertical,
++ },
++ margins = 20,
++ widget = wibox.container.margin,
++ },
++ bg = thm_bg,
++ widget = wibox.container.background,
++ shape = rrect(10),
+ },
+- margins = 20,
+- widget = wibox.container.margin,
++ layout = wibox.container.margin,
++ left = 0,
++ top = 5,
++ bottom = 7,
++ right = 0,
+ },
+ fg = thm_fg,
+- bg = thm_bg,
++ bg = thm_trs,
+ ontop = true,
+ visible = false,
+ shape = rrect(10),
+@@ -123,16 +140,28 @@
+ local statuswid = awful.popup({
+ widget = {
+ {
++ {
++ {
+
+- ststitlewid,
+- stswid,
+- layout = wibox.layout.flex.vertical,
++ ststitlewid,
++ stswid,
++ layout = wibox.layout.flex.vertical,
++ },
++ margins = 20,
++ widget = wibox.container.margin,
++ },
++ bg = thm_bg,
++ widget = wibox.container.background,
++ shape = rrect(10),
+ },
+- margins = 20,
+- widget = wibox.container.margin,
++ layout = wibox.container.margin,
++ left = 0,
++ top = 5,
++ bottom = 7,
++ right = 0,
+ },
+ fg = thm_fg,
+- bg = thm_bg,
++ bg = thm_trs,
+ ontop = true,
+ visible = false,
+ shape = rrect(10),
+@@ -158,6 +187,8 @@
+ })
+ -- }}}
+
++-- }}}
++
+ -- async func to get shell output and print volume to widget
+ local function spwn_volwid()
+ awful.spawn.easy_async_with_shell('playerctl --player ' .. M.current_mode .. ' volume', function(out)