Wayland Sway - programs in qubes don’t launch

I tried installing sway in dom0 using ‘sudo qubes-dom0-update sway’ and launched sway from lightdm. Checked that it was running Wayland, and funnily enough, it can run applications/programs from within dom0, but if I try to make it run then using qvm run or by launching the app menu from the command line, I get nothing. However, I know that on my previous computer on which I hade KDE installed, a kde Wayland session worked. Any idea how I could make this work?

Thanks

Wayland is not yet supported in Qubes OS:

Has this not changed in light of support for KDE Wayland sessions? I (in 4.3rc1) have adjusted the xdg autostart units to ensure that qubes-guid is passed the --kde flag, and that Xwayland-satellite is monkey-patched via LD_PRELOAD, but am still not seeing any DomU windows in Niri (looks like OP). I don’t see anything suspicious in the qubes-guid logs, though I haven’t checked the gui-agent logs inside a Qube yet.

I understand that KWin, like Mutter, can have some bespoke quirks, and that it might not be as simple as crawling the sources and trying to do what KWin does (to route through Xwayland) at runtime – but I’d like to know where the differences are, and unless someone with a background in the KDE Wayland Session effort chimes in, that’s the only place to start~

It should work, as long as you LD_PRELOAD the .so file on Xwayland.
Though Xwayland support for that was added not too long ago, so probably you need to be running 4.3.

(I am not certain about that and haven’t tried it with Xwayland-sattelite based compositors, only mutter)
I have had it working with mutter on Wayland.
If you don’t LD_PRELOAD, you get no qubes guid windows, but they work just fine if you do.

You need to LD_PRELOAD “”/usr/lib64/qubes-gui-daemon/shmoverride.so" when the compositor is launching Xwayland. (Seemed simply setting the variable when launching the compositor didn’t quite work last time I tried).
But how to do this probably differs for each compositor, as in the packaging for KDE they are using a KDE-specific approach.
I think it worked generally when I replaced /usr/bin/Xwayland with a script that launuches the real Xwayland binary put elsewhere, while setting the LD_PRELOAD.

Or you can simply add a small patch to the compositor when it launches Xwayland that sets the LD_PRELOAD. For example, in meta_xwayland_start_xserver function add “g_subprocess_launcher_setenv (launcher, “LD_PRELOAD”, “/usr/lib64/qubes-gui-daemon/shmoverride.so”, TRUE);” after creating the launcher for mutter.

Another issue is with the QUI tray widgets.
By default, they try to launch with GDK wayland backend on Wayland, which prevents them from even trying to Xembed themselves.
So you need to set GDK_BACKEND=x11 for those, I had set it in systemd service environment configuration for them, but I think they have very recently changed this upstream to always try x11 backend for them.

Also, I would guess the traditional style qubes notifications which are simply windows running in the apps would be very broken with Xwayland-sattelite using compositors, so you would need to use the recently added qubes notification proxy.

edit: see follow-up, hopefully none of this is relevent when i actually do as suggested >u<

Thank you! Filled with determination, I had another crack at it. Should probably link GUI virtualization | Qubes OS, for lurkers.

I stripped back my modifications to the XDG autostart units, ignoring qvm-start-daemon’s --kde flag for now and disabling Xwayland-related units so I can start them manualy (in both XDG-Autostart and the global-user service /usr/lib/systemd/user/xwayland-satellite.service), making sure to keep a Polkit daemon. I think narrowed my issue down to XAUTHORITY, which was not being set by SDDM. I switched back to LightDM, but that doesn’t setup Xauthority unless you’re starting an X11 session? Not my area of expertise, but I think we can fake something permissive…

$ export XAUTHORITY="$HOME/.Xauthority"
$ xauth add :0 . $(xxd -l 16 -p /dev/urandom)

$ systemctl stop --user app-qvm\\x2dstart\\x2ddaemon@autostart
# Nothing changes with `--kde` or if ran as root, which I did try bc of `root:qubes` perms on prior log files
$ /usr/bin/qvm-start-daemon --all --watch &
$ LD_PRELOAD=/usr/lib64/quibes-gui-daemon/shmoverride.so xwayland-satellite &
$ qvm-shutdown untrusted || true
$ qvm-run untrusted glxgears # ret 1
$ tail /var/log/qubes/guid.untrusted.log
Invalid MIT-MAGIC-COOKIE-1 key
# uhhh...
$ xhost +
access control disabled, clients can connect from any host
$ qvm-run untrusted glxgears

We get a window!! It’s blank white, but, goals! VSync’s correctly, too.

