KDE - changing the way you use Qubes

TL;DR: 1. Keyboard layout switching that works and preserves privacy; 2. Dynamically uncluttering the system tray in general (configurable).

Another one who appreciates Kubes OS here :wink:

I think most here know that there are a couple of bugs with KDE in dom0 that have not been fixed in years: keyboard layout switching does not propagate properly to any qube; and system tray icons based on Gtk3 are broken (invisible or white squares).

If we go deeper into Qubes OS keyboard layout switching and system trays in general though, and not just in KDE, there are things that can be improved:

  1. The current Qubes OS keyboard layout switching design, as I understand it, involves dom0 telling all qubes at the same time (and not just the one you’re currently using) to switch to the new keyboard layout (when it works, anyway). This seems to happen regardless of Layout Switching Policy in settings being per Application/Window/Desktop/Global, which doesn’t seem to change anything for qube windows (correct me if I’m wrong). Such a design is bad for fingerprinting, due to revealing to other untrusted qubes things like which languages you use and exactly when you use them, making a kind of a timing correlation side-channel. Even if eliminating all similar channels would be hard and outside of scope for Qubes OS, I’d argue this is among the worse ones, and moreover, revealing any of this is unnecessary. Relegating layout configuration and switching entirely to templates or qubes and away from dom0 would be one way to improve privacy (and it would work, unlike KDE&dom0).
  2. In Qubes OS we tend to get more system tray entries than in other OS, sometimes multiple instances of the same app, each from a different qube, for example:

It would be nice if there was a way to automatically hide some of the system tray entries when they are not needed, and show them again when they are. Like, hide entries that belong to qubes other than the one I’m currently using. It would reduce clutter and confusion (this would also help somewhat with broken icons).

The reason I’m bringing up 1 & 2 together is that solutions for both go well together.
My solutions/workarounds/hacks:

  1. (KDE or not) Install fcitx5 in each template where it’s needed and configure keyboard layouts/input methods and keyboard shortcuts in each qube where it’s needed separately. Apparently Fcitx5’s layout switching is not broken by Qubes OS’s layout switching (non-)integration, and it also has a working and visible system tray entry to indicate the current layout.
    In case of Fcitx5 not working with some of the apps or some other issue, remove it completely and install ibus instead. Beware that it’s based on Gtk3, so its systray icon is broken in KDE dom0, but you get a configurable layout/input method indicator in the center of the screen when pressing the keyboard shortcut for switching, which I think is good enough.
    Unless you want to be able to switch layouts for dom0 itself, you can disable them from System SettingsInput devicesKeyboardLayouts → delete the keyboard shortcuts you want to use in Fcitx5/IBus instead and uncheck Configure layouts.
  2. (KDE only) Using some ideas from other scripts, I put together something that does the job of automatically hiding&showing tray entries after manually specifying the names of qubes and tray entries to be shown with them. Yeah, it’s ugly, not the most convenient to set up initially, and won’t hide entries from [dispXXXX] reasonably, but at least for tray entries that can be specified you get a lot of control. If you want you can set up the simple “show only entries from the active qube” (or something more elaborate), except anything not specified in the script stays the way it’s configured in the System Tray Settings. The script works by toggling that very setting between the default Shown when relevant and Always hidden for the entries you want.

If you’re interested in this you can start by reviewing this JavaScript Plasma Desktop script, transferring it to ~/trayUpdate.js in dom0 (or typing it) and checking and configuring it in dom0 using nano ~/trayUpdate.js:

trayUpdate.js
// trayUpdate script by Stanley Qubrick, released in the public domain

// The name of the focused window's qube needs to be somehow provided as a variable called qubeFocused, such as by running a command like:
// qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "var qubeFocused = 'sys-net';$(< ~/trayUpdate.js)"
// to complete the script. Without a qubeFocused value this file is not a useful script.
//var qubeFocused = 'sys-net';
var qubeFocused;

