# Desktop
Freedom is a state of mind
Author: SqrtMinusOne

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.

References:

- A few cases of literate configuration. My blog post that explains some of techniques from this file.

## Global customization

### Colors

My favorite color theme is Palenight (color codes), and I want to have one source of truth for these colors. Except for Emacs itself, which has doom-palenight (and in which I occasionally switch to `doom-one-light`, e.g. when reading a long text), it can be done rather nicely with Org Mode.

First, let’s define a table with all the color codes:

color | key | value
--- | --- | ---
black | color0 | #292d3e
red | color1 | #f07178
green | color2 | #c3e88d
yellow | color3 | #ffcb6b
blue | color4 | #82aaff
magenta | color5 | #c792ea
cyan | color6 | #89ddff
white | color7 | #d0d0d0
light-black | color8 | #434758
light-red | color9 | #ff8b92
light-green | color10 | #ddffa7
light-yellow | color11 | #ffe585
light-blue | color12 | #9cc4ff
light-magenta | color13 | #e1acff
light-cyan | color14 | #a3f7ff
light-white | color15 | #ffffff
color-fg |  | #000000

The table above is the only source of truth for colors in this config.

Contents of this table can then be accessed from a code block. Let’s define one to return the color code based on its name:

    (let ((color (seq-some (lambda (e) (and (string= name (car e)) (nth 2 e))) table)))
      (if (> quote 0)
          (concat "\"" color "\"")
        color))

Test:

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

### Xresources

#### Colors in Xresources

However, I’d rather use the `Xresources` file wherever possible. Here is the code to generate an Xresources file from this table:

    (mapconcat
     (lambda (elem)
       (concat "*" (nth 1 elem) ": " (nth 2 elem)))
     (seq-filter
      (lambda (elem) (and (nth 1 elem)
    		      (not (string-empty-p (nth 1 elem)))))
      table)
     "\n")

    <<get-xresources()>>

    *background: <<get-color(name="black")>>
    *foreground: <<get-color(name="white")>>

So, whenever a program is capable of reading `.Xresources`, it will get colors from there, otherwise, it will get colors from noweb expressions in the literate config. Thus, in both cases, the color is set in a single Org Mode table.

#### Fonts

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 "indigo") 120)
    	(t 96)))

    Xft.dpi: <<get-dpi()>>

### Themes

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
gnome-themes-standard |
xsettingsd | X11 settings daemon
gnome-themes-extra |

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

    Net/ThemeName "Matcha-dark-azul"
    Net/IconThemeName "Papirus-Dark"
    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"

### MIME