guid.untrusted.log
$ tail /var/log/qubes/guid.untrusted.log
Cannot obtain current desktop
Icon size: 128x128
Falling back to setting _NET_WM_USER_TIME on the root window
WARNING: Running setxkbmap against an Xwayland server
Created 0x1c00187(0x200003) ovr=0 x/y 0/0 w/h 100/100
 XDestroyWindow 0x1c00187
cannot lookup 0x1c00187 in wid2windowdata
# [...]
set WM_NORMAL_HINTS for window 0x1c0018a to min=0/0, max=0/0, base=0/0, inc=0/0 (flags 0x0)
set title for window 0x1c0018a
set class hint for window 0x1c0018a to (untrusted:Xfsettingsd, untrusted:xfsettingsd)
Created 0x1c0018b(0x200002) ovr=0 x/y -1/-1 w/h 1/1
Created 0x1c0018c(0x200003) ovr=0 x/y 0/0 w/h 10/10
Created 0x1c0018d(0xa00002) ovr=0 x/y 0/0 w/h 300/300
set WM_NORMAL_HINTS for window 0x1c0018d to min=0/0, max=0/0, base=0/0, inc=0/0 (flags 0x1)
set title for window 0x1c0018d
set title for window 0x1c0018d
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=0, y=0, w=300, h=300
MSG_WINDOW_DUMP for 0x1c0018d(0xa00002): 300x300, type = 0
xcb_shm_attach_fd failed for window 0x1c0018d(remote 0xa00002)
ErrorHandler: BadMatch (invalid parameter attributes)
                 Major opcode: 130 ()
                 Minor opcode: 6
                 ResourceID:   0x11c
                 Failed serial number:  986
                 Current serial number: 984
shmimage for 0x1c0018d(remote 0xa00002), x: 0, y: 0, w: 300, h: 300
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=0, y=0, w=300, h=300
shmimage for 0x1c0018d(remote 0xa00002), x: 0, y: 0, w: 300, h: 300
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=0, y=0, w=300, h=300
process_xevent_configure(synth 0) local 0x1c0018d remote 0xa00002, 1256/1348, was 300/300, xy 0/0 was 0/0
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=0, y=0, w=1256, h=1348
handle_configure_from_vm, local 0x1c0018d remote 0xa00002, 1256/1348, was 1256/1348, ovr=0 (ignored), xy 0/0, was 0/0
MSG_WINDOW_DUMP for 0x1c0018d(0xa00002): 1256x1348, type = 0
xcb_shm_attach_fd failed for window 0x1c0018d(remote 0xa00002)
ErrorHandler: BadMatch (invalid parameter attributes)
                 Major opcode: 130 ()
                 Minor opcode: 6
                 ResourceID:   0x11c
                 Failed serial number:  988
                 Current serial number: 987
shmimage for 0x1c0018d(remote 0xa00002), x: 300, y: 0, w: 956, h: 300
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=300, y=0, w=956, h=300
shmimage for 0x1c0018d(remote 0xa00002), x: 0, y: 300, w: 1256, h: 1048
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=0, y=300, w=1256, h=1048
shmimage for 0x1c0018d(remote 0xa00002), x: 0, y: 0, w: 1256, h: 1348
  do_shm_update for 0x1c0018d(remote 0xa00002), after border calc: x=0, y=0, w=1256, h=1348
# [...]

xcb_shm_attach_fd failed for window 0x1c0018d(remote 0xa00002)

I’ve triple-checked that the running Xwayland-satellite has the LD_PRELOAD (via /proc/[...]/environ/output), but FWIW I didn’t see any log activity in Xwayland-satellite when I ran a foreground instance – qubesd logs look positive: I wouldn’t necessarily know if anything was missing but looks like it’s actively going down the happy-path, and the SHM errors probably indicate the remaining issue.

I do not have enough of an X11 background to make heads or tails of the xauth situation, but think I’ve learned enough that another long look at a running Plasma instance might be fruit-full, if only to get to this point at startup. Thank you for your insights!

(Seemed simply setting the variable when launching the compositor didn’t quite work last time I tried).
But how to do this probably differs for each compositor, as in the packaging for KDE they are using a KDE-specific approach.
I think it worked generally when I replaced /usr/bin/Xwayland with a script that launuches the real Xwayland binary put elsewhere, while setting the LD_PRELOAD.

