1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

doctor: add links to dr command

This commit is contained in:
bbedward
2026-01-04 22:44:19 -05:00
parent 5e03afe7f0
commit 9b027df1d5
5 changed files with 112 additions and 88 deletions

View File

@@ -17,3 +17,4 @@ This file is more of a quick reference so I know what to account for before next
- Theme registry - Theme registry
- Notification persistence & history - Notification persistence & history
- **BREAKING** vscode theme needs re-installed - **BREAKING** vscode theme needs re-installed
- dms doctor cmd

View File

@@ -147,6 +147,7 @@ func (c category) String() string {
const ( const (
checkNameMaxLength = 21 checkNameMaxLength = 21
doctorDocsURL = "https://danklinux.com/docs/dankmaterialshell/cli-doctor"
) )
type checkResult struct { type checkResult struct {
@@ -155,6 +156,7 @@ type checkResult struct {
status status status status
message string message string
details string details string
url string
} }
type checkResultJSON struct { type checkResultJSON struct {
@@ -163,6 +165,7 @@ type checkResultJSON struct {
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
URL string `json:"url,omitempty"`
} }
type doctorOutputJSON struct { type doctorOutputJSON struct {
@@ -182,6 +185,7 @@ func (r checkResult) toJSON() checkResultJSON {
Status: string(r.status), Status: string(r.status),
Message: r.message, Message: r.message,
Details: r.details, Details: r.details,
URL: r.url,
} }
} }
@@ -231,20 +235,21 @@ func checkSystemInfo() []checkResult {
if strings.Contains(err.Error(), "Unsupported distribution") { if strings.Contains(err.Error(), "Unsupported distribution") {
osRelease := readOSRelease() osRelease := readOSRelease()
if osRelease["ID"] == "nixos" { switch {
case osRelease["ID"] == "nixos":
status = statusOK status = statusOK
message = osRelease["PRETTY_NAME"] message = osRelease["PRETTY_NAME"]
if message == "" { if message == "" {
message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"]) message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"])
} }
details = "Supported for runtime (install via NixOS module or Flake)" details = "Supported for runtime (install via NixOS module or Flake)"
} else if osRelease["PRETTY_NAME"] != "" { case osRelease["PRETTY_NAME"] != "":
message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"]) message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"])
details = "DMS may work but automatic installation is not available" details = "DMS may work but automatic installation is not available"
} }
} }
results = append(results, checkResult{catSystem, "Operating System", status, message, details}) results = append(results, checkResult{catSystem, "Operating System", status, message, details, doctorDocsURL + "#operating-system"})
} else { } else {
status := statusOK status := statusOK
message := osInfo.PrettyName message := osInfo.PrettyName
@@ -258,6 +263,7 @@ func checkSystemInfo() []checkResult {
results = append(results, checkResult{ results = append(results, checkResult{
catSystem, "Operating System", status, message, catSystem, "Operating System", status, message,
fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture), fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture),
doctorDocsURL + "#operating-system",
}) })
} }
@@ -266,7 +272,7 @@ func checkSystemInfo() []checkResult {
if arch != "amd64" && arch != "arm64" { if arch != "amd64" && arch != "arm64" {
archStatus = statusError archStatus = statusError
} }
results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, ""}) results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, "", doctorDocsURL + "#architecture"})
waylandDisplay := os.Getenv("WAYLAND_DISPLAY") waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
xdgSessionType := os.Getenv("XDG_SESSION_TYPE") xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
@@ -276,13 +282,15 @@ func checkSystemInfo() []checkResult {
results = append(results, checkResult{ results = append(results, checkResult{
catSystem, "Display Server", statusOK, "Wayland", catSystem, "Display Server", statusOK, "Wayland",
fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay), fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay),
doctorDocsURL + "#display-server",
}) })
case xdgSessionType == "x11": case xdgSessionType == "x11":
results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", ""}) results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", "", doctorDocsURL + "#display-server"})
default: default:
results = append(results, checkResult{ results = append(results, checkResult{
catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)", catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)",
fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType), fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType),
doctorDocsURL + "#display-server",
}) })
} }
@@ -299,9 +307,10 @@ func checkEnvironmentVars() []checkResult {
func checkEnvVar(name string) []checkResult { func checkEnvVar(name string) []checkResult {
value := os.Getenv(name) value := os.Getenv(name)
if value != "" { if value != "" {
return []checkResult{{catEnvironment, name, statusInfo, value, ""}} return []checkResult{{catEnvironment, name, statusInfo, value, "", doctorDocsURL + "#environment-variables"}}
} else if doctorVerbose { }
return []checkResult{{catEnvironment, name, statusInfo, "Not set", ""}} if doctorVerbose {
return []checkResult{{catEnvironment, name, statusInfo, "Not set", "", doctorDocsURL + "#environment-variables"}}
} }
return nil return nil
} }
@@ -328,7 +337,7 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
} }
results := []checkResult{ results := []checkResult{
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails}, {catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
} }
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures) qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
@@ -336,13 +345,13 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
if doctorVerbose && qsPath != "" { if doctorVerbose && qsPath != "" {
qsDetails = qsPath qsDetails = qsPath
} }
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails}) results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
dmsVersion, dmsPath := getDMSShellVersion() dmsVersion, dmsPath := getDMSShellVersion()
if dmsVersion != "" { if dmsVersion != "" {
results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath}) results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath, doctorDocsURL + "#dms-shell"})
} else { } else {
results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install"}) results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install", doctorDocsURL + "#dms-shell"})
} }
return results return results
@@ -405,16 +414,16 @@ func checkDMSInstallation() []checkResult {
} }
if dmsPath == "" { if dmsPath == "" {
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path"}} return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
} }
results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath}) results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath, doctorDocsURL + "#dms-configuration"})
shellQml := filepath.Join(dmsPath, "shell.qml") shellQml := filepath.Join(dmsPath, "shell.qml")
if _, err := os.Stat(shellQml); err != nil { if _, err := os.Stat(shellQml); err != nil {
results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml}) results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml, doctorDocsURL + "#dms-configuration"})
} else { } else {
results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml}) results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml, doctorDocsURL + "#dms-configuration"})
} }
if doctorVerbose { if doctorVerbose {
@@ -427,7 +436,7 @@ func checkDMSInstallation() []checkResult {
case strings.Contains(dmsPath, ".config"): case strings.Contains(dmsPath, ".config"):
installType = "User config" installType = "User config"
} }
results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath}) results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath, doctorDocsURL + "#dms-configuration"})
} }
return results return results
@@ -450,24 +459,26 @@ func checkWindowManagers() []checkResult {
foundAny := false foundAny := false
for _, c := range compositors { for _, c := range compositors {
if slices.ContainsFunc(c.commands, utils.CommandExists) { if !slices.ContainsFunc(c.commands, utils.CommandExists) {
foundAny = true continue
var compositorPath string
for _, cmd := range c.commands {
if path, err := exec.LookPath(cmd); err == nil {
compositorPath = path
break
}
}
details := ""
if doctorVerbose && compositorPath != "" {
details = compositorPath
}
results = append(results, checkResult{
catCompositor, c.name, statusOK,
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
})
} }
foundAny = true
var compositorPath string
for _, cmd := range c.commands {
if path, err := exec.LookPath(cmd); err == nil {
compositorPath = path
break
}
}
details := ""
if doctorVerbose && compositorPath != "" {
details = compositorPath
}
results = append(results, checkResult{
catCompositor, c.name, statusOK,
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
doctorDocsURL + "#compositor",
})
} }
if !foundAny { if !foundAny {
@@ -475,11 +486,12 @@ func checkWindowManagers() []checkResult {
catCompositor, "Compositor", statusError, catCompositor, "Compositor", statusError,
"No supported Wayland compositor found", "No supported Wayland compositor found",
"Install Hyprland, niri, Sway, River, or Wayfire", "Install Hyprland, niri, Sway, River, or Wayfire",
doctorDocsURL + "#compositor",
}) })
} }
if wm := detectRunningWM(); wm != "" { if wm := detectRunningWM(); wm != "" {
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, ""}) results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
} }
return results return results
@@ -601,7 +613,7 @@ ShellRoot {
status, message = statusInfo, "Not available" status, message = statusInfo, "Not available"
missingFeatures = true missingFeatures = true
} }
results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc}) results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc, doctorDocsURL + "#quickshell-features"})
} }
return results, missingFeatures return results, missingFeatures
@@ -610,16 +622,16 @@ ShellRoot {
func checkI2CAvailability() checkResult { func checkI2CAvailability() checkResult {
ddc, err := brightness.NewDDCBackend() ddc, err := brightness.NewDDCBackend()
if err != nil { if err != nil {
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "Not available", "External monitor brightness control"} return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "Not available", "External monitor brightness control", doctorDocsURL + "#optional-features"}
} }
defer ddc.Close() defer ddc.Close()
devices, err := ddc.GetDevices() devices, err := ddc.GetDevices()
if err != nil || len(devices) == 0 { if err != nil || len(devices) == 0 {
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "No monitors detected", "External monitor brightness control"} return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "No monitors detected", "External monitor brightness control", doctorDocsURL + "#optional-features"}
} }
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control"} return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
} }
func detectNetworkBackend() string { func detectNetworkBackend() string {
@@ -649,25 +661,24 @@ func checkOptionalDependencies() []checkResult {
var results []checkResult var results []checkResult
if utils.IsServiceActive("accounts-daemon", false) { if utils.IsServiceActive("accounts-daemon", false) {
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts"}) results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
} else { } else {
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts"}) results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
} }
if utils.IsServiceActive("power-profiles-daemon", false) { if utils.IsServiceActive("power-profiles-daemon", false) {
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management"}) results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"})
} else { } else {
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management"}) results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"})
} }
i2cStatus := checkI2CAvailability() results = append(results, checkI2CAvailability())
results = append(results, i2cStatus)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], ""}) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"})
} else { } else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty"}) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"})
} }
deps := []struct { deps := []struct {
@@ -686,13 +697,12 @@ func checkOptionalDependencies() []checkResult {
for _, d := range deps { for _, d := range deps {
found, foundCmd := utils.CommandExists(d.cmd), d.cmd found, foundCmd := utils.CommandExists(d.cmd), d.cmd
if !found && d.altCmd != "" { if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
if utils.CommandExists(d.altCmd) { found, foundCmd = true, d.altCmd
found, foundCmd = true, d.altCmd
}
} }
if found { switch {
case found:
message := "Installed" message := "Installed"
details := d.desc details := d.desc
if d.name == "Network" { if d.name == "Network" {
@@ -711,11 +721,11 @@ func checkOptionalDependencies() []checkResult {
} }
} }
} }
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details}) results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
} else if d.important { case d.important:
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc}) results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"})
} else { default:
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc}) results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"})
} }
} }
@@ -738,19 +748,18 @@ func checkConfigurationFiles() []checkResult {
var results []checkResult var results []checkResult
for _, cf := range configFiles { for _, cf := range configFiles {
info, err := os.Stat(cf.path) info, err := os.Stat(cf.path)
if err == nil { if err != nil {
status := statusOK results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path, doctorDocsURL + "#config-files"})
message := "Present" continue
if info.Mode().Perm()&0200 == 0 {
status = statusWarn
message += " (read-only)"
}
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path})
} else {
results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path})
} }
status := statusOK
message := "Present"
if info.Mode().Perm()&0200 == 0 {
status = statusWarn
message += " (read-only)"
}
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
} }
return results return results
} }
@@ -764,29 +773,31 @@ func checkSystemdServices() []checkResult {
dmsState := getServiceState("dms", true) dmsState := getServiceState("dms", true)
if !dmsState.exists { if !dmsState.exists {
results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service"}) results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"})
} else { } else {
status, message := statusOK, dmsState.enabled status, message := statusOK, dmsState.enabled
if dmsState.active != "" { if dmsState.active != "" {
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active) message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
} }
if dmsState.enabled == "disabled" { switch {
case dmsState.enabled == "disabled":
status, message = statusWarn, "Disabled" status, message = statusWarn, "Disabled"
} else if dmsState.active == "failed" || dmsState.active == "inactive" { case dmsState.active == "failed" || dmsState.active == "inactive":
status = statusError status = statusError
} }
results = append(results, checkResult{catServices, "dms.service", status, message, ""}) results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
} }
greetdState := getServiceState("greetd", false) greetdState := getServiceState("greetd", false)
if greetdState.exists { switch {
case greetdState.exists:
status := statusOK status := statusOK
if greetdState.enabled == "disabled" { if greetdState.enabled == "disabled" {
status = statusInfo status = statusInfo
} }
results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, ""}) results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, "", doctorDocsURL + "#services"})
} else if doctorVerbose { case doctorVerbose:
results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service"}) results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service", doctorDocsURL + "#services"})
} }
return results return results

