# Hacking on Shift Shift is a convergent Plasma Mobile shell. This guide covers building and testing it locally without polluting your host system. The approach: keep every build dependency inside a **distrobox** container (openSUSE Tumbleweed), and preview the shell in a **nested KWin** window that runs on your host. --- ## 1. Host prerequisites You need three things installed on your host (outside the container): | Tool | Why | | ------------------ | ------------------------------------------ | | `podman` | Container runtime used by distrobox. | | `distrobox` | Manages the build container. | | `kwin_wayland` | Launches a nested Wayland compositor for preview. | | `dbus-run-session` | Provides a private D-Bus session to kwin. | On Plasma desktops, `kwin_wayland` and `dbus-run-session` are already present. For distrobox and podman, install them through your host package manager. --- ## 2. Create the build container Shift builds against the latest KDE Frameworks 6, Plasma 6, and Qt 6. openSUSE Tumbleweed tracks them closely and makes a good base. ### 2a. Work around missing `/etc/zypp/zypp.conf` As of April 2026, the Tumbleweed container image ships without `/etc/zypp/zypp.conf`, which causes `distrobox create` to fail during init (zypper refuses to run without it). Create a patched image first: ```bash podman run --name tw-fix registry.opensuse.org/opensuse/tumbleweed:latest \ bash -c 'mkdir -p /etc/zypp && echo "## zypp.conf" > /etc/zypp/zypp.conf' podman commit tw-fix localhost/tw-fixed:latest podman rm tw-fix ``` > **Note:** If a future Tumbleweed image ships with the file already > present, you can skip this step and use the upstream image directly. ### 2b. Create and initialise the distrobox ```bash distrobox create --name shift-tw --image localhost/tw-fixed:latest distrobox enter shift-tw -- echo "init ok" ``` Wait for the `Container Setup Complete!` message. The container's home directory is transparently mapped to your real `$HOME`, so the source tree is shared between host and container. --- ## 3. Install build dependencies All `zypper` commands run inside the container. Either prefix them with `distrobox enter shift-tw --` or open an interactive shell first with `distrobox enter shift-tw`. ### Build tools and libraries ```bash sudo zypper install --no-confirm \ cmake gcc-c++ ninja kf6-extra-cmake-modules \ qt6-core-devel qt6-gui-devel qt6-qml-devel qt6-quick-devel \ qt6-sensors-devel qt6-waylandclient-devel \ qt6-waylandclient-private-devel qt6-wayland-private-devel \ kf6-ki18n-devel kf6-kglobalaccel-devel kf6-kio-devel \ kf6-kconfig-devel kf6-kdbusaddons-devel kf6-kitemmodels-devel \ kf6-kservice-devel kf6-knotifications-devel kf6-kcmutils-devel \ kf6-kpackage-devel kf6-kjobwidgets-devel kf6-kwindowsystem-devel \ kf6-kauth-devel kf6-kirigami-devel kf6-ksvg-devel \ kf6-modemmanager-qt-devel kf6-networkmanager-qt-devel \ kirigami-addons6-devel libplasma6-devel plasma6-activities-devel \ libkscreen6-devel kwayland6-devel kpipewire6-devel \ kwin6-devel layer-shell-qt6-devel plasma6-workspace-devel \ plasma-wayland-protocols qcoro-qt6-devel \ libepoxy-devel libxcb-devel wayland-devel systemd-devel ``` ### Runtime dependencies (needed for preview, not for compilation) The nested preview runs the system `plasmashell` binary. It needs a complete Plasma Mobile runtime so all QML imports resolve: ```bash sudo zypper install --no-confirm \ plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \ layer-shell-qt6-imports kf6-bluez-qt-imports \ kf6-networkmanager-qt-imports \ breeze6-wallpapers plasma6-workspace-wallpapers ``` --- ## 4. Configure and build All build commands run inside the container. The source tree lives on the host filesystem (e.g. `~/Projects/Shift`); distrobox maps it automatically. ### Configure (first time or after CMakeLists.txt changes) ```bash distrobox enter shift-tw -- bash -c ' cd ~/Projects/Shift cmake -S . -B build-clean -G Ninja \ -DCMAKE_INSTALL_PREFIX=$PWD/.prefix \ -DCMAKE_BUILD_TYPE=Debug \ -DPLASMA_MOBILE_LOCAL_KAUTH_INSTALL=ON ' ``` `-DCMAKE_INSTALL_PREFIX=$PWD/.prefix` tells cmake to install into a local directory instead of `/usr`. `-DPLASMA_MOBILE_LOCAL_KAUTH_INSTALL=ON` redirects KAuth helper executables and polkit policy files into the local prefix so that `cmake --install` works without root. During configure, ECM auto-generates `build-clean/prefix.sh` — a shell snippet that prepends `.prefix` paths to `QT_PLUGIN_PATH`, `QML2_IMPORT_PATH`, `XDG_DATA_DIRS`, etc. The preview script sources this file so the system `plasmashell` finds our custom-built plugins first and falls back to system ones for everything else. ### Build everything ```bash distrobox enter shift-tw -- cmake --build ~/Projects/Shift/build-clean ``` Or build only the homescreen applet for a faster cycle: ```bash distrobox enter shift-tw -- cmake --build ~/Projects/Shift/build-clean \ --target org.kde.plasma.mobile.homescreen.folio ``` ### Install ```bash distrobox enter shift-tw -- cmake --install ~/Projects/Shift/build-clean ``` This populates `.prefix/`. There is no need to install to `~/.local` unless you also want to use the shell outside the preview window (e.g. in a full mobile session). --- ## 5. Preview in a nested KWin window The preview launches a **host** `kwin_wayland` compositor with its own Wayland socket, then starts `plasmashell` **inside the container** connected to that socket. The end result is a self-contained window showing the mobile shell — no need to log out or switch sessions. ### The preview script Create `preview.sh` in the project root: ```bash #!/usr/bin/env bash # Launch Shift in a nested KWin window for testing. # # kwin_wayland runs on the host (needs direct GPU access) with the project # prefix paths so it can load the convergentwindows KWin script and resolve # its QML imports. plasmashell runs inside the distrobox container. # # Usage: ./preview.sh [WIDTHxHEIGHT] # e.g. ./preview.sh # 1280x720 # ./preview.sh 1920x1080 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SIZE="${1:-1280x720}" WIDTH="${SIZE%%x*}" HEIGHT="${SIZE##*x}" PREFIX="$SCRIPT_DIR/.prefix" # Write an ephemeral inner launcher (kwin needs a single executable path) INNER=$(mktemp /tmp/shift-inner.XXXXXX.sh) chmod +x "$INNER" trap 'rm -f "$INNER"' EXIT cat > "$INNER" << ENDSCRIPT #!/usr/bin/env bash exec distrobox enter shift-tw -- bash -c ' cd "$SCRIPT_DIR" . ./build-clean/prefix.sh export WAYLAND_DISPLAY=shift-kwin export QT_QPA_PLATFORM=wayland export QT_QPA_PLATFORMTHEME=KDE export EGL_PLATFORM=wayland export QT_QUICK_CONTROLS_STYLE=org.kde.breeze export QT_QUICK_CONTROLS_MOBILE=true export PLASMA_PLATFORM=phone:handset export PLASMA_DEFAULT_SHELL=org.kde.plasma.mobileshell export QT_FORCE_STDERR_LOGGING=1 exec plasmashell --replace -p org.kde.plasma.mobileshell ' ENDSCRIPT # Expose the project prefix to the host kwin_wayland so it can find # KWin scripts (convergentwindows) and their QML dependencies. # Also overlay ~/.config/plasma-mobile so KWin reads the mobile kwinrc # (envmanager writes convergentwindowsEnabled, Placement, etc. there). export XDG_DATA_DIRS="$PREFIX/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" export XDG_CONFIG_DIRS="$HOME/.config/plasma-mobile:${XDG_CONFIG_DIRS:-/etc/xdg}" export QT_PLUGIN_PATH="$PREFIX/lib64/plugins:${QT_PLUGIN_PATH:-}" export QML2_IMPORT_PATH="$PREFIX/lib64/qml:${QML2_IMPORT_PATH:-}" exec dbus-run-session \ kwin_wayland --xwayland \ --socket shift-kwin \ --width "$WIDTH" \ --height "$HEIGHT" \ --exit-with-session "$INNER" ``` Make it executable: `chmod +x preview.sh`. ### How it works 1. `dbus-run-session` spins up an isolated D-Bus session so the nested compositor doesn't clash with your running desktop. 2. Four environment exports give the **host** `kwin_wayland` access to the project's `.prefix` (KWin scripts, QML plugins) and to `~/.config/plasma-mobile` (where envmanager writes convergence settings like `convergentwindowsEnabled`, `Placement`, etc.). 3. `kwin_wayland` opens a window on your current desktop and creates a Wayland socket named `shift-kwin`. Because of the exports it can load the `convergentwindows` script, which handles maximising windows on undock and restoring decorations on dock. 4. The inner script enters the distrobox, sources `prefix.sh` to put the custom build first in all search paths, then starts `plasmashell` from the container's `/usr/bin/plasmashell` — but with our plugins loaded from `.prefix`. 5. `--exit-with-session` makes kwin close when plasmashell exits, and vice versa. ### Running it ```bash ./preview.sh # 1280×720 (default) ./preview.sh 1920x1080 # Full-HD ./preview.sh 360x720 # Narrow phone ``` Close the KWin window to stop the preview. ### Convergence mode Shift's convergence mode (desktop-style dock, auto-hide, etc.) requires this in `~/.config/plasmamobilerc`: ```ini [General] convergenceModeEnabled=true ``` This file lives on the host (shared home), so just create or edit it before running the preview. --- ## 6. Edit – build – preview cycle The fast loop: ```bash # 1. Edit source files on the host with your editor. # 2. Build the changed target (runs inside the container): distrobox enter shift-tw -- cmake --build ~/Projects/Shift/build-clean \ --target org.kde.plasma.mobile.homescreen.folio # 3. Install: distrobox enter shift-tw -- cmake --install ~/Projects/Shift/build-clean # 4. Preview: ./preview.sh ``` > **Tip:** QML files installed to `.prefix/share/` or > `.prefix/lib64/qml/` are read at runtime. For pure-QML changes you > can skip the build step and just re-run `cmake --install` then > restart the preview. For C++ changes you need the full build. ### Key build targets | Target | What it builds | | ------ | -------------- | | *(none — full build)* | Everything: all applets, QML plugins, KCMs, quicksettings, initial-start modules. | | `org.kde.plasma.mobile.homescreen.folio` | Folio homescreen applet (app grid, dock, folders). | | `org.kde.plasma.mobile.panel` | Top status bar (clock, indicators). | | `org.kde.plasma.mobile.taskpanel` | Bottom navigation / gesture panel. | | `org.kde.plasma.mobile.homescreen.halcyon` | Halcyon homescreen (alternative to Folio). | --- ## 7. Troubleshooting ### `plasmashell: not found` The container needs the full `plasma6-workspace` package (which provides `/usr/bin/plasmashell`), not just `plasma6-workspace-devel`. Install it with `sudo zypper install plasma6-workspace`. ### `module "org.kde.foo" is not installed` A QML import is missing. The error names the module; find the package that provides it: ```bash # Inside the container: zypper se -x $(echo org.kde.foo | tr . /) # crude guess # Or search file contents: zypper wp /usr/lib64/qt6/qml/org/kde/foo/qmldir ``` Common culprits: | Module | Package | | ------ | ------- | | `org.kde.bluezqt` | `kf6-bluez-qt-imports` | | `org.kde.plasma.networkmanagement` | `plasma6-nm` | | `org.kde.plasma.private.volume` | `plasma6-pa` | | `org.kde.plasma.private.nanoshell` | `plasma6-nano` | | `org.kde.layershell` | `layer-shell-qt6-imports` | ### `Could not set containment property on rootObject` This means the Desktop.qml failed to load, almost always due to a missing QML module — look for the preceding `module "…" is not installed` line. ### `FATAL ERROR: could not add wayland socket shift-kwin` A previous preview didn't exit cleanly. Remove the stale lock: ```bash rm -f /run/user/$UID/shift-kwin.lock pkill -f 'kwin_wayland.*shift-kwin' ``` ### `distrobox create` hangs or zypper crashes during init Likely the missing `/etc/zypp/zypp.conf` bug. See [2a. Work around missing zypp.conf](#2a-work-around-missing-etczyppzyppconf). ### Harmless warnings you can ignore These appear in the preview output and are not errors: - `fusermount3: failed to access mountpoint` — FUSE is restricted inside the nested session. - `qt.qpa.services: Failed to register with host portal` — Portal registration unsupported in nested compositors. - `kf.solid.backends.udisks2: Failed to fetch all devices` — No udisks2 in the isolated D-Bus session. - `TypeError: Cannot read property 'volume' of null` — PulseAudio / PipeWire is not running in the sandbox. - `Could not load a session backend` — systemd --user is not running in the nested D-Bus session. --- ## 8. Project layout (quick reference) ``` Shift/ ├── CMakeLists.txt # Top-level build file ├── build-clean/ # Out-of-source build directory │ └── prefix.sh # Auto-generated by ECM; sources .prefix paths ├── .prefix/ # Local install tree (not committed) ├── preview.sh # Nested KWin launcher (see §5) ├── shell/ # Shell package (Desktop.qml, Panel.qml, applet overrides) ├── components/ │ └── mobileshell/ # QML & C++ for the mobile shell runtime plugin ├── containments/ │ ├── homescreens/folio/ # Folio homescreen applet │ ├── panel/ # Status bar │ └── taskpanel/ # Navigation bar / gesture panel ├── envmanager/ # Applies KWin/KDE config on convergence mode changes ├── quicksettings/ # Action drawer quick-setting tiles ├── kcms/ # System Settings modules └── kwin/ ├── mobiletaskswitcher/ # KWin task-switcher plugin └── scripts/convergentwindows/ # KWin script: maximize on undock, restore borders on dock ```