UP | HOME
[2016-10-05 Wed 00:00 (last updated: 2019-10-26 Sat 19:55) Press ? for navigation help]

Awesome-wm configuration

Table of Contents

Introduction

This file contains my configuration for the awesome window manager along with documentation of the different modules developed and used. The actual configuration (a set of lua files) can be produced by running org-babel-tangle on the original org file.

The resulting desktop is shown on Fig. 1. The following bash script produces the image. The most interesting part is the top wibox, which is shown enlarged on Fig. 2.

# Could get these from xrandr
res_x=1920
res_y=1080
screen_idx=1

width=$(( $res_x ))
height=$(( $res_y ))
start_x=$(( $screen_idx * $res_x ))
start_y=$(( 0 ))

scrot desktop.png
convert desktop.png -crop ${width}x${height}+${start_x}+${start_y} desktop-main.png
rm desktop.png -rf

desktop-main.png

Figure 1: My awesome-wm desktop.

This configuration is currently used on the following system:

OS
lsb_release -a
uname -a
Distributor ID:	Debian
Description:	Debian GNU/Linux 9.0 (stretch)
Release:	9.0
Codename:	stretch
Linux dell-desktop 4.9.0-1-amd64 #1 SMP Debian 4.9.2-2 (2017-01-12) x86_64 GNU/Linux
awesome
awesome --version
awesome v4.0 (Harder, Better, Faster, Stronger)
 • Compiled against Lua 5.1.5 (running with Lua 5.1)
 • D-Bus support: ✔
 • execinfo support: ✔
 • RandR 1.5 support: ✔
 • LGI version: 0.9.1
emacs
(emacs-version)
"GNU Emacs 25.1.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.5)
 of 2016-12-31, modified by Debian"
org-mode
(replace-regexp-in-string "@.*)" "@ ... )" (org-version nil t))
"Org mode version 9.0.4 (release_9.0.4-270-ga10ddb @ ... )"

Most of the code in this configuration comes from the default rc.lua delivered with awesome-wm. On my system, it is located under /etc/xdg/awesome/rc.lua.

Tangling this org file produces several lua files. The main configuration is contained in the rc.lua file (widgets, keybindings), and code for modules is tangled to separate files. To disable a component of this configuration, the corresponding code block must be skipped during tangling. To disable tangling of an individual block, the preferred way is to add the header-args property on the parent heading:

** Disabled heading
   :PROPERTIES:
   :header-args+: :tangle no
   :END:

This can be done by adding the text manually or by using the org-set-property function, which is bound to C-c C-x p by default. Alternatively, individual source blocks can be disabled by adding :tangle no on the #+BEGIN_SRC line.

It is important to note that when tangling this file, any change made directly to the lua files will be overridden by the code in this org file. This can be prevented by changing the permissions of the tangled files (see http://orgmode.org/manual/tangle_002dmode.html). Alternatively, the detangle functionality in org-mode can be utilized to merge changes from the lua files back to this org file: simply run org-detangle from the modified lua file and changes should be merged back. This requires the #+PROPERTY: header-args :comments link option which is set at the top of this file.

This org file can also be exported to html to produce the documentation. Note that exporting this file (e.g. to HTML) executes some bash scripts (for screenshots for instance), so use at your own risk.

Requirements

  • Since the configuration is written in an org-mode file, emacs is required to extract (tangle) the lua code. A makefile is provided to run the tangle operation on this file from the command line (the GNU make utility is required to use the makefile). It also requires a recent version of org-mode, and basic emacs modes for lua and python.
  • The music player widget requires cmus and uses eyeD3 to extract cover art from audio files.
  • The bash script used to display the volume through OSD requires the aosd_cat command.
  • The memory monitor widget uses the htop command to display a summary of the processes when moving the mouse over the widget. This can be replaced by the top command for instance.
  • The calendar widget lists events for each day using khal. I have it synchronized to my (owncloud) CalDAV server using vdirsyncer.
  • The mail widget relies on mu to find emails to show in the popup notification (emails are stored in maildir format). This could be replaced by some other script for IMAP emails.
  • The news widget requires a Tiny Tiny RSS instance, and the ttrss-python package; a wrapper python script to get news feeds is provided in this file.
  • If using the global prompt module, some modes include additional dependencies:
    • The music prompt requires mplayer and uses the locate program (part of the findutils package) to find audio files.
    • The calc prompt requires a python script to perform calculations from strings. This script is provided in this org file.
  • The transparency effect on focus (see the transparency section) requires a compositor, such as xcompmgr.
  • The password insertion feature (see the corresponding section) requires the pass utility, dmenu and the xclip and xdotool programs.
  • When generating the HTML documentation (by running C-c C-e h h on this buffer), screenshots are taken from the active desktop (assuming awesome-wm is the running desktop environment). This requires the scrot and imagemagick programs.

Makefile

A simple makefile can be used to generate the configuration files (.lua) and the documentation. Use make to update the configuration files from the org source, make doc-html to export the org file into html. The latex export (make doc-pdf) is not supported (there is no syntax highlighting support for lua in the minted package).

To get a reproducible make behavior, a minimal emacs initialization file is provided here:

[makefile-init.el]
;; Assume default folder for emacs packages (let ((default-directory "~/.emacs.d/elpa/")) (normal-top-level-add-subdirs-to-load-path)) ;; Explicitly load required packages (require 'org) (require 'lua-mode) (require 'python-mode) ;; Set source block languages (org-babel-do-load-languages 'org-babel-load-languages '( (emacs-lisp . t) (lua . t) (makefile . t) (python . t) (shell . t))) ;; Org options (setq org-confirm-babel-evaluate nil) (setq org-src-fontify-natively t) (setq org-src-preserve-indentation t) (setq org-edit-src-content-indentation 0) ;; Credential extraction function (uses auth-source) (defun get-ttrss-cred (&optional url) (let ((ttrss-url (if url url "127.0.0.1:81/tt-rss")) (ttrss-user nil) ;;(ttrss-user "myuser") (ttrss-pass nil) ;;(ttrss-pass "mypassword") ) (when (require 'auth-source) (let ((auth (nth 0 (auth-source-search :host ttrss-url :requires '(user secret))))) (when (and (plist-get auth :secret) (plist-get auth :user)) (setq ttrss-pass (funcall (plist-get auth :secret)) ttrss-user (plist-get auth :user))))) `(,ttrss-url ,ttrss-user ,ttrss-pass))) ;; Define credential extraction function before tangling (add-hook 'org-babel-pre-tangle-hook 'get-ttrss-cred)

The makefile is relatively simple, one trick is to not use emacs in –batch mode when generating the HTML documentation, since it seems to interact with syntax highlighting.

[makefile]
# Makefile for org file, with targets: # all: Tangle source blocks # doc-html: Export org-file to html # doc-pdf: Export org-file to pdf # Input org file input = awesome-wm src = $(input).org # Emacs command # Note: I can't seem to get syntax highlighting in the exported html when using # --batch emacs = emacs --batch --no-init-file --load makefile-init.el --find-file $(src) emacs_batch = ${emacs} --batch # Delete command RM = rm -rf all: $(emacs_batch) --funcall org-babel-tangle --kill doc-html: $(emacs) --funcall org-html-export-to-html --kill doc-pdf: $(emacs) --funcall org-latex-export-to-pdf --kill clean: $(RM) $(input).{html,tex,log,aux,dvi,pdf,ps,out,toc}

Imports

The first section of the rc.lua file contains library imports used throughout the file.

Standard awesome imports

Standard modules listed here should come installed with awesome.

[rc.lua]
-- Standard awesome library local gears = require("gears") local awful = require("awful") local util = require("awful.util") require("awful.autofocus") -- Widget and layout library local wibox = require("wibox") -- Theme handling library local beautiful = require("beautiful") local vicious = require("vicious") -- Notification library local naughty = require("naughty") local menubar = require("menubar") local hotkeys_popup = require("awful.hotkeys_popup").widget

Paths

Paths are stored in the awesome_paths object which is later used to search for launchbar programs, theme selection, maildir, icons, etc.

[rc.lua]
-- Define global folders awesome_paths = {} -- config_dir is used for local theme customizations and shortcut search -- (launchbar module) awesome_paths.config_dir = awful.util.getdir("config") -- system_dir is used for themes awesome_paths.system_dir = "/usr/share/awesome/" -- home_dir is used to locate the maildir, start dropbox awesome_paths.home_dir = os.getenv("HOME") -- Icon paths awesome_paths.icon_dir = "/usr/share/icons/oxygen/base/16x16/" awesome_paths.iconapps_dir = { awesome_paths.icon_dir, "/usr/share/icons/hicolor/16x16/", "/usr/share/icons/gnome/16x16/", "/usr/share/icons/Tango/16x16/", "/usr/share/pixmaps/" }

Debian specific

Use the freedesktop menu, which lists most applications in categories, similar to main menus in other desktop environments.

[rc.lua]
-- Load Debian menu entries local freedesktop = require("freedesktop")

Custom modules

The following imports are for custom modules implemented in this file. The filenames should match the names used with the require command.

[rc.lua]
-- Custom libraries

launchbar module

This module creates the launchers in the wibox from .desktop files (see the Launchbar section).

[rc.lua]
local launchbar = require('launchbar')

cmus module

Control and display information from the cmus music player (see the cmus section).

[rc.lua]
local cmus = require("cmus")

ttrss module

View RSS feed updates from Tiny Tiny RSS instance (see the Tiny Tiny RSS section).

[rc.lua]
local ttrss = require("ttrss")

themes module

Manage desktop themes (see the Theme section).

[rc.lua]
local theme_customization = require("theme_customization")

icon_finder module

Helper utility to find program icons in system paths (see the Icon finder section).

[rc.lua]
local icon_finder = require("icon_finder")

global_prompt module

Generic command launcher (gnome-do style) (see the Global prompt section).

[rc.lua]
local global_prompt = require("global_prompt")

my_utility module

Some utility functions (see the Utilities section).

[rc.lua]
local my_utility = require('my_utility')

Basic setup

This section contains general settings for the different desktop components: search paths, default applications, main key modifiers and some boiler plate code for error handling.

Default programs

Define default programs.

[rc.lua]
-- Define default programs terminal = "x-terminal-emulator" termapps = "urxvt" -- "terminator" editor = os.getenv("EDITOR") or "editor" editor_cmd = terminal .. " -e " .. editor internet_browser = "x-www-browser" mail_reader = "thunderbird" emacs = "emacs" explorer = "xdg-open" mixer = "pavucontrol" music_player = "mplayer" passmenu = "bash " .. awesome_paths.config_dir .. "/scripts/passmenu.sh --type"

Icons

Create an instance of the Icon finder module. It is used throughout the configuration to find icons for programs and session operations (Shutdown menu).

[rc.lua]
-- {{ Icon finder myiconfinder = icon_finder.new(awesome_paths.iconapps_dir) -- }}

Error handling

This is default code; it creates notifications when errors occur, which is helpful to debug issues in rc.lua.

[rc.lua]
-- {{{ 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 = tostring(err) }) in_error = false end) end -- }}} -- {{{ Helper functions local function client_menu_toggle_fn() local instance = nil return function () if instance and instance.wibox.visible then instance:hide() instance = nil else instance = awful.menu.clients({ theme = { width = 250 } }) end end end -- }}}

Modkey setting

The modkey is the main key modifier for special bindings: use Super as main modifier. Also set Alt key.

[rc.lua]
-- 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" altkey = "Mod1"

Desktop

This configuration is used with a dual screen system, where the widgets are identical on both screens (except for the systray, which can only be added to one screen). An example of wibox produced by this file is shown on Fig 2.

The following bash snippet produces the screenshot shown on Fig. 2 on export (the result is cached, so it is not re-generated if the content of the code block is not changed).

# Could get these from xrandr
res_x=1920
res_y=1080
screen_idx=0

width=$(( $res_x / 2 ))
height=25
start_x=$(( $screen_idx * $res_x ))
half_x=$(( $start_x + $width ))

scrot desktop.png
convert desktop.png -crop ${width}x${height}+${start_x}+0 desktop-wibox-l.png
convert desktop.png -crop ${width}x${height}+${half_x}+0 desktop-wibox-r.png
convert desktop-wibox-l.png desktop-wibox-r.png -append desktop-wibox.png
rm desktop.png desktop-wibox-l.png desktop-wibox-r.png -rf

desktop-wibox.png

Figure 2: Main wibox (split into two rows for display) with widgets (from left to right): awesome menu, tags, launchbar, tasklist, cmus, mail, ttrss, memory, volume, systray, layout, date + calendar, shutdown menu.

Layouts

This section defines the layouts for windows, I use the default code, with a few entries disabled.

[rc.lua]
-- Table of layouts to cover with awful.layout.inc, order matters. awful.layout.layouts = { -- awful.layout.suit.tile, awful.layout.suit.tile.left, awful.layout.suit.tile.bottom, -- awful.layout.suit.tile.top, awful.layout.suit.fair, awful.layout.suit.fair.horizontal, -- awful.layout.suit.spiral, awful.layout.suit.spiral.dwindle, awful.layout.suit.max, awful.layout.suit.max.fullscreen, awful.layout.suit.magnifier, awful.layout.suit.corner.nw, awful.layout.suit.floating } -- }}}

Theme

Themes are handled through the beautiful module, which creates the beautiful table upon instantiation. Themes are assumed to be located in sub-folders of the paths defined by the awesome_paths.themes_system_path and awesome_paths.themes_custom_path variables. They are defined by a theme.lua file (the filename can be customized by modifying theme_customatization.theme_file) and possibly a custom.lua file (the filename can be customized by modifying theme_customatization.custom_file). The theme.lua file fully defines the theme (colors, icons, etc.) and the custom.lua is used to locally modify theme values.

Some of the custom modules used in this configuration rely on an extended beautiful table. Any required field that is not in the original beautiful table must be defined in the themes/custom_defaults.lua file and can be overridden by individual themes in the custom.lua file.

Theme management module

The theme module has two main functions:

  • list the available themes into a theme menu,
  • define a complete beautiful table for a given theme.

The theme_customatization.set_custom_theme function takes a theme name as input along with the awesome_paths variable. It searches files named theme.lua in the custom and system theme paths. If a theme is found, the beautiful.init function is called to initialize the beautiful table. Next, the custom_defaults.lua file is executed to complete the beautiful variables with the custom ones required by this configuration. Finally, the beautiful table is augmented by inserting fields found in the theme's custom.lua file (if present).

