My general desktop environment configuration.

Parts prefixed with (OFF) are not used, but kept for historic purposes. For some reason GitHub’s org renderer ignores TODO status, hence such a prefix. Round brackets instead of square ones to prevent GitHub’s org renderer from screwing up.


Some remarks

Removed features:

Feature Last commit
rofi-buku e22476b0cc6315e104e5ce4de5559a61c830c429

Global customization


I used to define color codes here (see previous version of the file), now I just get colors from the current Emacs theme.

To use them, let’s define a noweb block:

(let ((color (or (my/color-value name))))
  (if (> quote 0)
      (concat "\"" color "\"")


<<get-color(name="red", quote=1)>>

Also, get a foreground for the current color:

(let ((val (if (ct-light-p (my/color-value name))
	       (my/color-value 'black)
	     (my/color-value 'white))))
  (if (eq quote 1)
      (concat "\"" val "\"")


<<get-fg-for-color(name="red", quote=1)>>

This table used to have values, now it has only keys:

color key
black color0
red color1
green color2
yellow color3
blue color4
magenta color5
cyan color6
white color7
light-black color8
light-red color9
light-green color10
light-yellow color11
light-blue color12
light-magenta color13
light-cyan color14
light-white color15


Colors in Xresources

Some programs get their colors from XResources. Let’s generate that file.

 (lambda (elem)
   (concat "*" (nth 1 elem) ": " (my/color-value (nth 0 elem))))
  (lambda (elem) (and (nth 1 elem)
		      (not (string-empty-p (nth 1 elem)))))

*background: <<get-color(name="bg")>>
*foreground: <<get-color(name="fg")>>

Run xrdb -load ~/.Xresources to apply the changes.


Also, Xresources are used to set Xft settings. Unfortunately, the DPI setting has to be unique for each machine, which means I cannot commit Xresources to the repo.

(let ((hostname (system-name)))
  (cond ((string-equal hostname "azure") 120)
	((string-equal hostname "eminence") 120)
	((string-equal hostname "violet") 120)
	((string-equal hostname "iris") 120)
	(t 96)))
Xft.dpi: <<get-dpi()>>


A few programs I use to customize the apperance are listed below.

Guix dependency Description
matcha-theme My preferred GTK theme
papirus-icon-theme My preferred Icon theme
xsettingsd X11 settings daemon

xsettingsd is a lightweight daemon which configures X11 applications. It is launched with shepherd in the Services section.

(if (my/light-p)

(if (my/light-p)
Net/ThemeName "<<get-gtk-theme()>>"
Net/IconThemeName "<<get-icons-theme()>>"
Gtk/DecorationLayout "menu:minimize,maximize,close"
Gtk/FontName "Sans 10"
Gtk/MonospaceFontName "JetBrainsMono Nerd Mono 12"
Gtk/CursorThemeName "Adwaita"
Xft/Antialias 1
Xft/Hinting 0
Xft/HintStyle "hintnone"


Setting the default MIME types

[Default Applications]
x-scheme-handler/tg=userapp-Telegram Desktop-7PVWF1.desktop

[Added Associations]
x-scheme-handler/tg=userapp-Telegram Desktop-7PVWF1.desktop;

Device-specific settings

Guix dependency Description
xrandr X11 CLI to RandR
xgamma A tool to alter monitor’s gamma correction
xinput Configure input devices

Set screen layout & other params depending on hostname

if [ "$hostname" = "indigo" ]; then
    xrandr --output DisplayPort-0 --off --output HDMI-A-0 --mode 1920x1080 --pos 0x0 --rotate normal --output DVI-D-0 --mode 1920x1080 --pos 1920x0 --rotate normal
elif [ "$hostname" = "eminence" ]; then
    xgamma -gamma 1.25
elif [ "$hostname" = "violet" ]; then
    xrandr --output HDMI-0 --primary --mode 1920x1080 --pos 0x0 --rotate normal --output DP-0 --off --output DP-1 --mode 1920x1080 --pos 1920x0 --rotate normal --output DP-2 --off --output DP-3 --off --output DP-4 --off --output DP-5 --off --output None-1-1 --off


Settings for Emacs X Window Manager, a tiling WM implemented in Emacs Lisp. This part has a few bits copied from my blog post.


Startup & UI


First things first, Emacs has to be launched as a window manager. On a more conventional system I’d create a .desktop file in some system folder that can be seen by a login manager, but in the case of Guix it’s a bit more complicated, because all such folders are not meant to be changed manually.

Category Guix dependency
desktop-misc xinit
desktop-misc xss-lock

However, GDM, the login manager that seems to be the default on Guix, launches ~/.xsession on the startup if it’s present, which is just fine for my purposes.

# Source .profile
. ~/.profile

# Disable access control for the current user
xhost +SI:localuser:$USER

# Fix for Java applications

# Apply XResourses
xrdb -merge ~/.Xresources

# Turn off the system bell
xset -b

# Use i3lock as a screen locker
xss-lock -- i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png &

# Some apps that have to be launched only once.
picom &
# nm-applet &
dunst &
copyq &

# Run the Emacs startup script as a session.
# exec dbus-launch --exit-with-session ~/.emacs.d/
exec dbus-launch --exit-with-session emacs -mm --debug-init -l ~/.emacs.d/desktop.el

Startup apps

Now that Emacs is launched, it is necessary to set up the EXWM-specific parts of config.

I want to launch some apps from EXWM instead of the Xsession file for two purposes:

  • the app may need to have the entire desktop environment set up
  • or it may need to be restarted if Emacs is killed.

As of now, these are polybar, feh and, shepherd:

(defun my/exwm-run-polybar ()
  (call-process "~/bin/"))

(defun my/exwm-set-wallpaper ()
  (call-process-shell-command "feh --bg-fill ~/Pictures/wallpaper.jpg"))

(defun my/exwm-run-shepherd ()
  (when (string-empty-p (shell-command-to-string "pgrep -u pavel shepherd"))
    (call-process "shepherd")))


The GUI pinentry doesn’t work too well with EXWM because of issues with popup windows, so we will use the Emacs one.

(use-package pinentry
  :straight t
  :after (exwm)
  (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry
  (setq auth-source-debug t)

  (setq epg-gpg-program "gpg") ;; not necessary
  (require 'epa-file)
  (setq epa-pinentry-mode 'loopback)
  (setq epg-pinentry-mode 'loopback)

(executable-find "pinentry")
default-cache-ttl 3600
max-cache-ttl 3600
pinentry-program <<find-pinentry()>>


Show the current workspace in the modeline.

(use-package exwm-modeline
  :straight t
  (add-hook 'exwm-init-hook #'exwm-modeline-mode)
    :foreground (my/color-value 'yellow)
    :weight 'bold)))


Check if running Arch and not Guix.

(defun my/is-arch ()
  (file-exists-p "/etc/arch-release"))


A bunch of functions related to managing windows in EXWM.

Moving windows

As I wrote in my Emacs and i3 post, I want to have a rather specific behavior when moving windows (which does resemble i3 in some way):

  • if there is space in the required direction, move the Emacs window there;
  • if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;

I can’t say it’s better or worse than the built-in functionality or one provided by evil, but I’m used to it and I think it fits better for managing a lot of windows.

So, first, we need a predicate that checks whether there is space in the given direction:

(require 'windmove)

(defun my/exwm-direction-exists-p (dir)
  "Check if there is space in the direction DIR.

Does not take the minibuffer into account."
  (cl-some (lambda (dir)
	     (let ((win (windmove-find-other-window dir)))
	       (and win (not (window-minibuffer-p win)))))
	   (pcase dir
	     ('width '(left right))
	     ('height '(up down)))))

And a function to implement that:

(defun my/exwm-move-window (dir)
  "Move the current window in the direction DIR."
  (let ((other-window (windmove-find-other-window dir))
	(other-direction (my/exwm-direction-exists-p
			  (pcase dir
			    ('up 'width)
			    ('down 'width)
			    ('left 'height)
			    ('right 'height)))))
     ((and other-window (not (window-minibuffer-p other-window)))
      (window-swap-states (selected-window) other-window))
      (evil-move-window dir)))))

My preferred keybindings for this part are, of course, s-<H|J|K|L>.

Resizing windows

I find this odd that there are different commands to resize tiling and floating windows. So let’s define one command to perform both resizes depending on the context:

(setq my/exwm-resize-value 5)

(defun my/exwm-resize-window (dir kind &optional value)
  "Resize the current window in the direction DIR.

DIR is either 'height or 'width, KIND is either 'shrink or
 'grow.  VALUE is `my/exwm-resize-value' by default.

If the window is an EXWM floating window, execute the
corresponding command from the exwm-layout group, execute the
command from the evil-window group."
  (unless value
    (setq value my/exwm-resize-value))
  (let* ((is-exwm-floating
	  (and (derived-mode-p 'exwm-mode)
	 (func (if is-exwm-floating
		     (pcase kind ('shrink "shrink") ('grow "enlarge"))
		     (pcase dir ('height "") ('width "-horizontally"))))
		   (pcase kind ('shrink "-decrease-") ('grow "-increase-"))
		   (symbol-name dir))))))
    (when is-exwm-floating
      (setq value (* 5 value)))
    (funcall func value)))

This function will call exwm-layout-<shrink|grow>[-horizontally] for EXWM floating window and evil-window-<decrease|increase>-<width|height> otherwise.

This function can be bound to the required keybindings directly, but I prefer a hydra to emulate the i3 submode:

(defhydra my/exwm-resize-hydra (:color pink :hint nil :foreign-keys run)
_l_: Increase width   _h_: Decrease width   _j_: Increase height   _k_: Decrease height

_=_: Balance          "
  ("h" (lambda () (interactive) (my/exwm-resize-window 'width 'shrink)))
  ("j" (lambda () (interactive) (my/exwm-resize-window 'height 'grow)))
  ("k" (lambda () (interactive) (my/exwm-resize-window 'height 'shrink)))
  ("l" (lambda () (interactive) (my/exwm-resize-window 'width 'grow)))
  ("=" balance-windows)
  ("q" nil "quit" :color blue))

Improving splitting windows

M-x evil-window-[v]split (bound to C-w v and C-w s by default) are the default evil command to do splits.

One EXWM-related issue though is that by default doing such a split “copies” the current buffer to the new window. But as EXWM buffer cannot be “copied” like that, some other buffer is displayed in the split, and generally, that’s not a buffer I want.

For instance, I prefer to have Chrome DevTools as a separate window. When I click “Inspect” on something, the DevTools window replaces my Ungoogled Chromium window. I press C-w v, and most often I have something like *scratch* buffer in the opened split instead of the previous Chromium window.

To implement better behavior, I define the following advice:

(defun my/exwm-fill-other-window (&rest _)
  "Open the most recently used buffer in the next window."
  (when (and (eq major-mode 'exwm-mode) (not (eq (next-window) (get-buffer-window))))
    (let ((other-exwm-buffer
	   (cl-loop with other-buffer = (persp-other-buffer)
		    for buf in (sort (persp-current-buffers) (lambda (a _) (eq a other-buffer)))
		    with current-buffer = (current-buffer)
		    when (and (not (eq current-buffer buf))
			      (buffer-live-p buf)
			      (not (string-match-p (persp--make-ignore-buffer-rx) (buffer-name buf)))
			      (not (get-buffer-window buf)))
		    return buf)))
      (when other-exwm-buffer
	(with-selected-window (next-window)
	  (switch-to-buffer other-exwm-buffer))))))

This is meant to be called after doing an either vertical or horizontal split, so it’s advised like that:

(advice-add 'evil-window-split :after #'my/exwm-fill-other-window)
(advice-add 'evil-window-vsplit :after #'my/exwm-fill-other-window)

This works as follows. If the current buffer is an EXWM buffer and there are other windows open (that is, (next-window) is not the current window), the function tries to find another suitable buffer to be opened in the split. And that also takes the perspectives into account, so buffers are searched only within the current perspective, and the buffer returned by persp-other-buffer will be the top candidate.


perspective.el is one package I like that provides workspaces for Emacs, called “perspectives”. Each perspective has a separate buffer list, window layout, and a few other things that make it easier to separate things within Emacs.

One feature I’d like to highlight is integration between perspective.el and treemacs, where one perspective can have a separate treemacs tree. Although now tab-bar.el seems to be getting into shape to compete with perspective.el, as of the time of this writing, there’s no such integration, at least not out of the box.

perspective.el works with EXWM more or less as one would expect - each EXWM workspace has its own set of perspectives. That way it feels somewhat like having multiple Emacs frames in a tiling window manager, although, of course, much more integrated with Emacs.

However, there are still some issues. For instance, I was having strange behaviors with floating windows, EXWM buffers in perspectives, etc. So I’ve made a package called perspective-exwm.el that does two things:

  • Advices away the issues I had. Take a look at the package homepage for more detail on that.
  • Provides some additional functionality that makes use of both perspective.el and EXWM.


(use-package perspective-exwm
  :straight t
  (setq perspective-exwm-override-initial-name
	'((0 . "misc")
	  (1 . "core")
	  (2 . "browser")
	  (3 . "comms")
	  (4 . "dev")))
  (setq perspective-exwm-cycle-max-message-length 180)
   :keymaps 'perspective-map
   "e" #'perspective-exwm-move-to-workspace
   "E" #'perspective-exwm-copy-to-workspace))

By default, a new Emacs buffer opens in the current perspective in the current workspace, but sure enough, it’s possible to change that.

For EXWM windows, the perspective-exwm package provides a function called perspective-exwm-assign-window, which is intended to be used in exwm-manage-finish-hook, for instance:

(defun my/exwm-configure-window ()
  (unless exwm--floating-frame
    (pcase exwm-class-name
      ((or "Firefox" "Nightly")
	:workspace-index 2
	:persp-name "browser"))
	:workspace-index 2
	:persp-name "browser"))
	:persp-name "term"))
      ((or "VK" "Slack" "discord" "TelegramDesktop" "Rocket.Chat")
	:workspace-index 3
	:persp-name "comms"))
      ((or "Chromium-browser" "jetbrains-datagrip")
	:workspace-index 4
	:persp-name "dev")))))

(add-hook 'exwm-manage-finish-hook #'my/exwm-configure-window)

Workspaces and multi-monitor setup

A section about improving management of EXWM workspaces.

Some features, common in other tiling WMs, are missing in EXWM out of the box, namely:

Here’s my take on implementing them.

Tracking recently used workspaces

First up though, we need to track the workspaces in the usage order. I’m not sure if there’s some built-in functionality in EXWM for that, but it seems simple enough to implement.

Here is a snippet of code that does it:

(setq my/exwm-last-workspaces '(1))

(defun my/exwm-store-last-workspace ()
  "Save the last workspace to `my/exwm-last-workspaces'."
  (setq my/exwm-last-workspaces
	(seq-uniq (cons exwm-workspace-current-index

(add-hook 'exwm-workspace-switch-hook

The variable my/exwm-last-workspaces stores the workspace indices; the first item is the index of the current workspace, the second item is the index of the previous workspace, and so on.

One note here is that workspaces may also disappear (e.g. after M-x exwm-workspace-delete), so we also need a function to clean the list:

(defun my/exwm-last-workspaces-clear ()
  "Clean `my/exwm-last-workspaces' from deleted workspaces."
  (setq my/exwm-last-workspaces
	 (lambda (i) (nth i exwm-workspace--list))

The monitor list

The second piece of the puzzle is getting the monitor list in the right order.

While it is possible to retrieve the monitor list from exwm-randr-workspace-monitor-plist, this won’t scale well beyond two monitors, mainly because changing this variable may screw up the order.

So the easiest way is to just define the variable like that:

(setq my/exwm-monitor-list
      (pcase (system-name)
	("indigo" '(nil "DVI-D-0"))
	("violet" '(nil "DP-1"))
	(_ '(nil))))

If you are changing the RandR configuration on the fly, this variable will also need to be changed, but for now, I don’t have such a necessity.

A function to get the current monitor:

(defun my/exwm-get-current-monitor ()
  "Return the current monitor name or nil."
  (plist-get exwm-randr-workspace-monitor-plist
	     (cl-position (selected-frame)

And a function to cycle the monitor list in either direction:

(defun my/exwm-get-other-monitor (dir)
  "Cycle the monitor list in the direction DIR.

DIR is either 'left or 'right."
   (% (+ (cl-position
	  :test #'string-equal)
	 (length my/exwm-monitor-list)
	 (pcase dir
	   ('right 1)
	   ('left -1)))
      (length my/exwm-monitor-list))

Switch to another monitor

With the functions from the previous two sections, we can implement switching to another monitor by switching to the most recently used workspace on that monitor.

One caveat here is that on the startup the my/exwm-last-workspaces variable won’t have any values from other monitor(s), so this list is concatenated with the list of available workspace indices.

(defun my/exwm-switch-to-other-monitor (&optional dir)
  "Switch to another monitor."
  (let ((mouse-autoselect-window nil))
     (cl-loop with other-monitor = (my/exwm-get-other-monitor (or dir 'right))
	      for i in (append my/exwm-last-workspaces
			       (cl-loop for i from 0
					for _ in exwm-workspace--list
					collect i))
	      if (if other-monitor
		     (string-equal (plist-get exwm-randr-workspace-monitor-plist i)
		   (not (plist-get exwm-randr-workspace-monitor-plist i)))
	      return i))))

I bind this function to s-q, as I’m used from i3.

Move the workspace to another monitor

Now, moving the workspace to another monitor.

This is actually quite easy to pull off - one just has to update exwm-randr-workspace-monitor-plist accordingly and run exwm-randr-refresh. I just add another check there because I don’t want some monitor to remain without workspaces at all.

(defun my/exwm-workspace-switch-monitor ()
  "Move the current workspace to another monitor."
  (let ((new-monitor (my/exwm-get-other-monitor 'right))
	(current-monitor (my/exwm-get-current-monitor)))
    (when (and current-monitor
	       (>= 1
		   (cl-loop for (key value) on exwm-randr-workspace-monitor-plist
			    by 'cddr
			    if (string-equal value current-monitor) sum 1)))
      (error "Can't remove the last workspace on the monitor!"))
    (setq exwm-randr-workspace-monitor-plist
	  (map-delete exwm-randr-workspace-monitor-plist exwm-workspace-current-index))
    (when new-monitor
      (setq exwm-randr-workspace-monitor-plist
	    (plist-put exwm-randr-workspace-monitor-plist

In my configuration this is bound to s-<tab>.

Windmove between monitors

And the final (for now) piece of the puzzle is using the same command to switch between windows and monitors. E.g. when the focus is on the right-most window on one monitor, I want the command to switch to the left-most window on the monitor to the right instead of saying “No window right from the selected window”, as windmove-right does.

So here is my implementation of that. It always does windmove-do-select-window for 'down and 'up. For 'right and 'left though, the function calls the previously defined function to switch to other monitor if windmove-find-other-window doesn’t return anything.

(defun my/exwm-windmove (dir)
  "Move to window or monitor in the direction DIR."
  (if (or (eq dir 'down) (eq dir 'up))
      (windmove-do-window-select dir)
    (let ((other-window (windmove-find-other-window dir))
	  (other-monitor (my/exwm-get-other-monitor dir))
	  (opposite-dir (pcase dir
			  ('left 'right)
			  ('right 'left))))
      (if other-window
	  (windmove-do-window-select dir)
	(let ((mouse-autoselect-window nil))
	  (my/exwm-switch-to-other-monitor dir))
	(cl-loop while (windmove-find-other-window opposite-dir)
		 do (windmove-do-window-select opposite-dir))))))


Setting up some completion interfaces that fit particularly well to use with EXWM. While rofi also works, I want to use Emacs functionality wherever possible to have one completion interface everywhere.


ivy-posframe is an extension to show ivy candidates in a posframe.

Take a look at this issue in the EXWM repo about setting it up.

Edit [2022-04-09 Sat]: This looks nice, but unfortunately too unstable. Disabling it.

(use-package ivy-posframe
  :straight t
  (setq ivy-posframe-parameters '((left-fringe . 10)
				  (right-fringe . 10)
				  (parent-frame . nil)
				  (max-width . 80)))
  (setq ivy-posframe-height-alist '((t . 20)))
  (setq ivy-posframe-width 180)
  (setq ivy-posframe-min-height 5)
  (setq ivy-posframe-display-functions-alist
	'((swiper . ivy-display-function-fallback)
	  (swiper-isearch . ivy-display-function-fallback)
	  (t . ivy-posframe-display)))
  (ivy-posframe-mode 1))
Disable mouse movement


(defun my/advise-fn-suspend-follow-mouse (fn &rest args)
  (let ((focus-follows-mouse nil)
	(mouse-autoselect-window nil)
	(pos (x-mouse-absolute-pixel-position)))
	(apply fn args)
      (x-set-mouse-absolute-pixel-position (car pos)
					   (cdr pos)))))
(with-eval-after-load 'ivy-posframe
  (advice-add #'ivy-posframe--read :around #'my/advise-fn-suspend-follow-mouse))
Disable changing focus

Not sure about that. The cursor occasionally changes focus when I’m exiting posframe, and this doesn’t catch all the cases.

(defun my/setup-posframe (&rest args)
   (lambda (var)
     (kill-local-variable var)
     (setf (symbol-value var) nil))

(defun my/restore-posframe (&rest args)
   (lambda ()
      (lambda (var)
	(kill-local-variable var)
	(setf (symbol-value var) t))

(with-eval-after-load 'ivy-posframe
  (advice-add #'posframe--create-posframe :after #'my/setup-posframe)
  (advice-add #'ivy-posframe-cleanup :after #'my/restore-posframe))

Linux app

counsel-linux-app is a counsel interface to select a Linux desktop application.

By default, it also shows paths from /gnu/store, so there is a custom formatter function.

(defun my/counsel-linux-app-format-function (name comment _exec)
  (format "% -45s%s"
	   (ivy--truncate-string name 45)
	   'face 'counsel-application-name)
	  (if comment
	      (concat ": " (ivy--truncate-string comment 100))

(setq counsel-linux-app-format-function #'my/counsel-linux-app-format-function)

Also, by default it tries to launch stuff with gtk-launch, which is in the gtk+ package.

Category Guix dependency
desktop-misc gtk+:bin


password-store-ivy is another package of mine, inspired by rofi-pass.

(use-package password-store-ivy
  :straight (:host github :repo "SqrtMinusOne/password-store-ivy")
  :after (exwm))


emojify is an Emacs package that adds emoji display to Emacs. While its primary capacity is no longer necessary in Emacs 28, it a few functions to insert emojis are still handy.

(use-package emojify
  :straight t)

Because I occasionally want to type emojis to other programs, I reuse a function from password-store-ivy:

(defun my/emojify-type ()
  "Type an emoji."
  (let ((emoji (emojify-completing-read "Type emoji: ")))
    (kill-new emoji)
      (password-store-ivy--get-wait-command 10)
      "xdotool key Shift+Insert"))))


EXWM keybindings

Setting keybindings for EXWM. This actually has to be in the :config block of the use-package form, that is it has to be run after EXWM is loaded, so I use noweb to put this block in the correct place.

First, some prefixes for keybindings that are always passed to EXWM instead of the X application in line-mode:

(setq exwm-input-prefix-keys

Also other local keybindings, that are also available only in line-mode:

(defmacro my/app-command (command)
  `(lambda () (interactive) (my/run-in-background ,command)))

 :keymaps '(exwm-mode-map)
 "C-q" #'exwm-input-send-next-key
 "<print>" (my/app-command "flameshot gui")
 "<mode-line> s-<mouse-4>" #'perspective-exwm-cycle-all-buffers-backward
 "<mode-line> s-<mouse-5>" #'perspective-exwm-cycle-all-buffers-forward
 "M-x" #'counsel-M-x
 "M-SPC" (general-key "SPC"))

Simulation keys.

(setq exwm-input-simulation-keys `((,(kbd "M-w") . ,(kbd "C-w"))
				   (,(kbd "M-c") . ,(kbd "C-c"))))

A quit function with a confirmation.

(defun my/exwm-quit ()
  (when (or (not (eq (selected-window) (next-window)))
	    (y-or-n-p "This is the last window. Are you sure?"))

And keybindings that are available in both char-mode and line-mode:

(setq exwm-input-global-keys
	;; Reset to line-mode
	(,(kbd "s-R") . exwm-reset)

	;; Switch windows
	(,(kbd "s-<left>") . (lambda () (interactive) (my/exwm-windmove 'left)))
	(,(kbd "s-<right>") . (lambda () (interactive) (my/exwm-windmove 'right)))
	(,(kbd "s-<up>") . (lambda () (interactive) (my/exwm-windmove 'up)))
	(,(kbd "s-<down>") . (lambda () (interactive) (my/exwm-windmove 'down)))

	(,(kbd "s-h"). (lambda () (interactive) (my/exwm-windmove 'left)))
	(,(kbd "s-l") . (lambda () (interactive) (my/exwm-windmove 'right)))
	(,(kbd "s-k") . (lambda () (interactive) (my/exwm-windmove 'up)))
	(,(kbd "s-j") . (lambda () (interactive) (my/exwm-windmove 'down)))

	;; Moving windows
	(,(kbd "s-H") . (lambda () (interactive) (my/exwm-move-window 'left)))
	(,(kbd "s-L") . (lambda () (interactive) (my/exwm-move-window 'right)))
	(,(kbd "s-K") . (lambda () (interactive) (my/exwm-move-window 'up)))
	(,(kbd "s-J") . (lambda () (interactive) (my/exwm-move-window 'down)))

	;; Fullscreen
	(,(kbd "s-f") . exwm-layout-toggle-fullscreen)
	(,(kbd "s-F") . exwm-floating-toggle-floating)

	;; Quit
	(,(kbd "s-Q") . my/exwm-quit)

	;; Split windows
	(,(kbd "s-s") . evil-window-vsplit)
	(,(kbd "s-v") . evil-window-hsplit)

	;; Switch perspectives
	(,(kbd "s-,") . persp-prev)
	(,(kbd "s-.") . persp-next)

	;; Switch buffers
	(,(kbd "s-e") . persp-ivy-switch-buffer)
	(,(kbd "s-E") . my/persp-ivy-switch-buffer-other-window)

	;; Resize windows
	(,(kbd "s-r") . my/exwm-resize-hydra/body)

	;; Apps & stuff
	(,(kbd "s-p") . counsel-linux-app)
	(,(kbd "s-P") . async-shell-command)
	(,(kbd "s-;") . my/exwm-apps-hydra/body)
	(,(kbd "s--") . password-store-ivy)
	(,(kbd "s-=") . my/emojify-type)
	(,(kbd "s-i") . ,(my/app-command "copyq menu"))

	;; Basic controls
	(,(kbd "<XF86AudioRaiseVolume>") . ,(my/app-command "ponymix increase 5 --max-volume 150"))
	(,(kbd "<XF86AudioLowerVolume>") . ,(my/app-command "ponymix decrease 5 --max-volume 150"))
	(,(kbd "<XF86MonBrightnessUp>") . ,(my/app-command "light -A 5"))
	(,(kbd "<XF86MonBrightnessDown>") . ,(my/app-command "light -U 5"))
	(,(kbd "<XF86AudioMute>") . ,(my/app-command "ponymix toggle"))

	(,(kbd "<XF86AudioPlay>") . ,(my/app-command "mpc toggle"))
	(,(kbd "<XF86AudioPause>") . ,(my/app-command "mpc pause"))
	(,(kbd "<print>") . ,(my/app-command "flameshot gui"))

	;; Input method
	(,(kbd "M-\\") . my/toggle-input-method)

	;; Switch workspace
	(,(kbd "s-q") . my/exwm-switch-to-other-monitor)
	(,(kbd "s-w") . exwm-workspace-switch)
	(,(kbd "s-W") . exwm-workspace-move-window)
	(,(kbd "s-<tab>") . my/exwm-workspace-switch-monitor)

	;; Perspectives
	(,(kbd "s-{") . perspective-exwm-cycle-all-buffers-backward)
	(,(kbd "s-}") . perspective-exwm-cycle-all-buffers-forward)
	(,(kbd "s-[") . perspective-exwm-cycle-exwm-buffers-backward)
	(,(kbd "s-]") . perspective-exwm-cycle-exwm-buffers-forward)
	(,(kbd "s-<mouse-4>") . perspective-exwm-cycle-exwm-buffers-backward)
	(,(kbd "s-<mouse-5>") . perspective-exwm-cycle-exwm-buffers-forward)
	(,(kbd "s-`") . perspective-exwm-switch-perspective)
	(,(kbd "s-o") . ,(my/app-command "rofi -show window"))

	;; 's-N': Switch to certain workspace with Super (Win) plus a number key (0 - 9)
	,@(mapcar (lambda (i)
		    `(,(kbd (format "s-%d" i)) .
		      (lambda ()
			(when (or (< ,i (exwm-workspace--count))
				  (y-or-n-p (format "Create workspace %d" ,i)))
			  (exwm-workspace-switch-create ,i) ))))
		  (number-sequence 0 9))))

A function to apply changes to exwm-input-global-keys.

(defun my/exwm-update-global-keys ()
  (setq exwm-input--global-keys nil)
  (dolist (i exwm-input-global-keys)
    (exwm-input--set-key (car i) (cdr i)))
  (when exwm--connection

App shortcuts

A transient hydra for shortcuts for the most frequent apps.

(defhydra my/exwm-apps-hydra (:color blue :hint nil)
_t_: Terminal (Alacritty)
_b_: Browser (Firefox)
_s_: Rocket.Chat
_e_: Telegram
_d_: Discord
  ("t" (lambda () (interactive) (my/run-in-background "alacritty")))
  ("b" (lambda () (interactive) (my/run-in-background "firefox")))
  ("s" (lambda () (interactive) (my/run-in-background "flatpak run chat.rocket.RocketChat")))
  ("e" (lambda () (interactive) (my/run-in-background "telegram-desktop")))
  ("d" (lambda () (interactive) (my/run-in-background "flatpak run com.discordapp.Discord"))))

Locking up

Run i3lock.

(defun my/exwm-lock ()
  (my/run-in-background "i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png"))


Catch and report all errors raised when invoking command hooks

(defun exwm-input--fake-last-command ()
  "Fool some packages into thinking there is a change in the buffer."
  (setq last-command #'exwm-input--noop)
  (condition-case hook-error
	(run-hooks 'pre-command-hook)
	(run-hooks 'post-command-hook))
     (exwm--log "Error occurred while running command hooks: %s\n\nBacktrace:\n\n%s"
		  (setq-local standard-output (current-buffer))

Improve floating windows behavior

These 3 settings seem to cause particular trouble with floating windows. Setting them to nil improves the stability greatly.

(defun my/fix-exwm-floating-windows ()
  (setq-local exwm-workspace-warp-cursor nil)
  (setq-local mouse-autoselect-window nil)
  (setq-local focus-follows-mouse nil))

(add-hook 'exwm-floating-setup-hook #'my/fix-exwm-floating-windows)

Fix exwm–on-ClientMessage

It seems like this strange commit: c90ac4 breaks focusing on an X frame when switching to a workspace, at least on Emacs <= 28. This reverts to the previous version.

(defun exwm--on-ClientMessage-old (raw-data _synthetic)
  "Handle ClientMessage event."
  (let ((obj (make-instance 'xcb:ClientMessage))
	type id data)
    (xcb:unmarshal obj raw-data)
    (setq type (slot-value obj 'type)
	  id (slot-value obj 'window)
	  data (slot-value (slot-value obj 'data) 'data32))
    (exwm--log "atom=%s(%s)" (x-get-atom-name type exwm-workspace--current)
     ((= type xcb:Atom:_NET_NUMBER_OF_DESKTOPS)
      (let ((current (exwm-workspace--count))
	    (requested (elt data 0)))
	;; Only allow increasing/decreasing the workspace number by 1.
	 ((< current requested)
	 ((and (> current requested)
	       (> current 1))
	  (let ((frame (car (last exwm-workspace--list))))
	    (exwm-workspace--get-remove-frame-next-workspace frame)
	    (delete-frame frame))))))
     ((= type xcb:Atom:_NET_CURRENT_DESKTOP)
      (exwm-workspace-switch (elt data 0)))
     ((= type xcb:Atom:_NET_ACTIVE_WINDOW)
      (let ((buffer (exwm--id->buffer id))
	    iconic window)
	(when (buffer-live-p buffer)
	  (with-current-buffer buffer
	    (when (eq exwm--frame exwm-workspace--current)
	      (if exwm--floating-frame
		  (select-frame exwm--floating-frame)
		(setq iconic (exwm-layout--iconic-state-p))
		(when iconic
		  ;; State change: iconic => normal.
		  (set-window-buffer (frame-selected-window exwm--frame)
		;; Focus transfer.
		(setq window (get-buffer-window nil t))
		(when (or iconic
			  (not (eq window (selected-window))))
		  (select-window window))))))))
     ((= type xcb:Atom:_NET_CLOSE_WINDOW)
      (let ((buffer (exwm--id->buffer id)))
	(when (buffer-live-p buffer)
	  (exwm--defer 0 #'kill-buffer buffer))))
     ((= type xcb:Atom:_NET_WM_MOVERESIZE)
      (let ((direction (elt data 2))
	    (buffer (exwm--id->buffer id)))
	(unless (and buffer
		     (not (buffer-local-value 'exwm--floating-frame buffer)))
	  (cond ((= direction
		 ;; FIXME
		((= direction
		 ;; FIXME
		((= direction xcb:ewmh:_NET_WM_MOVERESIZE_CANCEL)
		;; In case it's a workspace frame.
		((and (not buffer)
		      (catch 'break
			(dolist (f exwm-workspace--list)
			  (when (or (eq id (frame-parameter f 'exwm-outer-id))
				    (eq id (frame-parameter f 'exwm-id)))
			    (throw 'break t)))
		 ;; In case it's a floating frame,
		 ;; move the corresponding X window instead.
		 (unless buffer
		   (catch 'break
		     (dolist (pair exwm--id-buffer-alist)
		       (with-current-buffer (cdr pair)
			     (and exwm--floating-frame
				  (or (eq id
					  (frame-parameter exwm--floating-frame
				      (eq id
					  (frame-parameter exwm--floating-frame
			   (setq id exwm--id)
			   (throw 'break nil))))))
		 ;; Start to move it.
		 (exwm-floating--start-moveresize id direction))))))
     ((= type xcb:Atom:_NET_REQUEST_FRAME_EXTENTS)
      (let ((buffer (exwm--id->buffer id))
	    top btm)
	(if (or (not buffer)
		(not (buffer-local-value 'exwm--floating-frame buffer)))
	    (setq top 0
		  btm 0)
	  (setq top (window-header-line-height)
		btm (window-mode-line-height)))
	(xcb:+request exwm--connection
	    (make-instance 'xcb:ewmh:set-_NET_FRAME_EXTENTS
			   :window id
			   :left 0
			   :right 0
			   :top top
			   :bottom btm)))
      (xcb:flush exwm--connection))
     ;; _NET_WM_DESKTOP.
     ((= type xcb:Atom:_NET_WM_DESKTOP)
      (let ((buffer (exwm--id->buffer id)))
	(when (buffer-live-p buffer)
	  (exwm-workspace-move-window (elt data 0) id))))
     ;; _NET_WM_STATE
     ((= type xcb:Atom:_NET_WM_STATE)
      (let ((action (elt data 0))
	    (props (list (elt data 1) (elt data 2)))
	    (buffer (exwm--id->buffer id))
	;; only support _NET_WM_STATE_FULLSCREEN / _NET_WM_STATE_ADD for frames
	(when (and (not buffer)
		   (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props)
		   (= action xcb:ewmh:_NET_WM_STATE_ADD))
	      (make-instance 'xcb:ewmh:set-_NET_WM_STATE
			     :window id
			     :data (vector xcb:Atom:_NET_WM_STATE_FULLSCREEN)))
	  (xcb:flush exwm--connection))
	(when buffer                    ;ensure it's managed
	  (with-current-buffer buffer
	    (when (or (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props)
		      (memq xcb:Atom:_NET_WM_STATE_ABOVE props))
	      (cond ((= action xcb:ewmh:_NET_WM_STATE_ADD)
		     (unless (exwm-layout--fullscreen-p)
		       (exwm-layout-set-fullscreen id))
		     (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new))
		    ((= action xcb:ewmh:_NET_WM_STATE_REMOVE)
		     (when (exwm-layout--fullscreen-p)
		       (exwm-layout-unset-fullscreen id)))
		    ((= action xcb:ewmh:_NET_WM_STATE_TOGGLE)
		     (if (exwm-layout--fullscreen-p)
			 (exwm-layout-unset-fullscreen id)
		       (exwm-layout-set-fullscreen id)
		       (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new)))))
	    ;; FIXME: check (may require other properties set)
	    (when (memq xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION props)
	      (when (= action xcb:ewmh:_NET_WM_STATE_ADD)
		(unless (eq exwm--frame exwm-workspace--current)
		  (set-frame-parameter exwm--frame 'exwm-urgency t)
		  (setq exwm-workspace--switch-history-outdated t)))
	      ;; xcb:ewmh:_NET_WM_STATE_REMOVE?
	      ;; xcb:ewmh:_NET_WM_STATE_TOGGLE?
	    (xcb:+request exwm--connection
		(make-instance 'xcb:ewmh:set-_NET_WM_STATE
			       :window id :data (vconcat props-new)))
	    (xcb:flush exwm--connection)))))
     ((= type xcb:Atom:WM_PROTOCOLS)
      (let ((type (elt data 0)))
	(cond ((= type xcb:Atom:_NET_WM_PING)
	       (setq exwm-manage--ping-lock nil))
	      (t (exwm--log "Unhandled WM_PROTOCOLS of type: %d" type)))))
     ((= type xcb:Atom:WM_CHANGE_STATE)
      (let ((buffer (exwm--id->buffer id)))
	(when (and (buffer-live-p buffer)
		   (= (elt data 0) xcb:icccm:WM_STATE:IconicState))
	  (with-current-buffer buffer
	    (if exwm--floating-frame
		(call-interactively #'exwm-floating-hide)
      (exwm--log "Unhandled: %s(%d)"
		 (x-get-atom-name type exwm-workspace--current) type)))))

(with-eval-after-load 'exwm
  (advice-add 'exwm--on-ClientMessage :override #'exwm--on-ClientMessage-old))

Application-specific settings

Start Nyxt in char-mode.

(setq exwm-manage-configurations
   '(((member exwm-class-name '("Nyxt"))
	   char-mode t)))

EXWM config

And the EXWM config itself.

(defun my/exwm-init ()
  (exwm-workspace-switch 1)

  (my/run-in-background "gpgconf --reload gpg-agent")
  (when (my/is-arch)
    (my/run-in-background "set_layout")))

(defun my/exwm-update-class ()
  (exwm-workspace-rename-buffer (format "EXWM :: %s" exwm-class-name)))

(defun my/exwm-set-alpha (alpha)
  (setf (alist-get 'alpha default-frame-alist)
	`(,alpha . ,alpha))
  (cl-loop for frame being the frames
	   do (set-frame-parameter frame 'alpha `(,alpha . ,alpha))))

(use-package exwm
  :straight t
  (setq exwm-workspace-number 5)
  (add-hook 'exwm-init-hook #'my/exwm-init)
  (add-hook 'exwm-update-class-hook #'my/exwm-update-class)

  (require 'exwm-randr)
  (start-process-shell-command "xrandr" nil "~/bin/scripts/screen-layout")
  (when (string= (system-name) "violet")
    (setq my/exwm-another-monitor "DP-1")
    (setq exwm-randr-workspace-monitor-plist `(2 ,my/exwm-another-monitor 3 ,my/exwm-another-monitor)))

  (setq exwm-workspace-warp-cursor t)
  (setq mouse-autoselect-window t)
  (setq focus-follows-mouse t)


  (if (my/light-p)
      (my/exwm-set-alpha 100)
    (my/exwm-set-alpha 90))



Guix dependency Disabled
i3lock true

i3lock is disabled because the global one has to be used.

i3wm is a manual tiling window manager, which is currently my window manager of choice. I’ve tried several alternatives, including xmonad & EXWM, but i3 seems to fit my workflow best and decided to switch to EXWM. This section is kept for a few cases when I need to be extra sure that my WM doesn’t fail.

i3-gaps is an i3 fork with a few features like window gaps. I like to enable inner gaps when there is at least one container in a workspace.


General settings

set $mod Mod4
font pango:monospace 10

# Use Mouse+$mod to drag floating windows to their wanted position
floating_modifier $mod

# Move cursor between monitors
mouse_warping output

# Apply XFCE Settings
# exec xfsettingsd
# exec xiccd

# Set screen layout
exec ~/bin/scripts/screen-layout

# Most needed keybindigs
# reload the configuration file
bindsym $mod+Shift+c reload

# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym $mod+Shift+r restart

# exit i3 (logs you out of your X session)
bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"

Managing windows

Guix dependency

Some keybindings for managing windows.

emacs-i3-integration is a script to pass some command to Emacs to get a consistent set of keybindings in both i3 and Emacs. Check out the section in for details.

Kill focused windows

bindsym $mod+Shift+q exec emacs-i3-integration kill

Change focus

bindsym $mod+h exec emacs-i3-integration focus left
bindsym $mod+j exec emacs-i3-integration focus down
bindsym $mod+k exec emacs-i3-integration focus up
bindsym $mod+l exec emacs-i3-integration focus right

bindsym $mod+Left exec emacs-i3-integration focus left
bindsym $mod+Down exec emacs-i3-integration focus down
bindsym $mod+Up exec emacs-i3-integration focus up
bindsym $mod+Right exec emacs-i3-integration focus right

Move windows around

bindsym $mod+Shift+h exec emacs-i3-integration move left
bindsym $mod+Shift+j exec emacs-i3-integration move down
bindsym $mod+Shift+k exec emacs-i3-integration move up
bindsym $mod+Shift+l exec emacs-i3-integration move right

bindsym $mod+Shift+Left exec emacs-i3-integration move left
bindsym $mod+Shift+Down exec emacs-i3-integration move down
bindsym $mod+Shift+Up exec emacs-i3-integration move up
bindsym $mod+Shift+Right exec emacs-i3-integration move right

Split windows

bindsym $mod+s exec emacs-i3-integration split h
bindsym $mod+v exec emacs-i3-integration split v

Switch tabs

bindsym $mod+period exec i3-switch-tabs right
bindsym $mod+comma exec i3-switch-tabs left

Enter fullscreen mode

# enter fullscreen mode for the focused container
bindsym $mod+f fullscreen toggle
bindsym $mod+c fullscreen toggle global

Changing layout

bindsym $mod+w layout stacking
bindsym $mod+t layout tabbed
bindsym $mod+e exec emacs-i3-integration layout toggle split

Toggle tiling/floating, switch between tiled and floating windows

bindsym $mod+Shift+f floating toggle
bindsym $mod+z focus mode_toggle

Switching outputs

bindsym $mod+Tab move workspace to output right
bindsym $mod+q focus output right

Focus parent and child container

bindsym $mod+a focus parent
bindsym $mod+Shift+A focus child

Toggle sticky

bindsym $mod+Shift+i sticky toggle

Set windows as floating and sticky, move to the top right.

bindsym $mod+x floating enable; sticky enable; move position 1220 0; resize set width 700 px


set $w1 "1 🚀"
set $w2 "2 🌍"
set $w3 "3 💬"
set $w4 "4 🛠️️"
set $w7 "7 🛰️"
set $w8 "8 📝"
set $w9 "9 🎵"
set $w10 "10 📦"

bindsym $mod+1 workspace $w1
bindsym $mod+2 workspace $w2
bindsym $mod+3 workspace $w3
bindsym $mod+4 workspace $w4
bindsym $mod+5 workspace 5
bindsym $mod+6 workspace 6
bindsym $mod+7 workspace $w7
bindsym $mod+8 workspace $w8
bindsym $mod+9 workspace $w9
bindsym $mod+0 workspace $w10

# move focused container to workspace
bindsym $mod+Shift+1 move container to workspace $w1
bindsym $mod+Shift+2 move container to workspace $w2
bindsym $mod+Shift+3 move container to workspace $w3
bindsym $mod+Shift+4 move container to workspace $w4
bindsym $mod+Shift+5 move container to workspace 5
bindsym $mod+Shift+6 move container to workspace 6
bindsym $mod+Shift+7 move container to workspace $w7
bindsym $mod+Shift+8 move container to workspace $w8
bindsym $mod+Shift+9 move container to workspace $w9
bindsym $mod+Shift+0 move container to workspace $w10


Rules to automatically assign applications to workspaces and do other stuff, like enable floating.

Most apps can be distinguished by a WM class (you can get one with xprop), but in some cases it doesn’t work, e.g. for terminal applications. In that case rules can be based on a window title, for instance.

However, watch out for the following: rule such as for_window [title="ncmpcpp.*"] move to workspace $w9 will move any window with a title starting with ncmpcpp to workspace $w9. For instance, it moves your browser when you google “ncmpcpp”.

assign [class="Emacs"] $w1
assign [class="qutebrowser"] $w2
assign [class="firefox"] $w2
assign [class="VK"] $w3
assign [class="Slack"] $w3
assign [class="discord"] $w3
assign [class="TelegramDesktop"] $w3
assign [class="Postman"] $w4
assign [class="Chromium-browse"] $w4
assign [class="chromium"] $w4
assign [class="google-chrome"] $w4
assign [title="Vue Developer Tools"] $w4
assign [class="Google Play Music Desktop Player"] $w9
assign [class="jetbrains-datagrip"] $w4
assign [class="zoom"] $w7
assign [class="skype"] $w7
assign [class="Mailspring"] $w8
assign [class="Thunderbird"] $w8
assign [class="Joplin"] $w8
assign [class="keepassxc"] $w10

for_window [title="VirtScreen"] floating enable

for_window [title="ncmpcpp.*"] move to workspace $w9
for_window [title="newsboat.*"] move to workspace $w9
for_window [title=".*run_wego"] move to workspace $w9
for_window [class="cinnamon-settings*"] floating enable
for_window [title="Picture-in-Picture"] sticky enable
for_window [window_role="GtkFileChooserDialog"] resize set width 1000 px height 800 px
for_window [window_role="GtkFileChooserDialog"] move position center


Scratch terminal, inspired by this Luke Smith’s video.

Launch script

First of all, we have to distinguish a scratchpad terminal from a normal one. To do that, one can create st with a required classname.

Then, it would be cool not to duplicate scratchpads, so the following script first looks for a window with a created classname. If it exists, the script just toggles the scratchpad visibility. Otherwise, a new instance of a window is created.

COMMAND="alacritty --class $CLASSNAME -e tmux new-session -s $CLASSNAME"
pid=$(xdotool search --classname "dropdown_tmux")
if [[ ! -z $pid  ]]; then
    i3-msg scratchpad show
    setsid -f ${COMMAND}

i3 config

# Scratchpad
for_window [instance="dropdown_*"] floating enable
for_window [instance="dropdown_*"] move scratchpad
for_window [instance="dropdown_*"] sticky enable
for_window [instance="dropdown_*"] scratchpad show
for_window [instance="dropdown_*"] move position center

bindsym $mod+u exec ~/bin/scripts/dropdown

Gaps & borders

The main reason to use i3-gaps

# Borders
# for_window [class=".*"] border pixel 0
default_border pixel 3
hide_edge_borders both

# Gaps
set $default_inner 10
set $default_outer 0

gaps inner $default_inner
gaps outer $default_outer

smart_gaps on


mode "inner gaps" {
    bindsym plus gaps inner current plus 5
    bindsym minus gaps inner current minus 5
    bindsym Shift+plus gaps inner all plus 5
    bindsym Shift+minus gaps inner all minus 5
    bindsym 0 gaps inner current set 0
    bindsym Shift+0 gaps inner all set 0

    bindsym r gaps inner current set $default_inner
    bindsym Shift+r gaps inner all set $default_inner

    bindsym Return mode "default"
    bindsym Escape mode "default"

mode "outer gaps" {
    bindsym plus gaps outer current plus 5
    bindsym minus gaps outer current minus 5
    bindsym Shift+plus gaps outer all plus 5
    bindsym Shift+minus gaps outer all minus 5
    bindsym 0 gaps outer current set 0
    bindsym Shift+0 gaps outer all set 0

    bindsym r gaps outer current set $default_outer
    bindsym Shift+r gaps outer all set $default_outer

    bindsym Return mode "default"
    bindsym Escape mode "default"

bindsym $mod+g mode "inner gaps"
bindsym $mod+Shift+g mode "outer gaps"

Move & resize windows

Guix dependency

A more or less standard set of keybindings to move & resize floating windows. Just be careful to always make a way to return from these new modes, otherwise you’d end up in a rather precarious situation.

i3-balance-workspace is a small Python package to balance the i3 windows, but for the Emacs integration I also want this button to balance the Emacs windows, so here is a small script to do just that.

if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then
    emacsclient -e "(balance-windows)" &
mode "resize" {

    bindsym h exec emacs-i3-integration resize shrink width 10 px or 10 ppt
    bindsym j exec emacs-i3-integration resize grow height 10 px or 10 ppt
    bindsym k exec emacs-i3-integration resize shrink height 10 px or 10 ppt
    bindsym l exec emacs-i3-integration resize grow width 10 px or 10 ppt

    bindsym Shift+h exec emacs-i3-integration resize shrink width 100 px or 100 ppt
    bindsym Shift+j exec emacs-i3-integration resize grow height 100 px or 100 ppt
    bindsym Shift+k exec emacs-i3-integration resize shrink height 100 px or 100 ppt
    bindsym Shift+l exec emacs-i3-integration resize grow width 100 px or 100 ppt

    # same bindings, but for the arrow keys
    bindsym Left  exec emacs-i3-integration resize shrink width 10 px or 10 ppt
    bindsym Down  exec emacs-i3-integration resize grow height 10 px or 10 ppt
    bindsym Up    exec emacs-i3-integration resize shrink height 10 px or 10 ppt
    bindsym Right exec emacs-i3-integration resize grow width 10 px or 10 ppt

    bindsym Shift+Left  exec emacs-i3-integration resize shrink width 100 px or 100 ppt
    bindsym Shift+Down  exec emacs-i3-integration resize grow height 100 px or 100 ppt
    bindsym Shift+Up    exec emacs-i3-integration resize shrink height 100 px or 100 ppt
    bindsym Shift+Right exec emacs-i3-integration resize grow width 100 px or 100 ppt

    bindsym equal exec i3-emacs-balance-windows

    # back to normal: Enter or Escape
    bindsym Return mode "default"
    bindsym Escape mode "default"

bindsym $mod+r mode "resize"

mode "move" {
    bindsym $mod+Tab focus right

    bindsym Left  move left
    bindsym Down  move down
    bindsym Up    move up
    bindsym Right move right

    bindsym h     move left
    bindsym j     move down
    bindsym k     move up
    bindsym l     move right

    # back to normal: Enter or Escape
    bindsym Return mode "default"
    bindsym Escape mode "default"

bindsym $mod+m mode "move" focus floating

OFF (OFF) Intergration with dmenu

dmenu is a dynamic menu program for X. I’ve opted out of using it in favour of rofi, but here is a relevant bit of config.

Scripts are located in the bin/scripts folder.

# dmenu
bindsym $mod+d exec i3-dmenu-desktop --dmenu="dmenu -l 10"
bindsym $mod+apostrophe mode "dmenu"

mode "dmenu" {
    bindsym d exec i3-dmenu-desktop --dmenu="dmenu -l 10"; mode default
    bindsym p exec dmenu_run -l 10; mode default
    bindsym m exec dmenu-man; mode default
    bindsym b exec dmenu-buku; mode default
    bindsym f exec dmenu-explore; mode default
    bindsym t exec dmenu-tmuxp; mode default
    bindsym Escape mode "default"

bindsym $mod+b exec --no-startup-id dmenu-buku

Integration with rofi

Keybindings to launch rofi. For more detail, look the Rofi section.

bindsym $mod+p exec "rofi -modi 'drun,run' -show drun"
bindsym $mod+b exec --no-startup-id rofi-buku-mine
bindsym $mod+minus exec rofi-pass
bindsym $mod+equal exec rofimoji

bindsym $mod+apostrophe mode "rofi"

mode "rofi" {
    bindsym d exec "rofi -modi 'drun,run' -show drun"
    bindsym m exec rofi-man; mode default
    bindsym b exec rofi-buku-mine; mode default
    bindsym k exec rofi-pass; mode default
    bindsym Escape mode "default"

Launching apps & misc keybindings

I prefer to use a separate mode to launch most of my apps, with some exceptions.


# Launch apps
# start a terminal at workspace 1
bindsym $mod+Return exec "i3-msg 'workspace 1 🚀; exec alacritty'"

bindsym $mod+i exec "copyq menu"
bindsym $mod+Shift+x exec "i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png"

bindsym $mod+semicolon mode "apps"

mode "apps" {
    bindsym Escape mode "default"
    bindsym b exec firefox; mode default
    bindsym v exec vk; mode default
    bindsym s exec slack-wrapper; mode default;
    bindsym d exec "flatpak run com.discordapp.Discord"; mode default;
    bindsym m exec "alacritty -e ncmpcpp"; mode default
    bindsym c exec "copyq toggle"; mode default
    bindsym k exec "keepassxc"; mode default
    # bindsym e exec mailspring; mode default
    bindsym a exec emacs; mode default
    bindsym n exec "alacritty -e newsboat"; mode default
    bindsym w exec "alacritty /home/pavel/bin/scripts/run_wego"; mode default
    # bindsym a exec emacsclient -c; mode default
    # bindsym Shift+a exec emacs; mode default

Media controls & brightness

# Pulse Audio controls
bindsym XF86AudioRaiseVolume exec --no-startup-id "ponymix increase 5 --max-volume 150"
bindsym XF86AudioLowerVolume exec --no-startup-id "ponymix decrease 5 --max-volume 150"
bindsym XF86AudioMute exec --no-startup-id "ponymix toggle"

exec --no-startup-id xmodmap -e 'keycode 135 = Super_R' && xset -r 135
bindsym $mod+F2 exec --no-startup-id "ponymix increase 5"
bindsym $mod+F3 exec --no-startup-id "ponymix decrease 5"

# Media player controls
bindsym XF86AudioPlay exec mpc toggle
bindsym XF86AudioPause exec mpc pause
bindsym XF86AudioNext exec mpc next
bindsym XF86AudioPrev exec mpc prev

# Screen brightness
bindsym XF86MonBrightnessUp exec light -A 5
bindsym XF86MonBrightnessDown exec light -U 5


# Screenshots
bindsym --release Print exec "flameshot gui"
bindsym --release Shift+Print exec "xfce4-screenshooter"


Application of the XResources theme to the WM.

exec xrdb -merge $HOME/.Xresources

# Colors
set_from_resource $bg-color            background
set_from_resource $active-color        color4
set_from_resource $inactive-bg-color   color8
set_from_resource $text-color          foreground
set_from_resource $inactive-text-color color7
set_from_resource $urgent-bg-color     color1
set_from_resource $urgent-text-color   color0

# window colors
#                       border              background         text                 indicator       child border
client.focused          $active-color       $bg-color          $text-color          $bg-color       $active-color
client.unfocused        $bg-color           $inactive-bg-color $inactive-text-color $bg-color       $bg-color
client.focused_inactive $active-color       $inactive-bg-color $inactive-text-color $bg-color       $bg-color
client.urgent           $urgent-bg-color    $urgent-bg-color   $urgent-text-color   $bg-color       $urgent-bg-color

OFF (OFF) i3blocks

I’ve opted out of i3bar & i3blocks for polybar

bar {
    status_command i3blocks -c ~/.config/i3/i3blocks.conf
    i3bar_command i3bar
    font pango:monospace 12
    output HDMI-A-0
    tray_output none
    colors {
	background $bg-color
	separator #757575
	#                  border             background         text
	focused_workspace  $bg-color          $bg-color          $text-color
	inactive_workspace $inactive-bg-color $inactive-bg-color $inactive-text-color
	urgent_workspace   $urgent-bg-color   $urgent-bg-color   $urgent-text-color

bar {
    status_command i3blocks -c ~/.config/i3/i3blocks.conf
    i3bar_command i3bar
    font pango:monospace 10
    output DVI-D-0
    colors {
	background $bg-color
	separator #757575
	#                  border             background         text
	focused_workspace  $bg-color          $bg-color          $text-color
	inactive_workspace $inactive-bg-color $inactive-bg-color $inactive-text-color
	urgent_workspace   $urgent-bg-color   $urgent-bg-color   $urgent-text-color

Keyboard Layout

A script to set Russian-English keyboard layout:

setxkbmap -layout us,ru
setxkbmap -model pc105 -option 'grp:win_space_toggle' -option 'grp:alt_shift_toggle'

A script to toggle the layout

if setxkbmap -query | grep -q us,ru; then
    setxkbmap -layout us
    setxkbmap -option
    setxkbmap -layout us,ru
    setxkbmap -model pc105 -option 'grp:win_space_toggle' -option 'grp:alt_shift_toggle'

And the relevant i3 settings:

# Layout
exec_always --no-startup-id set_layout
bindsym $mod+slash exec toggle_layout


# Polybar
exec_always --no-startup-id "bash /home/pavel/bin/"

# Wallpaper
exec_always "feh --bg-fill ~/Pictures/wallpaper.jpg"

# Picom
exec picom

# Keynav
exec keynav

# Applets
exec --no-startup-id nm-applet
# exec --no-startup-id /usr/bin/blueman-applet

exec shepherd
exec dunst
exec copyq
exec "xmodmap ~/.Xmodmap"
# exec "xrdb -merge ~/.Xresources"
# exec "bash ~/bin/"


Category Guix dependency Description
desktop-polybar polybar statusbar

Polybar is a nice-looking, WM-agnostic statusbar program.

Don’t forget to install the Google Noto Color Emoji font. Guix package with all Noto fonts is way too large.


General settings

This is the most crazy advanced piece of my literate configuration so far.

My polybar has:

  • colors from the general color theme;
  • powerline-ish decorations between modules.


The “colors” part is straightforward enough. Once upon the time it was so…

Polybar can use Xresources, but the problem with that is you’re supposed to use colorX as foreground, not as background. This usually works fine with dark themes from doom-themes, but not so much with high-contrast modus-themes.


 (lambda (elem)
   (format "%s = %s" (car elem) (cdr elem)))
    (lambda (acc name)
      (let* ((color (my/color-value name)))
	(unless (member name '("black"))
	  (setq color (ct-iterate
		       (lambda (c) (ct-edit-hsl-l-inc c 2))
		       (lambda (c)
			 (ct-light-p c 65)))))
	(push (cons name color) acc)
	(push (cons (format "light-%s" name)
	(push (cons (format "dark-%s" name)
	      acc) )
    '("black" "red" "green" "yellow" "blue" "magenta" "cyan" "white")
    :initial-value nil))
  `(("background" . ,(or (my/color-value 'bg-active)
			 (my/color-value 'bg)))
    ("foreground" . "#000000")))

Glyph settings

As for the module decorations though, I find it ironic that with all this fancy rendering around I have to resort to Unicode glyphs.

Anyhow, the approach is to put a glyph between two blocks like this:

block1  block2

And set the foreground and background colors like that:

block1 glyph block2
foreground F1 B2 F2
background B1 B1 B2

So, that’s a start. First, let’s define the glyph symbols in the polybar config:

gleft = 
gright = 

Defining modules

As we want to interweave polybar modules with these glyphs in the right order and with the right colors, it is reasonable to define a single source of truth:

Index Module Color Glyph
1 pulseaudio light-magenta +
2 mpd magenta +
16 nvidia light-cyan +
3 cpu cyan +
15 temperature cyan +
9 battery cyan +
4 ram-memory light-green +
5 swap-memory green +
6 bandwidth light-red +
7 openvpn light-red
8 xkeyboard red +
10 weather light-yellow +
12 sun yellow +
13 aw-afk light-blue +
14 date blue +

Also excluding some modules from certain monitors, which for now is about excluding battery from the monitors of my desktop PC:

Monitor Exclude
DVI-D-0 battery
HDMI-A-0 battery
HDMI-0 battery
DP-1 battery
eDP nvidia
eDP-1 nvidia
DVI-D-0 nvidia
HDMI-A-0 nvidia
HDMI-1 nvidia

Another thing we need to do is to set the color of modules in accordance with the polybar_modules table. The background can be determined from the Color column with the following code block:

   (lambda (el) (string-equal (nth 1 el) module))

That block is meant to be invoked in each module definition.

Generating glyphs

To generate the required set of glyphs, we need a glyph for every possible combination of adjacent colors that can occur in polybar.

Most of these combinations can be inferred from the polybar_modules table, the rest are defined in another table:

Color 1 Color 2
background white
background light-magenta
blue background

(let* ((monitors
	  (seq-map (lambda (el) (nth 0 el)))
	 (lambda (monitor)
	    (lambda (el) (nth 1 el))
	     (lambda (el) (and (string-equal (nth 0 el) monitor)
			       (nth 1 el)))
	 `(,@monitors "")))
	   (lambda (exclude)
		(lambda (elt)
		  (not (or
			(member (nth 1 elt) exclude)
			(not (string-equal (nth 3 elt) "+")))))))))
       (color-changes nil))
  (dolist (e extra)
     (concat (nth 0 e) "--" (nth 1 e))))
  (dolist (comb module-glyph-combinations)
    (dotimes (i (1- (length comb)))
       (concat (nth 2 (nth i comb))
	       (nth 2 (nth (1+ i) comb))))))
   (lambda (el)
     (let ((colors (split-string el "--")))
       (format "
type = custom/text
content-background = ${colors.%s}
content-foreground = ${colors.%s}
content = ${glyph.gright}
content-font = 5"
	       (nth 0 colors)
	       (nth 1 colors)
	       (nth 0 colors)
	       (nth 1 colors))))

Here’s a rough outline of how the code works:

  • monitors is a list of unique monitors in exclude-table
  • exclude-combilnations is a list of lists of module names to be excluded for each monitor
  • module-glyphs-combinations is a list of lists of actual modules for each monitor
  • color-changes is a list of unique adjacent colors across modules in all monitors

Finally, color-changes is used to generate glyph modules that look like this:

type = custom/text
content-background = ${colors.light-cyan}
content-foreground = ${colors.cyan}
content = ${glyph.gright}
content-font = 5

As of now, 15 of such modules is generated.

Include this to the polybar config itself:


Generating set of modules

To configure polybar itself, we need to generate a set of modules for each monitor.

The parameters here, excluding the two required tables, are:

  • monitor - the current monitor on which to filter out the blocks by the polybar_modules_exclude table,
  • first-color - the first color of the first glyph,
  • last-color - the second color of the last glyph.

(let* ((exclude-modules
	  (seq-filter (lambda (el) (string-equal (nth 0 el) monitor)))
	  (seq-map (lambda (el) (nth 1 el)))))
	  (seq-filter (lambda (el) (not (member (nth 1 el) exclude-modules))))))
       (prev-color first-color)
       (ret nil))
    (lambda (el)
	(when (string-equal (nth 3 el) "+")
	  (setq ret (format "glyph-%s--%s " prev-color (nth 2 el)))
	  (setq prev-color (nth 2 el))
	(nth 1 el))))
    " ")
   (unless (string-empty-p last-color) (format " glyph-%s--%s " prev-color last-color))))

The polybar config doesn’t support conditional statements, but it does support environment variables, so I pass the parameters from in the launch script.

Global bar config

Global bar configuration.

Monitor config and base colors.

monitor = ${env:MONITOR:}
width = 100%
height = ${env:HEIGHT:27}
fixed-center = false
bottom = ${env:POLYBAR_BOTTOM:true}

background = ${colors.background}
foreground = ${}

Some geometry settings. These are set this way to make glyphs look the way they should

; line-size = 3
line-color = #f00

padding = 0

module-margin-left = 0
module-margin-right = 0
margin-bottom = 0
margin-top = 0

; underline-size = 0
border-size = 0

offset-x = 0
offset-y = 0
radius = 0.0


; font-0 = ${env:FONT0:pango:monospace:size=10;1}
; font-1 = ${env:FONT1:NotoEmoji:scale=10:antialias=false;0}
; font-2 = ${env:FONT2:fontawesome:pixelsize=10;1}
; font-3 = ${env:FONT3:JetBrains Mono Nerd Font:monospace:size=10;1}

font-0 = pango:monospace:size=13;2
font-1 = NotoEmoji:scale=10:antialias=false;1
font-2 = fontawesome:pixelsize=13;3
font-3 = JetBrains Mono Nerd Font:monospace:size=13;4
font-4 = JetBrains Mono Nerd Font:monospace:size=17;4

Modules. Because I sometimes set up different blocks on different monitors, they are set via environment variables.

modules-left = i3
; modules-center = test
modules-right = ${env:RIGHT_BLOCKS}

tray-position = ${env:TRAY:right}
tray-padding = 0
tray-maxsize = 16
tray-background = ${colors.background}

wm-restack = i3
; override-redirect = true

scroll-up = i3wm-wsnext
scroll-down = i3wm-wsprev

; cursor-click = pointer
; cursor-scroll = ns-resize

Misc settings.

screenchange-reload = true
compositing-background = source
compositing-foreground = over
compositing-overline = over
compositing-underline = over
compositing-border = over

margin-top = 0
margin-bottom = 0

Launch script

The script below allows me to:

  • have different blocks on my two different-sized monitors and my laptop;
  • have different settings on my desktop PC and laptop;
# Settings varying on the hostname
if [ "$hostname" = "azure" ]; then
elif [ "$hostname" = "eminence" ]; then
    if xrandr --query | grep " connected" | cut -d" " -f1 | grep -q "HDMI-A-0"; then
elif [ "$hostname" = "iris" ]; then

# Setting varying on the monitor
declare -A FONT_SIZES=(
declare -A EMOJI_SCALE=(
declare -A BAR_HEIGHT=(
declare -A BLOCKS=(


# Geolocation for some modules
export LOC="SPB"

# export IPSTACK_API_KEY=$(pass show My_Online/APIs/ipstack | head -n 1)

pkill polybar
for m in $(xrandr --query | grep " connected" | cut -d" " -f1); do
    export MONITOR=$m
    if [ "$MONITOR" = "$TRAY_MONITOR" ]; then
	export TRAY="right"
	export TRAY="none"
    if [[ -z "$SCALE" ]]; then
    # export FONT0="pango:monospace:size=$SIZE;1"
    # export FONT1="NotoEmoji:scale=$SCALE:antialias=false;1"
    # export FONT2="fontawesome:pixelsize=$SIZE;1"
    # export FONT3="JetBrains Mono Nerd Font:monospace:size=15;1"
    export TEMP_HWMON_PATH=${TEMP}
    polybar mybar &

Individual modules

Some of the custom modules below use Org mode noweb to evaluate colors, because it’s faster than querying xrdb at runtime. I wish I could reference polybar values there, but it looks like this is impossible.

If you want to copy something, you can go to the bin/polybar folder.


PulseAudio status

type = internal/pulseaudio
use-ui-max = true

bar-volume-width = 7
; bar-volume-foreground-0 = ${colors.white}
; bar-volume-foreground-1 = ${colors.yellow}
; bar-volume-foreground-2 = ${colors.yellow}
; bar-volume-foreground-3 = ${}
; bar-volume-foreground-4 = ${}
; bar-volume-foreground-5 = ${}
; bar-volume-foreground-6 = ${}
bar-volume-gradient = false
bar-volume-indicator = |
bar-volume-indicator-font = 2
bar-volume-fill = 
bar-volume-fill-font = 2
bar-volume-empty = 
bar-volume-empty-font = 2
; bar-volume-empty-foreground = ${colors.light-white}

format-volume = ♪ <ramp-volume> <label-volume>
label-volume = %percentage%%

ramp-volume-0 = 
ramp-volume-1 = 
ramp-volume-2 = 
ramp-volume-3 = 
ramp-volume-4 = 
ramp-volume-5 = 
ramp-volume-6 = 
ramp-volume-7 = 

format-muted = ♪ <label-muted>
label-muted = MUTE

format-volume-background = <<get-polybar-bg(module="pulseaudio")>>
format-muted-background = <<get-polybar-bg(module="pulseaudio")>>
format-volume-foreground = ${colors.foreground}
format-muted-foreground = ${colors.foreground}

; format-volume-underline = ${colors.white}
; format-muted-underline = ${colors.light-black}


Music Player Daemon status

type = internal/mpd

format-playing = <toggle> <label-time> <label-song>
format-paused = <toggle> <label-time> <label-song>
format-stopped = " "
label-song = [%album-artist%] %title%
label-time = %elapsed%/%total%

label-song-maxlen = 30
label-song-ellipsis = true

; format-playing-underline = ${colors.yellow}
; format-paused-underline = ${colors.yellow}
; format-stopped-underline = ${colors.yellow}

format-playing-background = <<get-polybar-bg(module="mpd")>>
format-paused-background = <<get-polybar-bg(module="mpd")>>
format-stopped-background = <<get-polybar-bg(module="mpd")>>
format-playing-foreground = ${colors.foreground}
format-paused-foreground = ${colors.foreground}
format-stopped-foreground = ${colors.foreground}

label-separator = 0
separator-foreground = ${}

icon-pause = 
icon-play = 
icon-stop = 
icon-prev = 1
icon-next = 2


CPU usage

type = internal/cpu
format = " <label>"
label = %percentage%%
format-background = <<get-polybar-bg(module="cpu")>>
format-foreground = ${colors.foreground}


Display NVIDIA usage with nvidia-smi

nvidia-smi --query-gpu=utilization.gpu,power.draw,temperature.gpu,memory.used --format=csv,noheader | sed -s 's/ %/%/;s/W, [0-9]\+/&°C/;s/,/  /g'
type = custom/script
exec = /home/pavel/bin/polybar/
interval = 2
format =  <label>
; tail = true

format-background = <<get-polybar-bg(module="nvidia")>>
format-foreground = ${colors.foreground}


RAM usage

type = internal/memory
interval = 10

ramp-used-0 = 
ramp-used-1 = 
ramp-used-2 = 
ramp-used-3 = 
ramp-used-4 = 
ramp-used-5 = 
ramp-used-6 = 
ramp-used-7 = 

format =  <label>

; format-underline = ${}
format-background = <<get-polybar-bg(module="ram-memory")>>
format-foreground = ${colors.foreground}


Swap usage

type = internal/memory
interval = 10

label= %gb_swap_used:.1f%
format-background = <<get-polybar-bg(module="swap-memory")>>
format-foreground = ${colors.foreground}


Upload/download speed

UPD <2022-07-24 Sun>: Somehow it doesn’t work with my current internet setup.

type = internal/network
interval = 1

interface = ${env:WLAN_INTERFACE}

; format-connected = [<ramp-signal>] <label-connected>

label-connected = ↓ %downspeed% ↑ %upspeed%
label-disconnected = X

; format-connected-underline = ${}
; format-disconnected-underline = ${}
format-connected-background = <<get-polybar-bg(module="network")>>
format-disconnected-background = <<get-polybar-bg(module="network")>>
format-connected-foreground = ${colors.foreground}
format-disconnected-foreground = ${colors.foreground}

ramp-signal-0 = 0
ramp-signal-1 = 1
ramp-signal-2 = 2
ramp-signal-3 = 3
ramp-signal-4 = 4
ramp-signal-5 = 5


My adaption of an i3blocks script called “bandwidth3”. I’ve only changed some defaults that are awkward to set with polybar.

type = custom/script
exec = /home/pavel/bin/polybar/
interval = 0
tail = true

format-background = <<get-polybar-bg(module="bandwidth")>>
format-foreground = ${colors.foreground}
# Copyright (C) 2015 James Murphy
# Copyright (C) 2022 Pavel Korytov
# Licensed under the terms of the GNU GPL v2 only.

printf_command="${PRINTF_COMMAND:-"printf \"↓ %-2.1f ↑ %2.1f [%s/s]\\n\", rx, wx, unit;"}"

function default_interface {
    ip route | awk '/^default via/ {print $5; exit}'

function check_proc_net_dev {
    if [ ! -f "/proc/net/dev" ]; then
	echo "/proc/net/dev not found"
	exit 1

function list_interfaces {
    echo "Interfaces in /proc/net/dev:"
    grep -o "^[^:]\\+:" /proc/net/dev | tr -d " :"

while getopts i:t:u:p:lh opt; do
    case "$opt" in
	i) iface="$OPTARG" ;;
	t) dt="$OPTARG" ;;
	u) unit="$OPTARG" ;;
	p) printf_command="$OPTARG" ;;
	l) list_interfaces && exit 0 ;;
	h) printf \
"Usage: bandwidth3 [-i interface] [-t time] [-u unit] [-p printf_command] [-l] [-h]
-i\tNetwork interface to measure. Default determined using \`ip route\`.
-t\tTime interval in seconds between measurements. Default: 3
-u\tUnits to measure bytes in. Default: Mb
\tAllowed units: Kb, KB, Mb, MB, Gb, GB, Tb, TB
\tUnits may have optional it/its/yte/ytes on the end, e.g. Mbits, KByte
-p\tAwk command to be called after a measurement is made.
\tDefault: printf \"<span font='FontAwesome'>  </span>%%-5.1f/%%5.1f %%s/s\\\\n\", rx, wx, unit;
\tExposed variables: rx, wx, tx, unit, iface
-l\tList available interfaces in /proc/net/dev
-h\tShow this help text
" && exit 0;;


while [ -z "$iface" ]; do
    echo No default interface
    sleep "$dt"

case "$unit" in
    Kb|Kbit|Kbits)   bytes_per_unit=$((1024 / 8));;
    KB|KByte|KBytes) bytes_per_unit=$((1024));;
    Mb|Mbit|Mbits)   bytes_per_unit=$((1024 * 1024 / 8));;
    MB|MByte|MBytes) bytes_per_unit=$((1024 * 1024));;
    Gb|Gbit|Gbits)   bytes_per_unit=$((1024 * 1024 * 1024 / 8));;
    GB|GByte|GBytes) bytes_per_unit=$((1024 * 1024 * 1024));;
    Tb|Tbit|Tbits)   bytes_per_unit=$((1024 * 1024 * 1024 * 1024 / 8));;
    TB|TByte|TBytes) bytes_per_unit=$((1024 * 1024 * 1024 * 1024));;
    *) echo Bad unit "$unit" && exit 1;;

scalar=$((bytes_per_unit * dt))
init_line=$(cat /proc/net/dev | grep "^[ ]*$iface:")
if [ -z "$init_line" ]; then
    echo Interface not found in /proc/net/dev: "$iface"
    exit 1

init_received=$(awk '{print $2}' <<< $init_line)
init_sent=$(awk '{print $10}' <<< $init_line)

(while true; do cat /proc/net/dev; sleep "$dt"; done) |\
    stdbuf -oL grep "^[ ]*$iface:"|\
    awk -v scalar="$scalar" -v unit="$unit" -v iface="$iface" '
    if(rx >= 0 && wx >= 0){


Category Guix dependency Description
desktop-polybar bind:utils Provides dig
desktop-polybar curl
desktop-polybar jq util to work with JSONs

A module to get a country of the current IP and openvpn status. Uses ipstack API.

ip=$(dig +short +timeout=1 2> /dev/null)
# API_KEY="$(pass show My_Online/APIs/ipstack | head -n 1)"
if [[ -z $ip || $ip == *"timed out"* ]]; then
    echo "%{u<<get-color(name="red")>>}%{+u} ?? %{u-}"
ip_info=$(curl -s${ip}?access_key=${API_KEY})
# emoji=$(echo $ip_info | jq -r '.location.country_flag_emoji')
code=$(echo $ip_info | jq -r '.country_code' 2> /dev/null)
vpn=$(pgrep -a openvpn$ | head -n 1 | awk '{print $NF }' | cut -d '.' -f 1)

if [[ -z $code ]]; then

if [ -n "$vpn" ]; then
    echo "%{u<<get-color(name="blue")>>}%{+u}  $code %{u-}"
    echo "%{u<<get-color(name="red")>>}%{+u}  $code %{u-}"
type = custom/script
exec = /home/pavel/bin/polybar/
interval = 1200


A module to check if openvpn is running.

vpn=$(pgrep -a openvpn$ | head -n 1 | awk '{print $NF }' | cut -d '.' -f 1)
if [ -n "$vpn" ]; then
    echo "  "
    echo "  "
type = custom/script
exec = /home/pavel/bin/polybar/
format-background = <<get-polybar-bg(module="openvpn")>>
format-foreground = ${colors.foreground}
interval = 1200


Current keyboard layout

type = internal/xkeyboard
format = <label-layout>

; format-underline = ${colors.magenta}
format-background = <<get-polybar-bg(module="xkeyboard")>>
format-foreground = ${colors.foreground}
label-layout = %icon%
layout-icon-0 = ru;RU
layout-icon-1 = us;US


type = internal/battery
battery = BAT0
adapter = ADP0

time-format = %H:%M
format-discharging = <ramp-capacity> <label-discharging>
format-discharging-background = <<get-polybar-bg(module="battery")>>
format-charging-background = <<get-polybar-bg(module="battery")>>
format-full-background = <<get-polybar-bg(module="battery")>>
format-foreground = ${colors.foreground}
label-discharging = %percentage%% %time%
label-charging =  %percentage%% %time%

ramp-capacity-0 = 
ramp-capacity-1 = 
ramp-capacity-2 = 
ramp-capacity-3 = 
ramp-capacity-4 = 


type = internal/temperature
interval = 2

hwmon-path = ${env:TEMP_HWMON_PATH}

format = <label>
format-foreground = ${colors.foreground}
format-background = <<get-polybar-bg(module="battery")>>
format-warn =  <label-warn>
format-warn-foreground = ${colors.foreground}
format-warn-background = <<get-polybar-bg(module="battery")>>


Gets current weather from


bar_weather=$(curl -s${location}?format=${bar_format} || echo "??")
if [ -z "$bar_weather" ]; then
    exit 1
elif [[ "$bar_weather" == *"Unknown"* || "$bar_weather" == *"Sorry"* || "$bar_weather" == *"Bad Gateway"* ]]; then
    echo "??"
    exit 1
    echo ${bar_weather}
type = custom/script
exec = /home/pavel/bin/polybar/
; format-underline = ${}
format-background = <<get-polybar-bg(module="weather")>>
format-foreground = ${colors.foreground}
interval = 1200


Category Guix dependency
desktop-polybar sunwait

Prints out the time of sunrise/sunset. Uses sunwait

declare -A LAT_DATA=(
declare -A LON_DATA=(
if [ -z "$LOC" ]; then
    echo "LOC?"
    exit -1

time=$(sunwait poll daylight rise ${LAT} $LON)

if [[ ${time} == 'DAY' ]]; then
    sunset=$(sunwait list daylight set ${LAT} ${LON})
    # echo "%{u<<get-color(name="yellow")>>}%{+u} $sunset %{u-}"
    echo $sunset
    sunrise=$(sunwait list daylight rise ${LAT} ${LON})
    # echo "%{u<<get-color(name="red")>>}%{+u} $sunrise %{u-}"
    echo $sunrise
type = custom/script
exec = /home/pavel/bin/polybar/
format-background = <<get-polybar-bg(module="sun")>>
format-foreground = ${colors.foreground}
interval = 60


Prints out a current uptime and non-AFK time from ActivityWatch server

Category Guix dependency
desktop-polybar dateutils
afk_event=$(curl -s -X GET "http://localhost:5600/api/0/buckets/aw-watcher-afk_$(hostname)/events?limit=1" -H "accept: application/json")
status=$(echo ${afk_event} | jq -r '.[0].data.status')
afk_time=$(echo "${afk_event}" | jq -r '.[0].duration' | xargs -I !  date -u -d @! +"%H:%M")

uptime=$(uptime | awk '{ print substr($3, 0, length($3) - 1) }' | xargs -I ! date -d ! +"%H:%M")
res="${afk_time} / ${uptime}"
if [[ $status == 'afk' ]]; then
    # echo "%{u<<get-color(name="red")>>}%{+u} [AFK] $res %{u-}"
    echo "[AFK] $res"
    # echo "%{u<<get-color(name="blue")>>}%{+u} $res %{u-}"
    echo "$res"
type = custom/script
exec = /home/pavel/bin/polybar/
interval = 60
format-background = <<get-polybar-bg(module="aw-afk")>>
format-foreground = ${colors.foreground}


Current date

type = internal/date
interval = 5

date =
date-alt = "%Y-%m-%d"

time = %H:%M
time-alt = %H:%M:%S

format-background = <<get-polybar-bg(module="date")>>
format-foreground = ${colors.foreground}
label = "%date% %time%"


Pomodoro module.

if ps -e | grep emacs >> /dev/null; then
    emacsclient --eval "(if (boundp 'pomm-current-mode-line-string) pomm-current-mode-line-string \"\") " | xargs echo -e
type = custom/script
exec = /home/pavel/bin/polybar/
interval = 1
format-underline = ${colors.light-green}
format-foreground = ${colors.foreground}


A simple separator

type = custom/text
content = "|"
content-foreground = ${colors.magenta}
content-padding = 0
content-margin = 0
interval = 100000


A separator, which appears only if monitor is set to have a tray in the launch script

if [ ! -z "$TRAY" ] && [ "$TRAY" != "none" ]; then
    echo "| "
type = custom/script
exec = /home/pavel/bin/polybar/
format-foreground = ${colors.magenta}
interval = 100000


Show i3wm workspaces

type = internal/i3
format = <label-state> <label-mode>
index-sort = true
wrapping-scroll = false

; Only show workspaces on the same output as the bar
pin-workspaces = true

label-mode-padding = 1
label-mode-foreground = ${colors.white}
label-mode-background = ${}

; focused = Active workspace on focused monitor
label-focused = %
label-focused-background = ${}
label-focused-underline= ${}
label-focused-padding = 1

; unfocused = Inactive workspace on any monitor
label-unfocused = %
label-unfocused-padding = 1
label-unfocused-foreground = ${colors.white}

; visible = Active workspace on unfocused monitor
label-visible = %
; label-visible-background = ${self.label-focused-background}
label-visible-underline = ${self.label-focused-underline}
label-visible-padding = ${self.label-focused-padding}

; urgent = Workspace with urgency hint set
label-urgent = %
label-urgent-background = ${}
label-urgent-foreground = ${}
label-urgent-padding = 1


Category Guix dependency
desktop-rofi rofi

rofi is another dynamic menu generator. It can act as dmenu replacement but offers a superset of dmenu’s features.


A theme based on the current Emacs theme. Inspired by dracula theme.

  (lambda (elem)
    (concat (nth 0 elem) ": " (my/color-value (nth 0 elem)) ";\n"))
/* Generated from [[file:../../*Theme][Theme:1]] */
 * {

    foreground:                  <<get-color(name="fg")>>;
    background:                  <<get-color(name="bg")>>;
    background-color:            <<get-color(name="bg")>>;
    separatorcolor:              @blue;
    border-color:                <<get-color(name="border")>>;
    selected-normal-background:  <<get-color(name="blue")>>;
    selected-normal-foreground:  <<get-fg-for-color(name="blue")>>;
    selected-active-background:  <<get-color(name="light-blue")>>;
    selected-active-foreground:  <<get-fg-for-color(name="light-blue")>>;
    selected-urgent-background:  <<get-color(name="red")>>;
    selected-urgent-foreground:  <<get-fg-for-color(name="red")>>;
    normal-foreground:           @foreground;
    normal-background:           @background;
    active-foreground:           @blue;
    active-background:           @background;
    urgent-foreground:           @red;
    urgent-background:           @background;
    alternate-normal-background: <<get-color(name="bg-alt")>>;
    alternate-normal-foreground: @foreground;
    alternate-active-background: <<get-fg-for-color(name="light-blue")>>;
    alternate-active-foreground: <<get-color(name="light-blue")>>;
    alternate-urgent-background: <<get-fg-for-color(name="red")>>;
    alternate-urgent-foreground: <<get-color(name="red")>>;
    spacing:                     2;
window {
    background-color: @background;
    border:           1;
    padding:          5;
mainbox {
    border:           0;
    padding:          0;
message {
    border:           1px dash 0px 0px ;
    border-color:     @separatorcolor;
    padding:          1px ;
textbox {
    text-color:       @foreground;
listview {
    fixed-height:     0;
    border:           2px dash 0px 0px ;
    border-color:     @separatorcolor;
    spacing:          2px ;
    scrollbar:        true;
    padding:          2px 0px 0px ;
element {
    border:           0;
    padding:          1px ;
element normal.normal {
    background-color: @normal-background;
    text-color:       @normal-foreground;
element normal.urgent {
    background-color: @urgent-background;
    text-color:       @urgent-foreground;
element {
    background-color: @active-background;
    text-color:       @active-foreground;
element selected.normal {
    background-color: @selected-normal-background;
    text-color:       @selected-normal-foreground;
element selected.urgent {
    background-color: @selected-urgent-background;
    text-color:       @selected-urgent-foreground;
element {
    background-color: @selected-active-background;
    text-color:       @selected-active-foreground;
element alternate.normal {
    background-color: @alternate-normal-background;
    text-color:       @alternate-normal-foreground;
element alternate.urgent {
    background-color: @alternate-urgent-background;
    text-color:       @alternate-urgent-foreground;
element {
    background-color: @alternate-active-background;
    text-color:       @alternate-active-foreground;
scrollbar {
    width:            4px ;
    border:           0;
    handle-color:     @normal-foreground;
    handle-width:     8px ;
    padding:          0;
sidebar {
    border:           2px dash 0px 0px ;
    border-color:     @separatorcolor;
button {
    spacing:          0;
    text-color:       @normal-foreground;
button selected {
    background-color: @selected-normal-background;
    text-color:       @selected-normal-foreground;
inputbar {
    spacing:          0px;
    text-color:       @normal-foreground;
    padding:          1px ;
    children:         [ prompt,textbox-prompt-colon,entry,case-indicator ];
case-indicator {
    spacing:          0;
    text-color:       @normal-foreground;
entry {
    spacing:          0;
    text-color:       @normal-foreground;
prompt {
    spacing:          0;
    text-color:       @normal-foreground;
textbox-prompt-colon {
    expand:           false;
    str:              ":";
    margin:           0px 0.3000em 0.0000em 0.0000em ;
    text-color:       inherit;


Man pages

Inspired by this Luke Smith’s video.

A script to open a man page with zathura. There is no particular reason why one should look through man pages in pdf viewer rather than in console, but why not.

SELECTED=$(man -k . | rofi -dmenu -l 20 | awk '{print $1}')
if [[ ! -z $SELECTED ]]; then
    man -Tpdf $SELECTED | zathura -


Category Guix dependency
desktop-rofi python-rofimoji


Category Guix dependency
desktop-rofi rofi-pass
desktop-rofi xset

A nice pass frontend for Rofi, which is even packaged for Guix.

default_autotype='username :tab pass'


Guix dependency

flameshot is my program of choice to make screenshots.

As it overwrites its own config all the time, I do not keep the file in VC.




Guix dependency

dunst is a lightweight notification daemon.

My customizations of the original config consist mostly of changing colors. Check out the default config or man dunst for the description of settings.


    monitor = 0
    follow = mouse
    geometry = "300x5-30+20"
    indicate_hidden = yes
    shrink = no
    transparency = 15
    notification_height = 0
    separator_height = 2
    padding = 8
    horizontal_padding = 8
    frame_width = 1
    frame_color = <<get-color(name="border", quote=1)>>
    separator_color = frame
    sort = yes
    idle_threshold = 120

    ### Text ###
    font = DejaVu Sans 9

    line_height = 0
    markup = full

    # The format of the message.  Possible variables are:
    #   %a  appname
    #   %s  summary
    #   %b  body
    #   %i  iconname (including its path)
    #   %I  iconname (without its path)
    #   %p  progress value if set ([  0%] to [100%]) or nothing
    #   %n  progress value if set without any extra characters
    #   %%  Literal %
    # Markup is allowed
    format = "<b>%s</b>\n%b"
    alignment = left
    show_age_threshold = 60
    word_wrap = yes
    ellipsize = middle
    ignore_newline = no
    stack_duplicates = true
    hide_duplicate_count = false
    show_indicators = yes

    ### Icons ###
    icon_position = left
    max_icon_size = 32
    icon_path = /usr/share/icons/Mint-Y/status/32/;/usr/share/icons/Mint-Y/devices/32

    ### History ###
    sticky_history = yes
    history_length = 20

    ### Misc/Advanced ###
    dmenu = /usr/bin/dmenu -p dunst:
    browser = /home/pavel/.guix-extra-profiles/browsers/browsers/bin/firefox
    always_run_script = true
    title = Dunst
    class = Dunst
    startup_notification = false
    verbosity = mesg
    corner_radius = 0

    ### Legacy
    force_xinerama = false

    ### mouse
    mouse_left_click = close_current
    mouse_middle_click = do_action
    mouse_right_click = close_all

    per_monitor_dpi = false

    close = ctrl+space
    close_all = ctrl+shift+space
    history = ctrl+grave
    context = ctrl+shift+period

    background = <<get-color(name="bg-other", quote=1)>>
    frame_color = <<get-color(name="border", quote=1)>>
    foreground = <<get-color(name="fg", quote=1)>>
    timeout = 10

    background = <<get-color(name="bg", quote=1)>>
    frame_color = <<get-color(name="border", quote=1)>>
    foreground = <<get-color(name="fg", quote=1)>>
    timeout = 10

    background = <<get-color(name="red", quote=1)>>
    foreground = <<get-fg-for-color(name="red", quote=1)>>
    frame_color = <<get-color(name="red", quote=1)>>
    timeout = 0


Firefox is my web browser of choice.


Tridactyl is a Firefox add-on that provides vim-like interface.

Run :nativeinstall at the first start.


The native messenger allows to configure the addon with a config file.

sanitize tridactyllocal tridactylsync

bind gn tabnew
bind gN tabclose

bind O fillcmdline tabopen

bind n findnext 1
bind N findnext -1
bind F hint -t

unbind <C-f>

set smoothscroll false
set findcase sensitive
colorscheme emacs

bind j scrollline 3
bind k scrollline -3
bind --mode=normal <C-i> mode ignore
bind --mode=ignore <C-i> mode normal

guiset_quiet gui full
guiset_quiet statuspanel left
guiset_quiet navbar none
guiset_quiet tabs always

set searchurls.g

set newtab about:blank

command fixamo_quiet jsb tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""'))
command fixamo js tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""').then(tri.excmds.fillcmdline_tmp(3000, "Permissions added to user.js. Please restart Firefox to make them take affect.")))


Then, the package has its separate theme.

I based it on base16-dracula by Chris Kempson, but replaced the colors with my Emacs theme.

:root {
  --tridactyl-fg: <<get-color(name="fg")>>;
  --tridactyl-bg: <<get-color(name="bg")>>;
  --tridactyl-url-fg: <<get-color(name="red")>>;
  --tridactyl-url-bg: <<get-color(name="bg")>>;
  --tridactyl-highlight-box-bg: <<get-color(name="blue")>>;
  --tridactyl-highlight-box-fg: <<get-fg-for-color(name="blue")>>;

  /* Command line */
  --tridactyl-cmdl-bg: <<get-color(name="bg-alt")>>
  --tridactyl-cmdl-fg: <<get-color(name="fg")>>

  /* Hint character tags */
  --tridactyl-hintspan-fg: <<get-fg-for-color(name="blue")>> !important;
  --tridactyl-hintspan-bg: <<get-color(name="blue")>> !important;

  /* Element Highlights */
  --tridactyl-hint-active-fg: none;
  --tridactyl-hint-active-bg: none;
  --tridactyl-hint-active-outline: none;
  /* --tridactyl-hint-activy-outline: var(--base08); */
  --tridactyl-hint-bg: none;
  --tridactyl-hint-outline: none;
  /* --tridactyl-hint-outline: var(--base08); */

/* a { */
/*   color: var(--base04); */
/* } */

#command-line-holder {
  order: 1;
  border: 2px solid <<get-color(name="blue")>>;
  background: <<get-color(name="bg")>>;

#tridactyl-input {
  padding: 1rem;
  color: var(--tridactyl-fg);
  width: 90%;
  font-size: 1.2rem;
  line-height: 1.5;
  background: var(--tridactyl-bg);
  padding-left: unset;
  padding: 1rem;

#completions table {
  font-size: 0.8rem;
  font-weight: 200;
  border-spacing: 0;
  table-layout: fixed;
  padding: 1rem;
  padding-top: 1rem;
  padding-bottom: 1rem;

#completions > div {
  max-height: calc(20 * var(--option-height));
  min-height: calc(10 * var(--option-height));


#completions {
  --option-height: 1.4em;
  color: var(--tridactyl-fg);
  background: var(--tridactyl-bg);
  display: inline-block;
  font-size: unset;
  font-weight: 200;
  overflow: hidden;
  width: 100%;
  border-top: unset;
  order: 2;

/* Olie doesn't know how CSS inheritance works */
#completions .HistoryCompletionSource {
  max-height: unset;
  min-height: unset;

#completions .HistoryCompletionSource table {
  width: 100%;
  font-size: 11pt;
  border-spacing: 0;
  table-layout: fixed;

/* redundancy 2: redundancy 2: more redundancy */
#completions .BmarkCompletionSource {
  max-height: unset;
  min-height: unset;

#completions table tr td.prefix,#completions table tr td.privatewindow,#completions table tr td.container,#completions table tr td.icon {
  display: none;

#completions .BufferCompletionSource table {
  width: unset;
  font-size: unset;
  border-spacing: unset;
  table-layout: unset;

#completions table tr .title {
  width: 50%;

#completions table tr {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;

#completions .sectionHeader {
  background: unset;
  font-weight: 200;
  border-bottom: unset;
  padding: 1rem !important;
  padding-left: unset;
  padding-bottom: 0.2rem;

#cmdline_iframe {
  position: fixed !important;
  bottom: unset;
  top: 25% !important;
  left: 10% !important;
  z-index: 2147483647 !important;
  width: 80% !important;
  box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 20px !important;

.TridactylStatusIndicator {
  position: fixed !important;
  bottom: 0 !important;
  background: var(--tridactyl-bg) !important;
  border: unset !important;
  border: 1px <<get-color(name="blue")>> solid !important;
  font-size: 12pt !important;
  /*font-weight: 200 !important;*/
  padding: 0.8ex !important;

#completions .focused {
  background: <<get-color(name="blue")>>;
  color: <<get-fg-for-color(name="blue")>>;

#completions .focused .url {
  background: <<get-color(name="blue")>>;
  color: <<get-fg-for-color(name="blue")>>;
/* #Ocean-normal { */
/*  border-color: green !important; */
/* } */

/* #Ocean-insert { */
/*  border-color: yellow !important; */
/* } */

Firefox Color

Firefox Color is a system that allows for easy experimentation with Firefox themes.

It can serialize themes into URLs like<theme>, so I thought it would be a piece of cake to generate one from my Emacs theme, right? Well…

As it turns out, Firefox uses npm package called json-url to create <theme>, which this package does by the following sequence:

  • msgpack v5
  • lzma
  • url-safe base64

I tried to reproduce the above in Emacs, but in the end gave up and used the package in a simple node script:

const JsonUrl = require('json-url');
const jsonCodec = JsonUrl('lzma');

const json = JSON.parse(process.argv[2]);
jsonCodec.compress(json).then((r) => process.stdout.write(r));

Which I then can use to create the URL.

(defun my/firefox-encode-json (string)
    (with-current-buffer standard-output
      (call-process "node" nil t nil
		    (expand-file-name "~/bin/firefox-theme/main.js")

(defun my/color-value-rgb (color)
  (let ((color (if (stringp color)
		 (my/color-value color))))
    `((r . ,(* 2.55 (ct-get-rgb-r color)))
      (g . ,(* 2.55 (ct-get-rgb-g color)))
      (b . ,(* 2.55 (ct-get-rgb-b color))))))

(defun my/firefox-get-json ()
  (let ((toolbar-color
	   (my/color-value 'bg-mode-line-active)
	   (my/color-value 'bg-mode-line)
	   (if (my/light-p)
	       (ct-edit-hsl-l-dec (my/color-value 'bg-alt) 10)
	     (ct-edit-hsl-l-inc (my/color-value 'bg-alt) 15)))))
	  (if (my/light-p) 'fg 'yellow))))
    `((colors . ((toolbar . ,toolbar-color)
		 (toolbar_text . ,text-color)
		 (frame . ,(my/color-value-rgb 'bg))
		 (tab_background_text . ,(my/color-value-rgb 'fg))
		 (toolbar_field . ,(my/color-value-rgb 'bg))
		 (toolbar_field_text . ,(my/color-value-rgb 'blue))
		 (tab_line . ,text-color)
		 (popup . ,(my/color-value-rgb 'bg-alt))
		 (popup_text . ,(my/color-value-rgb 'fg))
		 (tab_loading . ,text-color))))))

(defun my/firefox-get-color-url ()

(defun my/firefox-kill-color-url ()
  (kill-new (my/firefox-get-color-url)))



Guix dependency
Type Note
SYMLINK ./config/keynavrc -> .keynavrc

keynav is a program for controlling mouse with keyboard, mostly by screen bisection. This is a poor replacement for a proper keyboard-drived sofware, but…



# clear all previous keybindings

# Start & stop
ctrl+semicolon start
Super_L+bracketright start
Super_R+bracketright start
Escape end
ctrl+bracketleft end

# Macros
q record ~/.keynav_macros
shift+at playback

# Bisecting
a history-back
Left cut-left
Right cut-right
Down cut-down
Up cut-up
h cut-left
j cut-down
k cut-up
l cut-right
t windowzoom                          # Zoom to the current window
c cursorzoom 300 300                  # Limit the bisection area by 300x300

# Move the bisecting area
shift+h move-left
shift+j move-down
shift+k move-up
shift+l move-right
shift+Left move-left
shift+Right move-right
shift+Up move-up
shift+Down move-down

# Actions
space warp,click 3,end                # Right click
Return warp,click 1,end               # Left click
Shift+Return warp,doubleclick 1,end   # Double left click
semicolon warp,end                    # Move the cursor and exit
w warp                                # Just move the cursor
e end                                 # exit
u warp,click 4                        # scroll up
d warp,click 5                        # scroll down
1 click 1
2 click 2
3 click 3
4 click 4
5 click 5

Using with picom

I’ve noticed that the program does not play nice with picom’s fade effect. To fix that, add the following to you config:

fade-exclude = [
  "class_i = 'keynav'",
  "class_g = 'keynav'",


Guix dependency

picom is a compositor for X11. It allows effects such as transparency, blurring, etc.

Check out the sample configuration to get an idea on what’s possible. I only have some basic settings in mine.

Also, there are some fancy forks of picom (e.g. ibhagwan/picom adds rounded corners).



shadow = true;
shadow-radius = 2;
shadow-offset-x = -2;
shadow-offset-y = -2;

shadow-exclude = [
  "name = 'Notification'",
  "class_g = 'Conky'",
  "name ?= 'cpt_frame_window'",
  "class_g ?= 'Notify-osd'",
  "class_g = 'Cairo-clock'",


fading = true

fade-in-step = 0.03;
fade-out-step = 0.03;
fade-delta = 10

fade-exclude = [
  "class_i = 'keynav'",
  "class_g = 'keynav'",
  "class_i = 'emacs'",
  "class_g = 'emacs'",


I don’t use stuff like transparency for inactive windows.

The first 5 lines of opacity-rule make i3wm’s hidden windows 100% transparent, so I see the background behind the semi-transparent windows in i3wm’s stacked and tabbed layout. Here is StackExchange question about that.

I also noticed that for some reason it doesn’t play well with Emacs’s built-in transparency, so the last line sets up Emacs transparency at 90%.

inactive-opacity = 1;

frame-opacity = 1.0;
inactive-opacity-override = false;
focus-exclude = [ "class_g = 'Cairo-clock'" ];

opacity-rule = [
  "0:_NET_WM_STATE@[0]:32a = '_NET_WM_STATE_HIDDEN'",
  "0:_NET_WM_STATE@[1]:32a = '_NET_WM_STATE_HIDDEN'",
  "0:_NET_WM_STATE@[2]:32a = '_NET_WM_STATE_HIDDEN'",
  "0:_NET_WM_STATE@[3]:32a = '_NET_WM_STATE_HIDDEN'",
  "0:_NET_WM_STATE@[4]:32a = '_NET_WM_STATE_HIDDEN'",
  "90:class_g = 'Emacs'"

General settings

Default general settings. Editing some of these may be neeeded in case of performance issues.

backend = "xrender";
vsync = true
mark-wmwin-focused = true;
mark-ovredir-focused = true;
detect-rounded-corners = true;
detect-client-opacity = true;
refresh-rate = 0
detect-transient = true
detect-client-leader = true
use-damage = true
log-level = "warn";

  tooltip = { fade = true; shadow = true; opacity = 0.75; focus = true; full-shadow = false; };
  dock = { shadow = false; }
  dnd = { shadow = false; }
  popup_menu = { opacity = 1; }
  dropdown_menu = { opacity = 1; }


Category Guix dependency
office zathura
office zathura-ps
office zathura-pdf-mupdf
office zathura-djvu

Zathura is a pdf viewer with vim-like keybindings.

(if (my/light-p) "false" "true")
set abort-clear-search false
set guioptions cs
set selection-clipboard clipboard
set recolor <<zathura-recolor()>>
map <C-r> set recolor false
map <C-R> set recolor true

set recolor-lightcolor <<get-color(name="black", quote=1)>>

set completion-bg <<get-color(name="bg", quote=1)>>
set completion-fg <<get-color(name="fg", quote=1)>>
set completion-group-bg <<get-color(name="bg", quote=1)>>
set completion-group-fg <<get-color(name="fg", quote=1)>>
set completion-highlight-bg <<get-color(name="magenta", quote=1)>>
set completion-highlight-fg <<get-fg-for-color(name="magenta", quote=1)>>

set inputbar-bg <<get-color(name="light-black", quote=1)>>
set inputbar-fg <<get-color(name="white", quote=1)>>
set statusbar-bg <<get-color(name="light-black", quote=1)>>
set statusbar-fg <<get-color(name="white", quote=1)>>

set notification-error-bg <<get-color(name="red", quote=1)>>
set notification-error-fg <<get-fg-for-color(name="red", quote=1)>>
set notification-warning-bg <<get-color(name="yellow", quote=1)>>
set notification-warning-fg <<get-fg-for-color(name="yellow", quote=1)>>

Various software

This section generates manifests for various desktop software that I’m using.


Category Guix dependency
browsers ungoogled-chromium
browsers firefox

Office & Multimedia

Category Guix dependency
office libreoffice
office gimp
office krita
office ffmpeg
office kdenlive
office inkscape
office okular
office obs


Category Guix dependency
latex texlive
latex texlab-bin
latex biber
latex python-pygments
latex font-microsoft-web-core-fonts


Category Guix dependency Disabled
dev micromamba-bin
dev pandoc
dev docker-compose
dev postgresql
dev virt-manager
dev dnsmasq
dev git-filter-repo
dev node
dev openjdk:jdk
dev go
dev gopls
dev pkg-config
dev gcc-toolchain
dev lua
dev libfaketime
dev hugo-extended
dev make
dev sbcl t
dev git-lfs
dev mysql t
dev gource
dev php
dev python
dev python-virtualenv
dev leiningen
dev socat
dev wireshark
dev python-chess
dev python-cairosvg


(my/format-guix-dependencies category)











Desktop Misc


Desktop polybar


Desktop rofi



A lot of proprietary desktop applications can be installed most easily with flatpak & flathub.

Guix dependency

After installation, add the following repositories:

flatpak remote-add --user --if-not-exists flathub
flatpak remote-add --user --if-not-exists flathub-beta

Installation syntax is as follows:

flatpak install --user <remote> <package>

Packages to install:

Flatpak dependency Channel
com.github.wwmm.pulseeffects flathub
com.discordapp.Discord flathub
com.jetbrains.DataGrip flathub
chat.rocket.RocketChat flathub
 (lambda (c) (concat "flatpak install -y --user " (nth 1 c) " " (nth 0 c)))


Type Description
TODO Make nix manifest?

I probably should’ve used nix, as almost every program I packaged so far exists in the Nix repo.

But it’s easy enough to use Nix on Guix. nixpkgs

Don’t forget to run the following after the first installation:

nix-channel --update

Installing packages:

nix-env -i slack


GNU Shepherd is a service management system for GNU Guix.

I previously used supervisor, but shepherd also seems pretty capable.

Guix dependency


Category Guix dependency
music mpd
music ncmpcpp
music picard
music mpd-mpc
music shntool
music cuetools
music flac

Music player daemon

(define mpd
  (make <service>
    #:provides '(mpd)
    #:respawn? #t
    #:start (make-forkexec-constructor '("mpd" "--no-daemon"))
    #:stop (make-kill-destructor)))

MPD watcher

(define sqrt-data-agent-mpd
  (make <service>
    #:provides '(sqrt-data-agent-mpd)
    #:respawn? #t
    #:start (make-forkexec-constructor '("sqrt_data_agent_mpd"))
    #:stop (make-kill-destructor)
    #:requires '(mpd)))

GNU Mcron

GNU Mcron is a replacement for cron, written in Scheme.

(define mcron
  (make <service>
    #:provides '(mcron)
    #:respawn? #t
    #:start (make-forkexec-constructor '("mcron"))
    #:stop (make-kill-destructor)))


ActivityWatch is a FOSS time tracker. It tracks screen and application usage and has integrations with browsers, Emacs, etc.

Guix dependency


(define aw-server
  (make <service>
    #:provides '(aw-server)
    #:respawn? #t
    #:start (make-forkexec-constructor '("aw-server"))
    #:stop (make-kill-destructor)))

aw-watcher-afk has some problems with statup, so there is a wrapper script

sleep 5


(define aw-watcher-afk
  (make <service>
    #:provides '(aw-watcher-afk)
    #:requires '(aw-server)
    #:respawn? #t
    #:start (make-forkexec-constructor '("/home/pavel/bin/scripts/aw-watcher-afk-wrapper"))
    #:stop (make-kill-destructor)))


(define aw-watcher-window
  (make <service>
    #:provides '(aw-watcher-window)
    #:requires '(aw-server)
    #:respawn? #t
    #:start (make-forkexec-constructor '("aw-watcher-window"))
    #:stop (make-kill-destructor)))


(define pulseeffects
  (make <service>
    #:provides '(pulseeffects)
    #:respawn? #t
    #:start (make-forkexec-constructor '("flatpak" "run" "com.github.wwmm.pulseeffects" "--gapplication-service"))
    #:stop (make-kill-destructor)))


(define xsettingsd
  (make <service>
    #:provides '(xsettingsd)
    #:respawn? #t
    #:start (make-forkexec-constructor '("xsettingsd"))
    #:stop (make-kill-destructor)))


(define nm-applet
  (make <service>
    #:provides '(nm-applet)
    #:respawn? #t
    #:start (make-forkexec-constructor '("nm-applet"))
    #:stop (make-kill-destructor)))

Discord rich presence


(define discord-rich-presence
  (make <service>
    #:provides '(discord-rich-presence)
    #:one-shot? #t
    #:start (make-system-constructor "ln -sf {app/com.discordapp.Discord,$XDG_RUNTIME_DIR}/discord-ipc-0")))

Polkit Authentication agent

Launch an authentication agent. Necessary for stuff like pkexec. I suspect I’m not doing that the intended way, but it seems to work.

(define polkit-gnome
  (make <service>
    #:provides '(polkit-gnome)
    #:respawn? #t
    #:start (make-forkexec-constructor '("/home/pavel/.guix-extra-profiles/desktop-misc/desktop-misc/libexec/polkit-gnome-authentication-agent-1"))
    #:stop (make-kill-destructor)))


(define xmodmap
  (make <service>
    #:provides '(xmodmap)
    #:one-shot? #t
    #:start (make-system-constructor "xmodmap /home/pavel/.Xmodmap")))


Run my OpenVPN setup. Not lauching this automatially, as it requires an active connection.

(define vpn
  (make <service>
    #:provides '(vpn)
    #:respawn? #t
    #:start (make-forkexec-constructor '("/home/pavel/bin/scripts/vpn-start"))
    #:stop (make-kill-destructor)))


(define davmail
  (make <service>
    #:provides '(davmail)
    #:respawn? #t
    #:start (make-forkexec-constructor '("/home/pavel/bin/davmail"))
    #:stop (make-kill-destructor)))


(define vnstatd
  (make <service>
    #:provides '(vnstatd)
    #:respawn? #t
    #:start (make-forkexec-constructor '("vnstatd" "-n"))
    #:stop (make-kill-destructor)))


opensnitch is a linux firewall.

Install it via nix:

nix-env -I opensnitchd opensnitch-ui

sudoers has to be modified this to work.

(define opensnitchd
  (make <service>
    #:provides '(opensnitchd)
    #:respawn? #t
    #:start (make-forkexec-constructor '("sudo" "opensnitchd"))
    #:stop (make-kill-destructor)))

(define opensnitch-ui
  (make <service>
    #:provides '(opensnitch-ui)
    #:respawn? #t
    #:start (make-forkexec-constructor '("sudo" "opensnitch-ui"))
    #:stop (make-kill-destructor)))


(define ollama
  (make <service>
    #:provides '(ollama)
    #:respawn? #t
    #:start (make-forkexec-constructor '("/home/pavel/bin/ollama" "serve"))
    #:stop (make-kill-destructor)))

Shepherd config

For some reason, running start on a one-shot service started to hang shepherd, not sure why… Turining these off for now.

Register services:

 ;; discord-rich-presence
 ;; xmodmap
 ;; opensnitchd
 ;; opensnitch-ui

Daemonize shepherd

(action 'shepherd 'daemonize)

Run services

(for-each start '(mpd
		  ;; discord-rich-presence
		  ;; polkit-gnome
		  ;; ; xmodmap
		  ;; nm-applet
		  ;; opensnitchd
		  ;; opensnitch-ui

Guix settings

Other desktop programs I use are listed below.

Category Guix dependency Description
desktop-misc xprop Tool to display properties of X windows
desktop-misc arandr GUI to xrandr
desktop-misc light Control screen brightness
desktop-misc ponymix Control PulseAudio CLI
desktop-misc pavucontrol Control PulseAudio GUI
desktop-misc network-manager-applet Applet to manage network connections
desktop-misc xmodmap Program to modify keybindings on X server
desktop-misc fontconfig
desktop-misc polkit-gnome Polkit authentication agent
desktop-misc feh Image viewer. Used to set background
desktop-misc copyq Clipboard manager
desktop-misc thunar My preferred GUI file manager
desktop-misc xdg-utils gives xdg-open and stuff
desktop-misc gnome-font-viewer view fonts
desktop-misc qbittorrent torrent client
desktop-misc anydesk Remote desktop software
desktop-misc gnome-disk-utility Manage disks
desktop-misc gparted Manage partitions
desktop-misc xev Test input
desktop-misc bluez Provides bluetoothctl
desktop-misc telegram-desktop
desktop-misc font-google-noto-emoji
desktop-misc remmina
desktop-misc android-file-transfer
desktop-misc mcron

Table of Contents