Setting the default MIME types

    [Default Applications]
    text/html=firefox.desktop
    x-scheme-handler/http=firefox.desktop
    x-scheme-handler/https=firefox.desktop
    x-scheme-handler/about=firefox.desktop
    x-scheme-handler/unknown=firefox.desktop
    x-scheme-handler/tg=userapp-Telegram Desktop-7PVWF1.desktop
    image/png=feh.desktop
    image/jpg=feh.desktop
    image/jpeg=feh.desktop
    application/pdf=org.pwmt.zathura.desktop

    [Added Associations]
    x-scheme-handler/tg=userapp-Telegram Desktop-7PVWF1.desktop;
    application/pdf=org.pwmt.zathura.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

    hostname=$(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
    fi

## EXWM

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

References:

- EXWM Wiki
- Emacs From Scratch config
- Using EXWM and perspective.el on a multi-monitor setup

### Startup & UI

#### Xsession

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
    export _JAVA_AWT_WM_NONREPARENTING=1

    # 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/run-exwm.sh
    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/polybar.sh"))

    (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")))

#### Pinentry

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)
      :config
      (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry
      (setq auth-source-debug t)

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

    default-cache-ttl 3600
    max-cache-ttl 3600
    allow-emacs-pinentry
    allow-loopback-pinentry

#### Modeline

Show the current workspace in the modeline.

    (use-package exwm-modeline
      :straight (:host github :repo "SqrtMinusOne/exwm-modeline")
      :config
      (add-hook 'exwm-init-hook #'exwm-modeline-mode)
      (my/use-doom-colors
       (exwm-modeline-current-workspace
        :foreground (doom-color 'yellow)
        :weight 'bold)))

### Windows

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:

    (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)))))
        (cond
         ((and other-window (not (window-minibuffer-p other-window)))
          (window-swap-states (selected-window) other-window))
         (other-direction
          (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)
    	       exwm--floating-frame))
    	 (func (if is-exwm-floating
    		   (intern
    		    (concat
    		     "exwm-layout-"
    		     (pcase kind ('shrink "shrink") ('grow "enlarge"))
    		     "-window"
    		     (pcase dir ('height "") ('width "-horizontally"))))
    		 (intern
    		  (concat
    		   "evil-window"
    		   (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)
      "
    ^Resize^
    _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."
      (interactive)
      (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.

### Perspectives

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:

- Fixes issues I found with some advises and hooks. 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.

References:

- perspective-exwm.el repo

    (use-package perspective-exwm
      :straight t
      :config
      (setq perspective-exwm-override-initial-name
    	'((0 . "misc")
    	  (1 . "core")
    	  (2 . "browser")
    	  (3 . "comms")
    	  (4 . "dev")))
      (general-define-key
       :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 ()
      (interactive)
      (pcase exwm-class-name
        ((or "Firefox" "Nightly")
         (perspective-exwm-assign-window
          :workspace-index 2
          :persp-name "browser"))
        ("Alacritty"
         (perspective-exwm-assign-window
          :persp-name "term"))
        ((or "VK" "Slack" "discord" "TelegramDesktop")
         (perspective-exwm-assign-window
          :workspace-index 3
          :persp-name "comms"))
        ((or "Chromium-browser" "jetbrains-datagrip")
         (perspective-exwm-assign-window
          :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:

- a command to switch to another monitor;
- a command to move the current workspace to another monitor;
- using the same commands to switch between windows and monitors.

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
    			my/exwm-last-workspaces))))

    (add-hook 'exwm-workspace-switch-hook
    	  #'my/exwm-store-last-workspace)

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
    	(seq-filter
    	 (lambda (i) (nth i exwm-workspace--list))
    	 my/exwm-last-workspaces)))

#### 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-output-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"))
    	(_ '(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-output-plist
    	     (cl-position (selected-frame)
    			  exwm-workspace--list)))

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."
      (nth
       (% (+ (cl-position
    	  (my/exwm-get-current-monitor)
    	  my/exwm-monitor-list
    	  :test #'string-equal)
    	 (length my/exwm-monitor-list)
    	 (pcase dir
    	   ('right 1)
    	   ('left -1)))
          (length my/exwm-monitor-list))
       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."
      (interactive)
      (my/exwm-last-workspaces-clear)
      (let ((mouse-autoselect-window nil))
        (exwm-workspace-switch
         (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-output-plist i)
    				   other-monitor)
    		   (not (plist-get exwm-randr-workspace-output-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."
      (interactive)
      (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
    		       exwm-workspace-current-index
    		       new-monitor))))
      (exwm-randr-refresh))

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))))))

### Completions

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

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
      :disabled
      :config
      (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

SOURCE: https://github.com/ch11ng/exwm/issues/550#issuecomment-744784838

    (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)))
        (unwind-protect
    	(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)
      (mapc
       (lambda (var)
         (kill-local-variable var)
         (setf (symbol-value var) nil))
       '(exwm-workspace-warp-cursor
         mouse-autoselect-window
         focus-follows-mouse)))

    (defun my/restore-posframe (&rest args)
      (run-with-timer
       0.25
       (lambda ()
         (mapc
          (lambda (var)
    	(kill-local-variable var)
    	(setf (symbol-value var) t))
          '(exwm-workspace-warp-cursor
    	mouse-autoselect-window
    	focus-follows-mouse)))))

    (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"
    	  (propertize
    	   (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

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))

#### emojis

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."
      (interactive)
      (let ((emoji (emojify-completing-read "Type emoji: ")))
        (kill-new emoji)
        (password-store-ivy--async-commands
         (list
          (password-store-ivy--get-wait-command 10)
          "xdotool key Shift+Insert"))))

### Keybindings

#### 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
          `(?\C-x
    	?\C-w
    	?\M-x
    	?\M-u))

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

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

    (general-define-key
     :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-exwm-buffers-backward
     "<mode-line> s-<mouse-5>" #'perspective-exwm-cycle-exwm-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 ()
      (interactive)
      (when (or (not (eq (selected-window) (next-window)))
    	    (y-or-n-p "This is the last window. Are you sure?"))
        (evil-quit)))

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"))

    	;; 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-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 ()
    			(interactive)
    			(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 ()
      (interactive)
      (setq exwm-input--global-keys nil)
      (dolist (i exwm-input-global-keys)
        (exwm-input--set-key (car i) (cdr i)))
      (when exwm--connection
        (exwm-input--update-global-prefix-keys)))

#### App shortcuts

A transient hydra for shortcuts for the most frequent apps.

    (defun my/run-in-background (command)
      (let ((command-parts (split-string command "[ ]+")))
        (apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts)))))

    (defhydra my/exwm-apps-hydra (:color blue :hint nil)
      "
    ^Apps^
    _t_: Terminal (Alacritty)
    _b_: Browser (Firefox)
    _v_: VK
    _s_: Slack
    _e_: Telegram
    _d_: Discord
    "
      ("t" (lambda () (interactive) (my/run-in-background "alacritty")))
      ("b" (lambda () (interactive) (my/run-in-background "firefox")))
      ("v" (lambda () (interactive) (my/run-in-background "vk")))
      ("s" (lambda () (interactive) (my/run-in-background "slack-wrapper")))
      ("e" (lambda () (interactive) (my/run-in-background "flatpak run org.telegram.desktop")))
      ("d" (lambda () (interactive) (my/run-in-background "flatpak run com.discordapp.Discord"))))

#### Locking up

Run i3lock.

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

### Fixes

#### Catch and report all errors raised when invoking command hooks

- CREDIT: Thanks David! https://github.com/daviwil/exwm/commit/7b1be884124711af0a02eac740bdb69446bc54cc

    (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
          (progn
    	(run-hooks 'pre-command-hook)
    	(run-hooks 'post-command-hook))
        ((error)
         (exwm--log "Error occurred while running command hooks: %s\n\nBacktrace:\n\n%s"
    		hook-error
    		(with-temp-buffer
    		  (setq-local standard-output (current-buffer))
    		  (backtrace)
    		  (buffer-string))))))

#### 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)

### EXWM config

And the EXWM config itself.

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

      (my/exwm-run-polybar)
      (my/exwm-set-wallpaper)
      (my/exwm-run-shepherd)
      (my/run-in-background "gpgconf --reload gpg-agent"))

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

    (use-package exwm
      :straight t
      :config
      (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)
      (exwm-randr-enable)
      (start-process-shell-command "xrandr" nil "~/bin/scripts/screen-layout")
      (when (string= (system-name) "indigo")
        (setq my/exwm-another-monitor "DVI-D-0")
        (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)

      <<exwm-workspace-config>>
      <<exwm-keybindings>>
      <<exwm-mode-line-config>>
      <<exwm-fixes>>

      (set-frame-parameter (selected-frame) 'alpha '(90 . 90))
      (add-to-list 'default-frame-alist '(alpha . (90 . 90)))

      (perspective-exwm-mode)
      (exwm-enable))

## i3wm

Guix dependency | Disabled
--- | ---
i3-gaps |
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.

References:

- i3wm docs
- i3-gaps wiki

### 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
rust-i3-switch-tabs

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 Emacs.org 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

### Workspaces

    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

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

### Scratchpad

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.

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

#### 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

#### Keybindings

    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
python-i3-balance-workspace

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)" &
    fi
    i3_balance_workspace

    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.

#### Apps

    # 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

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

### Colors

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
    else
        setxkbmap -layout us,ru
        setxkbmap -model pc105 -option 'grp:win_space_toggle' -option 'grp:alt_shift_toggle'
    fi

And the relevant i3 settings:

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

### Autostart

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

    # 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/autostart.sh"

## Polybar

Category | Guix dependency | Description
--- | --- | ---
desktop-polybar | polybar | statusbar

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

I switched to polybar because I wanted to try out some WMs other than i3, but decided to stick with i3 for now. Still using polybar with EXWM and pretty happy with it.

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

References:

- polybar docs

### General settings

In relation to literate configuration, this is the most crazy advanced case of the former so far in my config.

My polybar has:

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

#### Colors

The “colors” part is straightforward enough. Polybar can use `Xresources`, so we just need to generate the appropriate bindings of Xresources to the polybar variables:

    (mapconcat
     (lambda (elem)
       (format "%s = ${xrdb:%s}" (nth 0 elem) (nth 1 elem)))
     (seq-filter
      (lambda (elem) (when-let (name (nth 1 elem))
    		   (not (string-empty-p name))))
      table)
     "\n")

    [colors]
    <<get-polybar-colors()>>

    background = ${xrdb:background}
    ; foreground = ${xrdb:foreground}

#### 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:

    [glyph]
    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 | +
9 | battery | light-cyan | +
3 | cpu | 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

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:

    (format
     "${colors.%s}"
     (nth
      2
      (seq-find
       (lambda (el) (string-equal (nth 1 el) module))
       table)))

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
    	(thread-last
    	  exclude-table
    	  (seq-map (lambda (el) (nth 0 el)))
    	  (seq-uniq)))
           (exclude-combinations
    	(seq-map
    	 (lambda (monitor)
    	   (seq-map
    	    (lambda (el) (nth 1 el))
    	    (seq-filter
    	     (lambda (el) (and (string-equal (nth 0 el) monitor)
    			       (nth 1 el)))
    	     exclude-table)))
    	 `(,@monitors "")))
           (module-glyph-combinations
    	(thread-last
    	  exclude-combinations
    	  (seq-map
    	   (lambda (exclude)
    	     (thread-last
    	       table
    	       (seq-filter
    		(lambda (elt)
    		  (not (or
    			(member (nth 1 elt) exclude)
    			(not (string-equal (nth 3 elt) "+")))))))))
    	  (seq-uniq)))
           (color-changes nil))
      (dolist (e extra)
        (add-to-list
         'color-changes
         (concat (nth 0 e) "--" (nth 1 e))))
      (dolist (comb module-glyph-combinations)
        (dotimes (i (1- (length comb)))
          (add-to-list
           'color-changes
           (concat (nth 2 (nth i comb))
    	       "--"
    	       (nth 2 (nth (1+ i) comb))))))
      (mapconcat
       (lambda (el)
         (let ((colors (split-string el "--")))
           (format "
    [module/glyph-%s--%s]
    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))))
       color-changes
       "\n"))

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:

    [module/glyph-light-cyan--cyan]
    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:

    <<polybar-generate-glyphs()>>