[theme_customization.lua]
-- Theme helper script local dofile = dofile local lfs = require("lfs") local beautiful = require("beautiful") local gears = require("gears") local awful = require("awful") local util = require("awful.util") theme_customization = {} theme_customization.theme_file = "theme.lua" theme_customization.custom_file = "custom.lua" local naughty = require("naughty") function theme_customization.set_custom_theme(theme, awesome_paths) -- Locate theme file path_list = { awesome_paths.themes_custom_path .. theme, awesome_paths.themes_system_path .. theme, } fname_theme = get_first_found_file(path_list, theme_customization.theme_file) if not fname_theme then return end -- Locate customization file fname_custom = get_first_found_file(path_list, theme_customization.custom_file) -- Initialize beautiful theme beautiful.init(fname_theme) -- Load default customization (fields required by rc.lua not defined by -- regular themes) fname_custom_defaults = awesome_paths.themes_custom_path .. "/custom_defaults.lua" if util.file_readable(fname_custom_defaults) then -- `custom_defaults` table custom_defaults = dofile(fname_custom_defaults) else custom_defaults = {} end -- Load theme customization if fname_custom then custom = dofile(fname_custom) -- `custom` table else custom = {a="b"} end -- Join all tables custom_tables = { custom_defaults, custom } for _, t in ipairs(custom_tables) do for key, value in pairs(t) do beautiful[key] = value end end end function get_first_found_file(path_list, fname) for _, f in ipairs(path_list) do fname_full = f .. "/" .. fname if util.file_readable(fname_full) then return fname_full end end return nil end function theme_customization.create_themes_menu(awesome_paths) -- List of search paths path_list = { awesome_paths.themes_custom_path, awesome_paths.themes_system_path, } -- Initialize table theme_list = {} -- Perform search for _, path in ipairs(path_list) do for fold in lfs.dir(path) do f_attr = lfs.attributes(path .. "/" .. fold, "mode") if f_attr and f_attr == "directory" and fold ~= "." and fold ~= ".." then fname_full = path .. "/" .. fold .. "/" .. theme_customization.theme_file if util.file_readable(fname_full) then if not theme_list[fold] then theme_list[fold] = path .. "/" .. fold .. "/" .. theme_customization.theme_file end end end end end -- Create menu menuitems = {} for theme_name, theme_file in pairs(theme_list) do theme = nil theme = dofile(theme_file) theme_icon = theme.awesome_icon theme = nil table.insert(menuitems, { theme_name, function () local theme_fname = awesome_paths.themes_custom_path .. "/theme" local file = io.open(theme_fname, "w") file:write(theme_name .. "\n") file:close() awesome.restart() end, theme_icon }) end return menuitems end return theme_customization

The custom_defaults file

The custom_defaults file returns a table with theme parameters required by modules and absent from the original beautiful table. For visual consistency, some of the fields use values from the beautiful table itself.

[themes/custom_defaults.lua]
local beautiful = require("beautiful") custom_defaults = {} -- mail widget custom_defaults.mail_fg_urgent = beautiful.fg_urgent custom_defaults.mail_fg_normal = beautiful.fg_normal custom_defaults.mail_fg_focus = beautiful.fg_focus -- memory widget custom_defaults.mem_bg = beautiful.bg_normal -- cmus widget custom_defaults.cmus_fg = beautiful.fg_normal -- screen highlighter custom_defaults.screen_highlight_bg_active = beautiful.bg_minimize custom_defaults.screen_highlight_fg_active = beautiful.fg_minimize custom_defaults.screen_highlight_bg_inactive = beautiful.bg_normal custom_defaults.screen_highlight_fg_inactive = beautiful.fg_normal return custom_defaults

Theme selection

In the main rc.lua file, the theme folders are first added to the awesome_paths table (awesome_paths.themes_system_path and awesome_paths.themes_custom_path). Then the theme to load is read from the themes/theme file. This file should contain a single line with the name of the theme to use (see the theme definition block). Note that lines starting with "#" are ignored (such lines occur when tangling this org-file, to allow "de-tangling"). Also note that parsing is rudimentary so empty lines may cause issues.

[rc.lua]
-- {{ Theme management awesome_paths.themes_system_path = awesome_paths.system_dir .. "/themes/" awesome_paths.themes_custom_path = awesome_paths.config_dir .. "/themes/" theme_fname = awesome_paths.themes_custom_path .. "/theme" theme_name = "default" if util.file_readable(theme_fname) then local f = io.open(theme_fname, "r") theme_lines = my_utility.lines(f:read("*all")) for k, v in pairs(theme_lines) do if string.find(v, "^%s*[^#]") ~= nil then theme_name = v:gsub("^%s*(.-)%s*$", "%1") end end f:close() end -- Set themes theme_customization.set_custom_theme(theme_name, awesome_paths) -- Build theme selection menu theme_menuitems = theme_customization.create_themes_menu(awesome_paths) -- }} local function set_wallpaper(s) -- Wallpaper if beautiful.wallpaper then local wallpaper = beautiful.wallpaper -- If wallpaper is a function, call it with the screen if type(wallpaper) == "function" then wallpaper = wallpaper(s) end gears.wallpaper.maximized(wallpaper, s, true) end end -- Re-set wallpaper when a screen's geometry changes (e.g. different resolution) screen.connect_signal("property::geometry", set_wallpaper) awful.screen.connect_for_each_screen(function(s) -- Wallpaper set_wallpaper(s) end) --}}

Selected theme

To change the default theme, change the content of the following themes/theme file:

[themes/theme]
xresources

Tags

Tags are similar to workspaces in other desktop environments. My configuration is for my home desktop, which has two screens. It is only slightly modified from the default: a few tags are named instead of numbered and the default layout on some tags is customized.

  • The first tag on each screen is for email: one for work using thunderbird and one for my personal email using mu4e (emacs). These are not started automatically, I have a rule to force thunderbird on the second screen, and I manually start mu4e on the desired tag.
  • The second tag on the second screen is used for my resident instances of newsboat (terminal RSS reader) and cmus (terminal music player). Both programs are launched on startup (see Startup applications) and rules are used to place them on the right screen and tag (see the Rules section).
[rc.lua]
-- {{{ Tags -- Define a tag table which hold all screen tags. awful.screen.connect_for_each_screen(function(s) -- Each screen has its own tag table. --awful.tag({ "@", "2", "3", "4", "5", "6", "7", "8", "9" }, s, -- awful.layout.layouts[1]) awful.tag.add("@", { layout = awful.layout.suit.tile.bottom, screen = s }) for i = 2, 8 do awful.tag.add(i, { layout = awful.layout.suit.tile.left, screen = s }) end awful.tag.add("9", { layout = awful.layout.suit.tile.floating, screen = s }) s.tags[1]:view_only() end) -- }}}

Main bar

This section is the main part of the configuration, it defines and configures the widgets present on the main wibox (Fig. 2).

Note that the widgets defined here must be later added to the wibox, which is done in the Wibox section.

First, we create the simplest widget: a separator used to add visual spacing between widgets.

[rc.lua]
---- {{{ Separators local separator = wibox.widget.textbox() separator:set_markup(" <span foreground='grey'>·</span> ") ---- }}}

Launchbar

In most desktop environments, application shortcuts are stored in .desktop files that give the application name, icon, command, etc. The specification is available here. The standard awful.widget.launcher module allows to add .desktop files to a launchbar which can be displayed in a wibox (as shown on Fig. 2).

A few examples can be found online, most notably:

  • The basic quick launch bar requires .desktop files to include a Position field. Usual .desktop files do not have this field so this is impractical.
  • This script lists all the .desktop files in a given folder and adds them to the launchbar. If the position field is missing, it is replaced, and launchers are added in alphabetical order.

Module definition

This version is based on the second code segment, the only difference is that the Exec entry of the .desktop file is cleaned up from its command line arguments, such as "%u" for firefox (they seem to cause problems when launching). The other addition is the use of the custom icon_finder module described in the Icon finder section.

[launchbar.lua]
-- Quick launchbar widget for Awesome WM -- http://awesome.naquadah.org/wiki/Quick_launch_bar_widget/3.5 -- Put into your awesome/ folder and add the following to rc.lua: -- local launchbar = require('launchbar') -- local mylb = launchbar("/path/to/directory/with/shortcuts") -- Then add mylb to the wibox. local layout = require("wibox.layout") local beautiful = require("beautiful") local launcher = require("awful.widget.launcher") local launchbar = {} local function getValue(t, key) local _, _, res = string.find(t, key .. " *= *([^%c]+)%c") return res end function launchbar.new(filedir, icon_finder_helper) if not filedir then error("Launchbar: filedir was not specified") end launchbar.icon_dirs = icondir local items = {} local widget = layout.fixed.horizontal() local files = assert(io.popen("ls " .. filedir .. "*.desktop", 'r')) for f in files:lines() do local ts = assert(io.open(f)) local t = ts:read("*all") ts:close() cmd = getValue(t, "Exec") cmd = cmd:gsub("%%u", "") cmd = cmd:gsub("%%U", "") cmd = cmd:gsub("%%F", "") table.insert( items, { image = icon_finder_helper:find(getValue(t,"Icon")), command = cmd, position = tonumber(getValue(t,"Position")) or 255 }) end files:close() table.sort(items, function(a,b) return a.position < b.position end) for _, v in ipairs(items) do if v.image then widget:add(launcher(v)) end end return widget end return setmetatable(launchbar, { __call = function(_, ...) return launchbar.new(...) end })

Instantiation

The simplest way to use the module is to symlink .desktop files into a folder and instantiate the launchbar object with the same folder as a parameter. My shortcuts are in a subfolder of the awesome config folder called shortcuts.

Note that the creation function also takes the icon finder instance as a parameter (created in the Icons section).

[rc.lua]
-- {{ Launchbar local mylaunchbar = launchbar.new(awesome_paths.config_dir .. "/shortcuts/", myiconfinder) -- }}

Shutdown menu

By default, awesome has no menu to restart or shutdown the computer, just an option to logout. The following adds a menu with common shutdown, restart, logout and lock items. There is no support for hibernation but it should be easy to add.

The shutdown and restart commands typically require the root password so the sudoers file must be edited to allow the desired user to run the shutdown command without password. On Debian, the sudoers file can be edited by running sudo visudo from a terminal. My sudoers file contains the following:

username ALL=SHUTDOWN
username ALL=NOPASSWD: SHUTDOWN

The menu is created as follows. First, the icons are obtained from the icon finder instance, then the menu is built, wrapping commands with the confirm_action function, which prompts for confirmation. Note that the filenames used to recover icons are the ones used in common themes (icons are in found in folders set in the awesome_paths variable).

[rc.lua]
-- {{ Shutdown menu -- Session management icons local icon_shutdown = myiconfinder:find("system-shutdown.png") local icon_restart = myiconfinder:find("system-reboot.png") local icon_logout = myiconfinder:find("system-log-out.png") local icon_lock = myiconfinder:find("system-lock-screen.png") -- Shutdown menu myleave_menuitems = { { "shutdown", function() my_utility.confirm_action( function() awful.spawn('sudo /sbin/shutdown -h now') end, "Shutdown") end, icon_shutdown }, { "restart", function() my_utility.confirm_action( function() awful.spawn('sudo /sbin/shutdown -r now') end, "Restart") end, icon_restart }, { "logout", function() my_utility.confirm_action( function() awesome.quit() end, "Logout") end, icon_logout }, { "lock", function() my_utility.confirm_action( function() awful.spawn("xscreensaver-command -lock") end, "Lock") end, icon_lock } } myleave_launcher = awful.widget.launcher({ image = icon_shutdown, menu = awful.menu({ items = myleave_menuitems}) }) -- }}

The main menu is based on the default configuration with a few additions:

  • Selected applications are added to an "Apps" submenu
  • A theme selection menu
  • A "Leave" menu to control the session (shutdown, restart, lock)

The icons are found using the icon finder module.

This code block also configures the menubar, which can be used to quickly find and launch applications by typing part of their name.

[rc.lua]
-- {{{ Menu -- Create a laucher widget and a main menu myawesomemenu = { { "hotkeys", function() return false, hotkeys_popup.show_help end}, { "manual", terminal .. " -e man awesome" }, { "edit config", emacs .. " " .. awesome.conffile }, { "restart", awesome.restart }, { "quit", function() my_utility.confirm_action( function() awesome.quit() end, "Logout") end } } mymainmenu = freedesktop.menu.build({ before = { { "awesome", myawesomemenu, beautiful.awesome_icon }, { "Terminal", terminal, myiconfinder:find("terminal") }, { "Apps", { { "cmus", termapps .. " -c cmus -e cmus", myiconfinder:find("multimedia-player") }, { "newsboat", termapps .. " -c newsboat -e newsboat", myiconfinder:find("internet-news-reader") }, { "Internet", internet_browser, myiconfinder:find("web-browser") }, { "Mail reader", mail_reader, myiconfinder:find("internet-mail") }, { "emacs", emacs, myiconfinder:find(emacs) } } } }, after = { { "Themes", theme_menuitems }, { "Leave", myleave_menuitems, icon_shutdown } } }) mylauncher = awful.widget.launcher({ image = beautiful.awesome_icon, menu = mymainmenu }) -- Menubar configuration -- Set the terminal for applications that require it menubar.utils.terminal = terminal -- }}}

Volume widget

The volume widget user the wibox.container.radialprofressbar widget defined in awesome-wm 4 versions. Volume control commands are also supported: a left click on the widget toggles the mute option, a right click on the widget spawns a menu with a single entry linking to the mixer program (see Default programs).