// In trayEntries below you need to manually provide the names of qubes and the system tray entries to automatically hide.
// You must add a Window Action for all of these qubes so that their corresponding entries can also reappear when specified here.
// Add and edit lines in the following format as desired:
// ["qube1", ["[qube1] Fcitx5 Tray Window", "[qube1] Other app's tray ID", "Any other entry relevant to qube1 but not to some other qube",]],
// You can copy the names of system tray entries from ~/.config/plasma-org.kde.plasma.desktop-appletsrc if you've ever touched their setting.
// This script always respects the user's "Always shown" choice in the system tray configuration, but it needs to hijack the "Always hidden"
// setting for system tray entries included in any trayEntries value. You can omit an entry from trayEntries entirely to keep its setting.
const trayEntries = new Map([
    ["sys-net",    ["[sys-net] NetworkManager Applet",]],
    ["sys-whonix", ["[sys-whonix] Sdwdate",]],
    ["",           []], // qubeFocused='' for dom0 windows. You can use a generic value like 'show all' to cover multiple qubes or to do other things.
    //["show all",    ["[sys-net] NetworkManager Applet","[sys-whonix] Sdwdate",]],
]);

// This function is needed for handling the case of no "Always hidden" entries.
// https://stackoverflow.com/questions/5164883/the-confusion-about-the-split-function-of-javascript-with-an-empty-string/25597485#25597485
String.prototype.splitPlus = function(sep) {
    var a = this.split(sep);
    if (a[0] === "" && a.length === 1) return [];
    return a;
};

// Based on "Adding a widget to the System Tray" example script 2025 The KDE Community CC-BY-SA-4.0
// https://develop.kde.org/docs/plasma/scripting/examples/#adding-a-widget-to-the-system-tray
for (i = 0; i < panelIds.length; ++i) { //search through the panels
    panel = panelById(panelIds[i]);
    if (!panel) continue;
    for (tmpIndex = 0; tmpIndex < panel.widgetIds.length; tmpIndex ++) {
        appletWidget = panel.widgetById(panel.widgetIds[tmpIndex]);
        if (appletWidget.type == "org.kde.plasma.systemtray") {
            systemtrayId = appletWidget.readConfig("SystrayContainmentId");
            if (systemtrayId) {
                var systray = desktopById(systemtrayId);
                systray.currentConfigGroup = ["General"];
                var hiddenItems = systray.readConfig("hiddenItems"); // "Always hidden" entries in the system tray configuration.
                // Add together the "Always shown" entries and the ones specified for qubeFocused:
                var entriesToShow = systray.readConfig("shownItems").splitPlus(",").concat(trayEntries.get(qubeFocused));
                // Turn the current hiddenItems into an array; add all provided trayEntries; filter out the ones to show; remove duplicates:
                var entriesToHide = [...new Set(hiddenItems.splitPlus(",").concat(...trayEntries.values()).filter(e => !entriesToShow.includes(e)))];
                //print("\nentriesToHide =\t" + JSON.stringify(entriesToHide));
                if (hiddenItems !== entriesToHide.join()) { // Avoid unnecessary config write and reload
                    systray.writeConfig("hiddenItems", entriesToHide);
                    systray.reloadConfig();
                }
            }
        }
    }
}

Then you can create a single Window Action that executes a command whenever any window gets the focus from System SettingsShortcutsCustom Shortcuts → right click or EditNewWindow ActionCommand/URL → name it window focusTrigger → select Window gets focus → select Window simple: → on the right side click Edit... → under Window Types select NormalOKAction → as the Command/URL either type the following one-liner:

qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "var qubeFocused='$(xprop -id $(xdotool getwindowfocus) _QUBES_VMNAME|cut -d\" -f2 -s)';$(< ~/trayUpdate.js)"

Or, for easier review and modification, enter the path to a new Bash script with the following contents which do the same thing:

trayUpdateFocused.sh
#!/bin/bash

CUR_WIN_ID=$(xdotool getwindowfocus)
CUR_VM=$(xprop -id $CUR_WIN_ID _QUBES_VMNAME | cut --delimiter=\" --fields=2 --only-delimited)
# We start constructing a Plasma Desktop script (JavaScript) here as a way to pass the name of the focused window's qube to it:
PLASMA_SCRIPT="var qubeFocused='$CUR_VM';$(< ~/trayUpdate.js)"
# Documentation for running Plasma Desktop scripts: https://develop.kde.org/docs/plasma/scripting/#running-scripts
qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "$PLASMA_SCRIPT"

Tell me what you think about all this.

If other people here appreciate per-qube tray icons as I do, an obvious improvement goal would be to remove the need to type names and instead do the job for all existing tray entries belonging to qubes, but care must be taken if it involves parsing the entry name strings which come from untrusted qubes. Switching to an approach that does not touch user settings at all would be nice too, as would be supporting XFCE.