#### 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
    	(thread-last
    	  exclude-table
    	  (seq-filter (lambda (el) (string-equal (nth 0 el) monitor)))
    	  (seq-map (lambda (el) (nth 1 el)))))
           (modules
    	(thread-last
    	  table
    	  (seq-filter (lambda (el) (not (member (nth 1 el) exclude-modules))))))
           (prev-color first-color)
           (ret nil))
      (concat
       (mapconcat
        (lambda (el)
          (apply
           #'concat
           (list
    	(when (string-equal (nth 3 el) "+")
    	  (setq ret (format "glyph-%s--%s " prev-color (nth 2 el)))
    	  (setq prev-color (nth 2 el))
    	  ret)
    	(nth 1 el))))
        modules
        " ")
       (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.

    [bar/mybar]
    monitor = ${env:MONITOR:}
    width = 100%
    height = ${env:HEIGHT:27}
    fixed-center = false
    bottom=true

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

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

Fonts

    ; 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.

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

    [global/wm]
    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;

    hostname=$(hostname)
    # Settings varying on the hostname
    if [ "$hostname" = "azure" ]; then
        TRAY_MONITOR="eDP-1"
    elif [ "$hostname" = "eminence" ]; then
        TRAY_MONITOR="eDP"
    else
        TRAY_MONITOR="HDMI-A-0"
    fi

    # Setting varying on the monitor
    declare -A FONT_SIZES=(
        ["eDP"]="13"
        ["eDP-1"]="13"
        ["DVI-D-0"]="13"
        ["HDMI-A-0"]="13"
    )
    declare -A EMOJI_SCALE=(
        ["eDP"]="9"
        ["eDP-1"]="9"
        ["DVI-D-0"]="10"
        ["HDMI-A-0"]="10"
    )
    declare -A BAR_HEIGHT=(
        ["eDP"]="29"
        ["eDP-1"]="29"
        ["DVI-D-0"]="29"
        ["HDMI-A-0"]="29"
    )
    declare -A BLOCKS=(
        ["eDP"]="<<polybar-generate-modules(monitor="eDP")>>"
        ["eDP-1"]="<<polybar-generate-modules(monitor="eDP-1")>>"
        ["DVI-D-0"]="<<polybar-generate-modules(monitor="DVI-D-0")>>"
        ["HDMI-A-0"]="<<polybar-generate-modules(monitor="HDMI-A-0")>>"
    )

    # 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"
        else
    	export TRAY="none"
        fi
        SIZE=${FONT_SIZES[$MONITOR]}
        SCALE=${EMOJI_SCALE[$MONITOR]}
        if [[ -z "$SCALE" ]]; then
    	continue
        fi
        # 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 HEIGHT=${BAR_HEIGHT[$MONITOR]}
        export RIGHT_BLOCKS=${BLOCKS[$MONITOR]}
        polybar mybar &
    done

### 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

PulseAudio status

    [module/pulseaudio]
    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 = ${colors.blue}
    ; bar-volume-foreground-4 = ${colors.blue}
    ; bar-volume-foreground-5 = ${colors.green}
    ; bar-volume-foreground-6 = ${colors.green}
    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-underline = ${colors.white}
    ; format-muted-underline = ${colors.light-black}

#### mpd

Music Player Daemon status

    [module/mpd]
    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")>>

    label-separator = 0
    separator-foreground = ${colors.red}

    icon-pause = 
    icon-play = 
    icon-stop = 
    icon-prev = 1
    icon-next = 2

#### cpu

CPU usage

    [module/cpu]
    type = internal/cpu
    format = " <label>"
    label = %percentage%%
    format-background = <<get-polybar-bg(module="cpu")>>

#### ram-memory

RAM usage

    [module/ram-memory]
    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>
    label=%gb_used:.1f%

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

#### swap-memory

Swap usage

    [module/swap-memory]
    type = internal/memory
    interval = 10

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

#### network

Upload/download speed

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

    [module/network]
    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 = ${colors.green}
    ; format-disconnected-underline = ${colors.red}
    format-connected-background = <<get-polybar-bg(module="network")>>
    format-disconnected-background = <<get-polybar-bg(module="network")>>

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

#### bandwidth

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

    [module/bandwidth]
    type = custom/script
    exec = /home/pavel/bin/polybar/bandwidth3.sh
    interval = 0
    tail = true

    format-background = <<get-polybar-bg(module="bandwidth")>>

    # Copyright (C) 2015 James Murphy
    # Copyright (C) 2022 Pavel Korytov
    # Licensed under the terms of the GNU GPL v2 only.

    iface="${BLOCK_INSTANCE}"
    iface="${IFACE:-$iface}"
    dt="${DT:-1}"
    unit="${UNIT:-KB}"
    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
        fi
    }

    function list_interfaces {
        check_proc_net_dev
        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]
    Options:
    -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;;
        esac
    done

    check_proc_net_dev

    iface="${iface:-$(default_interface)}"
    while [ -z "$iface" ]; do
        echo No default interface
        sleep "$dt"
        iface=$(default_interface)
    done

    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;;
    esac

    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
    fi

    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" '
    BEGIN{old_received='"$init_received"';old_sent='"$init_sent"'}
    {
        received=$2
        sent=$10
        rx=(received-old_received)/scalar;
        wx=(sent-old_sent)/scalar;
        tx=rx+wr;
        old_received=received;
        old_sent=sent;
        if(rx >= 0 && wx >= 0){
    	'"$printf_command"';
    	fflush(stdout);
        }
    }
    '

#### ipstack-vpn

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 myip.opendns.com @resolver1.opendns.com 2> /dev/null)
    # API_KEY="$(pass show My_Online/APIs/ipstack | head -n 1)"
    API_KEY=$IPSTACK_API_KEY
    if [[ -z $ip || $ip == *"timed out"* ]]; then
        echo "%{u<<get-color(name="red")>>}%{+u} ?? %{u-}"
        exit
    fi
    ip_info=$(curl -s http://api.ipstack.com/${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
        code="??"
    fi

    if [ -n "$vpn" ]; then
        echo "%{u<<get-color(name="blue")>>}%{+u}  $code %{u-}"
    else
        echo "%{u<<get-color(name="red")>>}%{+u}  $code %{u-}"
    fi

    [module/ipstack-vpn]
    type = custom/script
    exec = /home/pavel/bin/polybar/ipstack-vpn.sh
    interval = 1200

#### openvpn

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 "  "
    else
        echo "  "
    fi

    [module/openvpn]
    type = custom/script
    exec = /home/pavel/bin/polybar/openvpn.sh
    format-background = <<get-polybar-bg(module="openvpn")>>
    interval = 1200

#### xkeyboard

Current keyboard layout

    [module/xkeyboard]
    type = internal/xkeyboard
    format = <label-layout>

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

#### battery

    [module/battery]
    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")>>
    label-discharging = %percentage%% %time%
    label-charging =  %percentage%% %time%

    ramp-capacity-0 = 
    ramp-capacity-1 = 
    ramp-capacity-2 = 
    ramp-capacity-3 = 
    ramp-capacity-4 = 

#### weather

Gets current weather from wttr.in

    bar_format="${BAR_FORMAT:-"%t"}"
    location="${LOCATION:-"Saint-Petersburg"}"
    format_1=${FORMAT_1:-"qF"}
    format_2=${FORMAT_1:-"format=v2n"}

    bar_weather=$(curl -s wttr.in/${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
    else
        echo ${bar_weather}
    fi

    [module/weather]
    type = custom/script
    exec = /home/pavel/bin/polybar/weather.sh
    ; format-underline = ${colors.red}
    format-background = <<get-polybar-bg(module="weather")>>
    interval = 1200

#### sun

Category | Guix dependency
--- | ---
desktop-polybar | sunwait

Prints out the time of sunrise/sunset. Uses sunwait

    declare -A LAT_DATA=(
        ["TMN"]="57.15N"
        ["SPB"]="59.9375N"
    )
    declare -A LON_DATA=(
        ["TMN"]="65.533333E"
        ["SPB"]="30.308611E"
    )
    if [ -z "$LOC" ]; then
        echo "LOC?"
        exit -1
    fi
    LAT=${LAT_DATA[$LOC]}
    LON=${LON_DATA[$LOC]}

    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
    else
        sunrise=$(sunwait list daylight rise ${LAT} ${LON})
        # echo "%{u<<get-color(name="red")>>}%{+u} $sunrise %{u-}"
        echo $sunrise
    fi

    [module/sun]
    type = custom/script
    exec = /home/pavel/bin/polybar/sun.sh
    format-background = <<get-polybar-bg(module="sun")>>
    interval = 60

#### aw-afk

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"
    else
        # echo "%{u<<get-color(name="blue")>>}%{+u} $res %{u-}"
        echo "$res"
    fi

    [module/aw-afk]
    type = custom/script
    exec = /home/pavel/bin/polybar/aw_afk.sh
    interval = 60
    format-background = <<get-polybar-bg(module="aw-afk")>>

#### date

Current date

    [module/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")>>
    label = "%date% %time%"

#### pomm

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
    fi

    [module/pomm]
    type = custom/script
    exec = /home/pavel/bin/polybar/pomm.sh
    interval = 1
    format-underline = ${colors.light-green}

#### SEP

A simple separator

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

#### TSEP

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

    if [ ! -z "$TRAY" ] && [ "$TRAY" != "none" ]; then
        echo "| "
    fi

    [module/TSEP]
    type = custom/script
    exec = /home/pavel/bin/polybar/tray-sep.sh
    format-foreground = ${colors.magenta}
    interval = 100000

#### i3

Show i3wm workspaces

    [module/i3]
    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 = ${colors.blue}

    ; focused = Active workspace on focused monitor
    label-focused = %
    label-focused-background = ${colors.blue}
    label-focused-underline= ${colors.blue}
    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 = ${colors.red}
    label-urgent-foreground = ${colors.black}
    label-urgent-padding = 1

## Rofi

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.

### Theme

A theme, based on dracula theme for rofi, but with palenight colorscheme.

    (apply
     #'concat
     (mapcar
      (lambda (elem)
        (concat (nth 0 elem) ": " (nth 2 elem) ";\n"))
      table))

    /* Generated from [[file:../../Desktop.org::*Theme][Theme:1]] */
     * {
        <<get-rofi-colors()>>

        foreground:                  @white;
        background:                  @black;
        background-color:            @black;
        separatorcolor:              @blue;
        border-color:                @blue;
        selected-normal-foreground:  @black;
        selected-normal-background:  @blue;
        selected-active-foreground:  @black;
        selected-active-background:  @blue;
        selected-urgent-foreground:  @foreground;
        selected-urgent-background:  @red;
        normal-foreground:           @foreground;
        normal-background:           @background;
        active-foreground:           @blue;
        active-background:           @background;
        urgent-foreground:           @red;
        urgent-background:           @background;
        alternate-normal-foreground: @foreground;
        alternate-normal-background: @light-black;
        alternate-active-foreground: @blue;
        alternate-active-background: @light-black;
        alternate-urgent-foreground: @red;
        alternate-urgent-background: @light-black;
        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 normal.active {
        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 selected.active {
        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 alternate.active {
        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;
    }

### Scripts

#### Buku bookmarks

Inspired by the knatsakis/rofi-buku script.

    if [ $(hostname) = 'pdsk' ]; then
        BUKU="/home/pavel/.local/bin/buku"
    else
        BUKU="/home/pavel/Programs/miniconda3/bin/buku"
    fi

    # COMMAND="$BUKU -o %"
    # COMMAND="qutebrowser $(buku -f 10 -p %)"
    COMMAND="firefox %"
    if [[ $1 == '-e' ]]; then
        COMMAND="$BUKU -w %"
    fi
    $BUKU -f 4 -p | awk -F'\t' -v OFS='\t' '{
        split($4, tags, ",")
        joined = sep = ""
        for (i = 1; i in tags; i++) {
    	joined = joined sep "[" tags[i] "]"
    	sep = " "
        }
        url = substr($2, 1, 40)
        if (length($2) > 40) {
    	url = url "..."
        }
        if ($1 != "waiting for input") {
    	printf "%-5s %-60s %-45s %s\n", $1, $3, url, joined
        }
    }' | sort -k 2 | rofi -dmenu -matching normal -sort -sorting-method fzf -width 80 -l 20 | cut -d ' ' -f 1 | {
        read index;
        if [[ -z "$index" ]]; then
    	exit 0
        fi
        url=$($BUKU -f 10 -p $index)
        echo ${url#"waiting for input"} | cut -d ' ' -f 1 | xargs -I % $COMMAND
    }

#### 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 -
    fi

#### Emojis

Category | Guix dependency
--- | ---
desktop-rofi | python-rofimoji

#### pass

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

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

    USERNAME_field='username'
    EDITOR=vim
    default_autotype='username :tab pass'
    clip=both

## Flameshot

Guix dependency
flameshot

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.

    [General]
    disabledTrayIcon=false
    drawColor=#ff0000
    drawThickness=0
    saveAfterCopyPath=/home/pavel/Pictures
    savePath=/home/pavel/Pictures
    savePathFixed=false
    showStartupLaunchMessage=false
    uiColor=<<get-color(name="blue")>>

    [Shortcuts]
    TYPE_ARROW=A
    TYPE_CIRCLE=C
    TYPE_CIRCLECOUNT=
    TYPE_COMMIT_CURRENT_TOOL=Ctrl+Return
    TYPE_COPY=Ctrl+C
    TYPE_DRAWER=D
    TYPE_EXIT=Ctrl+Q
    TYPE_IMAGEUPLOADER=Return
    TYPE_MARKER=M
    TYPE_MOVESELECTION=Ctrl+M
    TYPE_MOVE_DOWN=Down
    TYPE_MOVE_LEFT=Left
    TYPE_MOVE_RIGHT=Right
    TYPE_MOVE_UP=Up
    TYPE_OPEN_APP=Ctrl+O
    TYPE_PENCIL=P
    TYPE_PIN=
    TYPE_PIXELATE=B
    TYPE_RECTANGLE=R
    TYPE_REDO=Ctrl+Shift+Z
    TYPE_RESIZE_DOWN=Shift+Down
    TYPE_RESIZE_LEFT=Shift+Left
    TYPE_RESIZE_RIGHT=Shift+Right
    TYPE_RESIZE_UP=Shift+Up
    TYPE_SAVE=Ctrl+S
    TYPE_SELECTION=S
    TYPE_SELECTIONINDICATOR=
    TYPE_SELECT_ALL=Ctrl+A
    TYPE_TEXT=T
    TYPE_TOGGLE_PANEL=Space
    TYPE_UNDO=Ctrl+Z

## dunst

Guix dependency
dunst
libnotify

Type | Note
--- | ---
TODO | Cleanup default config comments

dunst is a lightweight notification daemon.

My customizations of the original config consist mostly of changing colors.

References:

- dunst documentation

    [global]
        monitor = 0

        follow = mouse

        # The geometry of the window:
        #   [{width}]x{height}[+/-{x}+/-{y}]
        # The geometry of the message window.
        # The height is measured in number of notifications everything else
        # in pixels.  If the width is omitted but the height is given
        # ("-geometry x2"), the message window expands over the whole screen
        # (dmenu-like).  If width is 0, the window expands to the longest
        # message displayed.  A positive x is measured from the left, a
        # negative from the right side of the screen.  Y is measured from
        # the top and down respectively.
        # The width can be negative.  In this case the actual width is the
        # screen width minus the width defined in within the geometry option.
        geometry = "300x5-30+20"

        # Show how many messages are currently hidden (because of geometry).
        indicate_hidden = yes

        # Shrink window if its smaller than the width.  Will be ignored if
        # width is 0.
        shrink = no

        # The transparency of the window.  Range: [0; 100].
        # This option will only work if a compositing window manager is
        # present (e.g. xcompmgr, compiz, etc.).
        transparency = 15

        # The height of the entire notification.  If the height is smaller
        # than the font height and padding combined, it will be raised
        # to the font height and padding.
        notification_height = 0

        # Draw a line of "separator_height" pixel height between two
        # notifications.
        # Set to 0 to disable.
        separator_height = 2

        # Padding between text and separator.
        padding = 8

        # Horizontal padding.
        horizontal_padding = 8

        # Defines width in pixels of frame around the notification window.
        # Set to 0 to disable.
        frame_width = 1

        # Defines color of the frame around the notification window.
        frame_color = <<get-color(name="white", quote=1)>>

        # Define a color for the separator.
        # possible values are:
        #  * auto: dunst tries to find a color fitting to the background;
        #  * foreground: use the same color as the foreground;
        #  * frame: use the same color as the frame;
        #  * anything else will be interpreted as a X color.
        separator_color = frame

        # Sort messages by urgency.
        sort = yes

        # Don't remove messages, if the user is idle (no mouse or keyboard input)
        # for longer than idle_threshold seconds.
        # Set to 0 to disable.
        # A client can set the 'transient' hint to bypass this. See the rules
        # section for how to disable this if necessary
        idle_threshold = 120

        ### Text ###

        font = DejaVu Sans 9

        # The spacing between lines.  If the height is smaller than the
        # font height, it will get raised to the font height.
        line_height = 0

        # Possible values are:
        # full: Allow a small subset of html markup in notifications:
        #        <b>bold</b>
        #        <i>italic</i>
        #        <s>strikethrough</s>
        #        <u>underline</u>
        #
        #        For a complete reference see
        #        <http://developer.gnome.org/pango/stable/PangoMarkupFormat.html>.
        #
        # strip: This setting is provided for compatibility with some broken
        #        clients that send markup even though it's not enabled on the
        #        server. Dunst will try to strip the markup but the parsing is
        #        simplistic so using this option outside of matching rules for
        #        specific applications *IS GREATLY DISCOURAGED*.
        #
        # no:    Disable markup parsing, incoming notifications will be treated as
        #        plain text. Dunst will not advertise that it has the body-markup
        #        capability if this is set as a global setting.
        #
        # It's important to note that markup inside the format option will be parsed
        # regardless of what this is set to.
        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 of message text.
        # Possible values are "left", "center" and "right".
        alignment = left

        # Show age of message if message is older than show_age_threshold
        # seconds.
        # Set to -1 to disable.
        show_age_threshold = 60

        # Split notifications into multiple lines if they don't fit into
        # geometry.
        word_wrap = yes

        # When word_wrap is set to no, specify where to make an ellipsis in long lines.
        # Possible values are "start", "middle" and "end".
        ellipsize = middle

        # Ignore newlines '\n' in notifications.
        ignore_newline = no

        # Stack together notifications with the same content
        stack_duplicates = true

        # Hide the count of stacked notifications with the same content
        hide_duplicate_count = false

        # Display indicators for URLs (U) and actions (A).
        show_indicators = yes

        ### Icons ###

        # Align icons left/right/off
        icon_position = left

        # Scale larger icons down to this size, set to 0 to disable
        max_icon_size = 32

        # Paths to default icons.
        icon_path = /usr/share/icons/Mint-Y/status/32/;/usr/share/icons/Mint-Y/devices/32

        ### History ###

        # Should a notification popped up from history be sticky or timeout
        # as if it would normally do.
        sticky_history = yes

        # Maximum amount of notifications kept in history
        history_length = 20

        ### Misc/Advanced ###

        # dmenu path.
        dmenu = /usr/bin/dmenu -p dunst:

        # Browser for opening urls in context menu.
        browser = /usr/bin/sensible-browser

        # Always run rule-defined scripts, even if the notification is suppressed
        always_run_script = true

        # Define the title of the windows spawned by dunst
        title = Dunst

        # Define the class of the windows spawned by dunst
        class = Dunst

        # Print a notification on startup.
        # This is mainly for error detection, since dbus (re-)starts dunst
        # automatically after a crash.
        startup_notification = false

        # Manage dunst's desire for talking
        # Can be one of the following values:
        #  crit: Critical features. Dunst aborts
        #  warn: Only non-fatal warnings
        #  mesg: Important Messages
        #  info: all unimportant stuff
        # debug: all less than unimportant stuff
        verbosity = mesg

        # Define the corner radius of the notification window
        # in pixel size. If the radius is 0, you have no rounded
        # corners.
        # The radius will be automatically lowered if it exceeds half of the
        # notification height to avoid clipping text and/or icons.
        corner_radius = 0

        ### Legacy

        # Use the Xinerama extension instead of RandR for multi-monitor support.
        # This setting is provided for compatibility with older nVidia drivers that
        # do not support RandR and using it on systems that support RandR is highly
        # discouraged.
        #
        # By enabling this setting dunst will not be able to detect when a monitor
        # is connected or disconnected which might break follow mode if the screen
        # layout changes.
        force_xinerama = false

        ### mouse

        # Defines action of mouse event
        # Possible values are:
        # * none: Don't do anything.
        # * do_action: If the notification has exactly one action, or one is marked as default,
        #              invoke it. If there are multiple and no default, open the context menu.
        # * close_current: Close current notification.
        # * close_all: Close all notifications.
        mouse_left_click = close_current
        mouse_middle_click = do_action
        mouse_right_click = close_all

    # Experimental features that may or may not work correctly. Do not expect them
    # to have a consistent behaviour across releases.
    [experimental]
        # Calculate the dpi to use on a per-monitor basis.
        # If this setting is enabled the Xft.dpi value will be ignored and instead
        # dunst will attempt to calculate an appropriate dpi value for each monitor
        # using the resolution and physical size. This might be useful in setups
        # where there are multiple screens with very different dpi values.
        per_monitor_dpi = false

    [shortcuts]

        # Shortcuts are specified as [modifier+][modifier+]...key
        # Available modifiers are "ctrl", "mod1" (the alt-key), "mod2",
        # "mod3" and "mod4" (windows-key).
        # Xev might be helpful to find names for keys.

        # Close notification.
        close = ctrl+space

        # Close all notifications.
        close_all = ctrl+shift+space

        # Redisplay last message(s).
        # On the US keyboard layout "grave" is normally above TAB and left
        # of "1". Make sure this key actually exists on your keyboard layout,
        # e.g. check output of 'xmodmap -pke'
        history = ctrl+grave

        # Context menu.
        context = ctrl+shift+period

    [urgency_low]
        # IMPORTANT: colors have to be defined in quotation marks.
        # Otherwise the "#" and following would be interpreted as a comment.
        background = <<get-color(name="light-black", quote=1)>>
        frame_color = <<get-color(name="white", quote=1)>>
        foreground = <<get-color(name="light-white", quote=1)>>
        timeout = 10
        # Icon for notifications with low urgency, uncomment to enable
        #icon = /path/to/icon

    [urgency_normal]
        background = <<get-color(name="black", quote=1)>>
        frame_color = <<get-color(name="white", quote=1)>>
        foreground = <<get-color(name="light-white", quote=1)>>
        timeout = 10
        # Icon for notifications with normal urgency, uncomment to enable
        #icon = /path/to/icon

    [urgency_critical]
        background = <<get-color(name="red", quote=1)>>
        foreground = <<get-color(name="light-white", quote=1)>>
        frame_color = <<get-color(name="red", quote=1)>>
        timeout = 0
        # Icon for notifications with critical urgency, uncomment to enable
        #icon = /path/to/icon

## keynav

Guix dependency
keynav

Type | Note
--- | ---
SYMLINK | ./config/keynavrc -> .keynavrc

How many times you have been working with keyboard-driven programs and had to use a mouse just to press some pesky little button in a modal window?

keynav is a program that allows you to control the mouse with the keyboard with the general idea of bisecting the screen to get to the required point. I’m still not sure if there is any point in using it, but it’s rather funny. Unfortunately, the colors seem to be hardcoded.

One of the usecases I found so far is to use the program to scroll webpages when tridactyl’s scroll captures the wrong scroll area.

References:

- keynav documentation

### Config

    # clear all previous keybindings
    clear

    # 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'",
    ]

## Picom

Guix dependency
picom

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

Sample configuration is a good resource for getting an overview of the available settings. I have only a bunch of necessary settings in mine.

There are a bunch of forks for picom (e.g. ibhagwan/picom adds rounded corners) which seem to have some popularity, but I use the base one.

References:

- picom wiki
- Picom on ArchWiki
- Sample configuration

### Shadows

    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'",
      "_GTK_FRAME_EXTENTS@:c"
    ];

### Fading

    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'",
    ]

### Opacity

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";

    wintypes:
    {
      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; }
    };

## Zathura

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

Zathura is a pdf viewer with vim-like keybindings. One of my favorite features is an ability to invert the document colors.

    set abort-clear-search false
    set guioptions cs
    set selection-clipboard clipboard
    set recolor true
    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="black", quote=1)>>
    set completion-fg <<get-color(name="white", quote=1)>>
    set completion-group-bg <<get-color(name="light-black", quote=1)>>
    set completion-group-fg <<get-color(name="white", quote=1)>>
    set completion-highlight-bg <<get-color(name="magenta", quote=1)>>
    set completion-highlight-fg <<get-color(name="black", quote=1)>>

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

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

## Various software

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

### Browsers

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

### LaTeX

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

### Dev

Category | Guix dependency | Disabled
--- | --- | ---
dev | conda |
dev | pandoc |
dev | docker-compose |
dev | postgresql |
dev | virt-manager |
dev | git-filter-repo |
dev | node |
dev | openjdk:jdk |
dev | go |
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 |

### Manifests

    (my/format-guix-dependencies category)

Dev

    (specifications->manifest
     '(
       <<packages("dev")>>))

