/emacs-wallpaper

Setting the wallpaper with Emacs

Primary LanguageEmacs LispGNU General Public License v3.0GPL-3.0

wallpaper.el: Setting the wallpaper

https://melpa.org/packages/wallpaper-badge.svg https://github.com/farlado/emacs-wallpaper/workflows/CI/badge.svg

Table of contents

About this Package

Ricing EXWM is pretty cool, but even cooler is being able to manage everything about your rice using Emacs. This package is meant to make that a little bit easier, by managing your wallpapers for you. Compatibility is only guaranteed in X desktop sessions. Using this package with EXWM is the intention, but it can serve as a wallpaper daemon on other window managers with some work. This package was written literately, so code is included alongside the documentation.

;;; wallpaper.el --- Setting the wallpaper -*- lexical-binding: t -*-

;; Copyright (C) 2020

;; Author: Farlado <farlado@sdf.org>
;; URL: https://github.com/farlado/emacs-wallpaper
;; Keywords: unix, wallpaper, extensions
;; Version: 1.1.3
;; Package-Requires: ((emacs "25.1"))

;; This file is not part of GNU Emacs.

<<license>>



;;; Commentary:

;; Ricing `exwm' is pretty cool, but even cooler is being able to manage
;; everything about your rice using Emacs.  This package is meant to
;; make that a little bit easier, by managing your wallpapers for you.
;;
;; The provided modes are `wallpaper-cycle-mode' for cycling randomly
;; through wallpapers in a directory, and `wallpaper-per-workspace-mode'
;; for setting a specific wallpaper or wallpapers in each workspace or
;; a window manager.
;;
;; The following window managers are known to work with this package:
;; - EXWM
;; - Any that use vdesk for virtual desktops
;;
;; The following might work but aren't fully tested:
;; - i3
;;
;; Compatibility is only guaranteed with use of an X desktop session.


;;; Code:

License

This package is licensed under version 3 of the GNU General Public License.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

Installation

This package is available on MELPA. Using use-package, an example configuration may look as follows:

;; This is an example `use-package' configuration
;; It is not tangled into wallpaper.el
(use-package wallpaper
  :ensure t
  :hook ((exwm-randr-screen-change . wallpaper-set-wallpaper)
         (after-init . wallpaper-cycle-mode))
  :custom ((wallpaper-cycle-single t)
           (wallpaper-scaling 'scale)
           (wallpaper-cycle-interval 45)
           (wallpaper-cycle-directory "~/Pictures/wallpapers")))

Ensure that you have feh installed before use.

(unless (executable-find "feh")
  (display-warning 'wallpaper "External command `feh' not found!"))

This package also uses functions that are not compatible with versions of Emacs before 25.

(require 'cl-lib)

Contributing

Please make sure when modifying this package you are doing so literately. This means modifying literate-wallpaper.el and tangling your changes from there. Pull requests which modify wallpaper.el without modifying the literate file will most likely not be accepted until the changes are also present in literate-wallpaper.el with sufficient documentation where needed.

Configuration

Settings for this package can be configured using customize. This can be done by doing M-x customize-group RET wallpaper RET.

(defgroup wallpaper nil
  "Setting the wallpaper."
  :tag "Wallpaper"
  :group 'environment
  :prefix "wallpaper-")

Per-workspace wallpapers

Choosing wallpapers for workspaces

If you wish to have a unique wallpaper or set of wallpapers for each workspace, assign one or multiple wallpapers as absolute paths in strings to a workspace using the variable wallpaper-per-workspace-alist. No values assigned to a workspace means deferring either to static wallpaper(s) or a random wallpaper if wallpaper-static-wallpapers is blank.

(defcustom wallpaper-per-workspace-alist nil
  "List of wallpapers per workspace.

Each item is (WORKSPACE WALLPAPERS).  When WORKSPACE is the current
workspace, WALLPAPERS are any number of absolute paths for the
wallpapers to be set as from their absolute path."
  :tag "Per-workspace alist"
  :group 'wallpaper
  :type 'list)

An example for EXWM may look as follows:

;; This is an example, not tangled into wallpaper.el
(setq wallpaper-per-workspace-alist '((0 "/path/to/0.png")
                                      (1 "/path/to/1.png"
                                         "/path/to/3.png")
                                      (2)
                                      (3 "/so/on/so/forth/2.png")
                                      ...))

If you are using i3, start with 1 instead of 0 for the first workspace.

Getting the workspace number

Per-workspace wallpaper compatibility is ONLY guaranteed with EXWM, but I’ve left enough for this to be used with other window managers with good enough configuration. Once I’ve got some experience using it with other WMs, I’ll leave a guide in the about section.

In the meantime, wallpaper-per-workspace-get points to the function used to get the current workspace. The function wallpaper-per-workspace-exwm-get is provided for use with EXWM. This is the default value for this variable. There is also a function for i3: wallpaper-per-workspace-i3-get.

(defcustom wallpaper-per-workspace-get #'wallpaper-per-workspace-exwm-get
  "What function to use for determining the current workspace."
  :tag "Per-workspace function"
  :group 'wallpaper
  :type 'function)

Static wallpaper(s)

If you only want to use a certain wallpaper or set of wallpapers across monitors, set wallpaper-static-wallpaper-list to a list of strings for multiple absolute paths to the desired wallpapers, in the order of the monitors they should be on.

(defcustom wallpaper-static-wallpaper-list nil
  "List of wallpapers to use instead of randomly finding wallpapers.

Wallpapers must be entered in this list as absolute paths, in the order
of your monitors.  This list should be left blank if you intend to use
function `wallpaper-cycle-mode'."
  :tag "Static wallpaper(s)"
  :group 'wallpaper
  :type 'list)

For instance, if I wanted 1.png on my first monitor, 3.png on my second monitor, and 2.png on my third monitor, I would have to arrange them in the string as:

;; This is an example, not tangled into wallpaper.el
(setq wallpaper-static-wallpapers '("/path/to/1.png"
                                    "/path/to/3.png"
                                    "/path/to/2.png"))

Cycling wallpapers

Wallpaper cycle speed

If you are cycling through your wallpapers automatically and want to make wallpaper cycling faster or slower, set wallpaper-cycle-interval to the number of seconds you want to see each wallpaper. The default is a likely blazing fast fifteen seconds.

(defcustom wallpaper-cycle-interval 15
  "Interval in seconds for cycling in function `wallpaper-cycle-mode'."
  :tag "Wallpaper cycle interval"
  :group 'wallpaper
  :type 'integer)

Setting multiple wallpapers

If you are wanting to use the same wallpaper on each monitor when cycling wallpapers, set wallpaper-cycle-single to non-nil.

(defcustom wallpaper-cycle-single nil
  "Whether to use one wallpaper across all monitors.

This setting is not respected when `wallpaper-static-wallpapers' is
non-nil.  To have only one wallpaper for all monitors, ensure only
one path is listed in `wallpaper-static-wallpapers'."
  :tag "Single wallpaper"
  :group 'wallpaper
  :type 'boolean)

Setting the wallpaper directory

By default, wallpapers are searched for in $XDG_DATA_HOME/wallpapers when cycling wallpapers, but of course not everyone may want to store their wallpapers there, in which case you’ll have to set the variable wallpaper-cycle-directory to where your wallpapers are stored.

     (defcustom wallpaper-cycle-directory (expand-file-name "wallpapers"
							     (getenv "XDG_DATA_HOME"))
	"The directory in which to look for wallpapers."
	:tag "Wallpaper directory"
	:group 'wallpaper
	:type 'string)

Using specific file extensions

When cycling, random wallpapers will be grabbed from a directory. This regexp is used to pick which files within a directory are wallpapers.

(defcustom wallpaper-cycle-extension-regexp ".[gjpGJP][inpINP][efgEFG]+$"
  "The regexp used to locate wallpapers in `wallpaper-cycle-directory'."
  :tag "Wallpaper extension regexp"
  :group 'wallpaper
  :type 'string)

Wallpaper style

Scaling

There are five values possible for wallpaper-scaling:

  • scale: Scale the image to fit the screen, distorting the image
  • max: Show the whole image, leaving portions of the screen uncovered
  • fill: Fill the entire screen, cutting off regions of the image
  • tile: Tile the image across the screen for small images
  • center: Center the image on the screen

By default, fill is the value of wallpaper-scaling.

(defcustom wallpaper-scaling 'fill
  "What style of wallpaper scaling to use.

The options are
scale: Scale the image to fit the screen, distorting the image
max: Show the whole image, leaving portions of the screen uncovered
fill: Fill the entire screen, cutting off regions of the image
tile: Tile the image across the screen for small images
center: Center the image on the screen

The default option is fill."
  :tag "Wallpaper style"
  :group 'wallpaper
  :type '(radio (const :tag "Scale" scale)
                (const :tag "Maximize" max)
                (const :tag "Fill" fill)
                (const :tag "Tile" tile)
                (const :tag "Center" center)))

Background color

When max is the value for wallpaper-scaling, it leaves some portions of the screen uncovered by the image. Setting wallpaper-background to a valid hex code or XColor will change the color shown behind the image.

(defcustom wallpaper-background "#000000"
  "The background color to display behind the wallpaper."
  :tag "Background color"
  :group 'wallpaper
  :type 'string)

Setting the wallpaper once

The function wallpaper-set-wallpaper can be used to set the wallpaper one time. If wallpaper-per-workspace-mode is active, it will set the wallpaper according to the current workspace and the wallpapers assigned to it in wallpaper-per-workspace-alist, otherwise it will try to use the wallpaper(s) in wallpaper-static-wallpapers. If wallpaper-static-wallpapers is blank, it will randomly choose a PNG or JPG image found in wallpaper-cycle-directory. This function can be called interactively was well as in your configurations.

If you are using this package with EXWM, I would highly recommend you add wallpaper-set-wallpaper to exwm-randr-screen-change-hook or add the command to a function that is already in said hook. This way, every time you change monitors, the wallpaper is also automatically set and looks right.

All the headers that follow relate specifically to how the function works, and are more oriented towards those looking to understand the rationale behind the function in order to tell me how horribly the function is written help improve it. Feel free to skip on ahead if this doesn’t interest you. The short version of this is that a string is created with the feh command to be executed, and then a process is started to execute the command.

;;;###autoload
(defun wallpaper-set-wallpaper ()
  "Set the wallpaper.

This function will either choose a random wallpaper from
`wallpaper-cycle-directory' or use the wallpapers listed in
`wallpaper-static-wallpapers'."
  (interactive)
  (let ((wallpapers (or (wallpaper--per-workspace-wallpapers)
                        wallpaper-static-wallpaper-list
                        (wallpaper--random-wallpapers)))
        (command (concat "feh --no-fehbg " (wallpaper--background))))
    (if (not wallpapers)
        (message "No wallpapers selected.")
      (setq wallpaper-current-wallpapers nil)
      (dolist (wallpaper wallpapers)
        (setq command (concat command (wallpaper--scaling) wallpaper " "))
        (add-to-list 'wallpaper-current-wallpapers wallpaper))
      (start-process-shell-command
       "Wallpaper" nil command))))

A variable wallpaper-current-wallpapers keeps track of the wallpaper(s) currently in use regardless of how they were set.

(defvar wallpaper-current-wallpapers nil
  "List of the wallpaper(s) currently in use.

This variable is set automatically.  Hand modification of its value
may interfere with its proper behavior.")

Arguments for feh command

In order to properly form the wallpaper setting command, functions have been defined to return the flags required to properly construct the command.

Wallpaper style argument

Depending on the value of wallpaper-scaling, wallpaper--scaling returns the string to use as the wallpaper style argument for feh.

(defun wallpaper--scaling ()
  "Return the wallpaper scaling style to use."
  (cl-case wallpaper-scaling
    (scale "--bg-scale ")
    (max "--bg-max ")
    (fill "--bg-fill ")
    (tile "--bg-tile ")
    (center "--bg-center ")))

Background color argument

The background color assigned in wallpaper-background is returned by wallpaper--background as a string to add to the feh command.

(defun wallpaper--background ()
  "Return the background color to use as an argument for feh."
  (concat "--image-bg '" wallpaper-background "' "))

Per-workspace wallpaper(s)

This one seemed simple at first but got really dumb and then was made much simpler after a little bit more careful consideration.

(defun wallpaper--per-workspace-wallpapers ()
  "Return the wallpapers for the given workspace.

Returns nil if variable `wallpaper-per-workspace-mode' is nil."
  (when wallpaper-per-workspace-mode
    (cdr (assq (funcall wallpaper-per-workspace-get)
               wallpaper-per-workspace-alist))))

Getting the current workspace in EXWM

This is the default function for wallpaper-per-workspace-get. If EXWM is not configured, it will throw an error when trying to grab the current workspace.

(defun wallpaper-per-workspace-exwm-get ()
  "Return the current EXWM workspace."
  (if (boundp 'exwm-workspace-current-index)
      exwm-workspace-current-index
    (error "Cannot get current EXWM workspace!")))

Getting the current workspace in i3

This one is provided since i3 is the most popular tiling window manager.

(defun wallpaper-per-workspace-i3-get ()
  "Get the current i3 workspace."
  (if (= (shell-command "pgrep i3") 0)
      (if (executable-find "jq")
          (string-to-number
           (shell-command-to-string
            (concat "i3-msg -t get_workspaces | "
                    "jq -r '.[] | select(.focused==true).name'")))
        (error "External command `jq' is missing!"))
    (error "Window manager `i3' is not in use!")))

Getting the current workspace in vdesk

I sometimes use twm so having this is nice.

(defun wallpaper-per-workspace-vdesk-get ()
  "Get the current vdesk."
  (if (executable-find "vdesk")
      (string-to-number (shell-command-to-string "vdesk"))
    (error "External command `vdesk' is missing!")))

Random wallpaper(s)

The overall process has two over-arching steps. First, a list is gathered of all available wallpapers in wallpaper-cycle-directory. Then, the wallpapers currently in use are removed from that list. During this step, the list of wallpapers currently in use is also cleared. Then, for each monitor that can be detected as active by xrandr, a random wallpaper with the proper style argument is appended to the command string.

(defun wallpaper--random-wallpapers ()
  "Return a string of random wallpapers for each monitor.

If `wallpaper-cycle-single' is non-nil, only one wallpaper is returned."
  (let* ((available (wallpaper--get-available))
         (num-available (length available))
         (num-monitors (if wallpaper-cycle-single 1 (wallpaper--num-monitors)))
         (wallpapers nil))
    (dotimes (_ num-monitors)
      (let ((wallpaper (nth (random num-available) available)))
        (cl-pushnew wallpaper wallpapers)
        (setq available (delq wallpaper available))))
    wallpapers))

Getting possible wallpapers

Every file with the extension png or jpg (case-insensitive) inside of wallpaper-cycle-directory or its sub-directories is listed by the command wallpaper--wallpapers, and wallpaper--update-available clears wallpaper-current-wallpapers and returns a list of all wallpapers except those which were in wallpaper-current-wallpapers.

(defun wallpaper--wallpapers ()
  "Return a list of images found in `wallpaper-cycle-directory'."
  (directory-files-recursively wallpaper-cycle-directory
                               wallpaper-cycle-extension-regexp
                               nil))

(defun wallpaper--get-available ()
  "Return `wallpaper--wallpapers' with modification.

This function removes items from `wallpaper-current-wallpapers' from
the resultant list."
  (let ((wallpapers (wallpaper--wallpapers)))
    (dolist (wallpaper wallpaper-current-wallpapers)
      (setq wallpapers (delq wallpaper wallpapers)))
    wallpapers))

Getting the number of active monitors

The function wallpaper--num-monitors is used to determine exactly how many monitors are connected, by splitting a string formed by a shell command with a bit of plumbing to print only one word per active monitor.

(defun wallpaper--num-monitors ()
  "Return the number of connected monitors found by xrandr."
  (length (split-string (shell-command-to-string
                         "xrandr | grep \\* | awk '{print $1}'"))))

Cycling wallpapers automatically

Maybe, like me, even having a unique wallpaper on each monitor isn’t enough. You may want to cycle through your wallpapers and just sit idly all day watching the hundreds of wallpapers you have stored move by. In light of this need, I have a minor mode for that: wallpaper-cycle-mode.

;;;###autoload
(define-minor-mode wallpaper-cycle-mode
  "Toggle Wallpaper Cycle mode.

This mode will activate a timer which will call `wallpaper-set-wallpaper'
at the interval defined by `wallpaper-cycle-interval'.  See function
`wallpaper--toggle-cycle' for more information."
  :lighter " WP"
  :global t
  :group 'wallpaper
  (wallpaper--toggle-cycle))

(defun wallpaper--toggle-cycle ()
  "Stop or start a `wallpaper-set-wallpaper' timer."
  (cancel-function-timers #'wallpaper-set-wallpaper)
  (when wallpaper-cycle-mode
    (run-with-timer 0 wallpaper-cycle-interval #'wallpaper-set-wallpaper)))

Per-workspace wallpapers

An idea someone gave me is setting a wallpaper per workspace. This is the product of that work. Enabling wallpaper-per-workspace-mode will attempt to hook the function wallpaper-set-wallpaper into exwm-workspace-switch-hook, or otherwise enable use of wallpaper-per-workspace-alist for determining what wallpaper(s) to use.

;;;###autoload
(define-minor-mode wallpaper-per-workspace-mode
  "Toggle Wallpaper Per Workspace mode.

This mode will set specific wallpapers based on the current workspace.
See `wallpaper-per-workspace-alist' and `wallpaper-per-workspace-get'."
  :lighter " PW"
  :global t
  :group 'wallpaper
  (wallpaper--toggle-per-workspace))

(defun wallpaper--toggle-per-workspace ()
  "Add or remove setting the wallpaper to `exwm-workspace-switch-hook'."
  (if wallpaper-per-workspace-mode
      (progn
        (add-hook 'exwm-workspace-switch-hook #'wallpaper-set-wallpaper)
        (wallpaper-set-wallpaper))
    (remove-hook 'exwm-workspace-switch-hook #'wallpaper-set-wallpaper)))

Testing

These are specifically notes pertaining to testing this package, and are not really useful for anyone who isn’t directly working on it or just learning how to use the package. Documentation down here is relatively sparse, but made visible to show what tests are performed for each commit.

;;; -*- lexical-binding: t -*-

;;; Code:

Load required files

So far, this is a work in progress, so very little has to be loaded.

(require 'cl-lib)
(require 'wallpaper)
(require 'ert)

Testing static wallpapers

When setting the wallpaper using wallpaper-static-wallpaper-list, there is no change in wallpaper-current-wallpapers between points when the wallpaper is set. If there is a difference, there is a problem.

(defun wallpaper-test--static ()
  "Test whether using a static wallpaper list is working."
  (wallpaper-per-workspace-mode -1)
  (wallpaper-cycle-mode -1)
  (setq wallpaper-static-wallpaper-list '("foo"
                                          "bar"))
  (wallpaper-set-wallpaper)
  (not (equal wallpaper-static-wallpaper-list
              wallpaper-current-wallpapers)))

(ert-deftest wallpaper-test-static ()
  (should (wallpaper-test--static)))

Testing cycling wallpapers

This is another simple one: when wallpaper-cycle-mode is active, different wallpapers should be in wallpaper-current-wallpapers after each passing of wallpaper-cycle-interval. Because we can’t expect there to be a proper X session while testing, wallpaper-cycle-single must be non-nil.

(defun wallpaper-test--cycle ()
  "Test whether `wallpaper-cycle-mode' is setting wallpapers properly."
  (wallpaper-per-workspace-mode -1)
  (setq wallpaper-static-wallpaper-list nil
        wallpaper-cycle-directory (expand-file-name
                                   "test/img" (locate-dominating-file
                                               default-directory ".git"))
        wallpaper-cycle-interval 4
        wallpaper-cycle-single t)
  (wallpaper-cycle-mode 1)
  (let ((previous-wallpapers wallpaper-current-wallpapers))
    (sleep-for 6)
    (not (equal wallpaper-current-wallpapers previous-wallpapers))))

(ert-deftest wallpaper-test-cycle ()
  (should (wallpaper-test--cycle)))

Testing image extension regexp

This ensures the default regexp will work as intended.

(defun wallpaper-test--regexp ()
  "Test whether using a static wallpaper list is working."
  (let* ((wallpaper-cycle-directory (expand-file-name
                                     "test/img" (locate-dominating-file
                                                 default-directory ".git")))
         (wallpapers (wallpaper--wallpapers))
         (expected-1 (expand-file-name "1.png" wallpaper-cycle-directory))
         (expected-2 (expand-file-name "2.jpg" wallpaper-cycle-directory))
         (expected-3 (expand-file-name "3.gif" wallpaper-cycle-directory))
         (expected-4 (expand-file-name "4.jpeg" wallpaper-cycle-directory)))
    (and (member expected-1 wallpapers)
         (member expected-2 wallpapers)
         (member expected-3 wallpapers)
         (member expected-4 wallpapers))))

(ert-deftest wallpaper-test-regexp ()
  (should (wallpaper-test--regexp)))

Testing per-workspace wallpapers

This one was tricky to work with. The way to test it is really weird. I can attest to it working on EXWM and vdesk. Past that you’re on your own. This is a number of different tests crammed into one. First, we make sure that setting the wallpaper does in fact set the wallpaper(s) to the ones found in wallpaper--per-workspace-wallpapers for the given workspace, and then we make sure changing the workspace changes the assigned wallpapers accordingly. If any extraneous wallpapers are in the variable wallpaper-current-wallpapers, the dummy workspace indicator is set to a different value and the test fails.

(defvar wallpaper-test--current-workspace 0
  "Dummy variable for simulating workspace changes.")

(defun wallpaper-test--workspace-set (n)
  "Set `wallpaper-test--current-workspace' to N."
  (setq wallpaper-test--current-workspace n))

(defun wallpaper-test--workspace-get ()
  "Return `wallpaper-test--current-workspace'."
  wallpaper-test--current-workspace)

(defun wallpaper-test--per-workspace ()
  "Ensure per-workspace wallpaper setting is working."
  (wallpaper-cycle-mode -1)
  (setq wallpaper-per-workspace-get #'wallpaper-test--workspace-get
        wallpaper-per-workspace-alist '((0 "foo")
                                        (1 "bar"
                                           "baz")))
  (wallpaper-per-workspace-mode 1)
  (when (equal (wallpaper--per-workspace-wallpapers)
               wallpaper-current-wallpapers)
    (setq wallpaper-test--current-workspace 1)
    (wallpaper-set-wallpaper)
    (dolist (wallpaper wallpaper-current-wallpapers)
      (unless (or (equal wallpaper "bar")
                  (equal wallpaper "bar"))
        wallpaper-test--current-workspace 2)))
  (= wallpaper-test--current-workspace 1))

(ert-deftest wallpaper-test-per-workspace ()
  (should (wallpaper-test--per-workspace)))

End



(provide 'wallpaper)

;;; wallpaper.el ends here