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:
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/>.
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)
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.
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-")
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.
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)
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"))
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)
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)
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)
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)
There are five values possible for wallpaper-scaling
:
scale
: Scale the image to fit the screen, distorting the imagemax
: Show the whole image, leaving portions of the screen uncoveredfill
: Fill the entire screen, cutting off regions of the imagetile
: Tile the image across the screen for small imagescenter
: 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)))
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)
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.")
In order to properly form the wallpaper setting command, functions have been defined to return the flags required to properly construct the command.
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 ")))
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 "' "))
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))))
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!")))
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!")))
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!")))
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))
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))
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}'"))))
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)))
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)))
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:
So far, this is a work in progress, so very little has to be loaded.
(require 'cl-lib)
(require 'wallpaper)
(require 'ert)
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)))
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)))
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)))
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)))
(provide 'wallpaper)
;;; wallpaper.el ends here