Browsers

    (specifications->manifest
     '(
       <<packages("browsers")>>))

Music

    (specifications->manifest
     '(
       <<packages("music")>>))

Office

    (specifications->manifest
     '(
       <<packages("office")>>))

LaTeX

    (specifications->manifest
     '(
       <<packages("latex")>>))

Desktop Misc

    (specifications->manifest
     '(
       <<packages("desktop-misc")>>))

Desktop polybar

    (specifications->manifest
     '(
       <<packages("desktop-polybar")>>))

Desktop rofi

    (specifications->manifest
     '(
       <<packages("desktop-rofi")>>))

### Flatpak

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

Guix dependency
flatpak
xdg-desktop-portal

After installation, add the following repositories:

    flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
    flatpak remote-add --user --if-not-exists flathub-beta https://flathub.org/beta-repo/flathub-beta.flatpakrepo

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
us.zoom.Zoom | flathub
com.slack.Slack | flathub

    (mapconcat
     (lambda (c) (concat "flatpak install -y --user " (nth 1 c) " " (nth 0 c)))
     table
     "\n")

### Nix

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.

    https://nixos.org/channels/nixpkgs-unstable nixpkgs

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

    nix-channel --update

Installing packages:

    nix-env -i vk-messenger slack

## Services

GNU Shepherd is a service management system for GNU Guix.

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

