Using KDE Plasma Activities with Qubes OS

Hello !

I’ve been using KDE Plasma for years across several Linux distros and the “Activity” features always felt underwhelming as it felt too similar with Virtual Desktop without bringing anything really special.

I moved to QubesOS as my daily driver for the “reasonable” security and how easy it makes compartmentalizing your digital life, and it’s incredible and I have yet barely scratched the surface of the possibility with this system.

When I installed KDE Plasma in QubesOS I thought that I was missing an opportunity to merge Plasma Activities’ with QubesOS compartmentalization, so I created a small daemon which synchronize your Activities with your Qubes. QubesUser 95639 / KActivityQubesd · GitLab

It’s far from perfect and there’s a lot missing from what is still possible, but it works, and in my eyes it’s a bit better than good enough.

I hope this project will inspire you and allow you to refine your workflow. :smiley:

I’d love having this community feedback on this, I probably missed lots of better way to achieve this goal, and contribution are welcome.

Have fun ! :slight_smile:

4 Likes

For what’s it’s worth, I made a quick code review before OP made this public, it’s really minimalistic with ~200 lines of code and it does the job advertised, nothing more.

6 Likes

I’m sorry, but I can’t understand what this is supposed to actually do.

Make sure each qube has its own desktop or activity?

2 Likes

Yes, I think the benefits of doing this has been discussed in the KDE Plasma topic

1 Like

Activity and Virtual Desktop (which are very similar but actually two different features in KDE Plasma) help organize your apps and can be an important part of your workflow.

The idea behind this small project is to automate the creation and management of Activities when using QubesOS and the windows rules needed to only display them in the right Activity.

So you’ll have one activity for each Qubes (I’d like to have a bit more control on that, DispVM tend to be used alongside some other Qube and don’t really benefit from being in their own Activities), but you could still have several Virtual Desktop if you tend to use multiple software in an AppVM (terminal/files explorer/web browser/etc).

I’ll agree that this is not for everyone, and this project is born from my need of it for my particular workflow. But if you’re interested or have questions about the code I’ll be glad to answer ! :smiley:

I think that I should add some screenshot to illustrate the usage better.

3 Likes

I liked this idea, but it makes a real mess out of kde. When booting it makes an Activity for every template and qube, which makes it just about unbootable on low ram, Even after closing all the Activities, new qubes open in what ever activity you are in and not in new activities for me. This would be great if it worked.

I’ve been promoting KDE in Qubes for years - see assorted threads in the
Forum. But this didnt strike me as an efficient use of KDE.
I use activities per Security domain, not per qube, with a separate
Activity for catch all stuff. That seems to me to be the way to go.

I never presume to speak for the Qubes team.
When I comment in the Forum I speak for myself.

1 Like

Interesting, thanks, did you automate that with Window Rules or Kwin? Or set it up manually each time?

Automated, naturally.

The basic commands to use are these:
kactivities-cli --create-activity NAME
kactivities-cli --list-activities
kactivities-cli --remove-activity UUID

You can set up kwin rules like this:

#!/usr/bin/bash
 
activity_name=$2
if  kactivities-cli --list-activities |grep -q -w "$activity_name"  ;
then
ACTIVITY_UUID="$(kactivities-cli --list-activities  |awk -v aname=$2 '$0 ~ aname{ print $2 }' )"
RULE_UUID="$(uuidgen)"
count=$(kreadconfig6 --file kwinrulesrc --group General  --key count)
echo $count
RULE_LIST=$(kreadconfig6  --file kwinrulesrc --group General  --key rules 2>/dev/null )
NEW_RULES="$RULE_LIST,$RULE_UUID"
kwriteconfig6 --file kwinrulesrc --group General  --key count $count
kwriteconfig6 --file kwinrulesrc --group General  --key rules "$NEW_RULES"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key Description "Force $1 windows to Activity $2"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key title "[$1]"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key titlematch "2"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key wmclass "$1"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key wmclassmatch "2"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key activity "$ACTIVITY_UUID"
kwriteconfig6 --file kwinrulesrc --group $RULE_UUID  --key activityrule "2"
 
qdbus org.kde.KWin /KWin reconfigure
else
echo "No such activity"
exit
fi