View File

@@ -50,8 +50,8 @@ Rectangle {
Column { Column {
anchors.left: statusIcon.right anchors.left: statusIcon.right
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.right: categoryChip.visible ? categoryChip.left : parent.right anchors.right: categoryChip.visible ? categoryChip.left : (urlButton.visible ? urlButton.left : parent.right)
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 1 spacing: 1
@@ -76,8 +76,8 @@ Rectangle {
Rectangle { Rectangle {
id: categoryChip id: categoryChip
anchors.right: parent.right anchors.right: urlButton.visible ? urlButton.left : parent.right
anchors.rightMargin: Theme.spacingM anchors.rightMargin: urlButton.visible ? Theme.spacingXS : Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
height: Math.round(Theme.fontSizeSmall * 1.67) height: Math.round(Theme.fontSizeSmall * 1.67)
width: categoryText.implicitWidth + Theme.spacingS width: categoryText.implicitWidth + Theme.spacingS
@@ -93,4 +93,17 @@ Rectangle {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
DankActionButton {
id: urlButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "open_in_new"
iconSize: Theme.iconSize - 6
buttonSize: 24
visible: !!(root.resultData?.url)
tooltipText: root.resultData?.url || ""
onClicked: Qt.openUrlExternally(root.resultData.url)
}
} }

View File

@@ -102,12 +102,10 @@ Item {
property string text: "" property string text: ""
implicitWidth: Math.min(300, Math.max(120, textContent.implicitWidth + Theme.spacingM * 2)) leftPadding: Theme.spacingM
implicitHeight: textContent.implicitHeight + Theme.spacingS * 2 rightPadding: Theme.spacingM
width: implicitWidth topPadding: Theme.spacingS
height: implicitHeight bottomPadding: Theme.spacingS
padding: 0
closePolicy: Popup.NoAutoClose closePolicy: Popup.NoAutoClose
modal: false modal: false
dim: false dim: false
@@ -122,6 +120,7 @@ Item {
contentItem: Text { contentItem: Text {
id: textContent id: textContent
width: Math.min(implicitWidth, 500)
text: tooltip.text text: tooltip.text
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText

View File

@@ -23,7 +23,7 @@ MouseArea {
Timer { Timer {
id: hoverDelay id: hoverDelay
interval: 1000 interval: 400
repeat: false repeat: false
onTriggered: { onTriggered: {
tooltip.show(root.tooltipText, root, 0, 0, "bottom"); tooltip.show(root.tooltipText, root, 0, 0, "bottom");