Awesome-wm configuration
Table of Contents
- Introduction
- Imports
- Basic setup
- Desktop
- Main bar
- Effects
- Bindings
- Wibox
- Rules
- Signals
- Icon finder
- Global prompt
- Startup applications
- Utilities
- Resources
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
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 thetop
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 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
andxdotool
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
andimagemagick
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 "<IP_ADDRESS>: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
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
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 aPosition
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}) }) -- }}
Main menu
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 ) -- }}}
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:
- Show the current play state, song artist, title and current position in the wibox widget.
- Toggle play / pause on click
- Go to cmus on right click
- 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).
- 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 nameYYYY-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
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 "<IP_ADDRESS>: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
) withmodkey
+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=<EMAIL_ADDRESS>) # 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 theARGS
string passed from the prompt (the user input isKEYWORD 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
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 themusic.db
database and obtain a file to play. mc
- control the existing mplayer instance via a named pipe constructed
using
mkfifo
. Available commands arepause
(orp
) andquit
(orq
).
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 axrandr
script to setup my monitorsnumlock&
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("&", "&") t = t:gsub("<", "<") t = t:gsub(">", ">") t = t:gsub("'", "'") 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
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
- awesome wiki: https://awesome.naquadah.org/wiki (this may disappear in the future as described in https://github.com/awesomeWM/awesome-www/issues/7)
- awesome-wm API: http://new.awesomewm.org/doc/api
- blingbling examples: https://github.com/cedlemo/blingbling
- Themes: