Communicating with Zathura via DBus

by mafty — Sat 01 June 2024

Zathura is a pdf-viewer with vim-like keybindings that I've used for a few years. Overall I'm very satisfied with it, but there are two small issues that have bothered me for a while:

  1. The built in completion is very confusing to use. For example, when I hit TAB at the end of :open ~/Downl, it eagerly expands to a specific file under the Downloads directory, despite there being numerous files in ~/Downloads.

    P.S. in Emacs the default behavior of completion-at-point is to expand to the greatest common prefix, so I'm expecting expansion until ~/Downloads instead, you might disagree if you spend more time in the shell.

  2. Zathura doesn't come with any concept close to tabs, which is quite common among pdf viewers.

    P.S. There are workarounds for this: in X, there are options like tabbed to embed multiple instances of zathura using the XEmbed protocol; alternatively, one could use window managers/compositors that supports stacking windows. More on the latter approach: i3/sway have superb support for stacking/tabbed layout, and having zathura in my list of everyday softwares is one of the reasons why I love the design of i3/sway.

Not long ago, I came across a post on communicating with zathura via dbus, a way to use zathura that isn't contained in the manpage (!), and I experimented a bit with its possibilities.

The following command lists all zathura instances active on dbus:

dbus-send --session \
        --dest=org.freedesktop.DBus \
        --type=method_call \
        --print-reply \
        /org/freedesktop/DBus \
        org.freedesktop.DBus.ListNames

So we can get all these names with

get_id() {
    ZATHURAID=$(dbus-send --session \
              --dest=org.freedesktop.DBus \
              --type=method_call \
              --print-reply \
              /org/freedesktop/DBus \
              org.freedesktop.DBus.ListNames | grep -o 'org.pwmt.zathura.PID-[0-9]*')
}

or just one with

get_one_id() {
    ZATHURAID=$(dbus-send --session \
              --dest=org.freedesktop.DBus \
              --type=method_call \
              --print-reply \
              /org/freedesktop/DBus \
              org.freedesktop.DBus.ListNames | sed -nr '/^.*string "(.*zathura.*)"/{s//\1/p;q;}')
}

The list of methods are available in zathura's source code, and we can do a couple of things:

Select a file to open with fuzzel

Fuzzel is a dmenu and rofi alternative for Wayland.

#!/usr/bin/env zsh
file=$(
    fd -H -e pdf "" ~/.xeft ~/Documents ~/Downloads |
    fuzzel -d -w 100
    )

# insert definition of get_one_id here

get_one_id

if [[ -s $file ]]; then
    dbus-send --session \
          --dest=$ZATHURAID \
          --type=method_call \
          --print-reply \
          /org/pwmt/zathura \
          org.pwmt.zathura.ExecuteCommand \
          string:"open \"$file\""
fi

Remarks:

Bonus: change recolor colors

UPDATE ON 2024-06-08: Someone made a package for it!

Zathura supports recoloring, which is like M-x pdf-view-midnight-minor-mode if you've used pdf-tools in Emacs. The related options are documented in ZATHURARC(5):

recolor
       En/Disables recoloring

       • Value type: Boolean

       • Default value: false

recolor-darkcolor
       Defines the color value that is used to represent dark colors in recoloring mode

       • Value type: String

       • Default value: #FFFFFF

recolor-keephue
       En/Disables keeping original hue when recoloring

       • Value type: Boolean

       • Default value: false

recolor-lightcolor
       Defines the color value that is used to represent light colors in recoloring mode

       • Value type: String

       • Default value: #000000

recolor-reverse-video
       Defines if original image colors should be kept while recoloring.

       • Value type: Boolean

       • Default value: false

So to enable recolor by default, simply put the following two lines in zathurarc:

set recolor-keephue true        # doesn't hurt you
set recolor true

and we can set recolor-{dark,light}color along with the colors of statusbar (documented in same manpage) with the following shell script that receives four command line arguments that are fed to send_recolor_command below in the same order:

#!/usr/bin/env zsh

# insert definition of get_id here

send_command() {
    dbus-send --session \
        --dest=$id \
        --type=method_call \
        --print-reply \
        /org/pwmt/zathura \
        org.pwmt.zathura.ExecuteCommand \
        string:"$1 \"$2\""
}

send_recolor_command() {
    send_command "set recolor-darkcolor" "$2"
    send_command "set recolor-lightcolor" "$3"
    send_command "set statusbar-bg" "$4"
    send_command "set statusbar-fg" "$5"
}

echo "$1"
echo "$2"
get_id

if [ -n "$ZATHURAID" ]; then
    echo "$ZATHURAID" | while IFS= read -r id; do
    echo "got id $id"
    send_recolor_command $id $1 $2 $3 $4
    done
else
    echo "Error: Variable is empty"
fi

I saved the shell script at ~/.config/wayland-common/zathura-recolor.

Use Emacs theme

As an average Emacs user who proactively enslaves everything else in the desktop environment, this step naturally follows from above.

(setq zathura-set-theme-command "~/.config/wayland-common/zathura-recolor")

(defvar enables-sync-theme-to-zathura t)

(defun sync-theme-to-zathura ()
  (interactive)
  (when enables-sync-theme-to-zathura
   (let ((bg (face-attribute 'default :background))
     (fg (face-attribute 'default :foreground))
     (mbg (face-attribute 'mode-line :background))
     (mfg (face-attribute 'mode-line :foreground)))
     (call-process zathura-set-theme-command nil nil nil fg bg fg bg))))

P.S. unsurprisingly Emacs already has built-in support for dbus with dbus.el, but I'm too lazy to investigate that at the moment.

We can also advise consult-theme to change zathura's colors live:

P.S. if you also changes themes in a unnecessarily frequent manner but haven't yet used the command, you should definitely check it out! It is provided in consult.el, a package which ships with lots of other goodies like consult-imenu-multiple, consult-ripgrep, consult-line, etc.

(defun consult-theme--maybe-sync-to-zathura (theme)
  "Disable current themes and enable THEME from `consult-themes'.

The command supports previewing the currently selected theme."
  (interactive
   (list
    (let* ((regexp (consult--regexp-filter
                    (mapcar (lambda (x) (if (stringp x) x (format "\\`%s\\'" x)))
                            consult-themes)))
           (avail-themes (seq-filter
                          (lambda (x) (string-match-p regexp (symbol-name x)))
                          (cons 'default (custom-available-themes))))
           (saved-theme (car custom-enabled-themes)))
      (consult--read
       (mapcar #'symbol-name avail-themes)
       :prompt "Theme: "
       :require-match t
       :category 'theme
       :history 'consult--theme-history
       :lookup (lambda (selected &rest _)
                 (setq selected (and selected (intern-soft selected)))
                 (or (and selected (car (memq selected avail-themes)))
                     saved-theme))
       :state (lambda (action theme)
                (pcase action
                  ('return (consult-theme (or theme saved-theme)))
                  ((and 'preview (guard theme)) (consult-theme theme))))
       :default (symbol-name (or saved-theme 'default))))))
  (when (eq theme 'default) (setq theme nil))
  (unless (eq theme (car custom-enabled-themes))
    (mapc #'disable-theme custom-enabled-themes)
    (when theme
      (if (custom-theme-p theme)
          (progn (enable-theme theme) (sync-theme-to-zathura))
        (progn (load-theme theme :no-confirm)
           (sync-theme-to-zathura))))))

(add-function :override (symbol-function 'consult-theme) #'consult-theme--maybe-sync-to-zathura)