-- AwesomeWM Customizations -- by Mike Crute -- -- This file contains the customizations that I run for AwesomeWM. I prefer to -- stick as close to the stock rc.lua file as possible so it's easier to -- upgrade. Over time awesome has had a habit of making drastic -- non-backwards-compatible changes to that file. -- -- To understand some parts of this file it's useful to understand my runtime -- environment; I run both Fedora and Ubuntu depending on the context but -- always on a Dell XPS 13 (9370 and 9380). I don't run a traditional desktop -- environment nor do I run a settings daemon as they tend to be overly bloated -- and do much more than I care for so Awesome tends to subsume the important -- tasks (like display configuration) instead. I do depend heavily on systemd -- for user session task management. -- local awful = require("awful") local beautiful = require("beautiful") local dpi = require("beautiful.xresources").apply_dpi local gears = require("gears") local naughty = require("naughty") local wibox = require("wibox") local pomodoro = require("pomodoro") local capi = { mouse = mouse, } -- Require this here because we customize it here but also so rc.lua can use it -- transitively through this module. It makes our local customizations to -- rc.lua just a little easier to grep for. local battery = require("battery") local calendar = require("calendar") local large_clock = require("large_clock") local _cpu_widget = require("cpu-widget") local timezone = "America/Los_Angeles" local tz_file = os.getenv("HOME") .. "/.timezone" if gears.filesystem.file_readable(tz_file) then timezone = io.open(tz_file, "r"):read():gsub("^%s*(.-)%s*$", "%1") end -- EDIDs for simple displays that disable the internal screen and set -- the external display to auto resolution local simple_external_display_ids = { ["DELL U3219Q"] = true, -- 32" 4k Dell monitor ["49S405"] = true, -- 32" 4k Dell monitor ["LF32TU87"] = true, -- 32" 4k Samsung F32TU87x monitor ["DELL U3415W"] = true, -- 34" curved widescreen Dell monitors ["DELL U3417W"] = true, -- 34" curved widescreen Dell monitors ["GOOGLE JN32A"] = true, -- 32" Google Branded Monitor } -- EDIDs for internal displays that don't reconfigure themselves local internal_display_ids = { AUO_11048 = true, BOE_51207 = true, SHP_20757 = true, SDC_20289 = true, BOE_24329 = true, } -- Good clear zoom stops for the Dell XPS 13 93{7,8}0 local edp_zoom_modes_9380 = { -- "2880x1620", -- "2560x1440", -- "2048x1152", -- "1920x1080", -- "1600x900", -- "1368x768", "1280x720", "1024x576", "960x540", "864x486", "720x405", "640x360", } -- Good clear daily driver modes for the Dell XPS 13 93{7,8}0 local edp_display_modes_9380 = { "1600x900", "1920x1080", "2048x1152", "2560x1440", "2560x1600", "2880x1620", } local edp_display_modes_9320 = { "1280x800", "1400x900", "1680x1050", "1920x1200", "2048x1152", -- Clear but does not use whole screen "2560x1600", "3840x2400", } local edp_display_modes_9310 = { -- "3456x2160", "2560x1600", "1920x1200", "1680x1050", "1400x900", "1280x800", } local edp_display_modes_framework = { "2256x1504", } local default_edp_display_modes = { BOE_51207 = edp_display_modes_9380[3], -- Dell XPS 13 93{7,8}0 AUO_11048 = edp_display_modes_9380[3], -- X1 Carbon Display SHP_20757 = edp_display_modes_9380[5], -- Dell XPS 13 9320 SDC_20289 = edp_display_modes_9310[1], -- Dell XPS 13 9310 BOE_24329 = edp_display_modes_framework[1], -- Framework Laptop } local default_edp_zoom_modes = { BOE_51207 = edp_zoom_modes_9380, AUO_11048 = edp_zoom_modes_9380, SHP_20757 = edp_display_modes_9320, SDC_20289 = edp_display_modes_9310, BOE_24329 = edp_display_modes_framework, } -- Preferred daily driver mode -- This is populated by the get_default_edp_mode function, below local default_edp_mode = edp_display_modes_9380[3] local edp_zoom_modes = edp_zoom_modes_9380 -- dumps is a debugging tool that prints text representations of lua objects function dump(o) if type(o) == 'table' then local s = '{ ' for k,v in pairs(o) do if type(k) ~= 'number' then k = '"'..k..'"' end s = s .. '['..k..'] = ' .. dump(v) .. ',' end return s .. '} ' else return tostring(o) end end -- string_split splits strings on a delimiter and returns a table of the parts function string_split(s, delimiter) if s == nil then return nil end local result = {} for match in (s..delimiter):gmatch("(.-)"..delimiter) do table.insert(result, match) end return result end -- -- Low Battery Warning -- local battery_warning_delivered = false -- Connects a battery status callback to the custom battery widget code. This -- gets called each time the status bar polls for battery status and is -- responsible for displaying a very prominent notification when the battery is -- running too low. It will display the warning once at 10% and then every -- polling interval at 5% and below. Below 5% the Dell battery drains much more -- rapidly than above that level and there's a chance it will either suspend or -- shut down (losing work in the case of the later). -- -- This exists because I prefer to run my battery nearly to empty and then -- charge it to avoid excess wear on the battery. But I'd prefer to not lose -- work or be inconvenienced by a poorly timed suspend. battery.status_callback = function(bat) local should_warn = bat.charge <= 10 and (bat.status == 'discharging' or bat.status == 'not connected') local be_persistent = bat.charge <= 5 and (bat.status == 'discharging' or bat.status == 'not connected') if battery_warning_delivered and bat.status ~= 'discharging' then battery_warning_delivered = false end -- Just one notification below 10% should be enough to get us to an outlet if should_warn and not battery_warning_delivered then naughty.notify({ preset = naughty.config.presets.critical, title = "Battery Low", icon = "battery-empty", icon_size = 256 }) battery_warning_delivered = true end -- But if we make it to 5% then we're in danger of shutting down or -- suspending imminently so start being annoying if be_persistent then naughty.notify({ preset = naughty.config.presets.critical, title = "Battery Low", icon = "battery-empty", icon_size = 256 }) end end -- TODO: Migrate tags from removed screen to free tags on remaining screens -- -- https://www.reddit.com/r/awesomewm/comments/5r9mgu -- https://stackoverflow.com/questions/42056795 function handle_tag_removal(tag) tag.connect_signal("request::screen", function(t) local live_screen = nil for s in screen do if s ~= t.screen then live_screen = s break end end for nt in live_screen.tags do local clients = nt:clients() if #clients == 0 then t:swap(nt) break end end end) end -- TODO: Allow mouse resizing -- Copy of suit.fair that forces rows or columns to one local function do_equal(p, orientation) local wa = p.workarea local cls = p.clients -- Swap workarea dimensions, if our orientation is "east" if orientation == 'east' then wa.width, wa.height = wa.height, wa.width wa.x, wa.y = wa.y, wa.x end if #cls > 0 then local rows, cols if #cls == 2 then rows, cols = 1, 2 else rows = 1 --rows = math.ceil(math.sqrt(#cls)) cols = math.ceil(#cls / rows) end for k, c in ipairs(cls) do k = k - 1 local g = {} local row, col row = k % rows col = math.floor(k / rows) local lrows, lcols if k >= rows * cols - rows then lrows = #cls - (rows * cols - rows) lcols = cols else lrows = rows lcols = cols end if row == lrows - 1 then g.height = wa.height - math.ceil(wa.height / lrows) * row g.y = wa.height - g.height else g.height = math.ceil(wa.height / lrows) g.y = g.height * row end if col == lcols - 1 then g.width = wa.width - math.ceil(wa.width / lcols) * col g.x = wa.width - g.width else g.width = math.ceil(wa.width / lcols) g.x = g.width * col end g.y = g.y + wa.y g.x = g.x + wa.x -- Swap window dimensions, if our orientation is "east" if orientation == 'east' then g.width, g.height = g.height, g.width g.x, g.y = g.y, g.x end p.geometries[c] = g end end end -- cpu_widget builds a customized widget to indicate the CPU usage function cpu_widget() return _cpu_widget({ width = 20, step_width = 1, step_spacing = 0, color = '#ff0000' }) end -- set_solid_wallpaper sets the gears wallpaper to a solid color as defined in -- beautiful.bg_normal function set_solid_wallpaper(s) wallpaper = gears.color.create_solid_pattern(beautiful.bg_normal) gears.wallpaper.set(wallpaper) end -- customize_theme applies additional customizations on the beautiful theme -- after it has been setup by rc.lua function customize_theme() beautiful.bg_urgent = "#222222" beautiful.border_width = dpi(0) -- Setup the wallpaper if it exists wallpaper_path = gears.filesystem.get_dir("config") .. "tux-minimal-bg-dark.png" if gears.filesystem.file_readable(wallpaper_path) then beautiful.wallpaper = wallpaper_path end -- Setup custom icons for the custom equal layout in this file beautiful.layout_equalv = gears.filesystem.get_dir("config") .. "icons/equalvw.png" beautiful.layout_equalh = gears.filesystem.get_dir("config") .. "icons/equalhw.png" -- Use a prettier icon theme if possible icon_theme_dir = "/usr/share/icons/Adwaita/" if gears.filesystem.dir_readable(icon_theme_dir) then naughty.config.icon_dirs = gears.table.join(naughty.config.icon_dirs, { icon_theme_dir }) end end -- connect_signals hooks into startup, exit, and screen change events. On -- startup it will start the desktop.target in the user mode systemd which can -- pull in widgets and anything else that enhances the desktop environment. It -- will stop this target at exit and wait for that shutdown to complete. -- -- This function also configures callbacks that need to be run when something -- changes about the system. This used to connect some dbus events to configure -- keyboards and mice but just does display management now. function connect_signals() awesome.connect_signal("screen::change", configure_displays) -- When title bars are added to floating windows a border should also be -- set. Setting these properties in the floating callback doesn't work -- because they don't get applied until after the layout arrange has -- happened. awful.screen.connect_for_each_screen(function(s) s:connect_signal("arrange", function() for _, c in pairs(awful.client.visible(s)) do if c.floating then c.border_width = dpi(3) c.border_color = beautiful.border_focus else c.border_width = dpi(0) end end end) end) -- Add title bars to floating windows client.connect_signal("property::floating", function(c) if c.floating then awful.titlebar.show(c) else awful.titlebar.hide(c) end if c.requests_no_titlebar then awful.titlebar.hide(c) end end) awesome.connect_signal("startup", function() awful.spawn.spawn("systemctl --user start desktop.target") configure_displays() end) awesome.connect_signal("exit", function(is_restart) -- There is no need to do this if this is a restart because the current -- awesome exec's a new awesome which will inherit everything from the -- current session. if not is_restart then -- The use of os.execute here is intentional because it will block -- until systemd has finished stopping the target. os.execute("systemctl --user stop desktop.target") end end) end -- add_window_rules mixes additional window rules into awful.rules.rules. To -- gather the information needed here use the xprop tool. function add_window_rules() awful.rules.rules = gears.table.join(awful.rules.rules, { { rule_any = { class = { "xclock", "XClock"}, }, properties = { floating = true, ontop = true, focusable = true, skip_taskbar = true, placement = awful.placement.centered} }, { rule = { role = "GtkFileChooserDialog"}, properties = { placement = awful.placement.centered} }, { rule = { class = "Inkscape", type = "normal"}, properties = { floating = false, maximized = false} }, { rule_any = { class = { "pavucontrol", "Pavucontrol"}, }, properties = { floating = true, placement = awful.placement.centered} }, -- TODO: why does this not work with awful.spawn? { rule_any = { class = { "emoji-picker"}, }, properties = { floating = true, requests_no_titlebar = true, skip_taskbar = true, placement = function(d, _) local coords = capi.mouse.coords() d.x = coords.x d.y = coords.y end}, } }) end -- move_mouse_top_right moves the mouse out of the way to the top right corner -- of the screen but avoids overlapping the layout picker because that displays -- a tooltip. function move_mouse_top_right() local mg = screen[mouse.screen].geometry -- 25 is the width of the layout switcher, to prevent tooltips mouse.coords({ x = mg.x + mg.width - 23, y = mg.y + 1, }) end -- zoom_screen uses xrandr to enable a lower resolution display mode and -- panning, mimicking at some level, the control scroll-wheel zooming that Mac -- OS is capable of. -- -- This only works with the built-in display and not at all with external -- displays. function zoom_screen(zoom_stop) local mode = default_edp_mode local panning = "0x0" zoom_stop = tonumber(zoom_stop) or 0 if zoom_stop > #edp_zoom_modes then zoom_stop = #edp_zoom_modes end if zoom_stop ~= 0 then mode = edp_zoom_modes[zoom_stop] panning = default_edp_mode end awful.spawn.spawn("xrandr --output eDP-1 --mode " .. mode .. " --panning " .. panning) end -- -- Session-persistent Zooming Support -- -- Start at the zoom stop I generally go to anyhow, it's always possible to -- zoom in further our out. local default_zoom_stop = 3 local current_zoom_stop = default_zoom_stop -- zoom_screen_up is a shortcut for zooming up using zoom stops that work -- correctly with the display function zoom_screen_up() if current_zoom_stop == #edp_zoom_modes then current_zoom_stop = #edp_zoom_modes else current_zoom_stop = current_zoom_stop + 1 end zoom_screen(current_zoom_stop) end -- zoom_screen_down is a shortcut for zooming down the screen using zoom stops -- that work correctly with the display function zoom_screen_down() if current_zoom_stop == 0 then zoom_screen(0) else current_zoom_stop = current_zoom_stop - 1 zoom_screen(current_zoom_stop) end end -- zoom_screen_reset resets the zooming of the screen and disables panning function zoom_screen_reset() current_zoom_stop = default_zoom_stop zoom_screen(0) end -- make_spawn is a shortcut for generating awful.spawn.spawn closures and allows -- using this shorthand in other places in this file. function make_spawn(cmd) return function() awful.spawn.spawn(cmd) end end -- add_global_keys adds additional key bindings to the root key map. More -- specific documentation is inline with the function. function add_global_keys(globalkeys) -- On Fedora this is installed with dnf; Ubuntu as of 18.04 doesn't package -- light so it's built locally there. light = os.getenv("HOME") .. "/.local/bin/light" if not gears.filesystem.file_readable(light) then light = "light" end return gears.table.join(globalkeys, -- Audio Controls awful.key({ }, "XF86AudioMute", make_spawn("/usr/bin/pactl set-sink-mute @DEFAULT_SINK@ toggle")), awful.key({ }, "XF86AudioLowerVolume", make_spawn("/usr/bin/pactl set-sink-volume @DEFAULT_SINK@ '-5%'")), awful.key({ }, "XF86AudioRaiseVolume", make_spawn("/usr/bin/pactl set-sink-volume @DEFAULT_SINK@ '+5%'")), awful.key({ }, "XF86AudioMicMute", make_spawn("/usr/bin/pactl set-source-mute @DEFAULT_SOURCE@ toggle")), -- Audio Controls for Spotify -- TODO: Make this work with rhythmbox and other stuff. Maybe not -- needed at all if we can use some generic dbus functionality? --awful.key({ }, "XF86AudioPrev", make_spawn("dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous")), --awful.key({ }, "XF86AudioPlay", make_spawn("dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause")), --awful.key({ }, "XF86AudioNext", make_spawn("dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next")), awful.key({ }, "XF86AudioPrev", make_spawn("dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.rhythmbox /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous")), awful.key({ }, "XF86AudioPlay", make_spawn("dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.rhythmbox /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause")), awful.key({ }, "XF86AudioNext", make_spawn("dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.rhythmbox /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next")), -- Backlight Controls awful.key({ "Shift" }, "XF86MonBrightnessUp", make_spawn(light .. " -A 1")), awful.key({ "Shift" }, "XF86MonBrightnessDown", make_spawn(light .. " -U 1")), awful.key({ }, "XF86MonBrightnessUp", make_spawn(light .. " -A 5")), awful.key({ }, "XF86MonBrightnessDown", make_spawn(light .. " -U 5")), -- Screen Zooming Commands awful.key({ modkey }, "z", zoom_screen_up), awful.key({ modkey }, "-", zoom_screen_down), awful.key({ modkey }, "=", zoom_screen_reset), -- Other useful commands -- https://github.com/GaZaTu/x11-emoji-picker (see rules above for floating) awful.key({ modkey }, "e", make_spawn("emoji-picker")), awful.key({ modkey, "Shift" }, "p", move_mouse_top_right), awful.key({ "Control", "Shift" }, "`", make_spawn("xscreensaver-command -lock")) ) end -- get_layouts returns layouts to be used for awful.layout.layouts function get_layouts() return { awful.layout.suit.tile, awful.layout.suit.tile.bottom, awful.layout.suit.tile.left, awful.layout.suit.tile.top, awful.layout.suit.fair, awful.layout.suit.fair.horizontal, { name = "equalv", arrange = function(p) do_equal(p, "south") end }, { name = "equalh", arrange = function(p) do_equal(p, "east") end }, } end -- split_screen_vertical creates a virtual screen and splits the main screen in -- half vertically. This is mostly useful for the ultra-widescreen curved -- monitors. It makes Awesome treat the monitor as containing two totally -- independent screens as if it were two different monitors. function split_screen_vertical(s) local s = s or 1 local geo = screen[s].geometry local new_width = math.ceil(geo.width/2) screen[s]:fake_resize(geo.x, geo.y, new_width, geo.height) screen.fake_add(geo.x + new_width, geo.y, geo.width - new_width, geo.height) end -- split_screen_horizontal creates a virtual screen and splits the main screen -- in half horizontally. This is mostly useful for the ultra-high-resolution -- monitors. It makes Awesome treat the monitor as containing two totally -- independent screens as if it were two different monitors. function split_screen_horizontal(s) local s = s or 1 local geo = screen[s].geometry local new_height = math.ceil(geo.height/2) screen[s]:fake_resize(geo.x, geo.y, geo.width, new_height) screen.fake_add(geo.x, geo.y + new_height, geo.width, geo.height - new_height) end -- get_clock creates a textclock widget for the wibox. It's basically the -- defaults but overrides the timezone to account for some oddities that I was -- experiencing with Ubuntu when changing timezones. function get_clock() local wi = wibox.widget.textclock(" %a %b %d, %H:%M ", 60, timezone) local lc = large_clock() local cw = calendar({ placement = 'top_right', start_sunday = true, previous_month_button = 4, next_month_button = 5, }) wi:connect_signal("button::press", function(_, _, _, button) if button == 1 then cw.toggle() end if button == 3 then lc:toggle() end end) return wi end -- default_display_handler configures an output to use it's default xrandr mode -- and position it to the right of the laptop built-in display. This should be -- good enough for most screens. function default_display_handler(card, edp_mode) awful.spawn.spawn("xrandr --output eDP-1 --mode " .. edp_mode .. " --output " .. card .. " --auto --right-of eDP-1") end -- get_displays uses a shell script helper (~/bin/enumerate-displays) to -- retrieve a list of connected displays and their EDIDs. It will convert this -- into a table and return it. -- -- The shell script exists mainly because Lua doesn't have any built-in ability -- to list directories unless you compile a C extension (won't work with -- awesome), use Glib (eww), or parse ls/find (error-prone). function get_displays() local result = {} local parts = nil local outputs = nil -- TODO: Make this async (awful.spawn.with_line_callback) outputs = io.popen(os.getenv("HOME") .. "/bin/enumerate-displays") while true do parts = string_split(outputs:read("*l"), ":") if parts == nil then break end table.insert(result, { card = parts[1], edid = parts[2] }) end outputs:close() return result end -- get_default_edp_mode enumerates the displays and returns the preferred -- default display mode for the eDP-1 device. -- -- This function overrides the default set in the top of the file at first -- runtime with the correct version for the first display. function get_default_edp_mode() local displays = get_displays() return default_edp_display_modes[displays[1].edid] end default_edp_mode = get_default_edp_mode() -- get_default_zoom_modes enumerates the displays and returns the proffered -- zoom mode table for the eDP-1 device. -- -- This function overrides the default set in the top of the file at first -- runtime with the correct version for the first display. function get_default_zoom_modes() local displays = get_displays() return default_edp_zoom_modes[displays[1].edid] end edp_zoom_modes = get_default_zoom_modes() -- configure_displays uses xrandr to configure the displays connected to the -- system when they are connected for removed. -- -- This would normally be done by a settings daemon but since I don't run one -- we have to do it manually here. This function gets called as part of a -- screen::change signal. -- -- TODO: This should be a lot more intelligent and a lot less hard coded but it -- works for what I do so *shrug* -- -- TODO: This should keep track of old displays and disable them all instead of -- just the hardcoded list function configure_displays() local displays = get_displays() -- Only an internal display is connected if #displays == 1 and internal_display_ids[displays[1].edid] then awful.spawn.spawn("xrandr " .. "--output eDP-1 --mode " .. default_edp_mode .. " " .. "--output DP-1 --off " .. "--output DP-2 --off " .. "--output DP-2-8 --off " .. "--output DP-5 --off " .. "--panning 0x0") return end for _, s in pairs(displays) do if s.edid == "DELL U2715H" then -- 27" Dell monitors (assumed 2) awful.spawn.spawn("xrandr " .. "--output DP-1 --auto " .. "--output DP-2 --auto --right-of DP-1 " .. "--output eDP-1 --off") return elseif s.edid == "LG TV" then -- Home TV awful.spawn.spawn("xrandr " .. "--output eDP-1 --mode " .. default_edp_mode .. " --output " .. s.card .. " --mode 1920x1080 " .. "--right-of eDP-1") return elseif simple_external_display_ids[s.edid] then awful.spawn.spawn("xrandr --output " .. s.card .. " --auto --output eDP-1 --off") return elseif internal_display_ids[s.edid] then -- continue else default_display_handler(s.card, default_edp_mode) return end end end return { -- Public "API" used by rc.lua add_global_keys = add_global_keys, add_window_rules = add_window_rules, connect_signals = connect_signals, customize_theme = customize_theme, get_clock = get_clock, get_layouts = get_layouts, battery = battery, pomodoro = pomodoro, cpu_widget = cpu_widget, -- Public functions that are occasionally useful split_screen_vertical = split_screen_vertical, split_screen_horizontal = split_screen_horizontal, -- Public functions that are useful on import configure_displays = configure_displays, dump = dump, }