# 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. ```text Ubuntu ↓ Wayland ↓ Sway temporary compositor ↓ Nebula Shell C++20 + Qt 6 + Qt Quick/QML + LayerShellQt ``` Eventual target: ```text Ubuntu/Linux ↓ Wayland ↓ Nebula compositor ↓ Nebula Shell Qt Quick/QML ``` Normal applications remain ordinary Wayland / XWayland clients of the compositor: ```text Nebula Launcher ↓ ApplicationService ↓ application process ↓ Wayland / XWayland ↓ Sway ``` Window management is a separate path. QML never talks to Sway: ```text 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: ```text 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: - `WindowModel` roles: `windowId`, `title`, `appId`, `windowClass`, `workspace`, `focused`, `floating`, `fullscreen`, `x`, `y`, `width`, `height`, plus `displayName`, `iconName`, `minimized`, `maximized`, `output` - `WorkspaceModel` roles: `workspaceId`, `name`, `number`, `focused`, `visible`, `output`, `windowCount` - Live updates from Sway IPC `window` / `workspace` / `output` events (no polling, no `swaymsg` from 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: 1. `$XDG_DATA_HOME/applications` (default `~/.local/share/applications`) 2. `$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 ```text 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: ```bash 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+. ```bash 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`: ```bash 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: ```bash sway -c compositor/sway/nebula-sway.conf ``` or, recommended on the VM (sets cursor env and starts Sway from the repo root): ```bash sh compositor/sway/dev/run-sway-vm.sh ``` That should: 1. start Sway without a Sway bar 2. autostart `nebula-shell` through `scripts/run-nebula-vm.sh` 3. fill the current output 4. show a normal Adwaita 24px cursor 5. open launched apps as floating windows below the top bar 6. let the Nebula launcher start, focus, and close Foot and other installed apps Skip autostart while debugging the compositor: ```bash 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: ```bash 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: ```bash 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: ```bash swaymsg -t get_outputs ``` Set a mode explicitly if needed: ```bash swaymsg output mode 1600x900 swaymsg output 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: ```text 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 `WindowService` but 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`.