diff --git a/quickemu-noctalia-plugin/BarWidget.qml b/quickemu-noctalia-plugin/BarWidget.qml new file mode 100644 index 000000000..277e9adba --- /dev/null +++ b/quickemu-noctalia-plugin/BarWidget.qml @@ -0,0 +1,71 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets +import qs.Services.UI + +Item { + id: root + + property var pluginApi: null + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + readonly property bool pillDirection: BarService.getPillDirection(root) + + readonly property var mainInstance: pluginApi?.mainInstance + + readonly property real contentWidth: contentRow.implicitWidth + Style.marginM * 2 + readonly property real contentHeight: Style.capsuleHeight + + implicitWidth: contentWidth + implicitHeight: contentHeight + + Rectangle { + id: visualCapsule + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + width: root.contentWidth + height: root.contentHeight + color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor + radius: Style.radiusL + + RowLayout { + id: contentRow + anchors.centerIn: parent + spacing: Style.marginS + layoutDirection: Qt.LeftToRight + + NIcon { + icon: "server" + pointSize: Style.fontSizeL + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + } + + NText { + text: pluginApi?.tr("widget.title") + pointSize: Style.fontSizeS + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + font.weight: Style.fontWeightMedium + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + + onClicked: mouse => { + if (pluginApi) { + pluginApi.openPanel(root.screen, root); + } + } + } +} diff --git a/quickemu-noctalia-plugin/Main.qml b/quickemu-noctalia-plugin/Main.qml new file mode 100644 index 000000000..b14488cbf --- /dev/null +++ b/quickemu-noctalia-plugin/Main.qml @@ -0,0 +1,252 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Item { + id: root + + property var pluginApi: null + + // Resolve VM directory from settings, replacing ~ with $HOME + readonly property string vmDirectory: pluginApi?.pluginSettings?.vmDirectory || "~/quickemu/" + readonly property string homeDir: Quickshell.env("HOME") || "" + readonly property string resolvedVmDirectory: vmDirectory.replace("~", homeDir) + + property real downloadProgress: 0.0 + property bool isDownloading: false + property string lastError: "" + property string selectedCategory: "" + property bool _filterGuard: false + + // Models + ListModel { id: _vmListModel } + property alias vmListModel: _vmListModel + + ListModel { id: _osListModel } + property alias osListModel: _osListModel + + ListModel { id: _filteredOsListModel } + property alias filteredOsListModel: _filteredOsListModel + + ListModel { id: _osCategoryList } + property alias osCategoryList: _osCategoryList + + // --- Processes --- + + Process { + id: listProcess + command: ["find", root.resolvedVmDirectory, "-maxdepth", "1", "-name", "*.conf", "-exec", "basename", "-s", ".conf", "{}", ";"] + running: false + stdout: SplitParser { + onRead: data => { + var str = data.trim(); + if (str.length > 0) { + _vmListModel.append({ "vmName": str }); + } + } + } + stderr: SplitParser { onRead: data => root.lastError = data } + onRunningChanged: { + if (!running) { + Logger.i("Quickemu", "VM list refreshed β€” " + _vmListModel.count + " VMs found"); + } + } + } + + Process { + id: startProcess + command: [] + running: false + stdout: SplitParser { onRead: data => Logger.i("Quickemu", data) } + stderr: SplitParser { onRead: data => { root.lastError = data; Logger.e("Quickemu", data); } } + } + + Process { + id: editProcess + command: [] + running: false + stderr: SplitParser { onRead: data => { root.lastError = data; Logger.e("Quickemu", data); } } + } + + Process { + id: deleteProcess + command: [] + running: false + stderr: SplitParser { onRead: data => { root.lastError = data; Logger.e("Quickemu", data); } } + onRunningChanged: { + if (!running) { + refreshVmList(); + } + } + } + + Process { + id: createProcess + command: [] + workingDirectory: root.resolvedVmDirectory + running: false + stdout: SplitParser { + onRead: data => { + var str = data.trim(); + var match = str.match(/([0-9.]+)\s*%/); + if (match) { + root.downloadProgress = parseFloat(match[1]) / 100.0; + } else if (str.length > 0) { + Logger.i("Quickemu", str); + } + } + } + stderr: SplitParser { + onRead: data => { + Logger.e("Quickemu", data); + root.lastError = data; + } + } + onRunningChanged: { + if (running) { + root.isDownloading = true; + } else { + root.isDownloading = false; + Logger.i("Quickemu", "quickget finished"); + root.downloadProgress = 0.0; + refreshVmList(); + } + } + } + + Process { + id: listOsProcess + command: ["sh", "-c", "quickget --list | awk -F',' '{if (NR>1) print $1 \" \" $2}'"] + running: false + stdout: SplitParser { + onRead: data => { + var str = data.trim(); + if (str.length > 0) { + _osListModel.append({ "osName": str }); + _filteredOsListModel.append({ "osName": str }); + } + } + } + stderr: SplitParser { onRead: data => Logger.w("Quickemu", data) } + onRunningChanged: { + if (!running) { + Logger.i("Quickemu", "OS list populated with " + _osListModel.count + " options."); + buildCategoryList(); + } + } + } + + // --- Functions --- + + function buildCategoryList() { + _osCategoryList.clear(); + var seen = {}; + for (var i = 0; i < _osListModel.count; ++i) { + var full = _osListModel.get(i).osName; + var cat = full.split(" ")[0]; + if (!seen[cat]) { + seen[cat] = true; + _osCategoryList.append({ "category": cat }); + } + } + Logger.i("Quickemu", "Built " + _osCategoryList.count + " OS categories."); + } + + function filterByCategory(cat) { + _filterGuard = true; + root.selectedCategory = cat; + _filteredOsListModel.clear(); + for (var i = 0; i < _osListModel.count; ++i) { + var name = _osListModel.get(i).osName; + if (name.split(" ")[0] === cat) { + _filteredOsListModel.append({ "osName": name }); + } + } + _filterGuard = false; + } + + function clearCategoryFilter() { + _filterGuard = true; + root.selectedCategory = ""; + _filteredOsListModel.clear(); + for (var i = 0; i < _osListModel.count; ++i) { + _filteredOsListModel.append({ "osName": _osListModel.get(i).osName }); + } + _filterGuard = false; + } + + function updateFilteredOsList(query) { + if (_filterGuard) return; + _filterGuard = true; + _filteredOsListModel.clear(); + var q = query.toLowerCase(); + for (var i = 0; i < _osListModel.count; ++i) { + var name = _osListModel.get(i).osName; + if (name.toLowerCase().indexOf(q) !== -1) { + if (root.selectedCategory === "" || name.split(" ")[0] === root.selectedCategory) { + _filteredOsListModel.append({ "osName": name }); + } + } + } + _filterGuard = false; + } + + function clearError() { + root.lastError = ""; + } + + function refreshVmList() { + clearError(); + _vmListModel.clear(); + listProcess.running = false; + listProcess.running = true; + } + + function startVm(name, useSpice) { + clearError(); + var confPath = root.resolvedVmDirectory + name + ".conf"; + var cmd = ["quickemu", "--vm", confPath]; + if (useSpice) { + cmd.push("--display"); + cmd.push("spice"); + } + startProcess.command = cmd; + startProcess.running = false; + startProcess.running = true; + Logger.i("Quickemu", "Starting VM: " + name + (useSpice ? " (Spice)" : "")); + } + + function editVm(name) { + clearError(); + var confPath = root.resolvedVmDirectory + name + ".conf"; + editProcess.command = ["sh", "-c", "editor=$(xdg-mime query default text/plain | sed 's/.desktop//'); if [ -n \"$editor\" ]; then gtk-launch \"$editor\" \"$1\"; else xdg-open \"$1\"; fi", "--", confPath]; + editProcess.running = false; + editProcess.running = true; + Logger.i("Quickemu", "Editing VM config: " + confPath); + } + + function deleteVm(name) { + clearError(); + var confFile = root.resolvedVmDirectory + name + ".conf"; + var vmDir = root.resolvedVmDirectory + name; + deleteProcess.command = ["rm", "-rf", confFile, vmDir]; + deleteProcess.running = false; + deleteProcess.running = true; + Logger.i("Quickemu", "Deleting VM: " + name); + } + + function createVm(osArgs) { + clearError(); + root.downloadProgress = 0.0; + createProcess.command = ["sh", "-c", "quickget $1 | tr '\\r' '\\n'", "--", osArgs]; + createProcess.running = false; + createProcess.running = true; + Logger.i("Quickemu", "Creating VM: " + osArgs); + } + + Component.onCompleted: { + refreshVmList(); + listOsProcess.running = true; + } +} diff --git a/quickemu-noctalia-plugin/Panel.qml b/quickemu-noctalia-plugin/Panel.qml new file mode 100644 index 000000000..73a9f4924 --- /dev/null +++ b/quickemu-noctalia-plugin/Panel.qml @@ -0,0 +1,473 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Widgets +import qs.Services.UI + +Item { + id: root + + property var pluginApi: null + + readonly property var mainInstance: pluginApi?.mainInstance + + // Panel geometry anchor for Noctalia's windowing system + readonly property var geometryPlaceholder: panelContainer + readonly property bool allowAttach: true + + property real contentPreferredWidth: 550 * Style.uiScaleRatio + property real contentPreferredHeight: 650 * Style.uiScaleRatio + + anchors.fill: parent + + Rectangle { + id: panelContainer + anchors.fill: parent + color: "transparent" + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Error Banner + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: errorRow.implicitHeight + Style.marginS * 2 + visible: mainInstance && mainInstance.lastError !== "" + color: Qt.alpha(Color.mError, 0.15) + radius: Style.radiusS + border.color: Qt.alpha(Color.mError, 0.4) + border.width: Style.borderWidth || 1 + + RowLayout { + id: errorRow + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + NIcon { + icon: "alert-triangle" + pointSize: Style.fontSizeS + color: Color.mError + } + NText { + Layout.fillWidth: true + text: mainInstance ? mainInstance.lastError : "" + color: Color.mError + pointSize: Style.fontSizeXS + elide: Text.ElideRight + } + NButton { + icon: "x" + onClicked: if (mainInstance) mainInstance.clearError() + } + } + } + + // Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: "server" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + + NText { + text: pluginApi?.tr("panel.title") + color: Color.mOnSurface + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + NButton { + icon: "refresh-cw" + text: pluginApi?.tr("panel.refresh") + onClicked: { + if (mainInstance) mainInstance.refreshVmList(); + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Qt.alpha(Color.mOnSurface, 0.1) + } + + // Existing VMs section header + NText { + text: pluginApi?.tr("panel.existing-vms") + color: Color.mPrimary + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + + // VM List β€” this section gets all remaining vertical space + NBox { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 120 + + ListView { + id: vmList + anchors.fill: parent + anchors.margins: Style.marginS + model: mainInstance ? mainInstance.vmListModel : null + clip: true + spacing: Style.marginS + + delegate: Rectangle { + width: ListView.view.width + height: delegateRow.implicitHeight + Style.marginS * 2 + color: delegateMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : Color.mSurfaceVariant + radius: Style.radiusM + border.width: delegateMouseArea.containsMouse ? 1 : 0 + border.color: Qt.alpha(Color.mPrimary, 0.3) + + MouseArea { + id: delegateMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + + RowLayout { + id: delegateRow + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + NText { + text: model.vmName + color: Color.mOnSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightMedium + Layout.fillWidth: true + } + + NButton { + text: pluginApi?.tr("panel.start") + icon: "play" + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + onClicked: { + if (mainInstance) mainInstance.startVm(model.vmName); + } + } + NButton { + text: pluginApi?.tr("panel.start-spice") + icon: "monitor" + onClicked: { + if (mainInstance) mainInstance.startVm(model.vmName, true); + } + } + NButton { + text: pluginApi?.tr("panel.edit") + icon: "edit-2" + onClicked: { + if (mainInstance) mainInstance.editVm(model.vmName); + } + } + NButton { + text: pluginApi?.tr("panel.delete") + icon: "trash-2" + backgroundColor: Color.mError + textColor: Color.mOnError + onClicked: { + if (mainInstance) mainInstance.deleteVm(model.vmName); + } + } + } + } + + NText { + anchors.centerIn: parent + text: pluginApi?.tr("panel.no-vms") + color: Color.mOnSurfaceVariant + visible: vmList.count === 0 + pointSize: Style.fontSizeS + horizontalAlignment: Text.AlignHCenter + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Qt.alpha(Color.mOnSurface, 0.1) + } + + // Create New VM section header + NText { + text: pluginApi?.tr("panel.create-vm") + color: Color.mPrimary + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + + // Category sidebar + search/download β€” fixed height, does NOT fill + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 180 + Layout.maximumHeight: 200 + spacing: Style.marginS + + // Sidebar with OS categories + NBox { + Layout.preferredWidth: 130 * Style.uiScaleRatio + Layout.fillHeight: true + + ListView { + id: categoryList + anchors.fill: parent + anchors.margins: Style.marginXS + clip: true + spacing: 2 + + header: Rectangle { + width: ListView.view ? ListView.view.width : 0 + height: 28 + color: (mainInstance && mainInstance.selectedCategory === "") ? Qt.alpha(Color.mPrimary, 0.2) : "transparent" + radius: Style.radiusS + + NText { + anchors.centerIn: parent + text: pluginApi?.tr("panel.all-categories") + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightBold + color: (mainInstance && mainInstance.selectedCategory === "") ? Color.mPrimary : Color.mOnSurfaceVariant + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (mainInstance) mainInstance.clearCategoryFilter(); + } + } + } + + model: mainInstance ? mainInstance.osCategoryList : null + + delegate: Rectangle { + width: ListView.view.width + height: 26 + color: catMouse.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) + : (mainInstance && mainInstance.selectedCategory === model.category) ? Qt.alpha(Color.mPrimary, 0.15) + : "transparent" + radius: Style.radiusS + + NText { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Style.marginS + anchors.right: parent.right + anchors.rightMargin: Style.marginXS + text: model.category + pointSize: Style.fontSizeXS + color: (mainInstance && mainInstance.selectedCategory === model.category) ? Color.mPrimary : Color.mOnSurface + font.weight: (mainInstance && mainInstance.selectedCategory === model.category) ? Style.fontWeightBold : Style.fontWeightMedium + elide: Text.ElideRight + } + + MouseArea { + id: catMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (mainInstance) mainInstance.filterByCategory(model.category); + } + } + } + } + } + + // Right side: search + category label + download button + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.marginS + + // Track the user's actual selection separately + property string selectedOs: "" + + ComboBox { + id: osComboBox + Layout.fillWidth: true + Layout.preferredHeight: Style.capsuleHeight + model: mainInstance ? mainInstance.filteredOsListModel : null + textRole: "osName" + editable: true + + onEditTextChanged: { + if (mainInstance) { + mainInstance.updateFilteredOsList(editText); + } + } + + // When user picks from the dropdown, store the selection + onActivated: index => { + if (index >= 0 && mainInstance && mainInstance.filteredOsListModel.count > index) { + parent.selectedOs = mainInstance.filteredOsListModel.get(index).osName; + } + } + + background: Rectangle { + color: Color.mSurfaceVariant + radius: Style.radiusS + border.color: osComboBox.activeFocus ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1) + border.width: Style.borderWidth || 1 + } + contentItem: TextField { + text: osComboBox.editText + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + leftPadding: Style.marginS + font.pixelSize: Style.fontSizeS * Style.uiScaleRatio + placeholderText: pluginApi?.tr("panel.search-os") + placeholderTextColor: Color.mOnSurfaceVariant + background: Item {} + onTextChanged: osComboBox.editText = text + } + } + + NText { + visible: mainInstance && mainInstance.selectedCategory !== "" + text: (pluginApi?.tr("panel.category") || "") + ": " + (mainInstance ? mainInstance.selectedCategory : "") + pointSize: Style.fontSizeXS + color: Color.mPrimary + } + + Item { Layout.fillHeight: true } + + NButton { + Layout.fillWidth: true + text: pluginApi?.tr("panel.download") + icon: "download" + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + enabled: (parent.selectedOs !== "" || osComboBox.editText !== "") && (!mainInstance || !mainInstance.isDownloading) + onClicked: { + // Prefer the dropdown selection; fall back to typed text + var os = parent.selectedOs !== "" ? parent.selectedOs : osComboBox.editText; + if (mainInstance && os) { + mainInstance.createVm(os); + } + } + } + } + } + + // Progress Bar (visible only during download) + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + visible: mainInstance && mainInstance.downloadProgress > 0.0 + + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + radius: Style.radiusS + } + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * (mainInstance ? mainInstance.downloadProgress : 0.0) + color: Color.mPrimary + radius: Style.radiusS + + Behavior on width { + NumberAnimation { duration: 150 } + } + } + NText { + anchors.centerIn: parent + text: mainInstance ? Math.round(mainInstance.downloadProgress * 100) + "%" : "0%" + color: Color.mOnPrimary + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightBold + } + } + } + + // Download overlay β€” blocks all interaction during download + Rectangle { + id: downloadOverlay + anchors.fill: parent + color: Qt.alpha(Color.mSurface, 0.85) + visible: mainInstance && mainInstance.isDownloading + z: 100 + + MouseArea { + anchors.fill: parent + hoverEnabled: true + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginL + + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + running: downloadOverlay.visible + palette.dark: Color.mPrimary + } + + NText { + Layout.alignment: Qt.AlignHCenter + text: pluginApi?.tr("panel.downloading") + color: Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + } + + Item { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 250 * Style.uiScaleRatio + Layout.preferredHeight: Style.marginL + + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + radius: Style.radiusS + } + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * (mainInstance ? mainInstance.downloadProgress : 0.0) + color: Color.mPrimary + radius: Style.radiusS + + Behavior on width { + NumberAnimation { duration: 150 } + } + } + NText { + anchors.centerIn: parent + text: mainInstance ? Math.round(mainInstance.downloadProgress * 100) + "%" : "0%" + color: Color.mOnPrimary + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightBold + } + } + + NText { + Layout.alignment: Qt.AlignHCenter + text: pluginApi?.tr("panel.download-wait") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + } + } + } + } +} diff --git a/quickemu-noctalia-plugin/README.md b/quickemu-noctalia-plugin/README.md new file mode 100644 index 000000000..9c7a7b361 --- /dev/null +++ b/quickemu-noctalia-plugin/README.md @@ -0,0 +1,66 @@ +# πŸ–₯️ Quickemu Manager for Noctalia + +Welcome to **Quickemu Manager**, a highly polished, native [Noctalia](https://github.com/noctalia-dev/noctalia) shell plugin designed to let you seamlessly manage and create [Quickemu](https://github.com/quickemu-project/quickemu) virtual machines directly from your desktop bar. + +![Quickemu Plugin Preview](preview.png) + +--- + +## ✨ Features + +- **πŸš€ Instant OS Downloads**: Built-in support for downloading over **700+ operating systems** via `quickget`, complete with real-time progress bars. +- **πŸ” Filterable OS Search**: No more endless scrolling. The dropdown is fully searchableβ€”type to instantly filter through hundreds of operating systems. +- **🎨 Dynamic Noctalia Theming**: Integrates perfectly with modern Wayland desktop aesthetics. The plugin dynamically syncs with your global Noctalia `Style` and `Color` properties. +- **πŸ›‘οΈ Secure Shell Execution**: Process execution is hardened against command injection and path traversal via strict array-based argument passing. +- **πŸ”„ Background Execution**: Downloads and VM operations run autonomously in the background. You can safely close the widget without interrupting a large download. +- **βš™οΈ Dynamic Paths**: Automatically resolves paths relative to your home directory, making it portable and easy to use across different setups. + +--- + +## πŸ› οΈ Prerequisites + +Ensure you have the following installed and available in your system `$PATH`: +- [`quickemu`](https://github.com/quickemu-project/quickemu) +- [`quickget`](https://github.com/quickemu-project/quickemu) +- `xdg-utils` (for opening config files in your default editor) + +--- + +## πŸ“₯ Installation + +### Option 1: Noctalia Plugin Hub (Recommended) +You can easily install this via Noctalia's built-in plugin manager or by adding it to your `plugins.json`. + +### Option 2: Manual Installation +Install this plugin manually by cloning the repository directly into your Noctalia plugins folder: + +```bash +mkdir -p ~/.config/noctalia/plugins +git clone https://github.com/GughNess/quickemu-noctalia-plugin.git ~/.config/noctalia/plugins/quickemu +``` + +Once cloned: +1. Reload or restart Noctalia (`killall noctalia; noctalia &` or log out and back in). +2. Open your Noctalia settings menu. +3. Enable the `quickemu` plugin and add it to your desired bar section. + +--- + +## βš™οΈ Configuration + +By default, the plugin stores and looks for your Virtual Machines in `~/quickemu/`. + +If you store your VMs in a different location, you can easily change this directly through the Noctalia plugin settings UI, or by editing the plugin's metadata: + +```json +"metadata": { + "defaultSettings": { + "vmDirectory": "~/your/custom/directory/" + } +} +``` + +--- + +## πŸ“œ License +This project is licensed under the [MIT License](LICENSE). diff --git a/quickemu-noctalia-plugin/i18n/en.json b/quickemu-noctalia-plugin/i18n/en.json new file mode 100644 index 000000000..53ffa93eb --- /dev/null +++ b/quickemu-noctalia-plugin/i18n/en.json @@ -0,0 +1,22 @@ +{ + "widget": { + "title": "VMs" + }, + "panel": { + "title": "Quickemu Manager", + "refresh": "Refresh", + "existing-vms": "Existing VMs", + "create-vm": "Create New VM", + "start": "Start", + "start-spice": "Spice", + "edit": "Edit", + "delete": "Delete", + "no-vms": "No VMs found.", + "search-os": "Search OS...", + "download": "Download", + "downloading": "Downloading...", + "download-wait": "Please wait while the image downloads.", + "all-categories": "All", + "category": "Category" + } +} diff --git a/quickemu-noctalia-plugin/manifest.json b/quickemu-noctalia-plugin/manifest.json new file mode 100644 index 000000000..7bd98ccff --- /dev/null +++ b/quickemu-noctalia-plugin/manifest.json @@ -0,0 +1,21 @@ +{ + "id": "quickemu", + "name": "Quickemu", + "version": "1.0.0", + "minNoctaliaVersion": "4.0.0", + "author": "GughNess", + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "Manage and create Quickemu virtual machines from the bar.", + "tags": ["Bar", "Virtualization", "Panel"], + "entryPoints": { + "main": "Main.qml", + "barWidget": "BarWidget.qml", + "panel": "Panel.qml" + }, + "metadata": { + "defaultSettings": { + "vmDirectory": "~/quickemu/" + } + } +} diff --git a/quickemu-noctalia-plugin/preview.png b/quickemu-noctalia-plugin/preview.png new file mode 100644 index 000000000..33c2212dc Binary files /dev/null and b/quickemu-noctalia-plugin/preview.png differ