[rc.lua]
-- {{{ Volume volume_text = wibox.widget.textbox() volume_text:set_text("") volume_master = wibox.container.radialprogressbar() volume_master.border_color = nil volume_master.min_value = 0 volume_master.max_value = 100 volume_master.forced_width = 20 volume_master.border_width = 3 volume_master.widget = volume_text function update_volume_widget() volume_char_list = {"🔇", "🔈", "🔉"} awful.spawn.easy_async( awesome_paths.config_dir .. "/scripts/osdvol.sh get", function (stdout, stderr, reason, exitcode) for k, v in stdout:gmatch("([0-9]+)%%.*[[]([onf]+)[]].*") do -- There should be only one match vol_str = k ismute = v == "off" end vol = tonumber(vol_str) volume_master.value = vol if ismute then vol_char = volume_char_list[1] else bin = math.floor(vol / (100 / (#volume_char_list - 1))) if bin < 0 then bin = 0 elseif bin >= #volume_char_list - 1 then bin = #volume_char_list - 2 end vol_char = volume_char_list[bin + 2] end volume_text:set_text(vol_char) end ) end volume_menu_items = { { "mixer", function () run_or_raise(mixer, { instance = "mixer" }) end } } volume_menu = awful.menu.new( { items = volume_menu_items } ) volume_buttons = awful.util.table.join( awful.button({ }, 1, function () awful.spawn(awesome_paths.config_dir .. "/scripts/osdvol.sh mute") update_volume_widget() end), awful.button({ }, 4, function () awful.spawn(awesome_paths.config_dir .. "/scripts/osdvol.sh volup") update_volume_widget() end), awful.button({ }, 5, function () awful.spawn(awesome_paths.config_dir .. "/scripts/osdvol.sh voldown") update_volume_widget() end), awful.button({ }, 3, function () volume_menu:toggle() update_volume_widget() end) ) volume_master:buttons(volume_buttons) update_volume_widget() -- }}}

OSD volume

The volume widget relies on a bash script to control the volume (using the amixer command). It also displays the volume on the screen via OSD (using the aosd_cat command. The bash script is bound to volume keys on the keyboard (see Keyboard bindings).

[scripts/osdvol.sh]
#!/bin/bash # constants FONT='-*-fixed-*-*-*-*-100-*-*-*-*-*-*-*' COLOR="green" DELAY=4 POS="bottom" ALIGN="center" BARMOD="percentage" VOLTXT="Volume" VOLMUTEDTXT="Muted" VOLSTEP=5% # kills an existing osd_cat process # needed when holding down a key to force repaint of the onscreen message preKill() { killall aosd_cat } isMute() { VOLMUTE="$(amixer sget Master,0 | grep "Front Left:" | awk '{print $6}')" } # gets the actual volume value getVol() { VOL="$(amixer sget Master,0 | grep "Front Left:"| awk '{print $5}'|sed -r 's/[][]//g')" } # gets the actual volume value and prints is on the screen # with a percent bar + a percent number showVol() { getVol echo $VOL | aosd_cat -p 1 --fore-color=green --shadow-color=\#006633 \ --font="Droid Sans Mono 32" \ --x-offset=50 --y-offset=-0 --transparency=2 \ --fade-in=0 --fade-out=0 --fade-full=1000 } # reises the master channel by "VOLSTEP" volUp() { amixer sset Master,0 "$VOLSTEP+" } # decreases the master channel by "VOLSTEP" volDown() { amixer sset Master,0 "$VOLSTEP-" } # mutes the master channel volMute() { amixer sset Master,0 toggle } # main part preKill case "$1" in "volup") volUp showVol ;; "voldown") volDown showVol ;; "mute") volMute showVol ;; "get") getVol isMute echo "$VOL $VOLMUTE" ;; *) ;; esac

Memory

Memory is monitored using a simple graph widget. The memory metrics (total and and used memory) are obtained by calling the free utility. A popup window with the output of the top command is shown one mouse-over events.

[rc.lua]
-- {{{ Memory usage local memory_label = wibox.widget.textbox() memory_label:set_text("▤ ") memory_graph = wibox.widget.graph { width = 20, height = 20 } memory_widget = wibox.widget { max_value = 100, widget = memory_graph, forced_width = 20, color = beautiful.fg_normal, background_color = beautiful.bg_focus, border_color = beautiful.bg_normal } memory_widget_async = false -- Read maximum size awful.spawn.easy_async( "free -m", function (stdout, stderr, reason, code) memory_widget.max_value = tonumber( stdout:gmatch(".*Mem:[ ]*([0-9]+).*")()) end ) function memory_widget_update() awful.spawn.easy_async( "free -m", function (stdout, stderr, reason, code) memory_widget:add_value( tonumber(stdout:gmatch(".*Mem:[ ]*[0-9]+[ ]*([0-9]+).*")())) end ) return true end function memory_widget_htop_popup(str) top_str = "" num_lines_left = 20 for k, str in pairs(my_utility.lines(str)) do if num_lines_left > 0 then top_str = top_str .. "\n" .. str num_lines_left = num_lines_left - 1 else break end end memory_popup = naughty.notify({ title = "", text = top_str, font = "monospace 10", timeout = 0, hover_timeout = 0.5, screen = awful.screen.focused()}) end function memory_widget_htop() memory_cmd = "top -b -n1 -o %CPU" if memory_widget_async then awful.spawn.easy_async( memory_cmd, function (stdout, stderr, reason, exitcode) memory_widget_htop_popup(stdout) end ) else local out = assert(io.popen(memory_cmd, 'r')) local msg = out:read("*all") out:close() memory_widget_htop_popup(msg) end end memory_widget:connect_signal( 'mouse::enter', function () memory_widget_htop() end ) memory_widget:connect_signal( 'mouse::leave', function () if memory_popup then naughty.destroy(memory_popup) end end ) memory_widget_timer = gears.timer.start_new( 5, function () return memory_widget_update() end) -- }}}

Calendar

The calendar widget simply consists in display the output of the khal command as a popup when the mouse points to the date widet. This assumes that the khal program is installed and configured.

[rc.lua]
-- {{{ Calendar -- Create a textclock widget mytextclock = awful.widget.textclock() mytextclock_cal_async = false function mytextclock_widget_khal_popup(str) textclock_popup = naughty.notify({ title = "", text = str, font = "monospace 10", timeout = 0, hover_timeout = 0.5, screen = awful.screen.focused()}) end function textclock_widget_khal() cmd = "bash -c \"source ~/data/software/python-env/caldav/bin/" .. "activate && khal\"" if mytextclock_cal_async then awful.spawn.easy_async( cmd, function (stdout, stderr, reason, exitcode) mytextclock_widget_khal_popup(stdout) end ) else local out = assert(io.popen(cmd, 'r')) local msg = out:read("*all") out:close() mytextclock_widget_khal_popup(msg) end end mytextclock:connect_signal( 'mouse::enter', function () textclock_widget_khal() end ) mytextclock:connect_signal( 'mouse::leave', function () if textclock_popup then naughty.destroy(textclock_popup) end end ) -- }}}

Mail

I use maildir for my personal email, and mu for indexing. The mail widget uses the vicious maildir widget, combined with custom functions to display a summary of active emails when moving the mouse over the widget.

Module definition

This module was taken from this post, with small modifications to account for my local setup. In particular, the list of emails is obtained via a mu request (querystring).

[mailhoover.lua]
local string = string local tostring = tostring local io = io local table = table local pairs = pairs local os = os local awful = require("awful") local naughty = require("naughty") local beautiful = require('beautiful') local my_utility = require("my_utility") local mailhoover = {} local popup local mailhoover = {} function mailhoover:addToWidget(print,mywidget, querystring, maxcount, mail_colors) mywidget:connect_signal( 'mouse::enter', function () local query_format = "<span color='" .. beautiful.mail_fg_normal .. "'><b><u>%s</u>\n</b></span>" local info = mailhoover:read_index(print, querystring, maxcount, mail_colors) popup = naughty.notify({ title = "", text = string.format(query_format, querystring) .. "<br>" .. info, timeout = 0, hover_timeout = 0.5, screen = awful.screen.focused() }) end) mywidget:connect_signal('mouse::leave', function () naughty.destroy(popup) end) end function mailhoover:read_index(print, querystring, maxcount, mail_colors) local info = "" local count = 0 local f = assert(io.popen('mu find -f "d f s m" -s d -z ' .. querystring, 'r')) local out = f:read("*all") f:close() local nlines = 1 for line in out:gmatch("[^\r\n]+") do line = line:gsub("<", "[") line = line:gsub(">", "]") line = line:gsub("CDT", "") fold = line:gsub(".*/([^%s]+)/([^%s]+)$", "%1_%2") col_hex = beautiful.mail_fg_normal if mail_colors ~= nil then for k, v in pairs(mail_colors) do if fold:match(k) then col_hex = v break end end end line = my_utility.html_escape(line) info = info .. string.format("<span color='" .. col_hex .. "'>" .. line .. "</span>") .. '\n' nlines = nlines + 1 if nlines == maxcount then break end end return info end return mailhoover

Widget setup

The vicious module is used to report the number of new messages in a list of maildir folders. The color of the widget text changes from beautiful.mail_fg_normal to beautiful.mail_fg_urgent when new messages are present.

Additionally, a right-click on the widget spawns a small menu with options to open mail clients (thunderbird and mu4e).

[rc.lua]
-- Mail widget mailicon = wibox.widget.textbox() mailfolders = my_utility.scandir(awesome_paths.home_dir .. '/maildir/INBOX', '*/ -d') my_utility.table_path_remove(mailfolders, { ".", "..", "cur", "new", "tmp" }) table.insert(mailfolders, awesome_paths.home_dir .. '/maildir/INBOX') table.insert(mailfolders, awesome_paths.home_dir .. '/maildir/status/action') vicious.register(mailicon, vicious.widgets.mdir, function (widget, args) local new_messages = args[1] + args[2] if new_messages == 0 then return string.format('✉ 0') else return string.format('<span color="#dc322f">✉ ' .. new_messages .. '</span> | ') end end, 60, mailfolders) -- Mail mouse over local mailhoover = require("mailhoover") local mail_query = "maildir:/INBOX* or maildir:/status*" local mail_colors = { status_waiting_on = beautiful.mail_fg_normal, status_action = beautiful.mail_fg_urgent, INBOX = beautiful.mail_fg_focus, } mailhoover:addToWidget(print, mailicon, mail_query, 20, mail_colors) mailmenu_items = { { "emacs", function () run_or_raise("emacs -f mu4e", { class = "Emacs" }) end }, { "thunderbird", function () run_or_raise(mail_reader, { class = "Thunderbird" }) end } } mailmenu = awful.menu.new( { items = mailmenu_items } ) mailbuttons = awful.util.table.join( awful.button({ }, 3, function () mailmenu:toggle() end) ) mailicon:buttons(mailbuttons)

cmus

Music is controlled by cmus, which is launched on startup. The original widget was taken from here.

The widget has the following features:

  1. Show the current play state, song artist, title and current position in the wibox widget.
  2. Toggle play / pause on click
  3. Go to cmus on right click
  4. On mouse over events, show a track listing of the current playing album in a notification window (popup), with the cover art (parsed from the file metadata using eyeD3).
  5. A function to search for a given album by name or select an album at random and play it in cmus (Quick play command). This function is not used by the widget but by the global prompt module.

An example of the widget in action is shown on Fig. 3.

This module is not generic, modifications are likely to be necessary if the setup is different from mine. My music library is organized as follows:

  • All my music is in ~/data/music/ where each album is in a separate folder with name YYYY-ARTIST-ALBUM (the quick play command explicitly assumes this format, so it probably won't work with different conventions). The format is set in a regular expression (cmus_album_regex in the Global variables section).
  • Songs all have the cover art in their metadata. This is where the cover art displayed in the popup notification comes from. There is no clever mechanism to pick an image file in the album folder, although it should be possible to implement one.
  • The extracted cover art is cached in ~/cmus_img/.

This is admittedly pretty constraining, but supporting more generic setups seems feasible, with some work.

Imports

First, import the necessary libraries.

[cmus.lua]
local awful = require("awful") local naughty = require("naughty") local beautiful = require('beautiful') local util = require("awful.util") local my_utility = require("my_utility")

Utility functions

A few utility functions are used in the module:

get_path
Extract path from filename.
cmusover_addToWidget
Show album popup on mouse over.
get_mp3_info
Get a string representation of file ID3 tags using eyeD3. The output string is of the form "track number - track title - track length" where the track title is padded so that the whole string length is strlen (this does not seem to work with accentuated letters).
[cmus.lua]
function get_path(str, sep) sep = sep or '/' return str:match("(.*" .. sep .. ")") end function cmusover_addToWidget(widget) widget:connect_signal( 'mouse::enter', function () cmus_popup(0) end) widget:connect_signal( 'mouse::leave', function () naughty.destroy(popup) end) end function get_mp3_info(fname, strlen) -- FIXME: fix padding for accentuated letters. cmd = 'eyeD3 --no-color ' .. '"' .. fname .. '"' cmd_out = assert(io.popen(cmd, 'r')) out = cmd_out:read("*all") cmd_out:close() track = string.gsub(string.match(out, 'track: %d*'), 'track: ', '') title = string.gsub(string.match(out, 'title: %C*'), 'title: ', '') time_str = string.match(out, 'Time: [%d:]*') if time_str == nil then time = "-" else time = string.gsub(time_str, 'Time: ', '') end sfmt = string.format('%%-%d.%ds', strlen, strlen) return string.format('%2d - '..sfmt..' - %s', track, title, time) end

Global variables

Setup variables are used to find albums by folder name in the music folder (using the cmus_album_regex variable) and to cache the track listing and cover art.

[cmus.lua]
-- Album folder regexp cmus_album_regex = "^[0-9]\\{4\\}-" -- Album display (with cover) cmus_file_g = "" cmus_img_g = "" cmus_img_fold = os.getenv("HOME") .. "/cmus_img/" cmus_album_g = "" cmus_album_list_g = "" cmus_file_list_g = nil

Check running instance

This function detects whether a cmus instance is running by requesting its PID.

[cmus.lua]
-- Get cmus PID to check if it is running function getCmusPid() local fpid = assert(io.popen("pgrep cmus", 'r')) local pid = fpid:read("*n") fpid:close() return pid end

Controls

A cmus instance can be controlled from a terminal using the cmus-remote command. The following function passes simple controls to cmus-remote (play, pause, stop, next, etc.). In practice only the play / pause functionality is used: clicking on the widget toggles the play state (play / pause).

[cmus.lua]
-- Enable cmus control function cmus_control (action) local cmus_info, cmus_state local cmus_run = getCmusPid() if cmus_run then local out = assert(io.popen("cmus-remote -Q", 'r')) cmus_info = out:read("*all") out:close() if not cmus_info then return end cmus_state = string.gsub(string.match(cmus_info, "status %a*"), "status ", "") if cmus_state ~= "stopped" then if action == "next" then assert(io.popen("cmus-remote -n", 'r')) elseif action == "previous" then assert(io.popen("cmus-remote -r", 'r')) elseif action == "stop" then assert(io.popen("cmus-remote -s", 'r')) end end if action == "play_pause" then if cmus_state == "playing" or cmus_state == "paused" then assert(io.popen("cmus-remote -u", 'r')) elseif cmus_state == "stopped" then assert(io.popen("cmus-remote -p", 'r')) end end end end

Popup

The popup notification (Fig. 3) shows the content of the global variables defined earlier (see global variables) and updated by the cmus_hook function (see the Main hook section).

[cmus.lua]
function cmus_popup(timeout) local img if util.file_readable(cmus_img_g) then img = cmus_img_g else img = nil end if cmus_album_list_g ~= "" then popup = naughty.notify({ title = cmus_album_g, text = cmus_album_list_g, font = "monospace 10", icon = img, icon_size = 192, timeout = timeout, hover_timeout = 0.5, screen = awful.screen.focused() }) end end

The following bash script, uses awesome-client to spawn the popup notification and performs a screen capture (Fig. 3).

# Could get these from xrandr
res_x=1920
res_y=1080
screen_idx=1 # starts at 0

echo "local cmus = require(\"cmus\"); cmus_popup(5)" | awesome-client

width=$(( 3 * $res_x / 8 ))
height=250
start_x=$(( $res_x * $screen_idx + $res_x - $width ))

sleep 0.5

scrot desktop.png
convert desktop.png -crop ${width}x${height}+${start_x}+0 desktop-cmus-popup.png
rm desktop.png -rf

desktop-cmus-popup.png

Figure 3: Cmus popup notification, with album cover and track list.

Quick play command

The quick play command cmus_play is used to start playing a new album, picked randomly (rand mode) or from a string (album mode). There is no single song mode; it could probably be added, but I use a direct search and mplayer to play individual songs via the global prompt module.

Once the folder to play is found, some work is required to get cmus to play it. First, cmus must be in playlist mode (i.e. the play_library flag must of set to false). cmus-remote is used to interrogate the running instance and then a second time to disable the play_library mode if necessary. Then, the current playlist is cleared (cmus-remote -c), the desired folder is added to the playlist and playback is started. A combination of stop, next and play commands seems to be the best way to get the playlist to start.

The commands are written into a temporary bash script, which is executed at the end of the function (I was not able to get this to work without the intermediate bash script).

[cmus.lua]
function cmus_play(mode, fold) local out if mode == "rand" then alb_sel_cmd = "ls " .. global_prompt.music_folder .. " | " .. "grep \"" .. cmus_album_regex .. "\" | sort -R | head -n1" elseif mode == "album" then alb_sel_cmd = "ls " .. global_prompt.music_folder .. " | " .. "grep \"" .. cmus_album_regex .. "\" | grep -i \"" .. fold .. "\" |" .. "tail -n1" end local f = assert(io.popen(alb_sel_cmd, 'r')) out = f:read("*all"):gsub("\n$", "") f:close() -- Determine if play_library flag need to be toggled f = assert(io.popen("cmus-remote -Q | grep play_library | awk '{print $3}'")) local lib_flag = f:read("*all"):gsub("\n$", "") f:close() if lib_flag == "true" then mp_cmds = { "cmus-remote -C \"toggle play_library\"" } else mp_cmds = {} end -- table.insert(mp_cmds, "cmus-remote --stop") table.insert(mp_cmds, "cmus-remote -P -c \"" .. global_prompt.music_folder .. "/" .. out .. "\"") table.insert(mp_cmds, "cmus-remote -C \"view playlist\"") table.insert(mp_cmds, "cmus-remote --stop") table.insert(mp_cmds, "cmus-remote --next") table.insert(mp_cmds, "cmus-remote --play") if mp_cmds then local file local flag_batch = true -- problems when not using this if flag_batch then file = io.open("/tmp/cmus.sh", "w") end for _, cmd in ipairs(mp_cmds) do if flag_batch then file:write(cmd .. "\nsleep 1\n") else awful.spawn.with_shell(cmd) end end if flag_batch then file:close() awful.spawn.with_shell("bash /tmp/cmus.sh") end end end

Main hook

The cmus_hook function is responsible for updating the text of the music widget, and the content of the popup notification. It is run on a frequent timer (2 seconds by default).

The hook first checks for the presence of a running cmus instance using the getCmusPid function. If a running instance is found and its status is currently "playing" or "paused", the hook proceeds to extract information about the currently playing file. As shown in Fig. 2, the widget shows the playing status with a unicode character (play / pause), the artist and song title and the track position (formatted as "position" / "duration").

The rest of the function populates the album track list. cmus_album_str contains the album name and the year between parentheses. If the album name is different from the one cached in the global variable cmus_album_g, the track list is updated.

To update the track list, the cover art is first extracted from the current playing file using the eyeD3 utility. This requires all audio files to contain the album art as metadata. The extracted cover art is saved in the folder defined by the cmus_img_fold variable.

Further, all the .mp3 files in the same folder as the file being played are listed, their metadata is extracted using the get_mp3_info function, and they are added to the cmus_album_list_g variable. The song currently playing is highlighted in the list.

[cmus.lua]
function cmus_hook() -- check if cmus is running local cmus_run = getCmusPid() if cmus_run then out = assert(io.popen("cmus-remote -Q", 'r')) cmus_info = out:read("*all") out:close() if not cmus_info then return "." end cmus_status = string.match(cmus_info, "status %a*") if cmus_status == nil then return "." end cmus_state = string.gsub(cmus_status,"status ","") if cmus_state == "playing" or cmus_state == "paused" then cmus_artist = string.gsub(string.match( cmus_info, "tag artist %C*"), "tag artist ","") cmus_title = string.gsub(string.match(cmus_info, "tag title %C*"), "tag title ","") cmus_album = string.gsub(string.match(cmus_info, "tag album %C*"), "tag album ","") cmus_year = string.gsub(string.match(cmus_info, "tag date %C*"), "tag date ","") cmus_curtime = string.gsub(string.match(cmus_info, "position %d*"), "position ","") cmus_curtime_formated = math.floor(cmus_curtime/60) .. ':' .. string.format("%02d",cmus_curtime % 60) cmus_totaltime = string.gsub(string.match(cmus_info, "duration %d*"), "duration ","") cmus_totaltime_formated = math.floor(cmus_totaltime/60) .. ':' .. string.format("%02d",cmus_totaltime % 60) cmus_album_str = cmus_album .. " (" .. cmus_year .. ")" if cmus_artist == "" then cmus_artist = "unknown artist" end if cmus_title == "" then cmus_title = "unknown title" end -- cmus_title = string.format("%.5c", cmus_title) cmus_string = cmus_artist .. " - " .. cmus_title .. " (" .. cmus_curtime_formated .. "/" .. cmus_totaltime_formated .. ")" if cmus_state == "paused" then cmus_string = '⏸ ' .. cmus_string .. '' else cmus_string = '⏴ ' .. cmus_string .. '' end cmus_file = string.gsub(string.match(cmus_info, "file %C*"), "file ","") if cmus_album_str ~= cmus_album_g then cmd = "rm " .. cmus_img_fold .. "/*; " .. "eyeD3 --no-color --write-images=" .. cmus_img_fold .. "/ " .. "\"$(cmus-remote -Q|grep \"^file\"|" .. "sed -r 's/^file\\s*//g')\"" out = assert(io.popen(cmd .. " 2>&1", 'r')) cmus_img_dump = out:read("*all") out:close() if not string.match(cmus_img_dump, 'file not found') then local fname = string.match(cmus_img_dump, "Writing %C*") if fname ~= nil then cmus_img_g = string.gsub( string.gsub(fname, "Writing ",""), "...$", "") end end cmus_album_g = cmus_album_str cmus_file_list_g = my_utility.scandir(get_path(cmus_file), '*.mp3') end if cmus_file_g ~= cmus_file then cmus_file_g = cmus_file cmus_album_list_g = '' for k, f in pairs(cmus_file_list_g) do local f_esc = my_utility.filename_escape(f) local file_info = get_mp3_info(f_esc, 35) file_info = my_utility.html_escape(file_info) if f == cmus_file then file_info = "<span color='" .. beautiful.cmus_fg .. "'><b>" .. file_info .. "</b></span>" end cmus_album_list_g = cmus_album_list_g .. file_info .. '\n' end end else cmus_string = '-- not playing --' end return cmus_string else return '-- not running --' end end

cmus module usage

In the main rc.lua file, we create the widget, connect it to the cmus_hook function with a timer object (updated every 2 seconds), add the popup notification to mouse over events and setup the click actions.

[rc.lua]
-- {{{ cmus -- Cmus Widget tb_cmus = wibox.widget.textbox() tb_cmus:set_text("cmus") -- refresh Cmus widget cmus_timer = timer({timeout = 2}) cmus_timer:connect_signal( "timeout", function() tb_cmus:set_text('♪ ' .. cmus_hook() .. ' ') end) cmus_timer:start() -- pause on click cmusbuttons = awful.util.table.join( awful.button({ }, 1, function() cmus_control("play_pause") end), awful.button({ }, 3, function() run_or_raise(termapps .. " -name cmus -e cmus", { instance = "cmus" }) end) ) tb_cmus:buttons(cmusbuttons) cmusover_addToWidget(tb_cmus) -- }}}

Tiny Tiny RSS

The RSS widget relies on an instance of Tiny Tiny RSS and the ttrss python package. Extraction of RSS entries is performed using the following python wrapper script.

Python wrapper script

The python wrapper script is a simple interface to the ttrss python package. It requires the following packages (besides ttrss-python):

argparse
command-line arguments parser,
keyring
extract credentials from keyring,
textwrap
wraps output string within fixed width paragraph.
[scripts/ttrss-wrapper.py]
""" Get unread feeds from tt-rss site """ # %% Imports import ttrss.client import keyring import argparse import textwrap # %% Parse options if __name__ == '__main__': parser = argparse.ArgumentParser(description='Get tt-rss feeds.') parser.add_argument('-U', '--url', dest='url', required=True, type=str, help='TinyTiny RSS URL') parser.add_argument('-u', '--user', dest='user', required=True, type=str, help='Username') parser.add_argument('-p', '--pass', dest='passwd', required=False, type=str, help='Password') parser.add_argument('-W', '--width', dest='text_width', default=50, required=False, type=int, help='Width of text output') parser.add_argument('-c', '--count', dest='flag_count', action='store_true', required=False, help='Output only the number of unread items') parser.add_argument('-r', '--mark-read', dest='flag_mark_read', action='store_true', required=False, help='Mark all unread items as read') args = parser.parse_args() else: from collections import namedtuple Args = namedtuple('arg', ['user', 'passwd', 'url', 'text_width', 'flag_count', 'flag_mark_read']) args = Args(user='', url='', text_width=50, flag_count=False, flag_mark_read=False, passwd=None) # %% Get password if not args.passwd: args.passwd = keyring.get_password('tt-rss', args.user) # %% Create connection client = ttrss.client.TTRClient(args.url, args.user, args.passwd, auto_login=True, http_auth=()) client.login() # %% Get unread feeds hl = client.get_headlines(view_mode='unread') # %% Render output if args.flag_count: print(len(hl)) else: for hh in hl: if args.flag_mark_read: client.mark_read(hh.id) else: print("\n".join([ ("- " if ii == 0 else " ") + tt for ii, tt in enumerate( textwrap.wrap(hh.title, args.text_width)) ])) # %% Cleanup client.logout()

The script takes as parameters the URL to the Tiny Tiny RSS instance, a user name and a password. It gathers unread feeds and prints them into a string. It also adds the option to get the credentials from the keyring (e.g. gnome-keyring or KWallet). This functionality requires the keyring python package.

Here is the help text for the ttrss-wrapper.py script:

# Use " || echo " to ensure a success code is returned
python3 scripts/ttrss-wrapper.py --help || echo
usage: ttrss-wrapper.py [-h] -U URL -u USER [-p PASSWD] [-W TEXT_WIDTH] [-c]
                        [-r]

Get tt-rss feeds.

optional arguments:
  -h, --help            show this help message and exit
  -U URL, --url URL     TinyTiny RSS URL
  -u USER, --user USER  Username
  -p PASSWD, --pass PASSWD
                        Password
  -W TEXT_WIDTH, --width TEXT_WIDTH
                        Width of text output
  -c, --count           Output only the number of unread items
  -r, --mark-read       Mark all unread items as read

Credentials

In my setup, credentials are stored in the ~/.authinfo.gpg file. The following emacs-lisp snippet defines a function to get the username and password from the url. This should also work for the un-encrypted version using .authinfo.

If not using authinfo, the credentials can be hard-coded in this emacs-lisp snippet. In the following source block, replace line 3 by line 4 and line 5 by line 6 and set the correct password.

 1: (defun get-ttrss-cred (&optional url)
 2:   (let ((ttrss-url (if url url "127.0.0.1:81/tt-rss"))
 3:         (ttrss-user nil)
 4:         ;;(ttrss-user "myuser")
 5:         (ttrss-pass nil)
 6:         ;;(ttrss-pass "mypassword")
 7:         )
 8:     (when (require 'auth-source)
 9:       (let ((auth (nth 0 (auth-source-search :host ttrss-url
10:                                              :requires '(user secret)))))
11:         (when (and (plist-get auth :secret) (plist-get auth :user))
12:           (setq ttrss-pass (funcall (plist-get auth :secret))
13:                 ttrss-user (plist-get auth :user)))))
14:     `(,ttrss-url ,ttrss-user ,ttrss-pass)))

Note that this approach saves the credentials unencrypted in the ttrss.lua module. It may be preferable to rely on a keyring to get the password. However, the authinfo approach can still be safely used with version control if the org file is version-controlled and the tangled lua files are not.

tt-rss module

The tt-rss module defines functions used by the widget:

ttrss_get_count
return the number of unread feeds,
ttrss_get_feeds
return a formatted list of unread feeds,
ttrss_mark_read
mark all feeds as read,
ttrss_set_mouseover
setup the widget to display a popup notification with the list of unread feeds on mouse-over.

These functions internally use the ttrss_get function which interacts with the ttrss-wrapper.py python script.

[ttrss.lua]
-- Script options local ttrss_cmd = "python3 " .. awesome_paths.config_dir .. "/scripts/ttrss-wrapper.py " local ttrss_url = "http://" .. ttrss_cred[1] local ttrss_user = ttrss_cred[2] local ttrss_pass = ttrss_cred[3] local ttrss_textwidth = 100 local awful = require("awful") local naughty = require("naughty") function ttrss_get_count() return ttrss_get("count") end function ttrss_get_feeds() return ttrss_get("feeds") end function ttrss_mark_read() return ttrss_get("mark_read") end function ttrss_get(action) if action == "feeds" then args = "" elseif action == "count" then args = "-c" elseif action == "mark_read" then args = "-r" end cmd = ttrss_cmd .. " -U " .. ttrss_url .. " -u " .. ttrss_user .. " -p " .. ttrss_pass .. " -W " .. ttrss_textwidth .. " " .. args local out = assert(io.popen(cmd, 'r')) ttrss_info = out:read("*all") out:close() return ttrss_info end function ttrss_set_mouseover(mywidget) mywidget:connect_signal('mouse::enter', function () local info = ttrss_get_feeds() if info ~= "" then popup = naughty.notify({ title = "", text = info, timeout = 0, hover_timeout = 0.5, screen = awful.screen.focused()}) end end) mywidget:connect_signal('mouse::leave', function () naughty.destroy(popup) end) end

tt-rss widget

The ttrss widget is a textbox with the text updated on a timer (every 10 minutes by default) to display the number of unread items. On a mouse over event, a list of unread feeds is shown in a popup notification. Finally, click actions are added to the widget:

  • a left click shows a menu with entries "Refresh" and "Mark as read" which respectively refresh the widget count and mark all unread items as read,
  • a right click spawns a menu with shortcuts to the terminal and GUI RSS applications newsboat and liferea.
[rc.lua]
-- {{{ tt-rss tb_ttrss = wibox.widget.textbox() tb_ttrss:set_text("ttrss") -- refresh ttrss_timer = timer({timeout = 600}) ttrss_timer:connect_signal("timeout", function () tb_ttrss:set_text('☕ ' .. ttrss_get_count()) end) ttrss_timer:start() tb_ttrss:set_text('☕ ' .. ttrss_get_count()) ttrssmenu_l_items = { { "Refresh", function () tb_ttrss:set_text('☕ ' .. ttrss_get_count()) end }, { "Mark as read", function () ttrss_mark_read() tb_ttrss:set_text('☕ ' .. ttrss_get_count()) end } } ttrssmenu_r_items = { { "newsboat", function () run_or_raise( termapps .. " -name newsboat -e newsboat", { instance="newsboat"} ) end }, { "liferea", function () run_or_raise( "liferea", { class = "Liferea" }) end } } ttrssmenu_l = awful.menu.new( { items = ttrssmenu_l_items } ) ttrssmenu_r = awful.menu.new( { items = ttrssmenu_r_items } ) ttrss_buttons = awful.util.table.join( awful.button({ }, 1, function () ttrssmenu_l:toggle() end), awful.button({ }, 3, function () ttrssmenu_r:toggle() end) ) tb_ttrss:buttons(ttrss_buttons) ttrss_set_mouseover(tb_ttrss) -- }}}

Keyboard layout

Add a widget to control the keyboard layout.

[rc.lua]
-- Keyboard map indicator and switcher mykeyboardlayout = awful.widget.keyboardlayout()

Effects

This section contains setup of visual effects that can be used with awesome-wm.

Monitor highlighting

It is sometimes difficult to tell which monitor is the active one, so I implemented a simple hook to highlight the wibox on the active screen. The actual colors are defined through the theme manager.

[rc.lua]
-- {{{ Highlight current monitor screen_highlight_timer = timer({timeout = 0.2}) screen_highlight_idx = 1 screen_highlight_timer:connect_signal( "timeout", function () if awful.screen.focused() ~= screen_highlight_idx then screen_highlight_idx = awful.screen.focused() for s in screen do if s == awful.screen.focused() then col_bg = beautiful.screen_highlight_bg_active col_fg = beautiful.screen_highlight_fg_active else col_bg = beautiful.screen_highlight_bg_inactive col_fg = beautiful.screen_highlight_fg_inactive end s.mywibox:set_bg(col_bg) s.mywibox:set_fg(col_fg) end end end) screen_highlight_timer:start() -- }}}

Transparency

If using a compositor (e.g. xcompmgr), effects can be applied on windows. This block makes windows transparent when losing focus, and opaque when regaining focus. Note that this block is disabled by default (see the :PROPERTY: drawer).

-- {{ Transparency
client.connect_signal("focus", function(c)
                         c.border_color = beautiful.border_focus
                         c.opacity = 1
end)
client.connect_signal("unfocus", function(c)
                         c.border_color = beautiful.border_normal
                         c.opacity = 0.9
end)
-- }}

Window border change on focus

Add a signal to change the color of window borders when losing / receiving focus. This comes from the default configuration.

[rc.lua]
-- {{ Window border 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) -- }}

Snap client to screen edges

Awesome 4 adds edge snapping, the following disables it.

[rc.lua]
awful.mouse.snap.edge_enabled = false

Bindings

Bindings include mouse click actions for the different widgets and windows as well as keyboard shortcuts.

Mouse

Mouse bindings are defined using tables filled by awful.button elements.

General bindings

This section handles mouse actions on an empty workspace:

  • a right-click pops-up the main menu,
  • the mouse wheel moves to the previous / next tag.
[rc.lua]
-- {{{ Mouse bindings root.buttons(awful.util.table.join( awful.button({ }, 3, function () mymainmenu:toggle() end), awful.button({ }, 4, awful.tag.viewprev), awful.button({ }, 5, awful.tag.viewnext) ))

Windows

Mouse actions for clicks on windows are defined here:

  • a left click sets the focus,
  • modkey + left click drag moves the window,
  • modkey + right click drag resizes the window.

The clientbuttons table created by this block is later added to each window via the rules mechanism.

[rc.lua]
-- Focus, move, resize window on click 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)) -- }}}

Tag list

The mouse can be used to navigate between between tags, when the mouse is over the tag list widget:

  • a left click on a tag icon moves to the corresponding tag,
  • modkey + left click moves the current window to the desired tag,
  • a right click toggles the visibility of the corresponding tag (on top of the current tag, so all windows from the tag under the mouse will appear in the currently active tag),
  • modkey + right click toggles the current tag's visibility on the tag under the mouse,
  • the mouse wheel goes to the previous / next tag.
[rc.lua]
-- Create a wibox for each screen and add it local taglist_buttons = awful.util.table.join( awful.button({ }, 1, function(t) t:view_only() end), awful.button({ modkey }, 1, function(t) if client.focus then client.focus:move_to_tag(t) end end), awful.button({ }, 3, awful.tag.viewtoggle), awful.button({ modkey }, 3, function(t) if client.focus then client.focus:toggle_tag(t) end end), awful.button({ }, 4, function(t) awful.tag.viewprev(t.screen) end), awful.button({ }, 5, function(t) awful.tag.viewnext(t.screen) end) )

Task list

Task list mouse actions are the following:

  • a left click toggles window minimization,
  • a right click spawns a menu listing the open windows (on all screens and tags),
  • the mouse wheel cycles through windows in the current screen and tag.
[rc.lua]
local tasklist_buttons = awful.util.table.join( awful.button({ }, 1, function (c) if c == client.focus then c.minimized = true else -- Without this, the following -- :isvisible() makes no sense c.minimized = false if not c:isvisible() and c.first_tag then c.first_tag:view_only() end -- This will also un-minimize -- the client, if needed client.focus = c c:raise() end end), awful.button({ }, 3, client_menu_toggle_fn()), awful.button({ }, 4, function () awful.client.focus.byidx(1) end), awful.button({ }, 5, function () awful.client.focus.byidx(-1) end))

Keyboard

Keyboard bindings are split into two categories: global bindings and client (windows) bindings.

Global bindings

Keybindings are defined in subsections, and can be individually disabled by setting the tangle property to no.

First, we initialize the table for the keys.

[rc.lua]
-- {{{ Key bindings globalkeys = {}
Shortcuts popup

Display the help popup.

[rc.lua]
globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, }, "s", hotkeys_popup.show_help, {description="show help", group="awesome"}) )
Tags

This section defines tag navigation keybindings and window movement between tags.

  • Previous/next tag with modkey

    Move the the previous / next tag:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, }, "Left", awful.tag.viewprev, {description = "view previous", group = "tag"}), awful.key({ modkey, }, "Right", awful.tag.viewnext, {description = "view next", group = "tag"}) )
  • Previous/next tag on all screens with Ctrl + Alt

    Move to the previous / next tag (using Ctrl + Alt as on other desktop environments) on all screens:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ altkey, "Control" }, "Left", function () local screen_cur = awful.screen.focused() local tag_orig = screen_cur.selected_tag local tag_prev = tag_orig.index tag_prev = tag_prev - 1 if tag_prev == 0 then tag_prev = #screen_cur.tags end for s in screen do local tag = s.tags[tag_prev] if tag then tag:view_only() end end end, {description = "Move all screens to left tag", group = "tag"}), awful.key({ altkey, "Control" }, "Right", function () local screen_cur = awful.screen.focused() local tag_orig = screen_cur.selected_tag local tag_next = tag_orig.index tag_next = tag_next + 1 if tag_next > #screen_cur.tags then tag_next = 1 end for s in screen do local tag = s.tags[tag_next] if tag then tag:view_only() end end end, {description = "Move all screens to right tag", group = "tag"}) )
  • Move window to previous/next with Ctrl + Alt + Shift

    Move the current window to the previous / next tag:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ altkey, "Control", "Shift" }, "Left", function () local screen_cur = awful.screen.focused() local tag_orig = screen_cur.selected_tag local tag_prev = tag_orig.index tag_prev = tag_prev - 1 if tag_prev == 0 then tag_prev = #screen_cur.tags end if client.focus then local tag = screen_cur.tags[tag_prev] if tag then client.focus:move_to_tag(tag) end end for s in screen do if s ~= screen_cur then local tag = s.tags[tag_prev] if tag then tag:view_only() end end end if screen_cur.tags[tag_prev] then local tag = screen_cur.tags[tag_prev] tag:view_only() end end, {description = "Move client left tag", group = "tag"}), awful.key({ altkey, "Control", "Shift" }, "Right", function () local screen_cur = awful.screen.focused() local tag_orig = screen_cur.selected_tag local tag_next = tag_orig.index tag_next = tag_next + 1 if tag_next > #screen_cur.tags then tag_next = 1 end if client.focus then local tag = screen_cur.tags[tag_next] if tag then client.focus:move_to_tag(tag) end end for s in screen do if s ~= screen_cur then local tag = s.tags[tag_next] if tag then tag:view_only() end end end if screen_cur.tags[tag_next] then local tag = screen_cur.tags[tag_next] tag:view_only() end end, {description = "Move client right tag", group = "tag"}) )
  • Go to tag number

    Bind numbers to tags (with modkey):

    [rc.lua]
    -- Bind all key numbers to tags. -- Be careful: we use keycodes to make it works on any keyboard layout. -- This should map on the top row of your keyboard, usually 1 to 9. for i = 1, 9 do globalkeys = awful.util.table.join(globalkeys, -- View tag only. awful.key({ modkey }, "#" .. i + 9, function () local screen = awful.screen.focused() local tag = screen.tags[i] if tag then tag:view_only() end end, {description = "view tag #"..i, group = "tag"}) ) end
  • Go to tag number on all screens with Ctrl + Alt

    Alternative binding to go to a tag by number on all screens with Ctrl + Alt:

    [rc.lua]
    for i = 1, 9 do globalkeys = awful.util.table.join(globalkeys, -- View tag for all screens. awful.key({ altkey, "Control" }, "#" .. i + 9, function () for s in screen do local tag = s.tags[i] if tag then tag:view_only() end end end, {description = "toggle tag #" .. i, group = "tag"}) ) end
  • Change tag on other screen

    Move the other screen to a specific tag number (assumes two screens) using modkey + Alt and a tag number:

    [rc.lua]
    for i = 1, 9 do globalkeys = awful.util.table.join(globalkeys, -- View tag for other screen. awful.key({ modkey, altkey }, "#" .. i + 9, function () local screen_cur = awful.screen.focused() for s in screen do if s ~= screen_cur then local tag = s.tags[i] if tag then tag:view_only() end end end end, {description = "Change tag for other screen", group = "tag"}) ) end
  • Move window to tag

    Move the current client (window) to tag by number:

    [rc.lua]
    for i = 1, 9 do globalkeys = awful.util.table.join(globalkeys, -- Move client to tag. awful.key({ modkey, "Shift" }, "#" .. i + 9, function () if client.focus then local tag = client.focus.screen.tags[i] if tag then client.focus:move_to_tag(tag) end end end, {description = "move focused client to tag #"..i, group = "tag"}) ) end
  • Select tag

    Activate other tag on the current one:

    [rc.lua]
    for i = 1, 9 do globalkeys = awful.util.table.join(globalkeys, -- Toggle tag. awful.key({ modkey, "Control" }, "#" .. i + 9, function () local screen = awful.screen.focused() local tag = screen.tags[i] if tag then awful.tag.viewtoggle(tag) end end, {description = "toggle focused client on tag #" .. i, group = "tag"}) ) end
  • Show current tag on other tag

    Activate current tag on another one:

    [rc.lua]
    for i = 1, 9 do globalkeys = awful.util.table.join(globalkeys, -- Toggle tag. awful.key({ modkey, "Control", "Shift" }, "#" .. i + 9, function () if client.focus then local tag = client.focus.screen.tags[i] if tag then client.focus:toggle_tag(tag) end end end) ) end
  • Go to other screen

    Move to previous / next screen with modkey + Ctrl + j / k:

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, "Control" }, "j", function () awful.screen.focus_relative( 1) end, {description = "focus the next screen", group = "screen"}), awful.key({ modkey, "Control" }, "k", function () awful.screen.focus_relative(-1) end, {description = "focus the previous screen", group = "screen"}) )
  • Go to other screen (alternative)

    Use modkey + Ctrl + (Shift) TAB:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, "Control" }, "Tab", function () awful.screen.focus_relative(-1) end, {description = "focus the next screen", group = "screen"}), awful.key({ modkey, "Control", "Shift" }, "Tab", function () awful.screen.focus_relative(1) end, {description = "focus the previous screen", group = "screen"}) )
Clients (windows)

This section contains keybindings to operate on clients (windows).

  • Go to previous / next window

    Move to the previous / next window on the current screen using modkey + j / k.

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, -- Clients awful.key({ modkey, }, "j", function () awful.client.focus.byidx( 1) end, {description = "focus next by index", group = "client"}), awful.key({ modkey, }, "k", function () awful.client.focus.byidx(-1) end, {description = "focus prev by index", group = "client"}) )
  • Jump to last window

    Go to the tag with the last focused window:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, }, "Escape", awful.tag.history.restore, {description = "go back", group = "tag"}) )
  • Jump to urgent window

    Jump to window with urgent flag:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, }, "u", awful.client.urgent.jumpto, {description = "jump to urgent client", group = "client"}) )
  • Browse window history

    Loop through window history on the current tag and screen (like Alt + TAB):

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, }, "Tab", function () awful.client.focus.history.previous() if client.focus then client.focus:raise() end end, {description = "go back", group = "client"}) )
  • Restore window

    Restore minimized window:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, "Control" }, "n", function () local c = awful.client.restore() -- Focus restored client if c then client.focus = c c:raise() end end, {description = "restore minimized", group = "client"}) )
Layout

This sections contains keybindings related to layout manipulations.

  • Move window within layout

    Swap window with previous / next in current layout using modkey + shift + j / k.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, -- Layout manipulation awful.key({ modkey, "Shift" }, "j", function () awful.client.swap.byidx( 1) end, {description = "swap with next client by index", group = "client"}), awful.key({ modkey, "Shift" }, "k", function () awful.client.swap.byidx( -1) end, {description = "swap with previousclient by index", group = "client"}) )
  • Resize layout

    Control client size within layout:

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, }, "l", function () awful.tag.incmwfact( 0.05) end, {description = "increase master width factor", group = "layout"}), awful.key({ modkey, }, "h", function () awful.tag.incmwfact(-0.05) end, {description = "decrease master width factor", group = "layout"}), awful.key({ modkey, "Shift" }, "h", function () awful.tag.incnmaster( 1) end, {description = "increase the number of master clients", group = "layout"}), awful.key({ modkey, "Shift" }, "l", function () awful.tag.incnmaster(-1) end, {description = "decrease the number of master clients", group = "layout"}), awful.key({ modkey, "Control" }, "h", function () awful.tag.incncol( 1) end, {description = "increase the number of columns", group = "layout"}), awful.key({ modkey, "Control" }, "l", function () awful.tag.incncol(-1) end, {description = "decrease the number of columns", group = "layout"}) )
  • Change layout

    Cycle through available layouts using modkey + space.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, }, "space", function () awful.layout.inc( 1) end, {description = "select next", group = "layout"}), awful.key({ modkey, "Shift" }, "space", function () awful.layout.inc(-1) end, {description = "select previous", group = "layout"}) )
Application shortcuts

Keybindings for frequently used applications.

  • Toggle main menu

    Toggle main menu:

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, }, "w", function () mymainmenu:show() end, {description = "show main menu", group = "awesome"}) )
  • Terminal

    Open a terminal with modkey + return.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, }, "Return", function () awful.spawn(terminal) end, { description = "open a terminal", group = "launcher" }) )
  • Internet browser

    Start the web browser with modkey + ctrl + return.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, "Control" }, "Return", function () awful.spawn(internet_browser) end, { description = "internet browser", group = "launcher" }) )
  • Conkeror

    Start conkeror, an alternative web browser using modkey + ctrl + shift + return.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, "Control", "Shift" }, "Return", function () awful.spawn("conkeror") end, { description = "conkeror browser", group = "launcher" }) )
  • Emacs

    Start emacs with modkey + shift + return.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, "Shift" }, "Return", function () awful.spawn(emacs) end, { description = "start emacs", group = "launcher" }) )
  • File explorer

    Start the file explorer software with modkey + e.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey, }, "e", function () awful.spawn.with_shell(explorer .. " ~") end, { description = "open file explorer", group = "launcher" }) )
  • Awesome restart

    Restart awesome-wm (reload rc.lua) with modkey + ctrl + r.

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, "Control" }, "r", awesome.restart, {description = "reload awesome", group = "awesome"}) )
  • Logout

    Log out with modkey + shift + q.

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, "Shift" }, "q", awesome.quit, {description = "quit awesome", group = "awesome"}) )
Prompts

This section contains keybindings for prompts.

  • Shell execution

    Execute arbitrary shell commands with modkey + r.

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, -- Prompt awful.key({ modkey }, "r", function () awful.screen.focused().mypromptbox:run() end, {description = "run prompt", group = "launcher"}) )
  • Lua execution

    Execute lua code with modkey + x.

    [rc.lua]
    globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey }, "x", function () awful.prompt.run { prompt = "Run Lua code: ", textbox = awful.screen.focused().mypromptbox.widget, exe_callback = awful.util.eval, history_path = awful.util.get_cache_dir() .. "/history_eval" } end, {description = "lua execute prompt", group = "awesome"}) )
  • Global prompt

    Toggle the custom global prompt with modkey + z.

    [rc.lua]
    globalkeys = awful.util.table.join( globalkeys, awful.key({ modkey }, "z", function () awful.screen.focused().mywibox_globalprompt.visible = true awful.prompt.run { prompt = "► ", textbox = awful.screen.focused().myglobalpromptbox.widget, exe_callback = global_prompt.run, completion_callback = global_prompt.comp, history_path = awful.util.getdir("cache") .. "/history_global", history_max = 50, done_callback = function () for s in screen do s.mywibox_globalprompt.visible = false end end } end, {description = "Global prompt", group = "launcher"}) )
Menubar

Start the menubar with modkey + a.

[rc.lua]
-- Menubar globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey }, "p", function() menubar.show() end, {description = "show the menubar", group = "launcher"}) )
Volume control

Use special keyboard keys to control the audio volume:

[rc.lua]
-- Volume globalkeys = awful.util.table.join( globalkeys, awful.key({ }, "XF86AudioRaiseVolume", function () awful.spawn(awesome_paths.config_dir .. "/scripts/osdvol.sh volup") update_volume_widget() end), awful.key({ }, "XF86AudioLowerVolume", function () awful.spawn(awesome_paths.config_dir .. "/scripts/osdvol.sh voldown") update_volume_widget() end), awful.key({ }, "XF86AudioMute", function () awful.spawn(awesome_paths.config_dir .. "/scripts/osdvol.sh mute") update_volume_widget() end) )
Screen lock

Lock the screen using xscreensaver with modkey + ctrl + l.

[rc.lua]
-- Screen lock globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, "Control" }, "l", function () awful.spawn("xscreensaver-command -lock") end, {description = "Lock screen", group = "awesome"}) )
Password manager

I use the pass utility to manage passwords. It comes with a dmenu tool (passmenu) for inserting passwords (or copying them to the clipboard). The following is a slightly modified version of the passmenu script. The original script returns the first line for each password entry; instead, the following script reads the line preceding the first empty line.

[scripts/passmenu.sh]
#!/usr/bin/env bash # Add `--type` to type the password using xdotool shopt -s nullglob globstar # If set to true the password name must be passed via the command line (after # --type) flag_debug=false # Type or copy to clipboard typeit=0 if [[ $1 == "--type" ]]; then typeit=1 shift fi # Build list of passwords for dmenu prefix=${PASSWORD_STORE_DIR-~/.password-store} password_files=( "$prefix"/**/*.gpg ) password_files=( "${password_files[@]#"$prefix"/}" ) password_files=( "${password_files[@]%.gpg}" ) # Read password name from command line if used in debug mode if [[ "$flag_debug" = true ]]; then password=$1 else password=$(printf '%s\n' "${password_files[@]}" | dmenu "$@") fi [[ -n $password ]] || exit # Get password pass_full=$(pass show "$password") # Keep line before first empty line pass=$(echo "$pass_full" | awk '{if ($0 ~ /^$/){ exit; } else { a=$0; }} END {print a}') # Print, type or store password if [[ "$flag_debug" = true ]]; then echo "type=$typeit, $pass" else if [[ $typeit -eq 0 ]]; then echo $pass | xclip -selection c else echo $pass | xdotool type --clearmodifiers --file - fi fi

The following adds a keybinding to start the custom passmenu command. To use it, place the cursor on a password field (e.g. in a terminal or web page) and press the keybinding. Then use a fuzzy text search on the spawned dmenu. This will insert the corresponding password.

[rc.lua]
-- Screen lock globalkeys = awful.util.table.join(globalkeys, awful.key({ modkey, "Control" }, "p", function () awful.spawn(passmenu) end, {description = "Insert password", group = "awesome"}) )
Register keybindings

Register the keybindings defined in the globalkeys table:

[rc.lua]
-- Set keys root.keys(globalkeys)

Client bindings

The keybindings defined in this section control individual windows.

[rc.lua]
clientkeys = {}
Fullscreen

Get the current window in fullscreen with modkey + f.

[rc.lua]
clientkeys = awful.util.table.join( clientkeys, awful.key({ modkey, }, "f", function (c) c.fullscreen = not c.fullscreen end) )
Kill window

Kill current window with modkey + shift + c.

[rc.lua]
clientkeys = awful.util.table.join(clientkeys, awful.key({ modkey, "Shift" }, "c", function (c) c:kill() end) )
Toggle floating window mode

Set window to floating mode (does not use layout) with modkey + ctrl + space.

[rc.lua]
clientkeys = awful.util.table.join(clientkeys, awful.key({ modkey, "Control" }, "space", awful.client.floating.toggle ) )
Move window to master position

Swap window with window in the current layout's master position with modkey + ctrl + return. Note that this is currently overridden by the web browser shortcut.

[rc.lua]
clientkeys = awful.util.table.join( clientkeys, awful.key({ modkey, "Control" }, "Return", function (c) c:swap(awful.client.getmaster()) end) )
Move window to other screen

Send current window to other screen with modkey + o.

[rc.lua]
clientkeys = awful.util.table.join( clientkeys, awful.key({ modkey, }, "o", function (c) c:move_to_screen(c.screen.index - 1) end) )
Toggle on-top property

Toggle on-top flag (useful for floating windows) with modkey + t.

[rc.lua]
clientkeys = awful.util.table.join(clientkeys, awful.key({ modkey, }, "t", function (c) c.ontop = not c.ontop end) )
Minimize window

Minimize the current window with modkey + n.

[rc.lua]
clientkeys = awful.util.table.join(clientkeys, 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) )
Toggle maximized mode

Toggle maximize property for the current window with modkey + m.

[rc.lua]
clientkeys = awful.util.table.join(clientkeys, awful.key({ modkey, }, "m", function (c) c.maximized_horizontal = not c.maximized_horizontal c.maximized_vertical = not c.maximized_vertical end) )
Show window information

Show a popup notification with information on the current window (class, properties, etc.). Triggered by pressing modkey + i.

[rc.lua]
clientkeys = awful.util.table.join(clientkeys, awful.key({ modkey, }, "i", function (c) local geom = c:geometry() local t = "" if c.class then t = t .. "Class: " .. c.class .. "\n" end if c.instance then t = t .. "Instance: " .. c.instance .. "\n" end if c.role then t = t .. "Role: " .. c.role .. "\n" end if c.name then t = t .. "Name: " .. c.name .. "\n" end if c.type then t = t .. "Type: " .. c.type .. "\n" end if c.fullscreen then t = t .. "Fullscreen; yes\n" end if c.maximized_horizontal then t = t .. "Maximized Horizontal: yes\n" end if c.maximized_vertical then t = t .. "Maximized Vertical: yes\n" end if c.above then t = t .. "Above: yes\n" end if geom.width and geom.height and geom.x and geom.y then t = t .. "Dimensions: " .. "x:" .. geom.x .. " y:" .. geom.y .. " w:" .. geom.width .. " h:" .. geom.height end naughty.notify({ text = t, timeout = 5, }) end) ) -- }}}

Wibox

The wibox is the top bar shown on Fig. 1. It is populated with widgets previously set up, including the main menu, tag list, task list and the custom music / email / news widgets.

A wibox is created on each screen; the only difference between the screens is that only a single instance of the system tray widget can be added (it is added on screen 1 for my configuration).

[rc.lua]
-- {{ Wibox -- Create a wibox for each screen and add it awful.screen.connect_for_each_screen(function(s)

In the following sections, s is the current screen index.

Main prompt box

The prompts defined in the key bindings section toggle the visibility of a text widget on the wibox for user input. This text widget is defined here:

[rc.lua]
-- Create a promptbox for each screen s.mypromptbox = awful.widget.prompt() s.mypromptbox.font = "Consolas 18" -- Confirmation prompt s.mypromptbox_conf = awful.widget.prompt() -- Global prompt s.myglobalpromptbox = awful.widget.prompt() s.mywibox_globalprompt = awful.wibox({ position = "bottom", screen = s, font = "Consolas 18" }) s.mywibox_globalprompt:setup { layout = wibox.layout.align.horizontal, s.myglobalpromptbox } s.mywibox_globalprompt.visible = false

Launchers

Create launchers.

[rc.lua]
-- Create launchers s.mylaunchbar = launchbar.new(awesome_paths.config_dir .. "/shortcuts/", myiconfinder)

Wibox

This section defines the main wibox, which can be seen at the top in Fig. 1. It contains all the widgets and is defined as follows:

[rc.lua]
-- Create the wibox s.mywibox = awful.wibox({ position = "top", screen = s }) -- Create an imagebox widget which will contains an icon indicating which -- layout we're using. We need one layoutbox per screen. s.mylayoutbox = awful.widget.layoutbox(s) s.mylayoutbox:buttons( awful.util.table.join( awful.button({ }, 1, function () awful.layout.inc( 1) end), awful.button({ }, 3, function () awful.layout.inc(-1) end), awful.button({ }, 4, function () awful.layout.inc( 1) end), awful.button({ }, 5, function () awful.layout.inc(-1) end))) -- Create a tasklist widget s.mytasklist = awful.widget.tasklist( s, awful.widget.tasklist.filter.currenttags, tasklist_buttons) -- Create a taglist widget s.mytaglist = awful.widget.taglist(s, awful.widget.taglist.filter.all, taglist_buttons) -- Add widgets to the wibox s.mywibox:setup { layout = wibox.layout.align.horizontal, { -- Left widgets layout = wibox.layout.fixed.horizontal, mylauncher, s.mypromptbox_conf, s.mytaglist, s.mylaunchbar, s.mypromptbox }, s.mytasklist, -- Middle widget { -- Right widgets layout = wibox.layout.fixed.horizontal, tb_cmus, separator, mailicon, separator, tb_ttrss, separator, memory_label, memory_widget, separator, volume_master, separator, mykeyboardlayout, wibox.widget.systray(), mytextclock, --calendar, s.mylayoutbox, myleave_launcher }, }

The wibox is decomposed into three sections: left, center and right. Widgets are added to each section and then concatenated into a single layout which is in turn added to the wibox.

Close screen for loop

This code block closes the for loop creating a wibox on each screen.

[rc.lua]
end) -- }}}

Rules

Awesome allows the definition of rules for program window management based on their class or instance name. If the link is still up, this page contains some basic description of rules.

Only a few simple rules are defined here:

  • Set of few programs as "floating": mplayer, pinentry, gimp.
  • Fix cmus, newsboat on the second tag of the second screen.
  • Thunderbird goes on the first tag of the second screen.
  • I had issues with ristretto starting in a maximized state; I am not sure whether it is necessary, but I am explicitly disabling the "maximized" option here.
[rc.lua]
-- {{{ Rules -- Rules to apply to new clients (through the "manage" signal). awful.rules.rules = { -- All clients will match this rule. { rule = { }, properties = { border_width = beautiful.border_width, border_color = beautiful.border_normal, focus = awful.client.focus.filter, raise = true, keys = clientkeys, buttons = clientbuttons, screen = awful.screen.preferred } }, -- Floating clients. { rule_any = { instance = { "DTA", -- Firefox addon DownThemAll. "copyq", -- Includes session name in class. }, class = { "Arandr", "Gpick", "Kruler", "MessageWin", -- kalarm. "Sxiv", "Wpa_gui", "pinentry", "veromix", "xtightvncviewer", "MPlayer", "gimp", "plugin-container"}, name = { "Event Tester", -- xev. }, role = { "AlarmWindow", -- Thunderbird's calendar. "pop-up", -- e.g. Google Chrome's (detached) Developer Tools. } }, properties = { floating = true }}, -- Add titlebars to normal clients and dialogs --{ rule_any = {type = { "normal", "dialog" } -- }, properties = { titlebars_enabled = true } --}, -- Set Firefox to always map on the tag named "2" on screen 1. -- { rule = { class = "Firefox" }, -- properties = { screen = 1, tag = "2" } }, { rule = { instance = "cmus" }, properties = { screen = 2, tag = "2" } }, { rule = { instance = "tmplayer" }, properties = { screen = 2, tag = "2" } }, { rule = { instance = "newsboat" }, properties = { screen = 2, tag = "2" } }, { rule = { class = "Thunderbird" }, properties = { screen = 2, tag = "@" } }, { rule = { instance = "ristretto" }, properties = { maximized = false } }, } -- }}}

Signals

This is default code setting up focus following the mouse, titlebars (which are disabled) and border coloring.

[rc.lua]
-- {{{ Signals -- Signal function to execute when a new client appears. client.connect_signal("manage", function (c) -- Set the windows at the slave, -- i.e. put it at the end of others instead of setting it master. -- if not awesome.startup then awful.client.setslave(c) end if awesome.startup and not c.size_hints.user_position and not c.size_hints.program_position then -- Prevent clients from being unreachable after screen count changes. awful.placement.no_offscreen(c) end end) -- Add a titlebar if titlebars_enabled is set to true in the rules. client.connect_signal("request::titlebars", function(c) -- buttons for the titlebar local buttons = awful.util.table.join( awful.button({ }, 1, function() client.focus = c c:raise() awful.mouse.client.move(c) end), awful.button({ }, 3, function() client.focus = c c:raise() awful.mouse.client.resize(c) end) ) awful.titlebar(c) : setup { { -- Left awful.titlebar.widget.iconwidget(c), buttons = buttons, layout = wibox.layout.fixed.horizontal }, { -- Middle { -- Title align = "center", widget = awful.titlebar.widget.titlewidget(c) }, buttons = buttons, layout = wibox.layout.flex.horizontal }, { -- Right awful.titlebar.widget.floatingbutton (c), awful.titlebar.widget.maximizedbutton(c), awful.titlebar.widget.stickybutton (c), awful.titlebar.widget.ontopbutton (c), awful.titlebar.widget.closebutton (c), layout = wibox.layout.fixed.horizontal() }, layout = wibox.layout.align.horizontal } end) -- Enable sloppy focus, so that focus follows mouse. client.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) -- }}}

Icon finder

This is a module used to attempt to automatically find icons for programs. It is used by the launchbar module, and menus (main menu and shutdown menu). It searches for icons in folders defined in the awesome_paths.iconapps_dir variable (png and svg files).

[icon_finder.lua]
local util = require("awful.util") local beautiful = require("beautiful") local lfs = require("lfs") local io = io local string = string local table = table local ipairs = ipairs local tonumber = tonumber local icon_finder = {} icon_finder.__index = icon_finder function icon_finder.new(icondirs_list) local self = setmetatable({}, icon_finder) self.icondirs_list = icondirs_list self.subdir_list = { "", "actions", "animations", "apps", "categories", "devices", "emblems", "emotes", "mimetypes", "places", "special", "status" } self.ext_list = { "png", "svg", "xpm" } return self end function icon_finder:find(icon_name) if icon_name == nil then return nil end if icon_name and util.file_readable(icon_name) then return icon_name end wildcard = string.sub(icon_name, 1, 1) == '*' if wildcard then iname = string.sub(icon_name, 2, -1) else iname = icon_name end for _, ext in ipairs(self.ext_list) do iname = iname:gsub("%." .. ext .. "$", "") end if self.icondirs_list then for _, v_base in ipairs(self.icondirs_list) do for _, v_sub in ipairs(self.subdir_list) do v = v_base .. "/" .. v_sub .. "/" if wildcard then if lfs.attributes(v, "mode") == "directory" then for file in lfs.dir(v) do f_attr = lfs.attributes(v .. "/" .. file, "mode") if f_attr and f_attr == "file" then i = string.find(file, iname) if i then return v .. '/' .. file end end end end else for _, ext in ipairs(self.ext_list) do if util.file_readable(v .. "/" .. iname .. "." .. ext) then return v .. '/' .. iname .. "." .. ext end end end end end end -- Return default icon if missing return beautiful.awesome_icon end return setmetatable(icon_finder, { __call = function(cls, ...) return cls.new(...) end })

Global prompt

The default awesome-wm configuration provides two prompts: one to execute shell commands and one to execute lua code. The global_prompt module is a custom module aimed at simplifying the addition of prompts, similar to launchers such as gnome-do, kupfer for instance.

A few prompts are also defined in this module, more can be added as needed.

Imports

This module requires basic awesome-wm libraries, and file system tools.

[global_prompt.lua]
local awful = require("awful") local util = require("awful.util") local lfs = require("lfs") local runorraise = require("runorraise") local capi = { tag = tag, client = client, keygrabber = keygrabber, mouse = mouse, screen = screen } local io = io local naughty = require("naughty") local cmus = require("cmus") global_prompt = {}

Utilities

First, define some helper functions:

generic_completion_wrapper
build completion from a list of items.
table.val_to_str
convert value to string.
table.key_to_str
convert table key to string.
table.tostring
convert table to string.
[global_prompt.lua]
function generic_completion_wrapper(kw, list, str, cur_pos, ncomp) out_str, out_pos = awful.completion.generic(str, cur_pos, ncomp, list) out_str = kw .. " " .. out_str out_pos = out_pos + kw:len() + 1 return out_str, out_pos end function table.val_to_str ( v ) if "string" == type( v ) then v = string.gsub( v, "\n", "\\n" ) if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then return "'" .. v .. "'" end return '"' .. string.gsub(v,'"', '\\"' ) .. '"' else return "table" == type( v ) and table.tostring( v ) or tostring( v ) end end function table.key_to_str ( k ) if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then return k else return "[" .. table.val_to_str( k ) .. "]" end end function table.tostring( tbl ) local result, done = {}, {} for k, v in ipairs( tbl ) do table.insert( result, table.val_to_str( v ) ) done[ k ] = true end for k, v in pairs( tbl ) do if not done[ k ] then table.insert( result, table.key_to_str( k ) .. "=" .. table.val_to_str( v ) ) end end return "{" .. table.concat( result, "," ) .. "}" end

Prompts

The prompts included in the module are listed here. Prompts are called by entering a keyword, followed by some arguments in the promptbox (i.e. the user input is of the form KEYWORD ARGS). A prompt is defined as follows:

global_prompt.prompt_TEMPLATE = {
   keyword={"?", "h"}, -- list of strings
   help="Help message for prompt",
   run_function = function(kw, name)
      -- Execute code
   end,
   comp_function = function(kw, name, cur_pos, ncomp)
      return generic_completion_wrapper(kw, { "AUTOCOMPLETE-WORD-1",
                                              "AUTOCOMPLETE-WORD-2" },
                                        name, cur_pos, ncomp)
}

A prompt is a table with the following entries:

keyword
List of keywords for prompt.
help
Help message (for use with help prompt).
run_function
Function to run when calling the prompt. The name parameter is the ARGS string passed from the prompt (the user input is KEYWORD ARGS).
comp_function
Completion function which can be created using generic_completion_wrapper.

help

The help prompt prints a list of the available prompts, along with the corresponding help text as shown on Fig. 4. The keyword is h or ?.

[global_prompt.lua]
global_prompt.prompt_help = { keyword={"?", "h"}, help="Show a list of available commands", run_function = function(kw, name) help_str = "" for _, p in ipairs(global_prompt.prompt_list) do help_str = help_str .. "<b>[" for ki, key in ipairs(p["keyword"]) do if ki > 1 then help_str = help_str .. ", " end help_str = help_str .. key end help_str = help_str .. "]</b>: " .. p["help"] .. "\n" end naughty.notify( { text = "<b><u>Command list</u></b>\n" .. help_str } ) end, comp_function = nil }

The following script manually spawns the help box shown on Fig. 4 and performs a screenshot.

# Could get these from xrandr
res_x=1920
res_y=1080
screen_idx=0 # starts at 0

req_str="local global_prompt = require(\"global_prompt\");"
cmd_str="global_prompt.prompt_help.run_function()"

echo "$req_str $cmd_str" | awesome-client

width=$(( 1 * $res_x / 6 ))
height=160
start_x=$(( $res_x * $screen_idx + $res_x - $width ))

sleep 0.5

scrot desktop.png
convert desktop.png -crop ${width}x${height}+${start_x}+0 desktop-global_prompt-help.png
rm desktop.png -rf

desktop-global-prompt-help.png

Figure 4: Help notification from global prompt.

session

The session prompt offers session management commands (shutdown, restart, logout, lock), similar to the shutdown menu. The keyword is s, so s shutdown will trigger a shutdown (with confirmation prompt).

[global_prompt.lua]
global_prompt.prompt_session = { keyword={"s"}, help="Session control (shutdown, restart, logout, lock)", run_function = function(kw, cmd) -- list of commands: mplayer -input cmdlist | more if cmd == "shutdown" then my_utility.confirm_action( function() awful.spawn('sudo /sbin/shutdown -h now') end, "Shutdown") elseif cmd == "restart" then my_utility.confirm_action( function() awful.spawn('sudo /sbin/shutdown -r now') end, "Restart") elseif cmd == "logout" then my_utility.confirm_action( function() awesome.quit() end, "Logout") elseif cmd == "lock" then awful.spawn('xscreensaver-command -lock') end end, comp_function = function(kw, name, cur_pos, ncomp) return generic_completion_wrapper(kw, { "shutdown", "restart", "logout", "lock" }, name, cur_pos, ncomp) end }

window

The window prompt allows to quickly jump to an existing window. It lists open windows by their class and name and compares the argument to the listed class / title pairs. Tab completion is also implemented.

[global_prompt.lua]
global_prompt.prompt_window = { keyword={"j"}, help="Jump to window", run_function = function(kw, cmd) cmd_l = string.lower(cmd) client_list = {} for s = 1, screen.count() do client_list_s = capi.client.get(s) for _, c in ipairs(client_list_s) do if cmd_l == string.lower(c.class .. ": " .. c.name) then awful.client.jumpto(c) return end table.insert(client_list, {c, c.class .. ": " .. c.name}) end end for _, c in ipairs(client_list) do if string.lower(c[1].name):find(cmd_l) or string.lower(c[1].class):find(cmd_l) then awful.client.jumpto(c[1]) return end end end, comp_function = function(kw, name, cur_pos, ncomp) client_list = {} for s = 1, screen.count() do client_list_s = capi.client.get(s) for _, c in ipairs(client_list_s) do table.insert(client_list, string.lower(c.class .. ": " .. c.name)) end end return generic_completion_wrapper(kw, client_list, name, cur_pos, ncomp) end }

calc

The calculator prompt uses a python script to perform calculations. It allows simple math operations including common functions (e.g. trigonometric, logarithmic etc.).

Internal script

The calculator script was adapted from this one.

[scripts/calc.py]
#!/usr/bin/python3 """ A simple calculator """ # %% Imports import sys import math # %% Functions # http://www.peterbe.com/plog/calculator-in-python-for-dummies def calc(expr, advanced=True): def safe_eval(expr, symbols={}): return eval(expr, dict(__builtins__=None), symbols) def whole_number_to_float(match): group = match.group() if group.find('.') == -1: return group + '.0' return group expr = expr.replace('^','**') # expr = integers_regex.sub(whole_number_to_float, expr) if advanced: return safe_eval(expr, vars(math)) else: return safe_eval(expr) # %% main if __name__ == '__main__': print(calc(' '.join(sys.argv[1:])))
Prompt definition

The definition of the prompt is straightforward: the arguments are wrapped around double quotes and passed to the calc.py script. The result is shown in a popup notification.

[global_prompt.lua]
global_prompt.calc_cmd = "python3 " .. awesome_paths.config_dir .. "/scripts/calc.py " global_prompt.prompt_calc = { keyword={"c"}, help="Calculator", run_function = function(kw, expr) expr = expr:match("^%s*(.-)%s*$") if expr.sub(1, 1) ~= "\"" then expr = "\"" .. expr end if expr.sub(-1, 1) ~= "\"" then expr = expr .. "\"" end local c = assert(io.popen(global_prompt.calc_cmd .. " " .. expr, 'r')) local result = c:read("*all"):gsub("\n$", "") c:close() naughty.notify({ text = string.sub(expr, 2, -2) .. " = " .. result, timeout = 10 }) end, comp_function = nil }

cmus

The cmus prompt is an interface to the cmus quick play command. The supported arguments are:

play/pause/stop/next/prev
control playback,
info
spawn the cmus popup notification,
random
play a random album,
album
play an album with title matching the remaining prompt arguments.
[global_prompt.lua]
global_prompt.prompt_cmus = { keyword={"cm"}, help="cmus remote", run_function = function(kw, cmd) -- list of commands: mplayer -input cmdlist | more mp_cmd = nil mp_cmds = nil if cmd == "pause" or cmd == "p" then mp_cmd = "--pause" elseif cmd == "play" then mp_cmd = "--play" elseif cmd == "next" then mp_cmd = "--next" elseif cmd == "prev" then mp_cmd = "--prev" elseif cmd == "stop" then mp_cmd = "--stop" elseif cmd == "info" or cmd == "i" then cmus_popup(5) elseif cmd == "random" then cmus_play("rand") elseif string.sub(cmd, 1, 5) == "album" then album_name = string.sub(cmd, 6, -1):match("^%s*(.-)%s*$") search_str = album_name search_str = search_str:gsub("[ ]+", ".*") cmus_play("album", search_str) else mp_cmd = cmd end if mp_cmd then cmd = "cmus-remote " .. mp_cmd awful.spawn.with_shell(cmd) end end, comp_function = function(kw, name, cur_pos, ncomp) return generic_completion_wrapper(kw, { "pause", "play", "next", "prev", "random", "album", "info" }, name, cur_pos, ncomp) end }

music

The music prompt plays individual music files in mplayer, as opposed to the cmus tools, which play albums using cmus. The music folder is indexed into the music.db file using the updatedb program, and song searches are performed using the locate program (both parts of the findutils package). The music.db file is updated on a schedule (daily) via crontab:

# Crontab entry for music database update
0 9 * * * updatedb --require-visibility 0 -U ~/data/music/ -o ~/data/music/music.db > ~/log/update_musicdb.log

The music prompt defines two keywords:

m
play an audio file (with file format in the list defined by global_prompt.music_ext_list). The extra argument is either a full path to a file name in which case the file is played or a search string which is used to interrogate the music.db database and obtain a file to play.
mc
control the existing mplayer instance via a named pipe constructed using mkfifo. Available commands are pause (or p) and quit (or q).

Note that the spawned mplayer instance is given a custom class name (-c tmplayer) to apply a custom rule to it: it is placed on the same screen and client as the main cmus instance.

The completion function for the m keyword performs a search on the argument and returns the list of matching files in the music.db database. Completion for the mc keyword simply lists the available commands.

[global_prompt.lua]
global_prompt.mplayer_pipe = "/tmp/awesome-mplayer.pipe" global_prompt.music_folder = "/media/data/music/" global_prompt.music_locate_db = global_prompt.music_folder .. "music.db" global_prompt.music_ext_list = { ".mp3", ".flac", ".ogg", ".aac" } global_prompt.prompt_music = { keyword={"m", "mc"}, help="Open music files using locate db", run_function=function(kw, name) if kw == "m" then fname = nil if not name then return end if util.file_readable(global_prompt.music_folder .. "/" .. name) then fname = global_prompt.music_folder .. "/" .. name else locate_file = global_prompt.music_locate_db search_str = name search_str = search_str:gsub("['\"?!-:,]", ".") search_str = search_str:gsub("[ ]+", ".*") for _, ext in ipairs(global_prompt.music_ext_list) do local m = "locate -d " .. locate_file .. " -i --limit 1 -r \"" .. search_str .. ".*\\" .. ext .. "$\"" local c = assert(io.popen(m, 'r')) if c then local mfile = c:read("*line") fname = mfile c:close() break end end end if fname then if music_player == "mplayer" then if not lfs.attributes(global_prompt.mplayer_pipe, "mode") then awful.spawn("mkfifo " .. global_prompt.mplayer_pipe) end extra_args = " -slave -input file=" .. global_prompt.mplayer_pipe .. " " else extra_args = "" end awful.spawn(termapps .. " -name tmplayer -e bash -c \"" .. music_player .. extra_args .. " \\\"" .. fname .. "\\\"\"") else naughty.notify( { text = name .. " not found (" .. search_str .. ")" } ) end elseif kw == "mc" then -- list of commands: mplayer -input cmdlist | more if lfs.attributes(global_prompt.mplayer_pipe, "mode") == "named pipe" then if name == "pause" or name == "p" then mp_cmd = "pause" elseif name == "quit" or name == "q" then mp_cmd = "quit" else mp_cmd = name end cmd = "echo \"" .. mp_cmd .. "\" > " .. global_prompt.mplayer_pipe awful.spawn.with_shell(cmd) end end end, comp_function=function(kw, name, cur_pos, ncomp) if #name == 0 then return name, cur_pos end if kw == "m" then local file_list = {} search_str = name search_str = search_str:gsub("['\"?!-:,]", ".") search_str = search_str:gsub("[ ]+", ".*") local m = "locate -d " .. global_prompt.music_locate_db .. " -i -r " .. search_str local c = assert(io.popen(m, 'r')) if c then local mfiles = c:read("*all") c:close() for line in mfiles:gmatch("[^\r\n]+") do line_s = line:gsub(global_prompt.music_folder, "") if line_s then for _, ext in ipairs(global_prompt.music_ext_list) do if line_s:find(ext .. "$") then table.insert(file_list, line_s) end end end end else io.stderr:write(err) return kw .. ' ' .. name, cur_pos end if #file_list == 0 then return kw .. ' ' .. name, cur_pos end while ncomp > #file_list do ncomp = ncomp - #file_list end return "m " .. file_list[ncomp], 3 elseif kw == "mc" then return generic_completion_wrapper(kw, { "pause", "quit", "volume" }, name, cur_pos, ncomp) end end }

web   feature_request

The web prompt was originally aimed at handling questions where the answer would be obtained from a web search (e.g. using duckduckgo search API). This is currently not implemented and the web prompt simply spawns a web browser window, and starts a duckduckgo search with the input arguments.

[global_prompt.lua]
global_prompt.search_url = "https://duckduckgo.com/?q=%s" global_prompt.prompt_web = { keyword={"w"}, help="Web search", run_function = function(kw, expr) is_url = string.find(expr, "[.][0-9a-zA-Z]+$") naughty.notify({text=is_url}) if is_url == nil then expr_search = string.gsub(expr, "%s+", "+") expr_url = string.format(global_prompt.search_url, expr_search) else expr_url = expr end awful.spawn.with_shell(internet_browser .. " " .. expr_url) end, comp_function = nil }

Main prompt

Once the prompts are defined they are added to the prompt_list table. The final part of the module defines the main hook handling user input. The first word in the user input is matched against known keywords, and, if a matching prompt keyword is found, the remaining arguments are passed to the corresponding run_function and comp_function to handle ENTER and TAB key presses respectively.

[global_prompt.lua]
global_prompt.prompt_list = { global_prompt.prompt_help, global_prompt.prompt_music, global_prompt.prompt_calc, global_prompt.prompt_cmus, global_prompt.prompt_session, global_prompt.prompt_window, global_prompt.prompt_web } function global_prompt.run(str) str_stripped = str:match("^%s*(.-)%s*$") if not str_stripped:match("%s") then kw = str_stripped args = "" else kw = str:match("^%s*([^ ]+) .*$") args = str:match("^%s*[^ ]+ (.*)$"):match("^%s*(.-)%s*$") end for _, p in ipairs(global_prompt.prompt_list) do for _, key in ipairs(p["keyword"]) do if key == kw then return p["run_function"](kw, args) end end end end function global_prompt.comp(str, cur_pos, ncomp) str_stripped = str:match("^%s*(.-)%s*$") if not str_stripped:match("%s") then kw = str_stripped args = "" else kw = str:match("^%s*([^ ]+) .*$") args = str:match("^%s*[^ ]+ (.*)$") if args then args = args:match("^%s*(.-)%s*$") else args = "" end end for _, p in ipairs(global_prompt.prompt_list) do for _, key in ipairs(p["keyword"]) do if key == kw and p["comp_function"] then return p["comp_function"](kw, args, cur_pos, ncomp) end end end return generic_completion_wrapper("", {}, str, cur_pos, ncomp) end return global_prompt -- TODO: -- X web search (parse answer / open in (text) browseer) -- duckduckgo API -- translator -- X cmus control (random album?) -- mu4e control? -- X Goto window -- X mplayer: start in slave, control -- (http://ubuntuforums.org/showthread.php?t=1629000)

Startup applications

The end of the rc.lua file contains a list of applications to run on startup. Programs are spawned using the run_once utility. The applications launched on startup are:

  • screen_default.sh which is a xrandr script to setup my monitors
  • numlock& which switches the numlock key on
  • the dropbox daemon
  • the network-manager applet
  • xscreensaver for screen locking
  • the UPnP server rygel
  • the RSS reader newsboat
  • the music player cmus
[rc.lua]
my_utility.run_once("numlockx&") my_utility.run_once("bash " .. awesome_paths.config_dir .. "/scripts/screen_default.sh") my_utility.run_once(awesome_paths.home_dir .. "/.dropbox-dist/dropboxd") my_utility.run_once("nm-applet") -- networking my_utility.run_once("xscreensaver -nosplash") my_utility.run_once("rygel") -- Compositing (required for transparency) --my_utility.run_once("xcompmgr") my_utility.run_once(termapps .. " -name newsboat -e newsboat") my_utility.run_once(termapps .. " -name cmus -e cmus") --run_or_raise(terminal) --my_utility.run_once("pidgin",nil,nil,2)

Screen setup script

The screen_default.sh script contains the following command (which should be changed according to the monitor setup):

[scripts/screen_default.sh]
#!/bin/sh # Setup screens xrandr --setprovideroutputsource 1 0 xrandr --output HDMI-1 --primary --mode 1920x1080 --pos 0x0 --rotate normal \ --output DP-1 --off \ --output VGA-1 --off \ --output VGA-1-2 --mode 1600x900 --pos 3840x0 --rotate normal \ --output HDMI-1-2 --mode 1920x1080 --pos 1920x0 --rotate normal \ --output DVI-I-1-1 --off

Utilities

The my_utility module contains some helper functions used throughout the configuration.

[my_utility.lua]
local awful = require("awful") local naughty = require("naughty") local beautiful = require("beautiful") my_utility = {}

String split

The my_utility.lines function splits the input string by rows. A table with the lines is returned; it can be iterated over using for k, v in pairs(lines_output). This was taken from http://lua-users.org/wiki/SplitJoin.

[my_utility.lua]
function my_utility.lines(str) local t = {} local function helper(line) table.insert(t, line) return "" end helper((str:gsub("(.-)\r?\n", helper))) return t end

HTML escape

Notifications in awesome-wm accept HTML inputs, but there is no built-in function to cleanup strings. The my_utility.html_escape function provides some very basic escaping.

[my_utility.lua]
function my_utility.html_escape(str) local t = str t = t:gsub("&", "&amp;") t = t:gsub("<", "&lt;") t = t:gsub(">", "&gt;") t = t:gsub("'", "&apos;") return t end function my_utility.filename_escape(str) local t = str t = t:gsub("[$]", "\\$") return t end

File listing

The scandir utility lists files in a given folder (from http://stackoverflow.com/a/11130774).

[my_utility.lua]
-- Lua implementation of scandir function function my_utility.scandir(directory, pattern) local i, t, popen = 0, {}, io.popen local f = assert(popen('ls -a "'..directory..'/"'..pattern, 'r')) for filename in f:lines() do i = i + 1 t[i] = string.gsub(filename, "//", "/") end f:close() return t end

Table operations

[my_utility.lua]
-- Remove paths from list of paths (based on last folder) function my_utility.table_path_remove(table_in, remove_list) for i = 1, #remove_list do tstring = remove_list[i] for j = 1, #table_in do mstring = string.sub(table_in[j], -#tstring - 1, -2) if tstring == mstring then table.remove(table_in, j) break end end end end

Confirmation popup

The my_utility.confirm_action is used with the shutdown menu to prompt for confirmation before performing actions (e.g. shutdown). It uses a promptbox placed on each wibox and runs the function func when the "y" character is entered in the promptbox. Note that, since the promptbox is barely visible, the color of the whole wibox (on the current screen) is temporarily set to beautiful.bg_urgent / beautiful.fg_urgent.

[my_utility.lua]
function my_utility.confirm_action(func, name) awful.screen.focused().mywibox:set_bg(beautiful.bg_urgent) awful.screen.focused().mywibox:set_fg(beautiful.fg_urgent) awful.prompt.run { prompt = name .. " [y/N] ", textbox = awful.screen.focused().mypromptbox_conf.widget, exe_callback = function (t) if string.lower(t) == 'y' then func() end end, history_path = nil, done_callback = function () awful.screen.focused().mywibox:set_bg( beautiful.screen_highlight_bg_active) awful.screen.focused().mywibox:set_fg( beautiful.screen_highlight_fg_active) end } end

Here is an example of usage using awesome-client, with a screenshot of the confirmation prompt on Fig. 5.

# Could get these from xrandr
res_x=1920
res_y=1080
screen_idx=1 # starts at 0

req_str="local my_utility = require(\"my_utility\");"
cmd_str="my_utility.confirm_action(function () end, \"Shutdown\")"

echo "$req_str $cmd_str" | awesome-client

width=$(( 3 * $res_x / 8 ))
height=25
start_x=$(( $res_x * $screen_idx ))

sleep 0.5

scrot desktop.png
convert desktop.png -crop ${width}x${height}+${start_x}+0 desktop-confirm_action.png
rm desktop.png -rf

desktop-confirm-action.png

Figure 5: Confirmation prompt before shutdown (left-most text).

Run once

The run_once utility is a commonly used lua function to spawn a program if is not already running. The code is from https://awesome.naquadah.org/wiki/Autostart.

[my_utility.lua]
function my_utility.run_once(prg, arg_string, pname, s, tag) if not prg then do return nil end end if not pname then pname = prg end if not arg_string then cmd_pgrep = pname cmd_exe = prg else cmd_pgrep = pname .. " " .. arg_string cmd_exe = prg .. " " .. arg_string end cmd = "pgrep -f -u $USER -x '" .. cmd_pgrep .. "' || (" .. cmd_exe .. ")" if s and tag then local out = assert(io.popen("pgrep -f -x '" .. cmd_pgrep .. "'")) local pid = out:read("*all") out:close() if pid == "" then awful.spawn(cmd_exe, { tag = screen[s].tags[tag] }) end else awful.spawn.with_shell(cmd) end end

Return object

The module is instantiated by local my_utility = require("my_utility") and functions are called using my_utility.function(...).

[my_utility.lua]
return my_utility

Resources