That looks evil, but it’s straightforward.
It matches windows by title - like [personal], and the titlematch 2
is for a substring. If you wanted to match exactly, you would use 1.

The activityrule options are:
2 - Force (Window can only appear in specified activity)
3 - Apply initially (window appears in one activity but you can move
elsewhere)

I use Force - every new window matching the rule is forced to a
specified activity, which allows me to group windows from qubes within
the same domain on to the same activity.

If you have various desktops, you can also push windows to specific
desktop/activity combinations.

I use a different Qubes wallpaper for each activity as a strong guide to
the domain color.
Red wild strawberry
Orange salmon
Yellow sun
Green grass
Gray paper
Blue dawn
Purple plum

I use helper scripts linked to keyboard shortcuts to switch to specific
Activities when opening windows, to check the name/color of the window
that has focus, and so on.
These use the same template I’ve posted before:

#!/usr/bin/bash
ID=$(xdotool getwindowfocus)
QUBE=$(xprop _QUBES_VMNAME -id $ID|cut -f2 -d\" )
if [[ "$QUBE" == "_QUBES_VMNAME:  not found." ]]; then
  DOM0
else
  DO SOMETHING
fi

Of course, once you have the templates, it’s straightforward to set this
up, and once you have it as you like, you can salt the whole or part of set up.

I never presume to speak for the Qubes team.
When I comment in the Forum I speak for myself.

1 Like

Thank you @unman that is great. I played around with your script and found its an efficient way of using KDE.

I made some adjustments that might be useful.

#!/usr/bin/env bash
set -euo pipefail

config_file="$HOME/.config/kwinrulesrc"
cmd="${1:-}"

qdbus_reload() {
  if command -v qdbus >/dev/null 2>&1; then
    qdbus org.kde.KWin /KWin reconfigure
  elif command -v qdbus6 >/dev/null 2>&1; then
    qdbus6 org.kde.KWin /KWin reconfigure
  elif command -v qdbus-qt6 >/dev/null 2>&1; then
    qdbus-qt6 org.kde.KWin /KWin reconfigure
  else
    echo "Warning: no qdbus command found, please reload KWin manually"
  fi
}

list_rules() {
  if [[ ! -f "$config_file" ]]; then
    echo "No kwinrulesrc file found"
    exit 0
  fi
  rules="$(kreadconfig6 --file kwinrulesrc --group General --key rules 2>/dev/null || true)"
  if [[ -z "$rules" ]]; then
    echo "No rules found"
    exit 0
  fi
  IFS=',' read -r -a rule_ids <<< "$rules"
  for rule_id in "${rule_ids[@]}"; do
    desc="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key Description 2>/dev/null || true)"
    title="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key title 2>/dev/null || true)"
    wmclass="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key wmclass 2>/dev/null || true)"
    activity="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key activity 2>/dev/null || true)"
    echo "RULE: $rule_id"
    echo "  Description: $desc"
    echo "  Title: $title"
    echo "  WMClass: $wmclass"
    echo "  Activity: $activity"
  done
}

add_rule() {
  window_marker="${1:-}"
  activity_name="${2:-}"
  if [[ -z "$window_marker" || -z "$activity_name" ]]; then
    echo "Usage: $0 add WINDOW_MARKER ACTIVITY_NAME"
    exit 1
  fi
  if ! kactivities-cli --list-activities | grep -q -w "$activity_name"; then
    echo "No such activity: $activity_name"
    exit 1
  fi
  activity_uuid="$(kactivities-cli --list-activities | awk -v aname="$activity_name" '$0 ~ aname { print $2; exit }')"
  if [[ -z "$activity_uuid" ]]; then
    echo "Could not resolve UUID for activity: $activity_name"
    exit 1
  fi
  rule_uuid="$(uuidgen)"
  count="$(kreadconfig6 --file kwinrulesrc --group General --key count 2>/dev/null || echo 0)"
  rule_list="$(kreadconfig6 --file kwinrulesrc --group General --key rules 2>/dev/null || true)"
  if [[ -n "$rule_list" ]]; then
    new_rules="${rule_list},${rule_uuid}"
  else
    new_rules="${rule_uuid}"
  fi
  kwriteconfig6 --file kwinrulesrc --group General --key count "$count"
  kwriteconfig6 --file kwinrulesrc --group General --key rules "$new_rules"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key Description "Force ${window_marker} windows to Activity ${activity_name}"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key title "[${window_marker}]"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key titlematch "2"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key wmclass "${window_marker}"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key wmclassmatch "2"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key activity "$activity_uuid"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key activityrule "2"
  qdbus_reload
  echo "Added rule: $rule_uuid"
}

delete_rule() {
  needle="${1:-}"
  if [[ -z "$needle" ]]; then
    echo "Usage: $0 delete SEARCH_TEXT"
    exit 1
  fi
  rules="$(kreadconfig6 --file kwinrulesrc --group General --key rules 2>/dev/null || true)"
  if [[ -z "$rules" ]]; then
    echo "No rules found"
    exit 0
  fi
  IFS=',' read -r -a rule_ids <<< "$rules"
  kept_rules=()
  deleted_rule=""
  for rule_id in "${rule_ids[@]}"; do
    desc="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key Description 2>/dev/null || true)"
    title="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key title 2>/dev/null || true)"
    wmclass="$(kreadconfig6 --file kwinrulesrc --group "$rule_id" --key wmclass 2>/dev/null || true)"
    if [[ "$desc" == *"$needle"* || "$title" == *"$needle"* || "$wmclass" == *"$needle"* || "$rule_id" == *"$needle"* ]]; then
      deleted_rule="$rule_id"
      continue
    fi
    kept_rules+=("$rule_id")
  done
  if [[ -z "$deleted_rule" ]]; then
    echo "No matching rule found for: $needle"
    exit 1
  fi
  new_rules="$(IFS=','; echo "${kept_rules[*]}")"
  new_count="${#kept_rules[@]}"
  kwriteconfig6 --file kwinrulesrc --group General --key rules "$new_rules"
  kwriteconfig6 --file kwinrulesrc --group General --key count "$new_count"
  tmp_file="$(mktemp)"
  awk -v delete_id="$deleted_rule" '
    BEGIN { skip=0 }
    $0 ~ "^\\[" delete_id "\\]$" { skip=1; next }
    skip && $0 ~ "^\\[[^]]+\\]$" { skip=0 }
    !skip { print }
  ' "$config_file" > "$tmp_file"
  mv "$tmp_file" "$config_file"
  qdbus_reload
  echo "Deleted rule: $deleted_rule"
}

case "$cmd" in
  list) list_rules ;;
  add) add_rule "${2:-}" "${3:-}" ;;
  delete) delete_rule "${2:-}" ;;
  file) echo "$config_file" ;;
  *) echo "Usage: $0 list|add WINDOW_MARKER ACTIVITY_NAME|delete SEARCH_TEXT|file"; exit 1 ;;