### Music

Category | Guix dependency
--- | ---
music | mpd
music | ncmpcpp
music | picard
music | mpd-watcher
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 mpd-watcher
      (make <service>
        #:provides '(mpd-watcher)
        #:respawn? #t
        #:start (make-forkexec-constructor '("mpd_watcher"))
        #: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

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

Guix dependency
activitywatch-bin

aw-server

    (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
    aw-watcher-afk

aw-watcher-afk

    (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)))

aw-watcher-window

    (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)))

### PulseEffects

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

### xsettingsd

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

### nm-applet

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

### Discord rich presence

References:

- Rich Precense (discord rpc)

    (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)))

### Xmodmap

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

### VPN

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)))

### Davmail

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

### sqrt-data

    (define sqrt-data
      (make <service>
        #:provides '(sqrt-data)
        #:respawn? #t
        #:start (make-forkexec-constructor '("sqrt_data" "cron" "run-client-cron"))
        #:stop (make-kill-destructor)))

### Shepherd config

Register services

    (register-services
     mpd
     mpd-watcher
     mcron
     aw-server
     aw-watcher-afk
     aw-watcher-window
     pulseeffects
     xsettingsd
     discord-rich-presence
     polkit-gnome
     vpn
     davmail
     xmodmap
     nm-applet
     sqrt-data)

Daemonize shepherd

    (action 'shepherd 'daemonize)

Run services

    (for-each start '(mpd
    		  mpd-watcher
    		  mcron
    		  aw-server
    		  aw-watcher-afk
    		  aw-watcher-window
    		  pulseeffects
    		  xsettingsd
    		  discord-rich-presence
    		  polkit-gnome
    		  davmail
    		  xmodmap
    		  nm-applet
    		  sqrt-data))

### Sync

Guix dependency
megacmd-1.4

## 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 | vinagre | My VNC client of choice
desktop-misc | gnome-disk-utility | Manage disks
desktop-misc | gparted | Manage partitions
desktop-misc | xev | Test input
desktop-misc | bluez | Provides bluetoothctl

    (my/format-guix-dependencies)

    (specifications->manifest
     '(
       <<packages()>>))

Table of Contents
<Expand> <Collapse>