DEV Community

Cover image for Make a Music Bar ( listen.moe , mpd , spotify ) using Awesome Window Manager and Lua
Ashish Patwal
Ashish Patwal

Posted on • Updated on

Make a Music Bar ( listen.moe , mpd , spotify ) using Awesome Window Manager and Lua

Introduction πŸ‘¦πŸ‘‹

Hey there everybody !! Before starting, i want to say that this is my first online post, i am writing in my entire life. So please do bear with my silly mistakes or anything wrong i will be writing in this post.

So now going with the flow ; i am an avid music lover and i like the whole idea of development and programming alongside listening to music.

It's been a year since i migrated to linux ( Btw i use Arch Linux ), and i can't stress enough, how much my productivity, speed and ease of doing things have increased since then.

My system runs a vanilla arch setup ( barebone arch architecture without any setup ) running Awesome Window Manager. So recently, i thought about making a music bar from scratch. Since Awesome WM is highly configurable and comes with a lot helper utilities, it was really easy to setup such a thing.


Structure 🧬

~/.config/awesome
|--daemon
   |--init.lua
   |--moe.lua
   |--spotify.lua
   |--mpd.lua
|--widget
   |--moe.lua
   |--spotify_song.lua
   |--spotify_buttons.lua
   |--mpd_song.lua
   |--mpd_buttons.lua
|--musicbar.lua
|--helpers.lua
|--rc.lua
|--default.jpg
Enter fullscreen mode Exit fullscreen mode
  • daemons - contains the daemons or background process that provide us information about our subscribed events.

  • widget - contains the widgets that collectively make the musicbar widget.

  • musicbar.lua - the overall shell widget / parent widget that contains all other music widgets in widget directory.

  • helpers.lua - contains helper functions needed by many modules.

  • rc.lua - the main file that is executed by awesome on startup.

  • default.jpg - the default image to show if no cover image is available from listen.moe. Like this one :

default


Prerequisite πŸš€

Necessary Fonts:

Once you download them and unpack them, place them into ~/.fonts or ~/.local/share/fonts.

  • You will need to create the directory if it does not exist.
  • It does not matter that the actual font files (.ttf) are deep inside multiple directories. They will be detected as long as they can be accessed from ~/.fonts or ~/.local/share/fonts.

Finally, run the following in order for your system to detect the newly installed fonts.

   fc-cache -v
Enter fullscreen mode Exit fullscreen mode

Making some helper functions 🀝🀝

helpers.lua is a file which contains some functions that are going to be used frequently in this project codebase. These include colorizing text, connecting signals with a defined action, configuring shapes of widgets etc.

helpers.lua

local wibox = require("wibox")
local gears = require("gears")
local helpers = {}

helpers.colorize_text = function(text, color)
    return "<span foreground='"..color.."'>"..text.."</span>"
end

helpers.prrect = function(radius, tl, tr, br, bl)
    return function(cr, width, height)
        gears.shape.partially_rounded_rect(cr, width, height, tl, tr, br, bl, radius)
    end
end

function helpers.vertical_pad(height)
    return wibox.widget{
        forced_height = height,
        layout = wibox.layout.fixed.vertical
    }
end

function helpers.horizontal_pad(width)
    return wibox.widget{
        forced_width = width,
        layout = wibox.layout.fixed.horizontal
    }
end

function helpers.add_hover_cursor(w, hover_cursor)
    local original_cursor = "left_ptr"

    w:connect_signal("mouse::enter", function ()
        local w = _G.mouse.current_wibox
        if w then
            w.cursor = hover_cursor
        end
    end)

    w:connect_signal("mouse::leave", function ()
        local w = _G.mouse.current_wibox
        if w then
            w.cursor = original_cursor
        end
    end)
end

return helpers
Enter fullscreen mode Exit fullscreen mode

Configuring Daemons πŸ‘ΏπŸ‘Ώ

Our very first challenge is to make and configure daemons, which will provide us with information and updates over regular interval.

Daemons can be made using the signals library of awesome. Here we have to configure three daemons specifically for ( listen.moe, spotify, mpd )

The daemons will be initiated with an init.lua . Using it we can easily turn on/off a daemon .


init.lua

require("daemon.moe")
require("daemon.mpd")
require("daemon.spotify")
Enter fullscreen mode Exit fullscreen mode

Now, Here is the code for the 3 daemon files.


moe.lua

local awful = require("awful")

local function emit_info (moe_script_output)
  local count = moe_script_output:match('count(%d*)cover')
  local cover = moe_script_output:match('cover(.*)title')
  local title = moe_script_output:match('title(.*)artist')
  local artist = moe_script_output:match('artist(.*)end')
  awesome.emit_signal("daemon::moe", count, cover, title, artist)
end

local moe_script = [[
    sh -c 'python $HOME/projects/moe.py'
]]

