Improves focused-window geometry so chrome placement stays stable across workspaces and floating containers. WindowService now exposes focused workspace X/Y, resolves workspace by name, and publishes those offsets with focused geometry. The Sway backend now tracks the active floating frame through recursion and consistently derives frame/deco/content rects from that frame. QML chrome placement was updated to use workspace-relative coordinates and simpler direct margins (removing interpolation behaviors), with safer title-bar height handling for vertical centering.
Nebula Desktop
Production shell for the mouse-and-keyboard NebulaOS desktop. This is Nebula Desktop 0.2: a usable floating-first session with compositor-backed window management.
The user can enter the development session, use the full output, open the Nebula launcher, start real installed applications (including Foot), move and resize them as ordinary windows, see the focused application in the top bar, and close windows from the GUI.
Architecture
Nebula Desktop is a Qt Quick / QML Wayland shell. It draws system chrome only. It does not host application or game windows.
Ubuntu
↓
Wayland
↓
Sway
temporary compositor
↓
Nebula Shell
C++20 + Qt 6 + Qt Quick/QML + LayerShellQt
Eventual target:
Ubuntu/Linux
↓
Wayland
↓
Nebula compositor
↓
Nebula Shell
Qt Quick/QML
Normal applications remain ordinary Wayland / XWayland clients of the compositor:
Nebula Launcher
↓
ApplicationService
↓
application process
↓
Wayland / XWayland
↓
Sway
Window management is a separate path. QML never talks to Sway:
Nebula QML
↓
WindowService core/windows
↓
generic backend
↓
Sway IPC now
Nebula compositor later
↓
Sway
/ \
Wayland app XWayland app
Games, browsers, terminals, and other apps never pass through Qt Quick. Do not draw third-party application windows inside QML, and do not wrap Wayland clients in custom Nebula QML windows.
Production stack
| Piece | Technology |
|---|---|
| Language | C++20 |
| UI | Qt 6, Qt Quick, QML |
| Layer surfaces | LayerShellQt |
| Installed apps | core/applications (import Nebula.Applications) |
| Windows / workspaces | core/windows (import Nebula.Windows) |
| Build | CMake |
| Shared design system | packages/nebula-ui (import Nebula.UI) |
The executable is nebula-shell.
QML module URI: Nebula.Shell.
Surfaces
Independent Wayland layer-shell windows:
| Surface | Layer | Role |
|---|---|---|
| Desktop | BACKGROUND |
Fills the current output, no exclusive zone, no keyboard focus |
| Top Bar | TOP |
40 logical pixels, exclusive zone, spans the output width |
| Launcher | OVERLAY |
Covers the output below the top bar while open; hidden otherwise |
Stacking:
OVERLAY
Nebula Launcher
TOP
Nebula Top Bar
NORMAL WAYLAND WINDOWS
Foot, Firefox, applications, games, …
BACKGROUND
Nebula Desktop
Surfaces follow the live Wayland output geometry from Qt (OutputTracker). They do not assume 1920×1080 or other fixed desktop sizes. If VMware resizes the display, the shell updates.
Window management
Nebula Desktop 0.2 is floating-first. Sway is still the compositor, but ordinary applications no longer tile to fill the workspace.
- New Wayland (
app_id) and XWayland (class) windows open floating at about 70% of the workspace (usable area below the exclusive top bar), centred, with a simple cascade offset for subsequent windows. - Sway provides a temporary restrained title bar and border so windows can be identified, dragged, and resized. Custom Nebula decorations are later work.
- Super+Left/Right mouse remains a development move/resize fallback. Super+Left/Right/Up/Down snap or maximise using workspace-relative sizes.
WindowService owns the logical model:
WindowModelroles:windowId,title,appId,windowClass,workspace,focused,floating,fullscreen,x,y,width,height, plusdisplayName,iconName,minimized,maximized,outputWorkspaceModelroles:workspaceId,name,number,focused,visible,output,windowCount- Live updates from Sway IPC
window/workspace/outputevents (no polling, noswaymsgfrom QML) - Focused normal application only — desktop, top bar, launcher, and overlays are not the active app
- Operations:
focusWindow,closeWindow(compositor close request, not process kill),maximizeWindow,restoreWindow,setWindowFloating,moveWindow,resizeWindow,snapWindow,switchWorkspace - Maximise fills the workspace usable area (below the top bar). Restore returns the previous floating geometry when WindowService saved it
- Minimise exists on the service (Sway scratchpad internally). The UI does not mention scratchpad
If SWAYSOCK is unset or IPC fails, the shell keeps running. WindowService logs a warning and stays empty.
The top-bar context shows the focused application's user-facing name (via ApplicationService metadata when possible, otherwise a humanized app_id / class). With no normal window focused it shows Desktop.
The launcher has a temporary Open Windows list: click to focus, × to close. This is development-quality proof of the service, not Mission Control / Overview.
Applications
core/applications discovers freedesktop .desktop entries from XDG data directories:
$XDG_DATA_HOME/applications(default~/.local/share/applications)$XDG_DATA_DIRS/applications(default/usr/local/share:/usr/share)
Launcher-visible apps respect Hidden=true, NoDisplay=true, OnlyShowIn / NotShowIn, and TryExec when present. Localized Name values are used when available. StartupWMClass is stored so WindowService can map a compositor app_id / class to a friendly name.
Launching uses QProcess::startDetached after QProcess::splitCommand. Exec field codes (%f, %F, %u, %U, %i, %c, %k, %%, …) are handled for launches without file arguments. Desktop-entry strings are never passed to a shell.
Search matches name, generic name, comment, and categories. Searching term should find Foot (GenericName=Terminal). Searching firefox should find Firefox if it is installed.
Application icons use the system icon theme through Qt (QIcon::fromTheme). Unresolved icons use a Nebula fallback glyph, not a broken-image placeholder.
Mock launcher data remains only as a fallback when no installed applications are discovered.
Layout
shells/desktop/
├── README.md this file
├── shell/ production Qt/QML shell
│ ├── CMakeLists.txt
│ ├── src/
│ └── qml/
└── prototype-react/ archived design reference only
Shared visual language lives in packages/nebula-ui. Application discovery lives in core/applications. Window management lives in core/windows.
Linux build
This project will not compile on Windows. LayerShellQt is a Wayland API and is not stubbed.
On the Ubuntu NebulaOS VM:
sudo apt install \
qt6-base-dev \
qt6-declarative-dev \
qt6-wayland \
liblayershellqtinterface-dev \
qml6-module-org-kde-layershell \
qml6-module-qtquick \
qml6-module-qtquick-window \
adwaita-icon-theme \
cmake \
build-essential
Qt 6.4 or newer is required. loadFromModule is used on Qt 6.5+.
cd shells/desktop/shell
rm -rf build
cmake -S . -B build
cmake --build build -j$(nproc)
The production binary is shells/desktop/shell/build/nebula-shell. It does not force QT_QUICK_BACKEND=software. GPU-accelerated Qt Quick is the production default.
WindowService and the Sway IPC backend are linked as the NebulaWindows target (core/windows). Do not compile those .cpp files into the shell through relative paths.
QML lint
If the installed Qt provides qmllint:
qmllint \
shells/desktop/shell/qml/*.qml \
shells/desktop/shell/qml/surfaces/*.qml \
shells/desktop/shell/qml/components/*.qml \
shells/desktop/shell/qml/mock/*.qml \
packages/nebula-ui/qml/Nebula/UI/*.qml
Do not use ESLint or Oxlint on the production Qt shell.
Development session (VMware)
From the repository root, the intended flow is:
sway -c compositor/sway/nebula-sway.conf
or, recommended on the VM (sets cursor env and starts Sway from the repo root):
sh compositor/sway/dev/run-sway-vm.sh
That should:
- start Sway without a Sway bar
- autostart
nebula-shellthroughscripts/run-nebula-vm.sh - fill the current output
- show a normal Adwaita 24px cursor
- open launched apps as floating windows below the top bar
- let the Nebula launcher start, focus, and close Foot and other installed apps
Skip autostart while debugging the compositor:
NEBULA_SHELL_AUTOSTART=0 sway -c compositor/sway/nebula-sway.conf
VMware software rendering
The VMware virtual GPU currently cannot run the Qt Quick accelerated path correctly. The development wrapper therefore sets:
QT_QUICK_BACKEND=software
That variable is not a production default. scripts/run-nebula-vm.sh is the only place that sets it automatically. Production nebula-session / nebula-shell must run GPU-accelerated.
Manual equivalent, if you start the shell yourself:
QT_QUICK_BACKEND=software ./build/nebula-shell
Output / resolution
The development Sway config uses scale 1 and does not pin a resolution. Sway uses the output's preferred mode so the session follows the current display size. Window placement uses compositor workspace geometry, not 1920×1080.
Inspect outputs and modes:
swaymsg -t get_outputs
Set a mode explicitly if needed:
swaymsg output <name> mode 1600x900
swaymsg output <name> scale 1
In VMware, also set the guest display (or Autofit Guest) so the advertised modes match the host window.
Cursor
Sway owns the cursor:
XCURSOR_THEME=Adwaita
XCURSOR_SIZE=24
seat * xcursor_theme Adwaita 24
Install adwaita-icon-theme if the theme is missing. A branded Nebula cursor is a later task. WLR_NO_HARDWARE_CURSORS=1 is set only by the VMware Sway wrapper.
Emergency shortcuts
These remain for development fallback. They are not the normal UX once the GUI launcher and window list work.
| Shortcut | Action |
|---|---|
| Super+Enter | open Foot |
| Super+Left Mouse | move window |
| Super+Right Mouse | resize window |
| Super+Left / Right / Up | snap / maximise in the usable area |
| Super+Down | restore default floating size |
| Super+Shift+Q | close focused window |
| Super+Shift+E | exit Sway |
| Super+Shift+C | reload Sway config |
Design reference
prototype-react/ is an archived React/Vite UI. It is not the production shell. Keep it until the Qt shell reaches visual and behavioural parity, then remove it in a later cleanup.
Current limitations
- Sway is still the compositor. The future Nebula compositor is not started.
- Window decorations are Sway's temporary title bar/border, not Nebula chrome.
- The Open Windows section is a temporary launcher list, not Overview / Mission Control.
- Clicking the desktop background may not clear application focus; Sway still considers the last client focused. The top bar follows compositor focus.
- Minimise is available on
WindowServicebut is not a first-class desktop UI yet. - Keyboard snap shortcuts are development conveniences, not a Snap Layouts UI.
Out of scope
Not implemented in this milestone:
- Mission Control / Overview, dock, live window thumbnails
- custom Nebula titlebar renderer / CSD protocol
- PipeWire, NetworkManager, BlueZ, UPower
- notifications, XDG portals, Polkit
- login/lock screen, greetd
- a custom Nebula compositor
- Nebula Bigscreen
- Gamescope
Those come later. Bigscreen is expected to reuse Nebula.UI, Nebula.Applications, and Nebula.Windows.