~theorytoe/musicctl.lua

36eb729f6c9d60872bbb472cc556f3fe1cd1a7b2 — Evan Sarris 2 years ago main
Initial commit
A  => .gitignore +1 -0
@@ 1,1 @@
./doc/*

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)