awful.spawn.easy_async_with_shell("ps x | grep \"python /home/lucifer/projects/moe.py\" | grep -v grep | awk '{print $1}' | xargskill", function()
  awful.spawn.with_line_callback(moe_script, {
    stdout = function (line)
        emit_info(line)
    end
})
end)
Enter fullscreen mode Exit fullscreen mode

Firstly, we are requiring the 'awful' library for spawning process here. The function emit_info takes the stdout of a python script (moe.py - a python script which establishes a web socket connection with listen.moe and writes data on song change ) , grabs the necessary information out of it and emits a signal with that information.

The moe.py script can be placed anywhere but you should update it's path in the local moe_script. Next we use awful.spawn to kill the python script process ( moe.py ) if it already exists ( in case of restart of awesome ).

After that in the callback function we use awful.spawn.with_line_callback to asynchronously read each line generated by the moe.py script and call emit_info function for that line to grab the necessary information out of it to emit a signal.

Additionaly the moe.py script that provides the information for moe daemon :

#!/usr/bin/python

from sys import stdout
from os.path import exists
from time import sleep
from requests import get
import wget
import json
import asyncio
import websockets

path = '/home/lucifer/projects/'
album_cover_url = 'https://cdn.listen.moe/covers/'
artist_cover_url = 'https://cdn.listen.moe/artists/'


async def send_ws(ws, data):
    json_data = json.dumps(data)
    await ws.send(json_data)


async def _send_pings(ws, interval=45):
    while True:
        await asyncio.sleep(interval)
        msg = {'op': 9}
        await send_ws(ws, msg)


def _save_cover(cover, URL):
    PATH = path + cover
    if not exists(PATH):
        try:
            wget.download(URL, out=PATH, bar=None)
            # file = get(URL, allow_redirects=True, timeout=2.5)
            # open(PATH, 'wb').write(file.content)
            return True
        except:
            return False
    else:
        return True


async def main(loop):
    url = 'wss://listen.moe/gateway_v2'
    count = ''
    cover = '!Available'
    artist = 'Not Available'
    title = 'Not Available'
    while True:
        try:
            ws = await websockets.connect(url)
            break
        except:
            sleep(30)

    while True:
        data = json.loads(await ws.recv())

        if data['op'] == 0:
            heartbeat = data['d']['heartbeat'] / 1000
            loop.create_task(_send_pings(ws, heartbeat))
        elif data['op'] == 1:
            if data['d']:

                if data['d']['listeners']:
                    count = str(data['d']['listeners'])

                if data['d']['song']['title']:
                    title = data['d']['song']['title']

                if data['d']['song']['artists']:
                    artist = data['d']['song']['artists'][0]['nameRomaji'] or data['d']['song']['artists'][0]['name'] or 'NotAvailable'

                if data['d']['song']['albums'] and data['d']['song']['albums'][0]['image']:
                    cover = data['d']['song']['albums'][0]['image']
                    if not _save_cover(cover, album_cover_url + cover):
                        cover = '!Available'
                elif data['d']['song']['artists'] and data['d']['song']['artists'][0]['image']:
                    cover = data['d']['song']['artists'][0]['image']
                    if not _save_cover(cover, artist_cover_url):
                        cover = '!Available'

                stdout.write(
                    f'count{count}cover{cover}title{title}artist{artist}end\n')
                stdout.flush()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
Enter fullscreen mode Exit fullscreen mode

The script when run establishes a web socket connection to a dedicated listen.moe web socket server. The script sleeps 30 sec if no internet connection is established and then fires again to establish the connection.

The script receives a json response object from which we extract the artist name, title name and the cover image endpoint. Images are downloaded using the cover image endpoint with wget python package. The path variable holds the download directory for images and should be updated to your preference in the script.

Some may prefer keeping the covers of the song, but i usually don't like saving them. So i added a cronjob to remove these images on every reboot.

