Add desktop app service and VM dev session tooling

Introduces a new `core/applications` QML/C++ module (`Nebula.Applications`) to discover `.desktop` entries, expose filterable application models, and launch installed apps safely from the shell. The desktop launcher now uses real installed apps with themed icon loading and fallback glyphs, adds output-aware surface sizing via `OutputTracker`, and wires in image/icon providers. It also adds VMware-focused Sway/shell wrapper scripts, updates Sway config/session env defaults, and refreshes README docs to document the new development flow and Desktop 0.1 behavior.
This commit is contained in:
2026-08-26 17:19:30 +12:00
parent a7d204f241
commit b0c95b9e78
31 changed files with 1673 additions and 72 deletions
@@ -11,6 +11,17 @@ Item {
readonly property bool rowVariant: variant === "row"
readonly property int iconSize: rowVariant ? 32 : (variant === "recent" ? 32 : 40)
readonly property int glyphSize: rowVariant ? 16 : 18
readonly property string iconName: item && item.iconName ? item.iconName : ""
readonly property string fallbackGlyph: {
if (item && item.glyph)
return item.glyph
if (item && item.kind)
return item.kind
return "application"
}
readonly property url iconSource: iconName.length > 0
? ("image://desktopicon/" + encodeURIComponent(iconName))
: ""
implicitWidth: rowVariant ? parent ? parent.width : 200 : 90
implicitHeight: rowVariant ? 48 : 78
@@ -54,9 +65,23 @@ Item {
border.width: 1
}
Image {
id: tileIcon
anchors.centerIn: parent
width: root.iconSize - 8
height: root.iconSize - 8
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
cache: true
visible: status === Image.Ready
source: root.iconSource
}
NebulaIcon {
anchors.centerIn: parent
name: item && item.glyph ? item.glyph : "folder"
visible: tileIcon.status !== Image.Ready
name: root.fallbackGlyph
size: root.glyphSize
color: item && item.tint ? item.tint : Theme.accent
}
@@ -86,9 +111,23 @@ Item {
anchors.verticalCenter: parent.verticalCenter
color: Theme.iconWell
Image {
id: rowIcon
anchors.centerIn: parent
width: root.iconSize - 8
height: root.iconSize - 8
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
cache: true
visible: status === Image.Ready
source: root.iconSource
}
NebulaIcon {
anchors.centerIn: parent
name: item && (item.glyph || item.kind) ? (item.glyph || item.kind) : "folder"
visible: rowIcon.status !== Image.Ready
name: root.fallbackGlyph
size: root.glyphSize
color: item && item.tint ? item.tint : Theme.accent
}
@@ -1,5 +1,6 @@
import QtQuick
import Nebula.UI
import Nebula.Applications
Item {
id: root
@@ -13,8 +14,9 @@ Item {
Accessible.role: Accessible.Dialog
Accessible.name: qsTr("Launcher")
readonly property bool usingInstalledApps: ApplicationService.count > 0
readonly property bool searching: searchField.text.trim().length > 0
readonly property var filteredApps: {
readonly property var mockMatches: {
const query = searchField.text.trim().toLowerCase()
const apps = MockApps.apps
if (!query)
@@ -24,6 +26,9 @@ Item {
return haystack.indexOf(query) !== -1
})
}
readonly property int resultCount: usingInstalledApps
? ApplicationService.filteredModel.count
: mockMatches.length
Keys.onEscapePressed: {
event.accepted = true
@@ -62,6 +67,7 @@ Item {
id: searchField
width: parent.width
placeholderText: qsTr("Search applications")
onTextChanged: ApplicationService.filter = text
}
Item {
@@ -87,7 +93,7 @@ Item {
Column {
width: parent.width
spacing: 8
visible: !root.searching && MockApps.recents.length > 0
visible: !root.searching && !root.usingInstalledApps && MockApps.recents.length > 0
Text {
text: qsTr("Recent")
@@ -133,14 +139,33 @@ Item {
Grid {
id: appGrid
visible: root.filteredApps.length > 0
visible: root.resultCount > 0
width: parent.width
columns: 4
columnSpacing: 6
rowSpacing: 6
Repeater {
model: root.filteredApps
model: root.usingInstalledApps ? ApplicationService.filteredModel : []
delegate: AppItem {
required property string desktopId
required property string name
required property string comment
required property string iconName
width: (appGrid.width - 18) / 4
variant: "tile"
item: ({
desktopId: desktopId,
name: name,
comment: comment,
iconName: iconName
})
onSelected: root.launchApp(item)
}
}
Repeater {
model: root.usingInstalledApps ? [] : root.mockMatches
delegate: AppItem {
required property var modelData
width: (appGrid.width - 18) / 4
@@ -152,7 +177,7 @@ Item {
}
Text {
visible: root.filteredApps.length === 0
visible: root.resultCount === 0
width: parent.width
topPadding: 8
text: qsTr("No matching applications")
@@ -165,7 +190,7 @@ Item {
Column {
width: parent.width
spacing: 8
visible: !root.searching && MockApps.projects.length > 0
visible: !root.searching && !root.usingInstalledApps && MockApps.projects.length > 0
Text {
text: qsTr("Recent projects")
@@ -286,10 +311,12 @@ Item {
}
function launchApp(app) {
ShellState.launch(app.name)
if (app && app.desktopId)
ApplicationService.launch(app.desktopId)
ShellState.closeLauncher()
}
function openProject(project) {
ShellState.launch(project.name)
ShellState.closeLauncher()
}
}