Add Nebula.Windows with Sway IPC backend
Introduces a new `core/windows` module (`Nebula.Windows`) with `WindowService`, window/workspace models, and a Sway IPC backend for live window tracking and control (focus, close, move/resize, snap, maximize/restore, minimize, workspace switch). The desktop shell now links this module, uses compositor focus for top-bar active app text, and adds an Open Windows section in the launcher with focus/close actions. Also updates application metadata handling to parse `StartupWMClass` and map window `app_id`/class to friendly names and icons, and revises Sway config/docs for the new floating-first Desktop 0.2 behavior with temporary compositor-provided decorations.
This commit is contained in:
+70
-13
@@ -1,8 +1,8 @@
|
||||
# Nebula Desktop
|
||||
|
||||
Production shell for the mouse-and-keyboard NebulaOS desktop. This is Nebula Desktop **0.1**: a usable session foundation, not only a visual prototype.
|
||||
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, and start real installed applications (including Foot) without Sway keyboard shortcuts.
|
||||
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
|
||||
|
||||
@@ -47,7 +47,24 @@ Wayland / XWayland
|
||||
Sway
|
||||
```
|
||||
|
||||
Games, browsers, terminals, and other apps never pass through Qt Quick.
|
||||
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
|
||||
|
||||
@@ -57,6 +74,7 @@ Games, browsers, terminals, and other apps never pass through Qt Quick.
|
||||
| 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`) |
|
||||
|
||||
@@ -92,6 +110,30 @@ BACKGROUND
|
||||
|
||||
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:
|
||||
@@ -99,7 +141,7 @@ Surfaces follow the live Wayland output geometry from Qt (`OutputTracker`). They
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -107,8 +149,6 @@ Search matches name, generic name, comment, and categories. Searching `term` sho
|
||||
|
||||
Application icons use the system icon theme through Qt (`QIcon::fromTheme`). Unresolved icons use a Nebula fallback glyph, not a broken-image placeholder.
|
||||
|
||||
The top-bar context text stays **Desktop** in this milestone. Launching an app is not treated as focus; real window tracking comes next.
|
||||
|
||||
Mock launcher data remains only as a fallback when no installed applications are discovered.
|
||||
|
||||
## Layout
|
||||
@@ -123,7 +163,7 @@ shells/desktop/
|
||||
└── prototype-react/ archived design reference only
|
||||
```
|
||||
|
||||
Shared visual language lives in `packages/nebula-ui`. Application discovery lives in `core/applications`.
|
||||
Shared visual language lives in `packages/nebula-ui`. Application discovery lives in `core/applications`. Window management lives in `core/windows`.
|
||||
|
||||
## Linux build
|
||||
|
||||
@@ -157,6 +197,8 @@ 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`:
|
||||
@@ -192,7 +234,8 @@ That should:
|
||||
2. autostart `nebula-shell` through `scripts/run-nebula-vm.sh`
|
||||
3. fill the current output
|
||||
4. show a normal Adwaita 24px cursor
|
||||
5. let the Nebula launcher start Foot and other installed apps
|
||||
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:
|
||||
|
||||
@@ -218,7 +261,7 @@ 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.
|
||||
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:
|
||||
|
||||
@@ -249,11 +292,16 @@ Install `adwaita-icon-theme` if the theme is missing. A branded Nebula cursor is
|
||||
|
||||
### Emergency shortcuts
|
||||
|
||||
These remain for development fallback. They are not the normal UX once the GUI launcher works.
|
||||
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 |
|
||||
|
||||
@@ -261,12 +309,21 @@ These remain for development fallback. They are not the normal UX once the GUI l
|
||||
|
||||
`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:
|
||||
|
||||
- Sway IPC / focused-window tracking
|
||||
- workspaces, window overview, dock
|
||||
- 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
|
||||
@@ -274,4 +331,4 @@ Not implemented in this milestone:
|
||||
- Nebula Bigscreen
|
||||
- Gamescope
|
||||
|
||||
Those come later. Bigscreen is expected to reuse `Nebula.UI` and `Nebula.Applications`.
|
||||
Those come later. Bigscreen is expected to reuse `Nebula.UI`, `Nebula.Applications`, and `Nebula.Windows`.
|
||||
|
||||
@@ -13,7 +13,7 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Core Gui Qml Quick)
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Core Gui Qml Quick Network)
|
||||
find_package(LayerShellQt REQUIRED)
|
||||
|
||||
qt_standard_project_setup(REQUIRES 6.4)
|
||||
@@ -30,6 +30,11 @@ add_subdirectory(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/nebula-applications"
|
||||
)
|
||||
|
||||
add_subdirectory(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../../core/windows"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/nebula-windows"
|
||||
)
|
||||
|
||||
qt_add_executable(nebula-shell
|
||||
src/main.cpp
|
||||
src/DesktopIconProvider.cpp
|
||||
@@ -48,6 +53,7 @@ set(NEBULA_SHELL_QML_FILES
|
||||
qml/components/SystemArea.qml
|
||||
qml/components/Launcher.qml
|
||||
qml/components/AppItem.qml
|
||||
qml/components/OpenWindowItem.qml
|
||||
qml/mock/MockApps.qml
|
||||
)
|
||||
|
||||
@@ -79,6 +85,7 @@ qt_add_qml_module(nebula-shell
|
||||
QtQuick
|
||||
Nebula.UI
|
||||
Nebula.Applications
|
||||
Nebula.Windows
|
||||
)
|
||||
|
||||
target_link_libraries(nebula-shell
|
||||
@@ -90,6 +97,7 @@ target_link_libraries(nebula-shell
|
||||
LayerShellQt::Interface
|
||||
NebulaUI
|
||||
NebulaApplications
|
||||
NebulaWindows
|
||||
)
|
||||
|
||||
if(TARGET NebulaUIplugin)
|
||||
@@ -98,6 +106,9 @@ endif()
|
||||
if(TARGET NebulaApplicationsplugin)
|
||||
target_link_libraries(nebula-shell PRIVATE NebulaApplicationsplugin)
|
||||
endif()
|
||||
if(TARGET NebulaWindowsplugin)
|
||||
target_link_libraries(nebula-shell PRIVATE NebulaWindowsplugin)
|
||||
endif()
|
||||
|
||||
# Generated nebula-shell_qmltyperegistrations.cpp includes "OutputTracker.hpp"
|
||||
# via __has_include. Keep src/ on the include path so that succeeds.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Nebula.Windows
|
||||
|
||||
QtObject {
|
||||
property bool launcherOpen: false
|
||||
|
||||
// Focused-window tracking is a later compositor milestone. Launching an
|
||||
// application does not mean it has focus, so this stays "Desktop" for now.
|
||||
property string activeApplication: "Desktop"
|
||||
readonly property string activeApplication: WindowService.focusedApplicationName.length > 0
|
||||
? WindowService.focusedApplicationName
|
||||
: qsTr("Desktop")
|
||||
|
||||
function openLauncher() {
|
||||
launcherOpen = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Nebula.UI
|
||||
import Nebula.Applications
|
||||
import Nebula.Windows
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -187,6 +188,33 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
visible: !root.searching && WindowService.windowCount > 0
|
||||
|
||||
Text {
|
||||
text: qsTr("Open Windows")
|
||||
color: Theme.textSecondary
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 0.4
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: WindowService.model
|
||||
delegate: OpenWindowItem {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import QtQuick
|
||||
import Nebula.UI
|
||||
import Nebula.Windows
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string windowId
|
||||
required property string displayName
|
||||
required property string iconName
|
||||
required property bool focused
|
||||
required property bool minimized
|
||||
|
||||
implicitWidth: parent ? parent.width : 200
|
||||
implicitHeight: 36
|
||||
Accessible.role: Accessible.ListItem
|
||||
Accessible.name: displayName
|
||||
Accessible.onPressAction: root.activate()
|
||||
|
||||
readonly property url iconSource: iconName.length > 0
|
||||
? ("image://desktopicon/" + encodeURIComponent(iconName))
|
||||
: ""
|
||||
|
||||
function activate() {
|
||||
WindowService.focusWindow(windowId)
|
||||
ShellState.closeLauncher()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.radiusSmall
|
||||
color: {
|
||||
if (hover.containsMouse)
|
||||
return Theme.tileHover
|
||||
if (root.focused)
|
||||
return Theme.accentSoft
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 4
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.iconWell
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
anchors.centerIn: parent
|
||||
width: 14
|
||||
height: 14
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
cache: true
|
||||
visible: status === Image.Ready
|
||||
source: root.iconSource
|
||||
}
|
||||
|
||||
NebulaIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: icon.status !== Image.Ready
|
||||
name: "application"
|
||||
size: 12
|
||||
color: Theme.accent
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 22 - 28 - 16
|
||||
text: root.displayName
|
||||
color: {
|
||||
if (root.minimized)
|
||||
return Theme.textTertiary
|
||||
if (root.focused)
|
||||
return Theme.textPrimary
|
||||
return Theme.textSecondary
|
||||
}
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeApp
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 28
|
||||
height: parent.height
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "×"
|
||||
color: closeHover.containsMouse ? Theme.danger : Theme.textTertiary
|
||||
font.pixelSize: 14
|
||||
font.family: Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: WindowService.closeWindow(root.windowId)
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Close %1").arg(root.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 28
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activate()
|
||||
}
|
||||
|
||||
FocusFrame {
|
||||
radius: Theme.radiusSmall
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user