On-the-go now, but think you’re spot on: I should be prefixing Xwayland-satellite’s PATH with /usr/libexec/qubes/wrappers (per qubes-gui-daemon#b42f1ab) rather than setting LD_PRELOAD on it directly. Something I definitely learned during background-reading and promptly hopefully! glossed over. Between that and today’s XAUTH investigation (perhaps obsolete, post-patch, to fit your widow-less, rather than blank-window-ed, failure-case in mutter?), I should be set! Thx again!

Posting from Niri in Dom0! Yay! (4.3rc1)

For a minute I was worried I wasn’t out of the weeds, because it took a few tries – had to do a full restart after a Qubes daemon died, and I managed to segfault Xwayland (but thankfully don’t need to post logs >u<). I did need to set XAUTHORITY and run xhost + after starting Xwayland-satellite, which I understand isn’t best practice, or I saw Authorization required, but no authorization protocol specified (the initial hurdle). I’m still going to take a closer look at what Plasma is doing before staging this session to start-up without fiddling, will see what I can do~

I’m still going to take a closer look at what Plasma is doing before staging this session to start-up without fiddling, will see what I can do~

I tried my best to put eg. Autostarts and profile configs back to stock, but Plasma wouldn’t start and I don’t have time to roll dom0 back to an ol’ snapshot rn >u<

I think I’ve done my part in demonstrating the latest updates on this

Shared more info and some short clips in another relevant thread, sorry to bump them both but I think a lot of traffic comes here from search and that the cross-links will be helpful to the broader crowd c:

1 Like

Hello everyone!
I’m trying to make this work also; I’ll just make a little summary here for my personal reference and for other people as well.

  • This should work only in 4.3.0-rc1 and up. Current available version is 4.3.0-rc3. Short summary is that there is now better support for Wayland. PR here
  • Xwayland acts as a X11 translator; most apps still speak X11 protocol into Xwayland, which then speaks Wayland to the compositor (like Sway/KDE/Niri)
  • There is something called Xwayland-sattelite, It seems integrated with Niri 25.08 up. (article). I think it’s just what allows Xwayland and Niri to speak together? I don’t understand if it’s a standalone binary that’s running between Xwayland and Niri or rather a library that’s being used by Niri. I’ll just assume that it’s a library that sits between Xwayland and core Niri for now.
  • The key point is to preload a .so file into Xwayland which I’m explaining in a dedicated section of this post.
  • Xwayland, the compositor, etc, all run in dom0.

.so preloading

The .so that needs to be pre-loaded is /usr/lib64/qubes-gui-daemon/shmoverride.so

For that, we need to set the LD_PRELOAD variable.

There are different approaches:

  • wrapping the Xwayland binary with a script
  • configuring the compositor to use the stock wrapped Xwayland (/usr/libexec/qubes/wrappers/Xwayland)
  • patching the compositor to set the LD_PRELOAD environment variable when starting Xwayland

Setting this should be done at the compositor level (e.g. Sway/Niri) and the configuration depends on each.

GUI tray widgets

Concern for later, also probably will want to have my own custom status bar long term so not a big issue.

Notifications

They will be broken with compositors; should try to use the “qubes notification proxy”.

I think that’ll be a problem for later for me…

Installation steps

  1. Install Niri
  2. Enable Wayland stuff (I noted down the command for this and the above somewhere, will add it here later)
  3. Edit Niri configuration to use the QubesOS stock wrapped Xwayland instead of /usr/bin/Xwayland that it’s probably starting
  4. … pray that it works, and if it doesn’t, try fiddling with XAUTHORITY and xhost + as mentioned by @antlers

Got firefox to appear!

Posting this from Niri in 4.3.0-rc3 :sunglasses: !

I still have a long way to go, but I could figure out how to:

  1. Get a blank window to spawn
  2. Make it non-blank

@antlers thanks for the messages, it was a bit hard to follow but I couldn’t have done it without you :slight_smile:

How to replicate it?

I still have to integrate it with Niri correctly by editing the configuration.

I’ll post details later, because I took notes into dom0 and it’s a pain to copy-paste it here.

(I’m still new to qubes and don’t know all the commands to move the clipboard around etc)

Stuff to do

It’s far from complete, and IMO Niri deserves more love in QubesOS.

Replacing the status bar in Xfce

The status bar in Xfce contains a ton of useful stuff: attaching USB devices, getting wifi information, etc.

It would be nice if I could somehow use this status bar with Niri?

Or perhaps I could just edit waybar to have similar functionality?

It’s really useful to have a GUI to attach USB devices.

Getting colors around windows to signal which qubes they belong to

@antlers you mentioned that you created a script to do this, would you mind sharing?

Get notifications to work

There are no notifications.

There was a notifications proxy mentioned in this thread.

Perhaps it would be worth a look to figure out how to hook notifications and republish them to a notifications daemon such as mako?

The goal being to have notifications but nice keybindings to dismiss them / setting focus modes (which I never got around to on my main laptop, but which is possible apparently)

Appendix

What to do if stuff from a Qube doesn’t attach?

I wanted to get a shell into sys-net.

I needed to run this command for it to appear:

qvm-start-gui sys-net

I feel like I opened a big can of worm… Hopefully it won’t be too painful to get something nice…

(btw I just wanted to open it to get nmtui to work, in order to replace the Wi-Fi status bar thing when working on the go e.g. at a coffee shop)

(okay, here is the command to install nmtui : sudo dnf install -y NetworkManager-tui)

Getting colors around windows to signal which qubes they belong to

@antlers you mentioned that you created a script to do this, would you mind sharing?

Of course! I’d have included it in the first place if I’d thought it suitable for broader use, let alone up-streaming. I have not daily-driven it re: exactly your concerns about replacing tray functionality: I want to build something really fancy, like a vertical tab-bar of per-domain workspaces with status info and management actions – though I imagine a set of Rofi menus in the style of SXMO, calling out to eg. qvm-usb attach, would get us a lot of the way there.

What I’ve got is a daemon that (ab)uses Niri configs hot-reload by keeping a fenced code-block up-to-date based on Qubes domain metadata and ~/.local/share/qubes-kde/*.colors (itself populated when qvm-start-daemon is passed the --kde flag, eg. in /etc/xdg/autostart/qvm-start-deamon-kde.desktop). Ideally there would be an agnostic channel or trigger for color definitions, and Niri RPC commands for dynamic border rules, but if either were available I imagine I’d have found and used them so ig that’s it for now

@antlers thanks for the messages, it was a bit hard to follow but I couldn’t have done it without you :slight_smile:

thank you, means a lot c:

qvm-start-gui sys-net […] I feel like I opened a big can of worm

qvm-start-gui should be spawned per-domain automatically by eg. qvm-start-daemon, logged to /var/log/qubes/{qrexec,guid}.[domain].log; sometimes I had to restart such services if I saw XWayland-Satellite segfaulting in journalctl/dmesg but that was never an issue unless I was fiddling with the LD_PRELOAD or running my unstable Qt6 build, not sure what that would look like now that Niri is shipping with XWayland-Satellite…

~/.local/bin/niri-qubes
#!/usr/bin/env bash
set -euo pipefail

while true; do
  # -- Zero-eth memoization
  TIME_1="$(stat -c "%Y" /var/lib/qubes/qubes.xml)"
  if [[ $TIME_1 == ${LAST_TIME_1:-} ]]; then
    sleep 1
    continue
  fi
  LAST_TIME_1="$TIME_1"

  # -- Read qubes.xml
  CONTENT="$(grep '^   \? \?</\?\(domain\|label\)\|^        <property name="\(name\|label\)"' /var/lib/qubes/qubes.xml)"

  # -- First memoization
  HASH_1="$(md5sum <<< "$CONTENT" | awk '{printf $1}')"
  if [[ $HASH_1 == ${LAST_HASH_1:-} ]]; then
    sleep 1
    continue
  fi
  LAST_HASH_1="$HASH_1"

  declare -A DOMAINS
  RULES=""
  LABEL=""
  NAME="dom0"

  STATE="READING_DOMAIN_PROPS"
  while [[ $STATE != DONE ]] && read -r LINE; do
    # -- Read props
    if [[ $STATE == "READING_DOMAIN_PROPS" ]]; then
      # -- Label
      if [[ $LINE == '<property name="label">'*'</property>' ]]; then
        LABEL="${LINE##*\">}"
        LABEL="${LABEL%%<*}"
      # -- Name
      elif [[ $LINE == '<property name="name">'*'</property>' ]]; then
        NAME="${LINE##*\">}"
        NAME="${NAME%%<*}"
      elif [[ $LINE == *'</domains>'* ]]; then
        STATE="PROCESSING"
      fi

      # -- Save pairs
      if [[ -n "$LABEL" ]] && [[ -n "$NAME" ]]; then
        DOMAINS["$NAME"]="$LABEL"
        LABEL=""
        NAME=""
      fi
    fi

    # -- Construct window-rules
    if [[ $STATE == "PROCESSING" ]]; then
      # -- Second memoization
      HASH_2="$(md5sum <<< "${!DOMAINS[@]}${DOMAINS[@]}" | awk '{printf $1}')"
      if [[ $HASH_2 == ${LAST_HASH_2:-} ]]; then
        STATE="DONE"
        sleep 1
        continue
      fi
      LAST_HASH_2="$HASH_2"

      # -- Append for each
      for NAME in "${!DOMAINS[@]}"; do
        LABEL="${DOMAINS[$NAME]}"

        # XXX: hehehe
        while read -r PAIR; do
          eval "$PAIR"
        done <<< "$(grep -F 'activeBackground=' "$HOME/.local/share/qubes-kde/$LABEL.colors")"

        activeBackground=$(printf '#%02x%02x%02x' $(tr ',' ' ' <<< "$activeBackground"))
        inactiveBackground=$(printf '#%02x%02x%02x' $(tr ',' ' ' <<< "$inactiveBackground"))

        RULES+="\
window-rule {
  match app-id=\"$NAME:*\"
  border {
    active-color \"$activeBackground\"
    inactive-color \"$inactiveBackground\"
  }
}
"
      done

      # -- Splice info config
      # XXX: Probably not the best way to escape the newlines for sed, but it was
      # giving me a lot of trouble and (for some reason) this works, so....
      RULES="${RULES@Q}"
      RULES=$(cut -c 3-$((${#RULES}-1)) <<< "$RULES")
      sed \
        -i -z \
        's/\(\/\/ #+BEGIN_NIRI_QUBES\)\n.*\n\(\/\/ #+END_NIRI_QUBES\)/\1\n'"$RULES"'\n\2/g' \
        "$HOME/.config/niri/config.kdl"

      STATE="DONE"
    fi
  done <<< "$CONTENT"

  sleep 1s
done
~/.local/share/qubes-kde/blue.colors
[Colors:Window]
BackgroundNormal=52,101,164

[WM]
activeBackground=52,101,164
inactiveBackground=26,50,82
activeForeground=255,255,255
~/.config/niri/config.kdl
environment {
  DISPLAY ":0"
  GTK_BACKEND "x11"
  QT_QPA_PLATFORM "wayland"
  QT_QPA_PLATFORMTHEME "qt5ct"
  QT_QPA_PLATFORMTHEME_QT6 "qt6ct"
  XDG_CURRENT_DESKTOP "niri"
  XDG_SESSION_TYPE "wayland"
  XAUTHORITY "/home/antlers/.Xauthority"
}

spawn-at-startup "bash" "-c" "env PATH=\"/usr/libexec/qubes/wrappers:$PATH\" xwayland-satellite"
spawn-at-startup "bash" "-c" "sleep 1 && xhost +"
spawn-at-startup "quickshell" "-c" "noctalia-shell"
spawn-at-startup "bash" "-c" "multibg-wayland --compositor niri ~/Wallpapers/ &"
spawn-at-startup "bash" "-c" "niri-qubes &"

layout {
    focus-ring {
        off
    }

    border {
        width 6
        active-color "#ffc87f"
        inactive-color "#505050"
        urgent-color "#9b0000"
    }
}

prefer-no-csd

window-rule {
    match app-id="qrexec-policy-agent" title="^Operation execution$"
    match app-id="qubes-new-qube" title="^Create New Qube$"
    match app-id="qubes-qube-manager" title="^Clone qube$"
    match app-id="qubes-update-gui" title="^Applying updates to qubes$"
    open-floating true
}

// #+BEGIN_NIRI_QUBES
window-rule {
  match app-id="sys-usb:*"
  border {
    active-color "#cc0000"
    inactive-color "#660000"
  }
}
window-rule {
  match app-id="sys-firewall:*"
  border {
    active-color "#73d216"
    inactive-color "#39690b"
  }
}
window-rule {
  match app-id="dom0:*"
  border {
    active-color "#000000"
    inactive-color "#000000"
  }
}
window-rule {
  match app-id="disp3218:*"
  border {
    active-color "#cc0000"
    inactive-color "#660000"
  }
}
window-rule {
  match app-id="work:*"
  border {
    active-color "#3465a4"
    inactive-color "#1a3252"
  }
}
// [and so on...]
// #+END_NIRI_QUBES

P.S: On Exfiltration from Dom0, + rambling

The common solution is (an alias for):

dd if=$SRC_PATH | qvm-run --pass-io $DEST_DOMAIN -- dd of=$DEST_PATH

Sans “Useless Use of dd”:

cat $SRC_PATH | qvm-run --pass-io $DEST_DOMAIN -- 'cat > $DEST_PATH'

Sans “Useless Use of cat”:

< $SRC_PATH qvm-run --pass-io $DEST_DOMAIN -- '{ while read -r LINE; do printf '%s' "$LINE"; done; printf '\n; } > '"$DEST_PATH"

\j, probably wouldn’t be here at all if i didn’t have a bit of fun recalling and verifying the meme variants :stuck_out_tongue:

i do think it’s an interesting case of intentional-omission, and will try to move my notes and config repos into DomU’s w/ some sort of “deploy” pipeline into Dom0 (where needed). the “idiomatic” approach would be to pack-transfer-install an RPM of binaries and dotfiles (or Salt states), but i’m waiting to see how much my Dom0 and sys Qubes diverge from upstream – i can’t imagine doing something as complex as. maintaining an install with Nini in a GUI Domain without leaning into impermanence, and more personal CI solution may emerge along the way~

P.S: On Notifications

There are no notifications.

I’m not switching into it to check, but I don’t think I had any issues with notifications appearing (actions being another story…) when using shells that supported them, which implies something like mako (anything that would pick up a notify-send) should basically work OOT, without nearly the level of cursed proxy schemes afflicting the tray icons and menus (which IIRC are actually rendered in eg. sys-net and sys-usb!)

1 Like

My thought also. Perfect is the enemy of good, and I don’t think I’m in a position to get something perfect for now :slight_smile:

I’m a bit lost but I thought your demo of a vertical menu was cool, no idea how viable / hard to implement it is however.

Not having the tray icons is a good opportunity to learn the QubesOS CLI, in my opinion :eyes:

But yes, having a fancy menu would be nice for casual users.


Your solution of abusing the Niri config reload is totally cursed but really ingenious!


Thanks for the details, configs and the tips; it’ll take me a while to digest and integrate them, but I’ll come back and post about it :slight_smile:

RE: notifications

Indeed, I just had to install mako, and it worked out of the box.

No need to even edit my Niri configuration, it just worked out of the box.

qubes-dom0-update mako

(Sorry for the multi-post, but it’s better if I write as much info as possible before loosing interest and potentially abandoning my setup :slight_smile: )


Overall there are a lot of moving pieces to get this right.

What needs to be done:

  • install Niri and Wayland utilities
  • need to start qvm-start-daemon, with the following flags: --all --watch --kde
  • need to start xwayland-satellite, with the right PATH to use the wrapped Xwayland
  • need a custom script to update the Niri configuration (wip, need to rewrite it since there are no color files in 4.3.0-rc3 from what I can tell, we need to extract everything from the qubes.xml)
  • need to disable X11 authentication with xhost +

Important: in my experience, there should be only one process for each.

I’ll explain in details each of those in this post.

Installing niri and Wayland utilities

Here is what I installed:

qubes-dom0-update niri
qubes-dom0-update qubes-desktop-linux-common-wayland

Maybe Wayland utilities are not necessary, YMMV

qvm-start-daemon

This is supposed to be started by an XDG autostart.

The two relevant files are:

  • /etc/xdg/autostart/qvm-start-daemon.desktop
  • /etc/xdg/autostart/qvm-start-daemon-kde.desktop

I edited them so that the KDE variant starts on Niri, but it’s not working (it doesn’t get started on Niri anymore)

I’m a bit tired and just want something to work, so I just added a line in my Niri configuration to start the daemon:

spawn-at-startup "bash" "-c" "sleep 1 && xhost + && qvm-start-daemon --all --watch --kde"

Disabling X11 authentication

X11 authentication is not handled super well.

I’m not sure how it fits into the whole threat model, but what I did was to disable it with xhost +

See the line at the end of the section above for how I plugged it into Niri.

Note that there is a delay of 1 second because we need to wait for xwayland-satellite to be started in order for xhost to edit the right “X context”

Starting xwayland-satellite

This one is SUPER tricky.

The issue will be that there will be many xwayland-satellite running and that they will conflict (I assume).

The solution was to disable as much of them as I could.

There are three:

  1. The Niri xwayland-satellite
  2. The SystemD xwayland-satellite
  3. The mystery xwayland-satellite

1. Niri’s xwayland-satellite

This is the xwayland-satellite that is auto-started by Niri.

To disable this, I put this in my config:

xwayland-satellite {
  off
}

And here, I start my own xwayland-satellite with the right PATH:

spawn-at-startpu "bash" "-c" "env PATH=\"/usr/libexec/qubes/wrappers:/usr/bin\" xwayland-satellite"

2. SystemD’s xwayland-satellite

Not sure why this one got installed

sudo systemctl --global disable xwayland-satellite.service

I will put some tips here if someone (or me) attempts to do this again in the future:

  1. Make sure that there is one instance of each process running. If there are more than 1, there is a problem
  2. Niri autostarts xwayland-satellite which can be disabled with a config option
  3. SystemD starts xwayland-satellite as well
  4. XDG autostarts start xwayland-satellite as well
  5. xhost + should be ran after xwayland-satellite has started
  6. I think but not sure that qvm-start- daemon needs to start after xhost + has been added but not 100% sure
  7. There are no color files anymore apparently anymore in 4.3.0-rc3 which is something to take into account when writing the script that automatically writes the Niri configuration
  8. If a window appears but it’s blank, it means that the LD_PRELOAD wasn’t added (said another way, that xwayland-satellite is not using the right Xwayland; it should always use /usr/libexec/qubes/wrappers/Xwayland which sets the right LD_PRELOAD)
  9. If a window doesn’t appear, it’s possible that there are duplicate or missing processes. List of processes that should be started below:
  • qvm-start-daemon
  • xwayland-satellite
  • Xwayland
  1. If a window doesn’t appear, it’s also possible that you didn’t run xhost + or that you ran it before xwayland-satellite started and thus it failed. Make sure to wait 1s after xwayland-satellite started before running xhost +
  2. Starting xwayland-satellite can fail because there could be another instance running. The xwayland-satellite you see in ps -aux | grep xwayland-satellite could be another one that the one that is supposed to be running (make sure to confirm that the parent process is right using htop; also, you can check the env of the process by looking at /proc/<PID>/environ)

Putting more stuff here since Discourse disallows 3 replies in a row


Rewrote the script for 4.3.0-rc3

@antlers rewrote your script since there is no $HOME/.local/share/qubes-kde/$LABEL.colors anymore even when starting with the --kde flag.

Not sure if it’s a real change or if I’m just doing something wrong.

In my solution, I compute the active and inactive colors manually in Python using the base label color.

Otherwise it’s the same spirit as your script, and runs every 5 seconds.

It could definitely be simplified, some functions are not even used, but I’m happy with it.

My only concern is battery usage, not sure how much reading/writing files every 5 seconds could force the CPU to wakeup somehow and cause issues?

~/Documents/niri_borders_daemon.py
#!/usr/bin/env python3
#
# The goal of this script is to automatically apply
# QubesOS borders in Niri
#
# This works by checking the qubes.xml file every 5 seconds
# and then editing our Niri configuration file.
#
# For it to work, make sure to add these two lines in the middle of
# your Niri configuration:
# 
# // #+START_NIRI_QUBES_RULES
# // #+END_NIRI_QUBES_RULES
#
# And of course, start it in Niri automatically, with something
# like this:
#
# spawn-at-startup "bash" "-c" "~/Documents/niri_borders_daemon.py"

import json
import subprocess
import os
# import xml.sax as sax
# import xml.parsers.expat as expat
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from time import sleep

def get_niri_windows():
  """
  Call 'niri msg -j windows' to get the list
  of windows that are currently displayed.
  This returns the raw output parsed from JSON.
  """
  out = subprocess.run(['niri', 'msg', '-j', 'windows'], capture_output=True)
  windows_str = out.stdout.decode('utf-8')
  windows = json.loads(windows_str)
  return windows

# XXX: not used, but I want to keep it for later since it might
# become useful
def get_niri_qube_windows():
  """
  Return sugared Niri windows list, including
  the Qube name
  """
  ret = [] 
  for window in get_niri_windows():
    app_id = window['app_id']
    if ":" in app_id:
      qube_name = app_id.split(":", maxsplit=1)[0]
      app_name =  app_id.split(":", maxsplit=1)[1]    
    else:
      qube_name = "dom0"
      app_name =  app_id    

    ret.append({
      "qube_name": qube_name,
      "app_name": app_name,
      "window_id": window["id"]
    })
  return ret



#### qubes.xml reading and parsing

# The path to the qubes.xml path
# I think maybe it requires passing the --kde file for
# it to appear? It's not super super clear.
qubes_xml_path = "/var/lib/qubes/qubes.xml"

def get_qube_domains():
  """
  Get the list of Qube domains with their label
  """
  # Here, we open the qubes.xml file and read it.
  # There is probably an easier way to do this but well,
  # I am no Python expert and I don't have Internet
  # so I'll use raw syscalls lol.
  qubes_xml_fd = os.open(qubes_xml_path, flags=0)
  qubes_xml_stat = os.stat(qubes_xml_path)
  qubes_xml_str = os.read(qubes_xml_fd, qubes_xml_stat.st_size).decode('utf-8')

  # Note: it's really hard to get XML parsing to work without Internet
  # so I'll just use terrible "split" to get the data that I want...
  #
  # Here, I want to access the list of qubes (their names)
  # and find their colours, so I can build a Map<QubeName, Colour>
  # that I will then be able to apply in Niri.

  # This pattern finds all the "domains"
  pattern = re.compile("(?ms)<domain id=\".+?\" class=\".+?\">.+?</domain>")
  
  # Gets us a list of domains in XML str format
  domains_str_list = pattern.findall(qubes_xml_str)

  ret = []
  for domain in domains_str_list:
    # Get the properties into a nice dictionnary
    properties = re.findall("<property name=\"(.+?)\">(.+?)</property>", domain)    
    # Available properties:
    #   - label
    properties_dict = {}
    for prop in properties:
      properties_dict[prop[0]] = prop[1]

    ret.append(properties_dict)

  return ret
  
def add_to_color(input_color: str, amount: int) -> str:
  """Add (or remove) a given integer to each word of a color."""
  ret = ""
  for i in [0, 2, 4]:
    color_str = input_color[i:i+2]
    color_int = int(color_str, 16)
    color_int = min(max(color_int + amount, 0), 255) # clamp into hex range
    ret+= "{:02x}".format(color_int)
  return ret



def get_qube_labels():
  """
  Get the list of QubesOS labels.
  We are renaming the fields to make it
  clearer to the end user + adding an inactive
  colour.
  """
  tree = ET.parse(qubes_xml_path)
  root = tree.getroot()

  ret = []

  for label in root.findall("./labels/label"):
    label_color = label.get("color").replace("0x", "")
    # color_inactive = ""
    # for i in [0, 2, 4]:
    #   color_str = color_active[i:i+2]
    #   color_int = int(color_str, 16)
    #   color_int = max(color_int - 20, 0) # clamp
    #   color_inactive += "{:02x}".format(color_int)
   
    ret.append({
      "id": label.get("id"),
      "color_active": "#" + add_to_color(label_color, 30),
      "color_inactive": "#" + add_to_color(label_color, -30),
      "name": label.text
    })

  return ret

# Here is where we craft the Niri configuration
# for each domain!

def generate_niri_config():
  config = ""

  labels = get_qube_labels()
  for domain in get_qube_domains():
    # Skip malformed domains
    if "name" not in domain or "label" not in domain:
      continue

    label_data = None
    for label in labels:
      if label["name"] == domain["label"]:
        label_data = label
        break
    if not label_data: # skip if we don't have the label
      continue

    config += '''window-rule {PARENTHESIS_STARTING}
    match app-id="{domain_name}:*"
    border {PARENTHESIS_STARTING}
        active-color "{color_active}"
        inactive-color "{color_inactive}"
    {PARENTHESIS_ENDING}
{PARENTHESIS_ENDING}
'''.format(**{
      "domain_name": domain["name"],
      "color_active": label_data["color_active"],
      "color_inactive": label_data["color_inactive"],
      "PARENTHESIS_STARTING": "{",
      "PARENTHESIS_ENDING": "}"
    })

  return config.strip()


while True:
  # Generate the Niri blocks to insert
  niri_blocks = generate_niri_config()

  # Actually insert them into the config file.
  delim_start = "\n// #+START_NIRI_QUBES_RULES\n"
  delim_end = "\n// #+END_NIRI_QUBES_RULES\n"

  niri_config_path = str(Path.home()) + "/.config/niri/config.kdl"

  with open(str(Path.home()) + "/.config/niri/config.kdl", "r+") as f:
    config = f.read()

    if delim_start not in config or delim_end not in config:
      print("ERROR: Missing start or/and end delimiters in configuration.")
      print("Make sure to add the following in your config:")
      print(delim_start.strip())
      print(delim_end.strip())
      exit(1)

    # save config for later, when checking if there
    # is a difference or not
    old_config = config

    # Parse and reassemble config with the generated Niri blocks
    (before, middle) = config.split(delim_start)
    after = config.split(delim_end)[1]
    config = before + delim_start + niri_blocks + delim_end + after
    config = config.strip() + '\n' # for good measure

    print("done generating config")
    # Writing to the disk only if there is a difference
    # because maybe it'll minimize SSD tear?
    if config != old_config:
      with open(niri_config_path + ".bak", "w") as bak:
        bak.write(config)
      f.seek(0)
      f.write(config)
      f.truncate()
      print("INFO: Niri config ")


    sleep(5)
~/.config/niri/config.kdl

Finally, we add our script as a startup command in Niri + add delimiters:

// NOTE: the blocks below are autogenerated

// #+START_NIRI_QUBES_RULES
// #+END_NIRI_QUBES_RULES

// Line below auto-starts the Niri borders daemon
spawn-at-startup "bash" "-c" "~/Documents/niri_borders_daemon.py"

Next steps

  • adding window title (probably going to use Waybar for this)
  • getting a background
  • fixing screen brightness keys
  • fixing waybar indicators being wrong: Wifi, CPU, RAM
  • maybe documenting / reimplementing the most useful tray icons from Xfce?
  • writing documentation for new users

Getting windows title to show in Waybar

First create the .config/waybar directory and seed it:

mkdir ~/.config/waybar
cp /etc/xdg/waybar/config.json ~/.config/waybar
cp /etc/xdg/waybar/style.css ~/.config/waybar

Add this in the .config/waybar/config.jsonc

"niri/window": {}

This will display the currently focused window’s title in the
top left corner of the screen.

This is taking a lot of space so definitely not perfect but at least it works…

I also removed Wifi + CPU + Memory information by commenting them out because it takes space and provides no value.