Motivation
If you are a heavy user of the virtual workspaces feature offered by most window managers, it may have occured to you that it would be nice if each qube had its own dedicated workspace. This guide describes one way to achieve that.
Warnings
This guide is provided without warranty of any kind; follow it at your own risk only. While it is provided in good faith, you have no reason to trust it without thorougly checking the contents of the scripts.
Carefully review the provided scripts and only run them if you fully understand them. Despite my best efforts, they could contain bugs or introduce unforeseen vulnerabilities.
This guide is not suitable for beginners. It involves installing custom scripts in dom0, which is generally not advised. These scripts may need to be customized to fit your needs. If you’re unfamiliar with the command line, Qubes OS, or XFCE (configuring desktop icons, workspaces and keyboard shortcuts), you should not attempt to follow this guide. If something goes wrong, you will probably be left with a broken desktop environment with misbehaving windows and missing icons, potentially worse.
Exercise caution and consider backing up your system before proceeding.
Goal
Have dedicated workspaces for selected qubes with qube-specific icons, windows and keyboard shortcuts.
- One workspace for dom0.
- Workspaces for specific qubes.
- Workspaces for specific colors (also called labels) of qubes.
- An “other” workspace for qubes that don’t match any of the criteria above.
Example workspace list:
dom0
personal
work
green
yellow
red
other
The resulting user experience
-
Desktop icons are only shown on specific workspaces based on the qube that they belong to. The workspace is selected by the first rule that matches:
- If the icon does not come from a
.desktop
file, it is ignored (it shows up on all workspaces). - If the icon belongs to dom0, it is shown on the workspace for dom0.
- Otherwise a workspace named after the qube is tried, e.g., “personal”.
- A workspace named after the color (a.k.a. label) of the qube is tried next, e.g., “yellow”.
- A workspace named “other” is used as the fall-back option.
- If the icon does not come from a
-
Windows are automatically moved to a specific workspace based on the qube that they belong to. The workspace is selected by the first rule that matches:
- Windows for dom0 are allowed on all workspaces as they are often needed for all qubes and it would be too cumbersome to always switch to the dedicated workspace.
- Otherwise a workspace named after the qube is tried, e.g., “personal”.
- A workspace named after the color (a.k.a. label) of the qube is tried next, e.g., “yellow”.
- A workspace named “other” is used as the fall-back option.
-
Keyboard shortcuts can be set up to switch to a new or existing instance of an application in the qube corresponding to the active workspace. For example, pressing
ctrl-alt-t
on thepersonal
workspace can activate (launch or switch to) a Terminal in thepersonal
qube. The application launcher (alt-f2
) can also be set up to run in the qube corresponding to the current workspace and therefore launch applications there.- If the current workspace is named after a qube (or dom0), the command is launched in that qube (or dom0)
- Other workspaces do not uniqely identify a qube and thus can not be used for workspace-specific shortcuts. This includes workspaces named after a color (a.k.a. label), the “other” workspace and arbitrarily named workspaces.
Requirements
- Qubes 4.2
- XFCE as the window manager
- the option to hide desktop icons for hidden files being turned on (which is the default)
- English locale.
Other configurations may be made to work too, but this is the one that I used.
The scripts
manage-workspaces
#! /bin/bash
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
shopt -s nullglob
# Allows some basic localization/customization if desired
DOM0=dom0
OTHER=other
# In `xprop` terminology, workspaces are called desktops and reflecting this,
# the script also calls them desktops or desks for short.
# Get the list of all desktops.
get_desk_names() {
# This assumes that the names of desktops do not contain quotes or commas.
xprop -root -notype | grep '^_NET_DESKTOP_NAMES = ' | cut -d ' ' -f 3- | tr -d '"' | sed 's/, /\n/g'
}
# Check whether the passed in string is the name a desktop.
is_valid_desk_name() {
printf "%s\n" "${desk_names[@]}" | grep --silent --fixed-strings --line-regexp "$1"
}
# Determine which desktop to use for a given qube.
get_desk_name_for_qube() {
local qube_name="$1"
if $(is_valid_desk_name "$qube_name")
then
# If the qube name is a valid desktop name, use that.
printf "%s" "$qube_name"
return
fi
local qube_label="${qube_labels[$qube_name]}"
if $(is_valid_desk_name "$qube_label")
then
# If the qube's label is a valid desktop name, use that.
printf "%s" "$qube_label"
return
fi
if $(is_valid_desk_name "$OTHER")
then
# If there is an "other" desktop, use that.
printf "%s" "$OTHER"
fi
}
# Determine which desktop to use for a given launcher.
get_desk_name_for_launcher() {
local launcher="$1"
if [[ "$launcher" != *.desktop ]]
then
# If the launcher is not a .desktop file, ignore it. (It will shows up on all workspaces.)
return
fi
# Get qube name from launcher.
qube_name=$(grep X-Qubes-VmName= "$launcher" | cut -d = -f 2)
if [ -z "$qube_name" ]
then
# If there is no qube name in the launcher, then it is for dom0.
qube_name="$DOM0"
fi
get_desk_name_for_qube "$qube_name"
}
# Determine which desktop to use for a given window.
get_desk_name_for_window() {
local window_id="$1"
# This assumes that no qubes have a quote in their name
local qube_name=$(xprop -id "$1" -notype | grep '^_QUBES_VMNAME = "' | tr -d '"' | cut -d ' ' -f 3-)
if [ -z "$qube_name" ]
then
# Allow dom0 windows on all desktops
return
fi
get_desk_name_for_qube "$qube_name"
}
# Move a window to the desktop it belongs to.
organize_window() {
local window_id="$1"
local desk_name=$(get_desk_name_for_window "$window_id")
# Find the number of the desktop with the given name.
local desk_num=$(get_desk_names | grep --fixed-strings --line-regexp --line-number "$desk_name" | cut -d : -f 1)
if [ "$desk_num" ]
then
wmctrl -i -r "$window_id" -t "$((desk_num - 1))"
fi
}
# Hide a file by adding a dot to beginning of the filename.
hide_file() {
dir=$(dirname "$1")
file=$(basename "$1")
if [[ "$file" != .* ]]
then
mv "$dir/$file" "$dir/.$file"
fi
}
# Unhide a file by removing the dot at the beginning of the filename.
unhide_file() {
dir=$(dirname "$1")
file=$(basename "$1")
if [[ "$file" == .* ]]
then
mv "$dir/$file" "$dir/${file#.}"
fi
}
# Process changes in the window list or the current desktop.
xprop -root -spy -notype | stdbuf -oL grep '^_NET_CURRENT_DESKTOP = \|^_NET_CLIENT_LIST: window id # ' | \
while read -r line
do
# Reread desk names and qube labels in case they were changed (very unlikely but can happen)
readarray -t desk_names <<<$(get_desk_names)
declare -A qube_labels
while IFS='|' read -r name label
do
qube_labels["$name"]="$label"
done <<< $(qvm-ls -O NAME,LABEL --raw-data)
case "$line" in
'_NET_CLIENT_LIST: window id # '*)
# Window list change, process all windows (in reverse order because it is
# most likely that the last window needs to be moved).
printf "%s\n" "$line" | \
sed -e 's/_NET_CLIENT_LIST: window id # //' -e 's/, /\n/g' | \
tac | \
while read -r win_id
do
organize_window "$win_id"
done
;;
'_NET_CURRENT_DESKTOP = '*)
# Current desktop change.
desk_num=$(printf "%s" "$line" | cut -d ' ' -f 3)
# Process visible launchers.
for launcher in ~/Desktop/*
do
desk_name_for_launcher=$(get_desk_name_for_launcher "$launcher")
# Hide those that don't belong on the current desktop.
if [ "$desk_name_for_launcher" -a "$desk_name_for_launcher" != "${desk_names[$desk_num]}" ]
then
hide_file "$launcher"
fi
done
# Process hidden launchers.
for launcher in ~/Desktop/.*
do
desk_name_for_launcher=$(get_desk_name_for_launcher "$launcher")
# Show those that belong on the current desktop.
if [ "$desk_name_for_launcher" = "${desk_names[$desk_num]}" ]
then
unhide_file "$launcher"
fi
done
;;
esac
done
switch-to-or-run-in-workspace
#! /bin/bash
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
shopt -s nullglob
set -o pipefail
# Allows some basic localization/customization if desired
DOM0=dom0
target_class="$1"
shift
if [ -z "$1" ]
then
cat >&2 << EOF
Usage: $0 {target-class} {target-command}
Example: $0 org.mozilla.firefox firefox
The window class and the command both need to be specified, because one can not
be determined from the other. You can get the window class by launching the
application then finding the corresponding window in the output of the
"wmctrl -xl" command.
EOF
exit 1
fi
desk_num=$(xprop -root -notype | grep '^_NET_CURRENT_DESKTOP = ' | cut -d ' ' -f 3)
# This assumes that the names of desktops do not contain quotes or commas.
desk_name=$(xprop -root -notype | grep '^_NET_DESKTOP_NAMES = ' | cut -d ' ' -f 3- | tr -d '"' | sed 's/, /\n/g' | \
nl -v0 -w1 -s: | grep "^${desk_num}:" | cut -d: -f2)
# If there is a matching window, then switch to it. The logic is based on
# observing what `wmctrl -xl` shows for dom0 and domU windows.
if [ "$desk_name" = "$DOM0" ]
then
if wmctrl -xl | grep -v 'N/A N/A | grep --silent --fixed-strings "${target_class}"
then
wmctrl -xa "${target_class}"
exit 0
fi
else
if wmctrl -xl | grep --silent --fixed-strings "${desk_name}:${target_class}"
then
wmctrl -xa "${desk_name}:${target_class}"
exit 0
fi
fi
# If there was not matching window, then start a new instace.
notify-send "Starting '$@' on $desk_name"
if [ "$desk_name" = "$DOM0" ]
then
"$@" || notify-send "Failed to run '$@' on $desk_name"
else
# It is not strictly necessary to start the domain manually (qvm-run would do
# it if necessary), but this allows better error messages.
qvm-start --skip-if-running -- "$desk_name" 2>&1 | ifne xargs -0 notify-send "Starting domain failed" || exit 1
qvm-run -q "$desk_name" -- "$@" 2>&1 || notify-send "Failed to start '$@' on $desk_name"
fi
Installation
Save the scripts to a directory in your PATH
in dom0 (non-root user) and make them executable.
Carefully review the contents of the scripts and make sure that you understand what they do. If you have any doubts, do not run them.
Usage
manage-workspaces
Before running this script for the first time, make a backup of your ~/Desktop
directory. Running the script will hide the majority of your desktop icons (in order to only have the icons for the current workspace left visible). If you don’t want to keep using the script, you need to unhide your icons manually (restore the backup or see removal steps below).
The manage-workspaces
script is meant to be running continously and takes care of moving windows between workspaces and hiding and showing icons based on the current workspace. You can run it manually to try it and see any potential error messages (it may be possible that some dependencies need to be installed). If it runs successfully, you can configure XFCE to start it at the beginning of the session.
You can add, remove and rename workspaces either before or after starting the script.
switch-to-or-run-in-workspace
The switch-to-or-run-in-workspace
script is meant to be triggered by keyboard shortcuts (but can be run manually as well for testing). It takes two parameters, the windows class and the command name. You can get the window class of any application by launching it then finding the corresponding window in the output of the wmctrl -xl
command.
Example shortcut configuration:
shortcut | command | description |
---|---|---|
alt-f2 |
switch-to-or-run-in-workspace xfce4-appfinder xfce4-appfinder --collapsed |
Application launcher |
ctrl-alt-f |
switch-to-or-run-in-workspace org.mozilla.firefox firefox |
Firefox |
ctrl-alt-t |
switch-to-or-run-in-workspace xfce4-terminal xfce4-terminal |
Terminal |
How does it work under the hood?
wmctrl
allows moving windows to specific workspaces.xprop
allows accessing window metadata on a lower level. This is easier to parse and allows us to listen for window and workspace events instead of polling them withwmctrl
.- The qube for a window can be determined from the window metadata.
- The qube for a launcher can be determined from the .desktop file.
- By default, xfce does not show icons on the desktop for hidden files, therefore a desktop icon can be hidden by adding a dot to the start of the filename.
How to remove the scripts?
- Remove
manage-workspaces
from the session autostart and kill the one that is already running (or log out and back in to start a new session without it). - When the script is not running, some desktop icons will be stuck in a hidden state that needs to be fixed.
- If you made a backup of
~/Desktop
directory, then restore it. - Otherise remove the dot from the start of the desktop files in
~/Desktop
to make them show up again. You can use the following bash command for this:cd ~/Desktop; for name in .*.desktop; do mv "$name" "${name#.}"; done
.
- If you made a backup of
- Remove the keyboard shortcuts you added and restore the default ones if needed.
- Remove the scripts.