Complete Computing Environment: Window Management with AwesomeWM

Table of Contents

(provide 'cce-awesome)

Awesome WM Configuration

I'm trying again to use AwesomeWM, and modifying it more than I usually do to support multiple profiles. Basically, I want a WM that will change its configuration based on the formfactor of the device I'm using it from. I don't use my work machine the same way I use my laptop the same way I use my laptop docked at home. As such, my window manager makes some minor changes to itself depending on how that stuff is set up.

- name: awesome and its plugins installed
  dnf:
    name: "{{item}}"
    state: installed
  with_items:
  - awesome
  - vicious
  when: ansible_pkg_mgr == "dnf"

- name: awesome and its plugins installed
  apt:
    name: "{{item}}"
    state: installed
  when: ansible_pkg_mgr == "apt"
  with_items:
  - awesome
  - vicious


- name: awesomerc installed
  become: yes
  become_user: rrix
  copy:
    src: out/awesomerc.lua
    dest: ~/.config/awesome/rc.lua

<<libraries>>

<<startup-errors>>

<<common-setup>>

<<profile-detection>>

<<layout-engine>>

<<mouse-bindings>>

<<wibox>>

client.connect_signal("manage", function (c, startup)
<<focus-follows-mouse>>
<<border-focus>>
end)

<<raise-by-name-definition>>

globalkeys = awful.util.table.join(
  <<gb-core>>,
  <<gb-dmenus>>,

  <<gb-tag-management>>,
  <<gb-client-management>>,
  <<gb-layout-management>>,
  <<pulse-keys>>
)

root.keys(globalkeys)

<<clientkeys>>

<<awful-rules>>
<<awful-rules-work>>
<<awful-rules-home>>

Requried Libraries

-- Standard awesome library
local gears = require("gears")
local awful = require("awful")
awful.rules = require("awful.rules")
require("awful.autofocus")
-- Widget and layout library
local wibox = require("wibox")
-- Theme handling library
local beautiful = require("beautiful")
-- Notification library
local naughty = require("naughty")
local menubar = require("menubar")
-- Widget library
local vicious = require("vicious")
vicious.contrib = require("vicious.contrib")

Error Handling

-- Check if awesome encountered an error during startup and fell back to
-- another config (This code will only ever execute for the fallback config)
if awesome.startup_errors then
   naughty.notify({ preset = naughty.config.presets.critical,
                    title = "Oops, there were errors during startup!",
                    text = awesome.startup_errors })
end

-- Handle runtime errors after startup
do
   local in_error = false
   awesome.connect_signal("debug::error", function (err)
                             -- Make sure we don't go into an endless error loop
                             if in_error then return end
                             in_error = true

                             naughty.notify({ preset = naughty.config.presets.critical,
                                              title = "Oops, an error happened!",
                                              text = err })
                             in_error = false
   end)
end

Common Variables

-- Themes define colours, icons, font and wallpapers.
beautiful.init("/usr/share/awesome/themes/zenburn/theme.lua")
theme.font = "Fira Mono 14"

-- This is used later as the default terminal and editor to run.
terminal = "konsole"
editor_cmd = os.getenv("EDITOR") or "emacsclient -c"

-- Default modkey.
-- Usually, Mod4 is the key with a logo between Control and Alt.
-- If you do not like this or do not have such a key,
-- I suggest you to remap Mod4 to another key using xmodmap or other tools.
-- However, you can use another modifier like Mod1, but it may interact with others.
modkey = "Mod4"

menubar.utils.terminal = terminal -- Set the terminal for applications that require it
local profile = "home"

local active_outputs = {}
f = io.popen("xrandr")
for output in f:lines() do
   if string.find(output, " connected") then
      active_display=string.match(output, "%w+")
      table.insert(active_outputs, active_display)
   end
end
f:close()

if awful.util.table.hasitem(active_outputs, "DP2") and awful.util.table.hasitem(active_outputs, "DP1") then
   profile = "work"
elseif awful.util.table.hasitem(active_outputs, "DP2") then
   profile = "home"
elseif awful.util.table.hasitem(active_outputs, "LVDS1") then
   profile = "laptop"
elseif awful.util.table.hasitem(active_outputs, "eDP1") then
   profile = "laptop"
end

naughty.notify({text="Profile: " .. profile})
local layouts = {}
layouts["laptop"] = {
   awful.layout.suit.max,
   awful.layout.suit.magnifier,
   awful.layout.suit.tile,
   awful.layout.suit.fair,
   awful.layout.suit.tile.bottom
}
layouts["home"] = {
   awful.layout.suit.tile,
   awful.layout.suit.tile.bottom,
   awful.layout.suit.fair,
   awful.layout.suit.max
}
layouts["work"] = {
   awful.layout.suit.tile.top,
   awful.layout.suit.fair.horizontal,
   awful.layout.suit.magnifier
}

tags = {}
for s = 1, screen.count() do
   tags[s] = awful.tag({ "Work", "Web" }, s, layouts[profile][1])
end

Mouse Bindings

root.buttons(awful.util.table.join(
                awful.button({ }, 3, function () mymainmenu:toggle() end),
                awful.button({ }, 4, awful.tag.viewnext),
                awful.button({ }, 5, awful.tag.viewprev)
))

Signal Configuration

Enable focus follows mouse

c:connect_signal("mouse::enter", function(c)
                    if awful.layout.get(c.screen) ~= awful.layout.suit.magnifier
                    and awful.client.focus.filter(c) then
                       client.focus = c
                    end
end)

Show special border on focused window

client.connect_signal("focus", function(c)
                         c.border_color = beautiful.border_focus
end)
client.connect_signal("unfocus", function(c)
                         c.border_color = beautiful.border_normal
end)

Raise by Name

function raise_by_name ()
   out = ''
   for k, v in pairs(client.get()) do
      out = out .. v.name .. '\n'
      out = out .. v.class .. '\n'
   end
   file = io.open("/tmp/awesome-raise.txt", "w")
   file:write(out)
   file:close()
   x = io.popen('cat /tmp/awesome-raise.txt | dmenu -l 10 -b -i'):read()
   for k, v in pairs(client.get()) do
      if v.name ==  x or v.class == x then
         client.focus = v
         awful.tag.viewonly(v:tags()[1])
         v:swap(awful.client.getmaster())
         v:raise()
         return
      end
   end
end

Global Key bindings

-- Standard program
awful.key({ modkey, "Control" }, "Return", function () awful.util.spawn(terminal) end),
awful.key({ modkey, "Control" }, "r", awesome.restart),
awful.key({ modkey, "Shift"   }, "q", awesome.quit)
-- Menubar
awful.key({ modkey }, "p", function() menubar.show() end),
awful.key({ modkey }, "e", raise_by_name)

Layout Management

awful.key({ modkey,           }, "s",      awful.tag.viewprev       ),
awful.key({ modkey,           }, "d",       function () awful.screen.focus_relative(-1) end),
awful.key({ modkey,           }, "Escape", awful.tag.history.restore)
awful.key({ modkey,           }, "j",
   function ()
      awful.client.focus.byidx( 1)
      if client.focus then client.focus:raise() end
end),
awful.key({ modkey,           }, "k",
   function ()
      awful.client.focus.byidx(-1)
      if client.focus then client.focus:raise() end
end),

awful.key({ modkey, "Shift"   }, "j", function () awful.client.swap.byidx(  1)    end),
awful.key({ modkey, "Shift"   }, "k", function () awful.client.swap.byidx( -1)    end),
awful.key({ modkey,           }, "u", awful.client.urgent.jumpto)
awful.key({ modkey,           }, "l",     function () awful.tag.incmwfact( 0.05)    end),
awful.key({ modkey,           }, "h",     function () awful.tag.incmwfact(-0.05)    end),
awful.key({ modkey, "Shift"   }, "h",     function () awful.tag.incnmaster( 1)      end),
awful.key({ modkey, "Shift"   }, "l",     function () awful.tag.incnmaster(-1)      end),
awful.key({ modkey, "Control" }, "h",     function () awful.tag.incncol( 1)         end),
awful.key({ modkey, "Control" }, "l",     function () awful.tag.incncol(-1)         end),
awful.key({ modkey,           }, "space", function () awful.layout.inc(layouts[profile],  1) end),
awful.key({ modkey, "Shift"   }, "space", function () awful.layout.inc(layouts[profile], -1) end),
awful.key({ modkey, "Control" }, "n", awful.client.restore)

Window ("client") Key Bindings

This stuff is all standard, except that I have Super-c overridden so that it can't close Plasma windows.

function wrapped_kill(client)
   if client.class ~= 'plasmashell' then
      client:kill()
   end
end

clientkeys = awful.util.table.join(
   awful.key({ modkey,           }, "f",      function (c) c.fullscreen = not c.fullscreen  end),
   awful.key({ modkey, "Shift"   }, "c",      function (c) wrapped_kill(c)                  end),
   awful.key({ modkey, "Control" }, "space",  awful.client.floating.toggle                     ),
   awful.key({ modkey,           }, "Return", function (c) c:swap(awful.client.getmaster()) end),
   awful.key({ modkey,           }, "o",      awful.client.movetoscreen                        ),
   awful.key({ modkey,           }, "t",      function (c) c.ontop = not c.ontop            end),
   awful.key({ modkey, "Shift" }, "s",
      function (client)
         local num = awful.tag.getidx()
         local tagnum = num - math.floor(num/2) * 2 + 1
         local tag = awful.tag.gettags(client.screen)[tagnum]
         if  tag then
            awful.client.movetotag(tag)
         end
   end),
   awful.key({ modkey,           }, "n",
      function (c)
         -- The client currently has the input focus, so it cannot be
         -- minimized, since minimized clients can't have the focus.
         c.minimized = true
   end),
   awful.key({ modkey,           }, "m",
      function (c)
         c.maximized_horizontal = not c.maximized_horizontal
         c.maximized_vertical   = not c.maximized_vertical
   end)
)

clientbuttons = awful.util.table.join(
   awful.button({ }, 1, function (c) client.focus = c; c:raise() end),
   awful.button({ modkey }, 1, awful.mouse.client.move),
   awful.button({ modkey }, 3, awful.mouse.client.resize))

Window Placement Rules

Pretty standard stuff, except I float plasma and make sure it can't gain focus automatically and hide the main window on the 9th tag since I can't figure out how to make it render below the Wibox.

awful.rules.rules = {
   { rule = { },
     properties = { border_width = 4,
                    border_color = beautiful.border_normal,
                    focus = awful.client.focus.filter,
                    raise = true,
                    keys = clientkeys,
                    buttons = clientbuttons } },
   { rule = { class = "pinentry" },
     properties = { floating = true } },
   { rule = { class = "plasmashell" },
     properties = { floating = true,
                    raise = false,
                    tag = tags[1][9],
                    focusable = false,
                    focus = false } },
}
if profile == "work" then
   awful.rules.rules = awful.util.table.join(
      awful.rules.rules,
      { rule = { class = "qutebrowser" },
        properties = {tag = tags[1][2]} },
      { rule = { name = "Riot" },
        properties = {tag = tags[2][1]} },
      { rule = { name = "uChat" },
        properties = {tag = tags[2][1]} },
      { rule = { name = "HipChat" },
        properties = {tag = tags[2][1]} },
      { rule = { name = "MPC" },
        properties = {tag = tags[2][1]} }
   )
end
if profile == "home" or profile == "laptop" then
   awful.rules.rules = awful.util.table.join(
      awful.rules.rules,
      { rule = { instance = "qutebrowser" },
        properties = {tag = tags[1][2]} },
      { rule = { name = "MPC" },
        properties = {tag = tags[1][2]} }
   )
end

Wibox Utility Panel

-- Create a textclock widget
mytextclock = awful.widget.textclock()

-- Create a wibox for each screen and add it
mywibox = {}
mylayoutbox = {}
mytaglist = {}
mytaglist.buttons = awful.util.table.join(
   awful.button({ }, 1, awful.tag.viewonly),
   awful.button({ modkey }, 1, awful.client.movetotag),
   awful.button({ }, 3, awful.tag.viewtoggle),
   awful.button({ modkey }, 3, awful.client.toggletag),
   awful.button({ }, 4, function(t) awful.tag.viewnext(awful.tag.getscreen(t)) end),
   awful.button({ }, 5, function(t) awful.tag.viewprev(awful.tag.getscreen(t)) end)
)

function show_org_agenda ()
   local fd = io.open("/tmp/org-agenda.txt", "r")
   if not fd then
      return
   end
   local text = fd:read("*a")
   fd:close()
   text = text:gsub("<", "&lt;")
   text = text:gsub(">", "&gt;")
   text = text:gsub("&", "&amp;")
   -- highlight week agenda line
   text = text:gsub("(Day%-agenda[ ]+%(W%d%d?%):)", '<span underline="single">%1</span>') --
   -- highlight times
   text = text:gsub("(%d%d?:%d%d)", '<span fgcolor="'.. theme.fg_focus  ..'">%1</span>')
   -- highlight tags
   text = text:gsub("( +:[^ ]+:)\n", "\n")
   -- highlight TODOs
   text = text:gsub("(NEXT) ", '<span fgcolor="' .. theme.fg_urgent .. '"><b>%1</b></span> ')
   -- highlight categories
   org_agenda_pupup = naughty.notify(
      { text     = text,
        timeout  = 999999999,
        width    = 1000,
        position = "bottom_right",
        screen   = mouse.screen })
end

-- dispose the popup
function dispose_org_agenda ()
   if org_agenda_pupup ~= nil then
      naughty.destroy(org_agenda_pupup)
      org_agenda_pupup = nig
   end
end

orgwidget = wibox.widget.textbox()

orgwidget:connect_signal("mouse::enter", show_org_agenda)
orgwidget:connect_signal("mouse::leave", dispose_org_agenda)


mytimer = timer({ timeout = 30 })
mytimer:connect_signal("timeout", function()
                          f = io.open("/tmp/org-clock")
                          if not f then return; end
                          text = f:read("*all")
                          if text == "Not clocking right now." then
                             orgwidget:set_color(beautiful.fg_urgent)
                          end
                          orgwidget:set_text(text)
end)
mytimer:start()
mytimer:emit_signal("timeout")

for s = 1, screen.count() do
   -- Create the wibox
   mywibox[s] = awful.wibox({ position = "bottom", screen = s, height = 30 })
   local left_layout = wibox.layout.fixed.horizontal()
   local right_layout = wibox.layout.fixed.horizontal()

   -- Create a taglist widget
   mytaglist[s] = awful.widget.taglist(s, awful.widget.taglist.filter.all, mytaglist.buttons)
   left_layout:add(mytaglist[s])

   -- Initialize widget
   pulsewidget = awful.widget.progressbar()
   pulsewidget:set_border_color(theme.border_focus)
   pulsewidget:set_background_color(theme.bg_normal)
   pulsewidget:set_color(theme.bg_focus)
   pulsewidget:set_width(100)
   -- Register widget
   vicious.register(pulsewidget, vicious.contrib.pulse, "$1", 5)
   left_layout:add(pulsewidget)

   local function pacmd(args)
    local f = io.popen("pacmd "..args)
    local line = f:read("*all")
    f:close()
    return line
   end

   local function escape(text)
    local special_chars = { ["."] = "%.", ["-"] = "%-" }
    return text:gsub("[%.%-]", special_chars)
   end

   local cached_sinks = {}
   local function get_sink_name(sink)
       if type(sink) == "string" then return sink end
       -- avoid nil keys
       local key = sink or 1
       -- Cache requests
       if not cached_sinks[key] then
    local line = pacmd("list-sinks")
    for s in string.gmatch(line, "name: <(.-)>") do
        table.insert(cached_sinks, s)
    end
       end

       return cached_sinks[key]
   end

   function pulse_add(percent, sink)
       sink = get_sink_name(sink)
       if sink == nil then return end

       local data = pacmd("dump")

       local pattern = "set%-sink%-volume "..escape(sink).." (0x[%x]+)"
       local initial_vol =  tonumber(string.match(data, pattern))

       local vol = initial_vol + percent/100*0x10000
       if vol > 0x10000 then vol = 0x10000 end
       if vol < 0 then vol = 0 end
       vol = math.floor(vol)

       local cmd = string.format("pacmd set-sink-volume %s 0x%x >/dev/null", sink, vol)
       return os.execute(cmd)
   end
   left_layout:add(wibox.widget.textbox("  "))
   if profile == "home" then
   -- Initialize widget
   mpdwidget = wibox.widget.textbox()
   -- Register widget
   vicious.register(mpdwidget, vicious.widgets.mpd,
                    function (widget, args)
                       if   args["{state}"] == "Stop" then return "MPD Stopped"
                       elseif   args["{state}"] == "N/A" then return "MPD Unavailable"
                       else return '<span color="white">MPD:</span> '..
                             args["{Artist}"]..' - '.. args["{Title}"] .. ' (' .. args["{state}"] .. ')'
                       end
                    end , 10, { host = "hypervisor01", port = 6600 } )
   left_layout:add(mpdwidget)
   end
   if s == 1 then
      right_layout:add(wibox.widget.systray())
      right_layout:add(wibox.widget.textbox("  "))
   end
   -- Initialize widget
   mdirwidget = wibox.widget.textbox()
   -- Register widget
   local mdir_loc = {}
   if profile == "laptop" or profile == "home" then
     mdir_loc = {"/home/rrix/Maildir/fastmail/INBOX"}
   else
     mdir_loc = {"/home/rrix/Maildir/gmail/Inbox"}
   end
   vicious.register(mdirwidget, vicious.widgets.mdir, "Mail: $1(new) $2(unr)", 60, mdir_loc)
   right_layout:add(mdirwidget)
   right_layout:add(wibox.widget.textbox("  "))
   -- Battery widget
   mybattery = wibox.widget.textbox()
   vicious.register(mybattery, vicious.widgets.bat,
                    function(widget, args)
                       local chrage_state_str = ""
                       if args[1] == "+" then
                          charge_state_str = '<span color="'.. beautiful.fg_normal .. '">Done charging in'
                       else
                          if args[2] < 10 then
                             charge_state_str = '<span color="' .. beautiful.fg_urgent .. '">Empty in'
                          else
                             charge_state_str = '<span color="' .. beautiful.fg_focus .. '">Empty in'
                          end
                       end
                       return string.format(" Battery at %s%% (%s %s</span>)",
                                            args[2],
                                            charge_state_str,
                                            args[3])
                    end, 17, "BAT0")
   right_layout:add(mybattery)
   right_layout:add(wibox.widget.textbox("  "))
   right_layout:add(mytextclock)

   -- Create an imagebox widget which will contains an icon indicating which layout we're using.
   -- We need one layoutbox per screen.
   mylayoutbox[s] = awful.widget.layoutbox(s)
   mylayoutbox[s]:buttons(awful.util.table.join(
                             awful.button({ }, 1, function () awful.layout.inc(layouts, 1) end),
                             awful.button({ }, 3, function () awful.layout.inc(layouts, -1) end),
                             awful.button({ }, 4, function () awful.layout.inc(layouts, 1) end),
                             awful.button({ }, 5, function () awful.layout.inc(layouts, -1) end)))
   right_layout:add(mylayoutbox[s])

   -- Now bring it all together
   local layout = wibox.layout.align.horizontal()
   layout:set_left(left_layout)
   layout:set_middle(orgwidget)
   layout:set_right(right_layout)

   mywibox[s]:set_widget(layout)
end

Battery Bar

-- Battery widget
mybattery = wibox.widget.textbox()
vicious.register(mybattery, vicious.widgets.bat,
                 function(widget, args)
                    local chrage_state_str = ""
                    if args[1] == "+" then
                       charge_state_str = '<span color="'.. beautiful.fg_normal .. '">Done charging in'
                    else
                       if args[2] < 10 then
                          charge_state_str = '<span color="' .. beautiful.fg_urgent .. '">Empty in'
                       else
                          charge_state_str = '<span color="' .. beautiful.fg_focus .. '">Empty in'
                       end
                    end
                    return string.format(" Battery at %s%% (%s %s</span>)",
                                         args[2],
                                         charge_state_str,
                                         args[3])
                 end, 17, "BAT0")
right_layout:add(mybattery)

Tag List

-- Create a taglist widget
mytaglist[s] = awful.widget.taglist(s, awful.widget.taglist.filter.all, mytaglist.buttons)
left_layout:add(mytaglist[s])

Layout Box

-- Create an imagebox widget which will contains an icon indicating which layout we're using.
-- We need one layoutbox per screen.
mylayoutbox[s] = awful.widget.layoutbox(s)
mylayoutbox[s]:buttons(awful.util.table.join(
                          awful.button({ }, 1, function () awful.layout.inc(layouts, 1) end),
                          awful.button({ }, 3, function () awful.layout.inc(layouts, -1) end),
                          awful.button({ }, 4, function () awful.layout.inc(layouts, 1) end),
                          awful.button({ }, 5, function () awful.layout.inc(layouts, -1) end)))
right_layout:add(mylayoutbox[s])

Maildir status

-- Initialize widget
mdirwidget = wibox.widget.textbox()
-- Register widget
local mdir_loc = {}
if profile == "laptop" or profile == "home" then
  mdir_loc = {"/home/rrix/Maildir/fastmail/INBOX"}
else
  mdir_loc = {"/home/rrix/Maildir/gmail/Inbox"}
end
vicious.register(mdirwidget, vicious.widgets.mdir, "Mail: $1(new) $2(unr)", 60, mdir_loc)
right_layout:add(mdirwidget)

Pulse Audio

-- Initialize widget
pulsewidget = awful.widget.progressbar()
pulsewidget:set_border_color(theme.border_focus)
pulsewidget:set_background_color(theme.bg_normal)
pulsewidget:set_color(theme.bg_focus)
pulsewidget:set_width(100)
-- Register widget
vicious.register(pulsewidget, vicious.contrib.pulse, "$1", 5)
left_layout:add(pulsewidget)

local function pacmd(args)
    local f = io.popen("pacmd "..args)
    local line = f:read("*all")
    f:close()
    return line
end

local function escape(text)
    local special_chars = { ["."] = "%.", ["-"] = "%-" }
    return text:gsub("[%.%-]", special_chars)
end

local cached_sinks = {}
local function get_sink_name(sink)
    if type(sink) == "string" then return sink end
    -- avoid nil keys
    local key = sink or 1
    -- Cache requests
    if not cached_sinks[key] then
    local line = pacmd("list-sinks")
    for s in string.gmatch(line, "name: <(.-)>") do
        table.insert(cached_sinks, s)
    end
    end

    return cached_sinks[key]
end

function pulse_add(percent, sink)
    sink = get_sink_name(sink)
    if sink == nil then return end

    local data = pacmd("dump")

    local pattern = "set%-sink%-volume "..escape(sink).." (0x[%x]+)"
    local initial_vol =  tonumber(string.match(data, pattern))

    local vol = initial_vol + percent/100*0x10000
    if vol > 0x10000 then vol = 0x10000 end
    if vol < 0 then vol = 0 end
    vol = math.floor(vol)

    local cmd = string.format("pacmd set-sink-volume %s 0x%x >/dev/null", sink, vol)
    return os.execute(cmd)
end
-- These make sense on my QMK layout, Super+right pinky keys
awful.key({ modkey, "Shift" }, "=", function() pulse_add(10) end),
awful.key({ modkey          }, "=", function() pulse_add(-10) end),
awful.key({ modkey          }, "5", function() vicious.contrib.pulse.toggle() end)

MPC Status

if profile == "home" then
-- Initialize widget
mpdwidget = wibox.widget.textbox()
-- Register widget
vicious.register(mpdwidget, vicious.widgets.mpd,
                 function (widget, args)
                    if   args["{state}"] == "Stop" then return "MPD Stopped"
                    elseif   args["{state}"] == "N/A" then return "MPD Unavailable"
                    else return '<span color="white">MPD:</span> '..
                          args["{Artist}"]..' - '.. args["{Title}"] .. ' (' .. args["{state}"] .. ')'
                    end
                 end , 10, { host = "hypervisor01", port = 6600 } )
left_layout:add(mpdwidget)
end

Org-Mode clock status and Agenda

;; update agenda file after changes to org files
(defun th-org-mode-init ()
  (add-hook 'after-save-hook 'th-org-update-agenda-file t t))

(add-hook 'org-mode-hook 'th-org-mode-init)

;; that's the export function
(defun th-org-update-agenda-file (&optional force)
  (interactive)
  (save-excursion
    (save-window-excursion
      (let ((file "/tmp/agenda.html"))
        (org-agenda-list)
        (org-agenda-write file))
      (let ((file "/tmp/org-agenda.txt"))
        (org-agenda-list)
        (org-agenda-write file)))))

;; do it once at startup
(th-org-update-agenda-file t)
function show_org_agenda ()
   local fd = io.open("/tmp/org-agenda.txt", "r")
   if not fd then
      return
   end
   local text = fd:read("*a")
   fd:close()
   text = text:gsub("<", "&lt;")
   text = text:gsub(">", "&gt;")
   text = text:gsub("&", "&amp;")
   -- highlight week agenda line
   text = text:gsub("(Day%-agenda[ ]+%(W%d%d?%):)", '<span underline="single">%1</span>') --
   -- highlight times
   text = text:gsub("(%d%d?:%d%d)", '<span fgcolor="'.. theme.fg_focus  ..'">%1</span>')
   -- highlight tags
   text = text:gsub("( +:[^ ]+:)\n", "\n")
   -- highlight TODOs
   text = text:gsub("(NEXT) ", '<span fgcolor="' .. theme.fg_urgent .. '"><b>%1</b></span> ')
   -- highlight categories
   org_agenda_pupup = naughty.notify(
      { text     = text,
        timeout  = 999999999,
        width    = 1000,
        position = "bottom_right",
        screen   = mouse.screen })
end

-- dispose the popup
function dispose_org_agenda ()
   if org_agenda_pupup ~= nil then
      naughty.destroy(org_agenda_pupup)
      org_agenda_pupup = nig
   end
end

orgwidget = wibox.widget.textbox()

orgwidget:connect_signal("mouse::enter", show_org_agenda)
orgwidget:connect_signal("mouse::leave", dispose_org_agenda)


mytimer = timer({ timeout = 30 })
mytimer:connect_signal("timeout", function()
                          f = io.open("/tmp/org-clock")
                          if not f then return; end
                          text = f:read("*all")
                          if text == "Not clocking right now." then
                             orgwidget:set_color(beautiful.fg_urgent)
                          end
                          orgwidget:set_text(text)
end)
mytimer:start()
mytimer:emit_signal("timeout")
layout:set_middle(orgwidget)

Desktop wallpaper and composition

AwesomeWM can do things like set background and such, but for my use case, I just set the X root window to provide the background, using feh. I have a SystemD service that simply starts feh and tells it to write out a background to the root X window. I use an XPM file due to old habits, but any file can be put in to the arguments here.

- name: feh installed
  dnf:
    name: feh
    state: installed
  when: ansible_pkg_mgr == "dnf"

- name: feh installed
  apt:
    name: feh
    state: installed
  when: ansible_pkg_mgr == "apt"


- name: systemd user services installed
  become: yes
  become_user: rrix
  copy:
    src: out/{{item}}.service
    dest: ~/.config/systemd/user/{{item}}.service
  with_items:
  - fehbg
  - xcompmgr
  - conky

- name: systemd services enabled
  become: yes
  become_user: rrix
  shell: "systemctl --user enable {{item}}"
  with_items:
  - fehbg
  - xcompmgr
  - conky

- name: conky configurations installed
  become: yes
  become_user: rrix
  copy:
    src: out/{{item}}.conky
    dest: ~/.conky/{{item}}.conky
  with_items:
  - system
  - random
  - clock

- name: conky wrapper installed
  become: yes
  become_user: rrix
  copy:
    src: out/conky-start
    dest: ~/bin/conky-start
    mode: 0775
[Unit]
Description=Set X root background

[Service]
ExecStart=/usr/bin/feh --bg-fill --random --recursive /home/rrix/sync/wallpapers

[Install]
WantedBy=default.target

The latest versions of AwesomeWM support translucent widgets and windows, provided you're running a composition manager. Conky also takes advantage of this.

[Unit]
Description=X Compositing Manager

[Service]
ExecStart=/usr/bin/xcompmgr

[Install]
WantedBy=default.target

Conky

I run three processes of Conky with a fairly stock looking configuration.

Shared

override_utf8_locale yes
use_xft yes
xftfont Fira Mono:size=14
xftalpha 0.8
background yes
cpu_avg_samples 2
default_color grey
default_outline_color green
default_shade_color red
double_buffer yes
draw_borders no
draw_graph_borders yes
draw_outline no
draw_shades no
gap_x 20
gap_y 60
maximum_width 400
minimum_size 300 5
no_buffers yes
override_utf8_locale no
total_run_times 0
update_interval 2.0
uppercase no
own_window yes
own_window_argb_value 110
own_window_argb_visual true
own_window_argb_visual yes
own_window_class Conky
own_window_colour 000000
own_window_hints undecorated,below,sticky,skip_taskbar,skip_pager
own_window_transparent yes
own_window_type override

system.conky

The first one is your run of the mill system stats monitor.

override_utf8_locale yes
use_xft yes
xftfont Fira Mono:size=14
xftalpha 0.8
background yes
cpu_avg_samples 2
default_color grey
default_outline_color green
default_shade_color red
double_buffer yes
draw_borders no
draw_graph_borders yes
draw_outline no
draw_shades no
gap_x 20
gap_y 60
maximum_width 400
minimum_size 300 5
no_buffers yes
override_utf8_locale no
total_run_times 0
update_interval 2.0
uppercase no
own_window yes
own_window_argb_value 110
own_window_argb_visual true
own_window_argb_visual yes
own_window_class Conky
own_window_colour 000000
own_window_hints undecorated,below,sticky,skip_taskbar,skip_pager
own_window_transparent yes
own_window_type override

alignment top_right
TEXT
${font sans-serif:bold:size=12}SYSTEM ${hr 2}
${font sans-serif:normal:size=12}$sysname $kernel $alignr $machine
Host:$alignr$nodename
File System: $alignr${fs_type}

${font sans-serif:bold:size=12}PROCESSORS ${hr 2}
${font sans-serif:normal:size=12}${cpugraph cpu0 33ffff 0000ff}
CPU1: ${cpu cpu1}% ${cpubar cpu1}
CPU2: ${cpu cpu2}% ${cpubar cpu2}
CPU3: ${cpu cpu3}% ${cpubar cpu3}
CPU4: ${cpu cpu4}% ${cpubar cpu4}

${font sans-serif:bold:size=12}RAM ${hr 2}
${font sans-serif:normal:size=12} $alignc $mem / $memmax $alignr $memperc%
$membar

${font sans-serif:bold:size=12}DISK ${hr 2}
${font sans-serif:normal:size=12} $alignc Root ${fs_used /} / ${fs_size /} $alignr ${fs_used_perc /}%
${fs_bar /}
${font sans-serif:normal:size=12} $alignc Home ${fs_used /home} /home ${fs_size /home} $alignr ${fs_used_perc /home}%
${fs_bar /home}

${font sans-serif:bold:size=12}TOP PROCESSES ${hr 2}
${font sans-serif:normal:size=12}${top_mem name 1}${alignr}${top mem 1} %
${top_mem name 2}${alignr}${top mem 2} %
$font${top_mem name 3}${alignr}${top mem 3} %
$font${top_mem name 4}${alignr}${top mem 4} %
$font${top_mem name 5}${alignr}${top mem 5} %

${font sans-serif:bold:size=12}NETWORK ${hr 2}
${font sans-serif:normal:size=12}IP address: $alignr ${addr wlp3s0}
${downspeedgraph wlp3s0 99cc33 006600}
DownSpeed: $alignc ${downspeed wlp3s0} $alignr total: ${totaldown wlp3s0}
${upspeedgraph wlp3s0  ffcc00 ff0000}
UpSpeed: $alignc ${upspeed wlp3s0} $alignr total: ${totalup wlp3s0}

${font sans-serif:bold:size=12}BATTERY ${hr 2}
${font}BAT0${alignr}${battery BAT0}
${battery_bar BAT0}
${font}BAT1${alignr}${battery BAT1}
${battery_bar BAT1}

random.conky

#AccuWeather (r) RSS weather tool for conky
#
#USAGE: weather.sh <locationcode>
#
#(c) Michael Seiler 2007

METRIC=1 #Should be 0 or 1; 0 for F, 1 for C

if [ -z $1 ]; then
    echo
    echo "USAGE: weather.sh <locationcode>"
    echo
    exit 0;
fi

curl -s http://rss.accuweather.com/rss/liveweather_rss.asp\?metric\=${METRIC}\&locCode\=$1 | perl -ne 'if (/Currently/) {chomp;/\<title\>Currently: (.*)?\<\/title\>/; print "$1"; }'
- name: weather wrapper script installed
  become: yes
  become_user: rrix
  copy:
    src: out/weather.sh
    dest: ~/bin/weather.sh
    mode: 0775

The second one shows me just some random crap. Mpd, fortune and the weather for O A K L A N D I A

override_utf8_locale yes
use_xft yes
xftfont Fira Mono:size=14
xftalpha 0.8
background yes
cpu_avg_samples 2
default_color grey
default_outline_color green
default_shade_color red
double_buffer yes
draw_borders no
draw_graph_borders yes
draw_outline no
draw_shades no
gap_x 20
gap_y 60
maximum_width 400
minimum_size 300 5
no_buffers yes
override_utf8_locale no
total_run_times 0
update_interval 2.0
uppercase no
own_window yes
own_window_argb_value 110
own_window_argb_visual true
own_window_argb_visual yes
own_window_class Conky
own_window_colour 000000
own_window_hints undecorated,below,sticky,skip_taskbar,skip_pager
own_window_transparent yes
own_window_type override

alignment top_left

TEXT
${font sans-serif:bold:size=12}WEATHER NOW ${hr 2}
${font} ${execi 300 weather.sh 94611}

${font sans-serif:bold:size=12}FORTUNE ${hr 2}
${font}${execi 60 fortune uber | fold -w 25 -s}

${font sans-serif:bold:size=12}PLAYING ${hr 2}
${if_mpd_playing} ${font}Currently ${mpd_status}: ${font Fira Mono:bold:size=12}${mpd_smart}
${mpd_bar} $else ${font}Nothing Playing! $endif
${font}Left Volume: ${exec amixer sget Master 0 | grep Left..Playback | egrep -o '\b[0-9]{2}\b'}%
${execbar amixer sget Master 0 | grep Left..Playback | egrep -o '\b[0-9]{2}\b'}
${font}Right Volume: ${exec amixer sget Master 0 | grep Right..Playback | egrep -o '\b[0-9]{2}\b'}%
${execbar amixer sget Master 0 | grep Right..Playback | egrep -o '\b[0-9]{2}\b'}

clock.conky

The third one is a simple clock with an added caveat that it also dumps my current org-clock status, which I pull in to a file from a cron job every few minutes.

override_utf8_locale yes
use_xft yes
xftfont Fira Mono:size=14
xftalpha 0.8
background yes
cpu_avg_samples 2
default_color grey
default_outline_color green
default_shade_color red
double_buffer yes
draw_borders no
draw_graph_borders yes
draw_outline no
draw_shades no
gap_x 20
gap_y 60
maximum_width 400
minimum_size 300 5
no_buffers yes
override_utf8_locale no
total_run_times 0
update_interval 2.0
uppercase no
own_window yes
own_window_argb_value 110
own_window_argb_visual true
own_window_argb_visual yes
own_window_class Conky
own_window_colour 000000
own_window_hints undecorated,below,sticky,skip_taskbar,skip_pager
own_window_transparent yes
own_window_type override
alignment top_middle

TEXT
${alignc}${font Fira Mono:size=26}${time %H:%M}${font}
${alignc}${time %a %d %b %Y}
${alignc}${exec cat /tmp/org-clock}
${alignc}Up:$uptime
(require 'org-clock)
(defun cce/update-org-clock-status-timer ()
  (with-current-buffer (find-file-noselect "/tmp/org-clock")
    (erase-buffer)
    (if (org-clocking-p)
        (insert (org-clock-get-clock-string))
      (insert "Not clocking right now."))
    (let ((inhibit-message t))
      (write-file (buffer-file-name)))))

(unless (fboundp 'cce/org-clock-status-timer)
  (setq cce/org-clock-status-timer
        (run-with-timer 0 60 #'cce/update-org-clock-status-timer)))

Tying it all together

We have a script that starts all of them:

for f in /home/rrix/.conky/*.conky; do
    conky -c $f -d;
done

And then a SystemD service that runs that:

[Unit]
Description=Conky Widget system

[Service]
Type=forking
ExecStart=/home/rrix/bin/conky-start
ExecStop=/usr/bin/killall conky

[Install]
WantedBy=default.target

Tray Items

It's the year of the GNU/Linux and we have tray items that still use xembed. Woo? I have a handful of tray items that I want systemd to autostart for me.

- name: tray items installed
  dnf:
    name: "{{item}}"
    state: installed
  when: ansible_pkg_mgr == "dnf"
  with_items:
  - network-manager-applet
  - qlipper

- name: tray items installed
  apt:
    name: "{{item}}"
    state: installed
  when: ansible_pkg_mgr == "apt"
  with_items:
  - network-manager-gnome
  - qlipper

- name: systemd user services installed
  become: yes
  become_user: rrix
  copy:
    src: out/{{item}}.service
    dest: ~/.config/systemd/user
  with_items:
  - nm-applet
  - qlipper

- name: systemd reloaded
  become: yes
  become_user: rrix
  shell: systemctl --user daemon-reload

- name: systemd user services enabled
  become: yes
  become_user: rrix
  shell: "systemctl --user enable {{item}}"
  with_items:
  - nm-applet
  - qlipper

nm-applet NetworkManager CLI

[Unit]
Description=Network Manager applet
After=network.target

[Service]
ExecStart=/usr/bin/nm-applet

[Install]
WantedBy=default.target

qlipper is like Klipper but with less KDE dependencies. kde-workspace is a huge blob of code to import just for Klipper which is kind of a shame because it's a great tool.

[Unit]
Description=Qlipper

[Service]
ExecStart=/usr/bin/qlipper

[Install]
WantedBy=default.target

Author: Ryan Rix

Created: 2017-10-03 Tue 10:05

Validate XHTML 1.0