@reboot rm /home/lucifer/projects/*.{png,jpg,jpeg,tmp}
Enter fullscreen mode Exit fullscreen mode

spotify.lua

local awful = require("awful")

local function emit_info(playerctl_output)
    local artist = playerctl_output:match('artist_start(.*)title_start')
    local title = playerctl_output:match('title_start(.*)status_start')
    -- Use the lower case of status
    local status = playerctl_output:match('status_start(.*)'):lower()
    status = string.gsub(status, '^%s*(.-)%s*$', '%1')

    awesome.emit_signal("daemon::spotify", artist, title, status)
end

-- Sleeps until spotify changes state (pause/play/next/prev)
local spotify_script = [[
  sh -c '
    playerctl --player spotify metadata --format 'artist_start{{artist}}title_start{{title}}status_start{{status}}' --follow
  ']]

-- Kill old playerctl process
awful.spawn.easy_async_with_shell("ps x | grep \"playerctl --player spotify metadata\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
    -- Emit song info with each line printed
    awful.spawn.with_line_callback(spotify_script, {
        stdout = function(line)
            emit_info(line)
        end
    })
end)
Enter fullscreen mode Exit fullscreen mode

spotify.lua takes the stdout from playerctl ( A utility command line tool to control MPRIS-enabled media players ). You need to have playerctl installed to make it work.

The script is very much alike to moe.lua except that it depends on playerctl for obtain information regarding the status of spotify player ( song name, album name, playing status ).


mpd.lua

local awful = require("awful")

local function emit_info()
    awful.spawn.easy_async_with_shell("sh -c 'mpc -f ARTIST@%artist%@TITLE@%title%@FILE@%file%@'",
        function(stdout)
            local artist = stdout:match('^ARTIST@(.*)@TITLE')
            local title = stdout:match('@TITLE@(.*)@FILE')
            local status = stdout:match('\n%[(.*)%]')

            if not artist or artist == "" then
              artist = "N/A"
            end
            if not title or title == "" then
              title = stdout:match('@FILE@(.*)@')
              if not title or title == "" then
                  title = "N/A"
              end
            end

            local paused
            if status == "playing" then
                paused = false
            else
                paused = true
            end

            awesome.emit_signal("daemon::mpd", artist, title, paused)
        end
    )
end

-- Run once to initialize widgets
emit_info()

-- Sleeps until mpd changes state (pause/play/next/prev)
local mpd_script = [[
  sh -c '
    mpc idleloop player
  ']]

-- Kill old mpc idleloop player process
awful.spawn.easy_async_with_shell("ps x | grep \"mpc idleloop player\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
    -- Emit song info with each line printed
    awful.spawn.with_line_callback(mpd_script, {
        stdout = function()
            emit_info()
        end
    })
end)

----------------------------------------------------------

-- MPD Volume
local function emit_volume_info()
    awful.spawn.easy_async_with_shell("mpc volume | awk '{print substr($2, 1, length($2)-1)}'",
        function(stdout)
            awesome.emit_signal("daemon::mpd_volume", tonumber(stdout))
        end
    )
end

-- Run once to initialize widgets
emit_volume_info()

-- Sleeps until mpd volume changes
-- >> We use `sed '1~2d'` to remove every other line since the mixer event
-- is printed twice for every volume update.
-- >> The `-u` option forces sed to work in unbuffered mode in order to print
-- without waiting for `mpc idleloop mixer` to finish
local mpd_volume_script = [[
  sh -c "
    mpc idleloop mixer | sed -u '1~2d'
  "]]

-- Kill old mpc idleloop mixer process
awful.spawn.easy_async_with_shell("ps x | grep \"mpc idleloop mixer\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
    -- Emit song info with each line printed
    awful.spawn.with_line_callback(mpd_volume_script, {
        stdout = function()
            emit_volume_info()
        end
    })
end)

local mpd_options_script = [[
  sh -c "
    mpc idleloop options
  "]]

local function emit_options_info()
    awful.spawn.easy_async_with_shell("mpc | tail -1",
        function(stdout)
            local loop = stdout:match('repeat: (.*)')
            local random = stdout:match('random: (.*)')
            awesome.emit_signal("daemon::mpd_options", loop:sub(1, 2) == "on", random:sub(1, 2)== "on")
        end
    )
end

-- Run once to initialize widgets
emit_options_info()

-- Kill old mpc idleloop options process
awful.spawn.easy_async_with_shell("ps x | grep \"mpc idleloop options\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
    -- Emit song info with each line printed
    awful.spawn.with_line_callback(mpd_options_script, {
        stdout = function()
            emit_options_info()
        end
    })
end)
Enter fullscreen mode Exit fullscreen mode

mpd.lua script provides us with updates from our mpd client regarding ( song name, album name, player status, volume, random/loop enabled ). The script uses mpc ( command line client for MPD player ) to read the status of MPD player.

To get subscribed to events of MPD player, we use the mpc idleloop. Thus on any event ( song change, volume up, pause ) we get some feedback over which we can implement some action based on the event that occoured.

There are 3 signals being generated by the mpd.lua ( song status , volume status , options status ). I basically use the first one only ( song status ), but you can also use the other two to get subscribed about the volume and option updates as well.


Making Widgets πŸ”₯πŸ”₯

Now that we have configured our daemons, our next task is to make widgets for displaying the information provided to us. Our widgets will depend upon these daemons to update their state and change themselves based on subscribed events.

There are 5 widgets we are going to make :

  1. moe.lua
  2. spotify_song.lua
  3. spotify_buttons.lua
  4. mpd_song.lua
  5. mpd_buttons.lua

moe.lua

local awful = require("awful")
local wibox = require("wibox")
local naughty = require("naughty")
local helpers = require("helpers")

local default_image = os.getenv("HOME")..".config/awesome/default.jpg"

local moe_playing_colors = {
    x.color7,
    x.color8,
    x.color9,
    x.color10,
    x.color11,
    x.color12,
}

local moe_cover = wibox.widget.imagebox()
local moe_title = wibox.widget.textbox()
local moe_artist = wibox.widget.textbox()
local moe_listeners_count = wibox.widget.textbox()

local moe_play_icon = wibox.widget.textbox()
moe_play_icon.markup = helpers.colorize_text("ο…„", x.color4)
moe_play_icon.font = "Material Icons medium 30"
moe_play_icon.align = "center"
moe_play_icon.valign = "center"

local moe_listeners_icon = wibox.widget.textbox()
moe_listeners_icon.markup = helpers.colorize_text("", x.color6)
moe_listeners_icon.font = "Material Icons medium 27"
moe_listeners_icon.align = "center"
moe_listeners_icon.valign = "center"

local function toggle_icon()
    if moe_play_icon.text == "ο…„" then
      moe_play_icon.markup = helpers.colorize_text("οŠ‹", x.color4)
    else
      moe_play_icon.markup = helpers.colorize_text("ο…„", x.color4)
    end
end

moe_play_icon:buttons(
    gears.table.join(
        awful.button({ }, 1, function ()
            toggle_icon()
            awful.spawn.with_shell("moe")
        end)
))

helpers.add_hover_cursor(moe_play_icon, "hand1")


local moe_widget = wibox.widget {
    -- Cover Image
    {
        {
            {
                image = default_image,
                clip_shape = helpers.rrect(dpi(16)),
                widget = moe_cover
            },
            halign = 'center',
            valign = 'center',
            layout = wibox.container.place
         },
      ---shape = helpers.rrect(box_radius / 2),
      ---widget = wibox.container.background
     height = dpi(250),
     width = dpi(250),
     layout = wibox.container.constraint
    },
    helpers.vertical_pad(dpi(10)),
    -- Title widget
    {
        {
            align = "center",
            markup = helpers.colorize_text("MoeChan", x.color4),
            font = "sans medium 14",
            widget = moe_title
        },
        left = dpi(20),
        right = dpi(20),
        widget = wibox.container.margin
    },
    -- Artist widget
    {
        {
            align = "center",
            text = "unavailable",
            font = "sans medium 12",
            widget = moe_artist
        },
        left = dpi(20),
        right = dpi(20),
        widget = wibox.container.margin
    },
    helpers.vertical_pad(dpi(5)),
    {
        {
        -- play icon
            {
                moe_play_icon,
                widget = wibox.container.background
            },
            helpers.horizontal_pad(dpi(70)),
        -- headphone icon
            {
                moe_listeners_icon,
                widget = wibox.container.background,
            },
        -- listener count
            {
                align = "center",
                text = "",
                font = "sans medium 12",
                widget = moe_listeners_count
            },
            spacing = 10,
            widget = wibox.layout.fixed.horizontal
        },
        align = "center",
        valign = "center",
        widget = wibox.container.place
    },
    spacing = 4,
    layout = wibox.layout.fixed.vertical
}

awesome.connect_signal("daemon::moe", function(count ,cover, title, artist)
    if tostring(cover) == "!Available" then
      moe_cover.image = default_image    
    else
      moe_cover.image = os.getenv("HOME").."/projects/"..cover
    end
    moe_title.markup = helpers.colorize_text(title, moe_playing_colors[math.random(6)])
    moe_artist.text = artist
    moe_listeners_count.text = tostring(count)
    naughty.notify({ title = "Moe | Now Playing", message = title.." by "..artist })
end)

return moe_widget
Enter fullscreen mode Exit fullscreen mode

moe.lua returns the listen.moe widget for the musicbar. The libraries include :

  • awful -> For buttons and spawning processes.
  • wibox -> The main widget box library.
  • naughty -> The notification library.
  • helpers -> The user defined helper library.

The file consists of moe_playing_colors which are essentially theme colors taken from xrdb and are defined in rc.lua ( later in this post ). Alongside we have a local variable default image which is basically a image that is put in place of cover image if it's not available.

Next we have created 1 imagebox and 4 textbox for imagecover, song title, artist title, play icon and listener count.

moe_play_icon:buttons control the action when the icon is clicked. When it does, it fires a shell script 'moe' ( defined below ) which plays the music from listen.moe using mpv player as a background process.

You can either install the fonts defined in {textbox}.font or change them according to your preference.

moe_widget is our main listen.moe widget which consists of all individual wibox widgets ( cover image, song title, artist title, play icon, listeners count ).

For subscribing to the daemon updates ( daemon/moe.lua ), we use the awesome.connect_signal and based on the information update our widget state.

Additionaly the shell script moe :

#!/bin/sh

moe="https://listen.moe/stream"

pkill -f $moe || mpv "$moe"
Enter fullscreen mode Exit fullscreen mode

Keep in mind you need to add this script to your path to make it available everywhere.


spotify_song.lua

local wibox = require("wibox")

-- Declare widgets
local spotify_artist = wibox.widget.textbox()
local spotify_title = wibox.widget.textbox()

-- Main widget that includes all others
local spotify_widget = wibox.widget {
    -- Title widget
    {
        align = "center",
        text = "Spotify",
        font = "sans 14",
        widget = spotify_title
    },
    -- Artist widget
    {
        align = "center",
        text = "unavailable",
        font = "sans 10",
        widget = spotify_artist
    },
    spacing = 2,
    layout = wibox.layout.fixed.vertical
}

-- Subcribe to spotify updates
awesome.connect_signal("daemon::spotify", function(artist, title, status)
    -- Do whatever you want with artist, title, status
    -- ...
    spotify_artist.text = artist
    spotify_title.text = title

    -- Example notification (might not be needed if spotify already sends one)
    -- if status == "playing" then
    -- naughty.notify({ title = "Spotify | Now Playing", message= title.." by "..artist })
    -- end
end)

return spotify_widget
Enter fullscreen mode Exit fullscreen mode

spotify.lua returns the spotify song widget which contains the song artist and song title.

The code is very much similar to moe.lua widget except that here we are connecting to the daemon::spotify signal.

Since spotify already notifies us about song changes i do not like receiving extra notification, thus i commented out the notification line in awesome.connect_signal. This is just to avoid getting 2 notifications.


spotify_buttons.lua

local gears = require("gears")
local awful = require("awful")
local wibox = require("wibox")
local helpers = require("helpers")

local spotify_prev_symbol = wibox.widget.textbox()
spotify_prev_symbol.markup = helpers.colorize_text("β‰ͺ", x.foreground.."33")
spotify_prev_symbol.font = "Material Icons Bold 18"
spotify_prev_symbol.align = "center"
spotify_prev_symbol.valign = "center"
local spotify_next_symbol = wibox.widget.textbox()
spotify_next_symbol.markup = helpers.colorize_text("≫", x.foreground.."33")
spotify_next_symbol.font = "Material Icons Bold 18"
spotify_next_symbol.align = "center"
spotify_next_symbol.valign = "center"

-- local note_symbol = ""
-- local note_symbol = "β™«"
local note_symbol = "ッ"
local big_note = wibox.widget.textbox()
big_note.font = "Material Icons Bold 17"
big_note.align = "center"
big_note.markup = helpers.colorize_text(note_symbol, x.foreground.."33")
local small_note = wibox.widget.textbox()
small_note.align = "center"
small_note.markup = helpers.colorize_text(note_symbol, x.foreground.."55")
small_note.font = "Material Icons Bold 13"
-- small_note.valign = "bottom"
local double_note = wibox.widget {
    big_note,
    -- small_note,
    {
        small_note,
        top = dpi(11),
        widget = wibox.container.margin
    },
    spacing = dpi(-5),
    layout = wibox.layout.fixed.horizontal
}

local spotify_toggle_icon = wibox.widget {
    double_note,
    -- bg = "#00000000",
    widget = wibox.container.background
}
spotify_toggle_icon:buttons(gears.table.join(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("playerctl --player spotify play-pause")
    end)
))

local spotify_prev_icon = wibox.widget {
    spotify_prev_symbol,
    shape = gears.shape.circle,
    widget = wibox.container.background
}
spotify_prev_icon:buttons(gears.table.join(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("playerctl --player spotify previous")
    end)
))

local spotify_next_icon = wibox.widget {
    spotify_next_symbol,
    shape = gears.shape.circle,
    widget = wibox.container.background
}
spotify_next_icon:buttons(gears.table.join(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("playerctl --player spotify next")
    end)
))

local music_playing_colors = {
    x.color1,
    x.color2,
    x.color3,
    x.color4,
    x.color5,
    x.color6,
}

awesome.connect_signal("daemon::spotify", function(artist, title, status)
    local accent, small_note_color
    if string.lower(status) == "paused" then
        accent = x.foreground.."33"
        small_note_color = x.foreground.."55"
    else
        accent = music_playing_colors[math.random(6)]
        small_note_color = x.foreground
    end

    big_note.markup = helpers.colorize_text(note_symbol, accent)
    small_note.markup = helpers.colorize_text(note_symbol, small_note_color)

    spotify_prev_symbol.markup = helpers.colorize_text(spotify_prev_symbol.text, accent)
    spotify_next_symbol.markup = helpers.colorize_text(spotify_next_symbol.text, accent)
end)

local spotify_buttons = wibox.widget {
    nil,
    {
        spotify_prev_icon,
        spotify_toggle_icon,
        spotify_next_icon,
        spacing = dpi(14),
        layout  = wibox.layout.fixed.horizontal
    },
    expand = "none",
    layout = wibox.layout.align.horizontal,
}

-- Add clickable mouse effects on some widgets
helpers.add_hover_cursor(spotify_next_icon, "hand1")
helpers.add_hover_cursor(spotify_prev_icon, "hand1")
helpers.add_hover_cursor(spotify_toggle_icon, "hand1")

return spotify_buttons

Enter fullscreen mode Exit fullscreen mode

spotify_buttons.lua contains the buttons for controlling the spotify directly from your musicbar.

The play/pause, previous, next icons are binded to certain action. The playerctl utility is the tool which is used for carrying out these actions ( pause/play, next song, previous song ).

The color of the icons changes when a song is playing. The colors are randomly picked from an array music_playing_colors.


mpd_song.lua

local gears = require("gears")
local wibox = require("wibox")

-- Set colors
local title_color = x.color7
local artist_color = x.color7
local paused_color = x.color8

local mpd_title = wibox.widget{
    text = "---------",
    align = "center",
    valign = "center",
    widget = wibox.widget.textbox
}

local mpd_artist = wibox.widget{
    text = "---------",
    align = "center",
    valign = "center",
    widget = wibox.widget.textbox
}

-- Main widget
local mpd_song = wibox.widget{
    mpd_title,
    mpd_artist,
    layout = wibox.layout.fixed.vertical
}

local artist_fg
local artist_bg
awesome.connect_signal("daemon::mpd", function(artist, title, status)
    if status == "paused" then
        artist_fg = paused_color
        title_fg = paused_color
    else
        artist_fg = artist_color
        title_fg = title_color
    end

    -- Escape &'s
    title = string.gsub(title, "&", "&amp;")
    artist = string.gsub(artist, "&", "&amp;")

    mpd_title.markup =
        "<span foreground='" .. title_fg .."'>"
        .. title .. "</span>"
    mpd_artist.markup =
        "<span foreground='" .. artist_fg .."'>"
        .. artist .. "</span>"
end)

return mpd_song

Enter fullscreen mode Exit fullscreen mode

mpd_song.lua returns us the widget for displaying songs playing through MPD. Most of this is similar to spotify_song.lua.

Here we are connecting to the daemon::mpd signal to receive the information about events we are subscribed to.


mpd_buttons.lua

-- Text buttons for mpd control using "Material Design Icons" font
local gears = require("gears")
local awful = require("awful")
local wibox = require("wibox")
local helpers = require("helpers")

local mpd_prev_symbol = wibox.widget.textbox()
mpd_prev_symbol.markup = helpers.colorize_text("ξ—‹", x.foreground)
mpd_prev_symbol.font = "Material Icons Bold 18"
mpd_prev_symbol.align = "center"
mpd_prev_symbol.valign = "center"
local mpd_next_symbol = wibox.widget.textbox()
mpd_next_symbol.markup = helpers.colorize_text("ξ—Œ", x.foreground)
mpd_next_symbol.font = "Material Icons Bold 18"
mpd_next_symbol.align = "center"
mpd_next_symbol.valign = "center"

local note_symbol = ""
local big_note = wibox.widget.textbox(note_symbol)
big_note.font = "Material Icons Bold 15"
big_note.align = "center"
local small_note = wibox.widget.textbox()
small_note.align = "center"
small_note.markup = helpers.colorize_text(note_symbol, x.foreground)
small_note.font = "Material Icons Bold 11"
-- small_note.valign = "bottom"
local double_note = wibox.widget {
    big_note,
    -- small_note,
    {
        small_note,
        top = dpi(11),
        widget = wibox.container.margin
    },
    spacing = dpi(-9),
    layout = wibox.layout.fixed.horizontal
}

local mpd_toggle_icon = wibox.widget {
    double_note,
    -- bg = "#00000000",
    widget = wibox.container.background
}
mpd_toggle_icon:buttons(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("mpc -q toggle")
    end)
)

local mpd_prev_icon = wibox.widget {
    mpd_prev_symbol,
    shape = gears.shape.circle,
    widget = wibox.container.background
}
mpd_prev_icon:buttons(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("mpc -q prev")
    end)
)

local mpd_next_icon = wibox.widget {
    mpd_next_symbol,
    shape = gears.shape.circle,
    widget = wibox.container.background
}
mpd_next_icon:buttons(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("mpc -q next")
    end)
)

local music_playing_counter = 0
local last_artist
local last_title
local music_playing_colors = {
    x.color1,
    x.color2,
    x.color3,
    x.color4,
    x.color5,
    x.color6,
}
local last_color = music_playing_colors[1]

awesome.connect_signal("daemon::mpd", function(artist, title, paused)
    local accent, small_note_color
    if paused then
        accent = x.foreground.."33"
        small_note_color = x.foreground.."55"
    else
        if artist ~= last_artist and title ~= last_title then
            accent = music_playing_colors[(music_playing_counter % #music_playing_colors) + 1]
            music_playing_counter = music_playing_counter + 1
        else
            accent = last_color
        end
        last_artist = artist
        last_title = title
        last_color = accent
        small_note_color = x.foreground
    end

    big_note.markup = helpers.colorize_text(note_symbol, accent)
    small_note.markup = helpers.colorize_text(note_symbol, small_note_color)
    -- mpd_prev_icon.bg = accent
    -- mpd_next_icon.bg = accent
    mpd_prev_symbol.markup = helpers.colorize_text(mpd_prev_symbol.text, accent)
    mpd_next_symbol.markup = helpers.colorize_text(mpd_next_symbol.text, accent)
end)

local mpd_buttons = wibox.widget {
    nil,
    {
        mpd_prev_icon,
        mpd_toggle_icon,
        mpd_next_icon,
        spacing = dpi(14),
        layout  = wibox.layout.fixed.horizontal
    },
    expand = "none",
    layout = wibox.layout.align.horizontal,
}

-- Add clickable mouse effects on some widgets
helpers.add_hover_cursor(mpd_next_icon, "hand1")
helpers.add_hover_cursor(mpd_prev_icon, "hand1")
helpers.add_hover_cursor(mpd_toggle_icon, "hand1")

return mpd_buttons
Enter fullscreen mode Exit fullscreen mode

mpd_buttons.lua returns the widget for button control for controlling you MPD client straight from musicbar. It has the same spotify_buttons.lua buttons.

However, one minor change in mpd_buttons.lua is that it keeps track of the last color used and thus doesn't display same color twice in succession for the icons color.


Putting together the Musicbar πŸ“Ž

Now that we have finally configured all our daemons and widgets, it is finally time to assemble the full musicbar with these widgets and daemons.

The music bar is erected on the right side of my screen and pops out when i hover the mouse pointer over the right edge.

musicbar.lua

-- modules
local awful = require("awful")
local helpers = require("helpers")
local wibox = require("wibox")

-- Moe
local moe = require("widget.moe")

-- Spotify
local spotify_buttons = require("widget.spotify_buttons")
local spotify = require("widget.spotify")
local spotify_widget_children = spotify:get_all_children()
local spotify_title = spotify_widget_children[1]
local spotify_artist = spotify_widget_children[2]
spotify_title.forced_height = dpi(22)
spotify_artist.forced_height = dpi(16)

-- Mpd
local mpd_buttons = require("widget.mpd_buttons")
local mpd_song = require("widget.mpd_song")
local mpd_widget_children = mpd_song:get_all_children()
local mpd_title = mpd_widget_children[1]
local mpd_artist = mpd_widget_children[2]
mpd_title.font = "sans medium 14"
mpd_artist.font = "sans medium 10"

-- Set forced height in order to limit the widgets to one line.
-- Might need to be adjusted depending on the font.
mpd_title.forced_height = dpi(22)
mpd_artist.forced_height = dpi(16)

mpd_song:buttons(gears.table.join(
    awful.button({ }, 1, function ()
        awful.spawn.with_shell("mpc -q toggle")
    end),
    awful.button({ }, 3, apps.music),
    awful.button({ }, 4, function ()
        awful.spawn.with_shell("mpc -q prev")
    end),
    awful.button({ }, 5, function ()
        awful.spawn.with_shell("mpc -q next")
    end)
))



-- Create the music sidebar
music_sidebar = wibox({visible = false, ontop = true, type = "dock", screen = screen.primary})
music_sidebar.bg = "#00000000" -- For anti aliasing
music_sidebar.fg = x.color7
music_sidebar.opacity = 1
music_sidebar.height = screen.primary.geometry.height/1.5
music_sidebar.width = dpi(300)
music_sidebar.y = 0

awful.placement.right(music_sidebar)
awful.placement.maximize_vertically(sidebar, { honor_workarea = true, margins = { top = dpi(5) * 2 } })

music_sidebar:buttons(gears.table.join(
    -- Middle click - Hide sidebar
    awful.button({ }, 2, function ()
        music_sidebar_hide()
    end)
))

music_sidebar:connect_signal("mouse::leave", function ()
        music_sidebar_hide()
end)

music_sidebar_show = function()
    music_sidebar.visible = true
end

music_sidebar_hide = function()
    music_sidebar.visible = false
end

music_sidebar_toggle = function()
    if music_sidebar.visible then
        music_sidebar_hide()
    else
        music_sidebar.visible = true
    end
end

-- Activate sidebar by moving the mouse at the edge of the screen

local music_sidebar_activator = wibox({y = music_sidebar.y, width = 1, visible = true, ontop = false, opacity = 0, below = true, screen = screen.primary})
    music_sidebar_activator.height = music_sidebar.height
    music_sidebar_activator:connect_signal("mouse::enter", function ()
        music_sidebar.visible = true
    end)
        awful.placement.right(music_sidebar_activator)

-- Music sidebar placement
music_sidebar:setup {
            {
                {
                    {
                        {
                          helpers.vertical_pad(dpi(25)),
                          moe,
                          helpers.vertical_pad(dpi(15)),
                          layout = wibox.layout.fixed.vertical
                        },
                      halign = 'center',
                      valign = 'center',
                      layout = wibox.container.place
                    },
                  shape = helpers.prrect(dpi(40), true, false, false, true),
                  bg = x.color8.."30",
                  widget = wibox.container.background
                },
                {
                  {
                    helpers.vertical_pad(dpi(30)),
                    {
                      spotify_buttons,
                      spotify,
                      spacing = dpi(5),
                      layout = wibox.layout.fixed.vertical
                    },
                    layout = wibox.layout.fixed.vertical
                  },
                  left = dpi(20),
                  right = dpi(20),
                  widget = wibox.container.margin
                },
                {
                    {
                        mpd_buttons,
                        mpd_song,
                        spacing = dpi(5),
                        layout = wibox.layout.fixed.vertical
                    },
                    top = dpi(40),
                    bottom = dpi(20),
                    left = dpi(20),
                    right = dpi(20),
                    widget = wibox.container.margin
                },
                helpers.vertical_pad(dpi(25)),
                layout = wibox.layout.fixed.vertical
            },
    shape = helpers.prrect(dpi(40), true, false, false, true),
    bg = x.background,
    widget = wibox.container.background
}
Enter fullscreen mode Exit fullscreen mode

Bringing the musicbar alive in rc.lua ✨✨

Now that we have assembled all the required pieces to the puzzle, it's time to assemble and put them to work in rc.lua.

Add the following lines to the top of your rc.lua

-- Initialization
-- ===================================================================
-- Theme handling library
local beautiful = require("beautiful")
local xrdb = beautiful.xresources.get_current_theme()
-- Make dpi function global
dpi = beautiful.xresources.apply_dpi
-- Make xresources colors global
x = {
    --           xrdb variable
    background = xrdb.background,
    foreground = xrdb.foreground,
    color0     = xrdb.color0,
    color1     = xrdb.color1,
    color2     = xrdb.color2,
    color3     = xrdb.color3,
    color4     = xrdb.color4,
    color5     = xrdb.color5,
    color6     = xrdb.color6,
    color7     = xrdb.color7,
    color8     = xrdb.color8,
    color9     = xrdb.color9,
    color10    = xrdb.color10,
    color11    = xrdb.color11,
    color12    = xrdb.color12,
    color13    = xrdb.color13,
    color14    = xrdb.color14,
    color15    = xrdb.color15,
}

-- Load helper functions
local helpers = require("helpers")

-- Load the musicbar 
require("musicbar")

-- >> Daemons
-- Make sure to initialize it last in order to allow all widgets to connect to their needed daemon signals.
require("daemon")


-----------<< Rest of your config >>-----------------
Enter fullscreen mode Exit fullscreen mode

With this, hopefully if you'll restart your awesome window manager you will get a musicbar on your right edge when you hover the mouse over that edge.

Thank You, for following me to the end. Hopefully, it was benificial to you as much as it was to me.


Resources βœ”οΈ

All configs along with directory structure :

GitHub logo ashish-patwal / Moe-Lua-Daemon

Music Bar for Awesome WM ( listen.moe , spotify, MPD )

Update

  • This repository now holds the code for musicbar ( listen.moe, mpd, spotify ).

Basic Info

  • A lua daemon for fetching information from listen.moe using websocket through python websocket script.
  • Also contains code for mpd and spotify music bar .
  • Used for Awesome Window Manager .

ToDo

  • Add widget for displaying info
  • Add async fetching of images of artists / album covers to display in widget
  • Ability to stream the music alongside using mpv
  • Ability to Start / Stop

Screenshots






Do give me a follow if you liked the content. I will be posting some really interesting stuff.

GitHub logo ashish-patwal / dotfiles

My dotfiles for the ArchLinux OS . Dual Boot . Heavily Riced . Scripting is ❀️ .

Discussion (2)

Collapse
shubhampatilsd profile image
Shubham Patil

WOW this is really cool. It must have been a pain to work in Lua though right?

Collapse
ashishpatwal profile image
Ashish Patwal Author

I think lua just sounds scary . Infact it's the most beginners friendly language as its easy to learn . Awesome WM is just like a framework for lua ( like django is for python ) . It takes time to implement in Awesome but it's worth every penny .