1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Lucas
f163b97c17 doctor: add json output (#1263)
* doctor: add json output

* doctor: fix systemd failed state as ok
2026-01-03 20:41:10 -05:00
bbedward
436c99927e settings: detect read-only on save attempts 2026-01-03 20:34:36 -05:00
bbedward
aa72eacae7 notifications: add image persistence 2026-01-03 19:56:08 -05:00
8 changed files with 197 additions and 23 deletions

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -95,10 +96,14 @@ var doctorCmd = &cobra.Command{
Run: runDoctor, Run: runDoctor,
} }
var doctorVerbose bool var (
doctorVerbose bool
doctorJSON bool
)
func init() { func init() {
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions") doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format")
} }
type category int type category int
@@ -152,8 +157,38 @@ type checkResult struct {
details string details string
} }
type checkResultJSON struct {
Category string `json:"category"`
Name string `json:"name"`
Status string `json:"status"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
type doctorOutputJSON struct {
Summary struct {
Errors int `json:"errors"`
Warnings int `json:"warnings"`
OK int `json:"ok"`
Info int `json:"info"`
} `json:"summary"`
Results []checkResultJSON `json:"results"`
}
func (r checkResult) toJSON() checkResultJSON {
return checkResultJSON{
Category: r.category.String(),
Name: r.name,
Status: string(r.status),
Message: r.message,
Details: r.details,
}
}
func runDoctor(cmd *cobra.Command, args []string) { func runDoctor(cmd *cobra.Command, args []string) {
printDoctorHeader() if !doctorJSON {
printDoctorHeader()
}
qsFeatures, qsMissingFeatures := checkQuickshellFeatures() qsFeatures, qsMissingFeatures := checkQuickshellFeatures()
@@ -169,8 +204,12 @@ func runDoctor(cmd *cobra.Command, args []string) {
checkEnvironmentVars(), checkEnvironmentVars(),
) )
printResults(results) if doctorJSON {
printSummary(results, qsMissingFeatures) printResultsJSON(results)
} else {
printResults(results)
printSummary(results, qsMissingFeatures)
}
} }
func printDoctorHeader() { func printDoctorHeader() {
@@ -733,6 +772,8 @@ func checkSystemdServices() []checkResult {
} }
if dmsState.enabled == "disabled" { if dmsState.enabled == "disabled" {
status, message = statusWarn, "Disabled" status, message = statusWarn, "Disabled"
} else if dmsState.active == "failed" || dmsState.active == "inactive" {
status = statusError
} }
results = append(results, checkResult{catServices, "dms.service", status, message, ""}) results = append(results, checkResult{catServices, "dms.service", status, message, ""})
} }
@@ -799,6 +840,31 @@ func printResults(results []checkResult) {
} }
} }
func printResultsJSON(results []checkResult) {
var ds DoctorStatus
for _, r := range results {
ds.Add(r)
}
output := doctorOutputJSON{}
output.Summary.Errors = ds.ErrorCount()
output.Summary.Warnings = ds.WarningCount()
output.Summary.OK = ds.OKCount()
output.Summary.Info = len(ds.Info)
output.Results = make([]checkResultJSON, 0, len(results))
for _, r := range results {
output.Results = append(output.Results, r.toJSON())
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(output); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
}
}
func printResultLine(r checkResult, styles tui.Styles) { func printResultLine(r checkResult, styles tui.Styles) {
icon, style := r.status.IconStyle(styles) icon, style := r.status.IconStyle(styles)

View File

@@ -127,11 +127,19 @@ Singleton {
} }
function _onWritableCheckComplete(writable) { function _onWritableCheckComplete(writable) {
const wasReadOnly = _isReadOnly;
_isReadOnly = !writable; _isReadOnly = !writable;
if (_isReadOnly) { if (_isReadOnly) {
console.info("SessionData: session.json is read-only (NixOS home-manager mode)"); _hasUnsavedChanges = _checkForUnsavedChanges();
} else if (_pendingMigration) { if (!wasReadOnly)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); console.info("SessionData: session.json is now read-only");
} else {
_loadedSessionSnapshot = getCurrentSessionJson();
_hasUnsavedChanges = false;
if (wasReadOnly)
console.info("SessionData: session.json is now writable");
if (_pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
} }
_pendingMigration = null; _pendingMigration = null;
} }
@@ -215,11 +223,9 @@ Singleton {
function saveSettings() { function saveSettings() {
if (isGreeterMode || _parseError || !_hasLoaded) if (isGreeterMode || _parseError || !_hasLoaded)
return; return;
if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges();
return;
}
settingsFile.setText(getCurrentSessionJson()); settingsFile.setText(getCurrentSessionJson());
if (_isReadOnly)
_checkSessionWritable();
} }
function migrateFromUndefinedToV1(settings) { function migrateFromUndefinedToV1(settings) {
@@ -953,6 +959,10 @@ Singleton {
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
} }
onSaveFailed: error => {
root._isReadOnly = true;
root._hasUnsavedChanges = root._checkForUnsavedChanges();
}
} }
FileView { FileView {

View File

@@ -840,11 +840,19 @@ Singleton {
} }
function _onWritableCheckComplete(writable) { function _onWritableCheckComplete(writable) {
const wasReadOnly = _isReadOnly;
_isReadOnly = !writable; _isReadOnly = !writable;
if (_isReadOnly) { if (_isReadOnly) {
console.info("SettingsData: settings.json is read-only (NixOS home-manager mode)"); _hasUnsavedChanges = _checkForUnsavedChanges();
} else if (_pendingMigration) { if (!wasReadOnly)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); console.info("SettingsData: settings.json is now read-only");
} else {
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasUnsavedChanges = false;
if (wasReadOnly)
console.info("SettingsData: settings.json is now writable");
if (_pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
} }
_pendingMigration = null; _pendingMigration = null;
} }
@@ -889,11 +897,9 @@ Singleton {
function saveSettings() { function saveSettings() {
if (_loading || _parseError || !_hasLoaded) if (_loading || _parseError || !_hasLoaded)
return; return;
if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges();
return;
}
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2)); settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
if (_isReadOnly)
_checkSettingsWritable();
} }
function savePluginSettings() { function savePluginSettings() {
@@ -1889,6 +1895,10 @@ Singleton {
applyStoredTheme(); applyStoredTheme();
} }
} }
onSaveFailed: error => {
root._isReadOnly = true;
root._hasUnsavedChanges = root._checkForUnsavedChanges();
}
} }
FileView { FileView {

View File

@@ -103,7 +103,7 @@ Variants {
} }
pluginService: (liveInstanceData.widgetType !== "desktopClock" && liveInstanceData.widgetType !== "systemMonitor") ? PluginService : null pluginService: (liveInstanceData.widgetType !== "desktopClock" && liveInstanceData.widgetType !== "systemMonitor") ? PluginService : null
screen: screenDelegate.screen screen: screenDelegate.screen
visible: shouldBeVisible widgetEnabled: shouldBeVisible
} }
} }
} }

View File

@@ -362,6 +362,7 @@ PanelWindow {
id: iconContainer id: iconContainer
readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== "" readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== ""
readonly property bool needsImagePersist: hasNotificationImage && notificationData.image.startsWith("image://qsimage/") && !notificationData.persistedImagePath
width: 63 width: 63
height: 63 height: 63
@@ -391,6 +392,22 @@ PanelWindow {
const appName = notificationData?.appName || "?"; const appName = notificationData?.appName || "?";
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
} }
onImageStatusChanged: {
if (imageStatus === Image.Ready && needsImagePersist) {
const cachePath = NotificationService.getImageCachePath(notificationData);
saveImageToFile(cachePath);
}
}
onImageSaved: filePath => {
if (!notificationData)
return;
notificationData.persistedImagePath = filePath;
const wrapperId = notificationData.notification?.id?.toString() || "";
if (wrapperId)
NotificationService.updateHistoryImage(wrapperId, filePath);
}
} }
Rectangle { Rectangle {

View File

@@ -17,6 +17,7 @@ Item {
property var pluginService: null property var pluginService: null
property string instanceId: "" property string instanceId: ""
property var instanceData: null property var instanceData: null
property bool widgetEnabled: true
readonly property bool isBuiltin: pluginId === "desktopClock" || pluginId === "systemMonitor" readonly property bool isBuiltin: pluginId === "desktopClock" || pluginId === "systemMonitor"
readonly property var activeComponent: isBuiltin ? builtinComponent : PluginService.pluginDesktopComponents[pluginId] ?? null readonly property var activeComponent: isBuiltin ? builtinComponent : PluginService.pluginDesktopComponents[pluginId] ?? null
@@ -202,7 +203,7 @@ Item {
PanelWindow { PanelWindow {
id: widgetWindow id: widgetWindow
screen: root.screen screen: root.screen
visible: root.visible && root.activeComponent !== null visible: root.widgetEnabled && root.activeComponent !== null
color: "transparent" color: "transparent"
WlrLayershell.namespace: "quickshell:desktop-widget:" + root.pluginId + (root.instanceId ? ":" + root.instanceId : "") WlrLayershell.namespace: "quickshell:desktop-widget:" + root.pluginId + (root.instanceId ? ":" + root.instanceId : "")

View File

@@ -17,6 +17,7 @@ Singleton {
property var historyList: [] property var historyList: []
readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json" readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json"
readonly property string imageCacheDir: Paths.strip(Paths.cache) + "/notification_images"
property bool historyLoaded: false property bool historyLoaded: false
property list<NotifWrapper> notificationQueue: [] property list<NotifWrapper> notificationQueue: []
@@ -46,6 +47,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
_recomputeGroups(); _recomputeGroups();
Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache)]); Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache)]);
Quickshell.execDetached(["mkdir", "-p", imageCacheDir]);
} }
FileView { FileView {
@@ -72,10 +74,46 @@ Singleton {
onTriggered: root.performSaveHistory() onTriggered: root.performSaveHistory()
} }
function getImageCachePath(wrapper) {
const ts = wrapper.time ? wrapper.time.getTime() : Date.now();
const id = wrapper.notification?.id?.toString() || "0";
return imageCacheDir + "/notif_" + ts + "_" + id + ".png";
}
function updateHistoryImage(wrapperId, imagePath) {
const idx = historyList.findIndex(n => n.id === wrapperId);
if (idx < 0)
return;
const item = historyList[idx];
const updated = {
id: item.id,
summary: item.summary,
body: item.body,
htmlBody: item.htmlBody,
appName: item.appName,
appIcon: item.appIcon,
image: "file://" + imagePath,
urgency: item.urgency,
timestamp: item.timestamp,
desktopEntry: item.desktopEntry
};
const newList = historyList.slice();
newList[idx] = updated;
historyList = newList;
saveHistory();
}
function addToHistory(wrapper) { function addToHistory(wrapper) {
if (!wrapper) if (!wrapper)
return; return;
const urg = typeof wrapper.urgency === "number" ? wrapper.urgency : 1; const urg = typeof wrapper.urgency === "number" ? wrapper.urgency : 1;
const imageUrl = wrapper.image || "";
let persistableImage = "";
if (wrapper.persistedImagePath) {
persistableImage = "file://" + wrapper.persistedImagePath;
} else if (imageUrl && !imageUrl.startsWith("image://qsimage/")) {
persistableImage = imageUrl;
}
const data = { const data = {
id: wrapper.notification?.id?.toString() || Date.now().toString(), id: wrapper.notification?.id?.toString() || Date.now().toString(),
summary: wrapper.summary || "", summary: wrapper.summary || "",
@@ -83,7 +121,7 @@ Singleton {
htmlBody: wrapper.htmlBody || wrapper.body || "", htmlBody: wrapper.htmlBody || wrapper.body || "",
appName: wrapper.appName || "", appName: wrapper.appName || "",
appIcon: wrapper.appIcon || "", appIcon: wrapper.appIcon || "",
image: wrapper.cleanImage || "", image: persistableImage,
urgency: urg, urgency: urg,
timestamp: wrapper.time.getTime(), timestamp: wrapper.time.getTime(),
desktopEntry: wrapper.desktopEntry || "" desktopEntry: wrapper.desktopEntry || ""
@@ -148,9 +186,19 @@ Singleton {
} }
} }
function _deleteCachedImage(imagePath) {
if (!imagePath || !imagePath.startsWith("file://"))
return;
const filePath = imagePath.replace("file://", "");
if (filePath.startsWith(imageCacheDir)) {
Quickshell.execDetached(["rm", "-f", filePath]);
}
}
function removeFromHistory(notificationId) { function removeFromHistory(notificationId) {
const idx = historyList.findIndex(n => n.id === notificationId); const idx = historyList.findIndex(n => n.id === notificationId);
if (idx >= 0) { if (idx >= 0) {
_deleteCachedImage(historyList[idx].image);
historyList = historyList.filter((_, i) => i !== idx); historyList = historyList.filter((_, i) => i !== idx);
saveHistory(); saveHistory();
return true; return true;
@@ -159,6 +207,9 @@ Singleton {
} }
function clearHistory() { function clearHistory() {
for (const item of historyList) {
_deleteCachedImage(item.image);
}
historyList = []; historyList = [];
saveHistory(); saveHistory();
} }
@@ -268,15 +319,22 @@ Singleton {
const now = Date.now(); const now = Date.now();
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000; const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
const toRemove = historyList.filter(item => (now - item.timestamp) > maxAgeMs);
const pruned = historyList.filter(item => (now - item.timestamp) <= maxAgeMs); const pruned = historyList.filter(item => (now - item.timestamp) <= maxAgeMs);
if (pruned.length !== historyList.length) { if (pruned.length !== historyList.length) {
for (const item of toRemove) {
_deleteCachedImage(item.image);
}
historyList = pruned; historyList = pruned;
saveHistory(); saveHistory();
} }
} }
function deleteHistory() { function deleteHistory() {
for (const item of historyList) {
_deleteCachedImage(item.image);
}
historyList = []; historyList = [];
historyAdapter.notifications = []; historyAdapter.notifications = [];
historyFileView.writeAdapter(); historyFileView.writeAdapter();
@@ -461,6 +519,7 @@ Singleton {
property bool removedByLimit: false property bool removedByLimit: false
property bool isPersistent: true property bool isPersistent: true
property int seq: 0 property int seq: 0
property string persistedImagePath: ""
onPopupChanged: { onPopupChanged: {
if (!popup) { if (!popup) {

View File

@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -13,6 +12,19 @@ Rectangle {
property bool hasImage: imageSource !== "" property bool hasImage: imageSource !== ""
property alias imageStatus: internalImage.status property alias imageStatus: internalImage.status
signal imageSaved(string filePath)
function saveImageToFile(filePath) {
if (internalImage.status !== Image.Ready)
return false;
internalImage.grabToImage(function (result) {
if (result && result.saveToFile(filePath)) {
root.imageSaved(filePath);
}
});
return true;
}
radius: width / 2 radius: width / 2
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: "transparent" border.color: "transparent"
@@ -67,7 +79,6 @@ Rectangle {
visible: (internalImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== "" visible: (internalImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
} }
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== "" visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""