esac

For me it (so far) works well.
The work flow is:
Create Activities: Ie Network, Vaulted, etc… Set the key combo, icon, wallpaper color (I try to match so Vault is black, Network: green etc…)
Then the script can be used to list the rules, delete them, output them to file and add.
So if you wanted all apps with “vault” in title to go in the Vaulted activity you would:
./Qubes-kwin-rules.sh add vault Vaulted
or
./Qubes-kwin-rules.sh add sys-whonix network

To list all the rules:
./Qubes-kwin-rules.sh list

and then to delete you would for example
./Qubes-kwin-rules.sh delete Vaulted

I have tried to organize them by how they are network so I don’t get confused and accidently put the wrong data in the wrong pipe.

So thanks @unman for taking the time and effort to share that. Much appreciated. :upside_down_face:

  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key titlematch "2"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key wmclass "${window_marker}"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key wmclassmatch "2"

change to

  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key titlematch "3"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key wmclass "${window_marker}"
  kwriteconfig6 --file kwinrulesrc --group "$rule_uuid" --key wmclassmatch "3"

To use REGEX commands
eg…
".*Browser.*" "Your_Activity_Name" will put anything with Browser in its title into an activity.
or
".*[bB][rR][oO][wW][sS][eE][rR].*" "Your_Activity_Name" Makes it case insensitive
or
"^disp" "Your_Activity_Name"
Anything starting with disp