mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
43 Commits
stable
...
53f5240d41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f5240d41 | ||
|
|
27f0df07af | ||
|
|
ad940b5884 | ||
|
|
ec8ab47462 | ||
|
|
35cbfeb008 | ||
|
|
7036362b9b | ||
|
|
2bcb33e85c | ||
|
|
76ac036f85 | ||
|
|
581073394a | ||
|
|
d7b7086b21 | ||
|
|
59be179821 | ||
|
|
1cf2f6b946 | ||
|
|
a57a9c2121 | ||
|
|
67568c3746 | ||
|
|
afce792b80 | ||
|
|
f5c7493dbb | ||
|
|
f9b9d98638 | ||
|
|
2a97e03fa6 | ||
|
|
d6dacc2975 | ||
|
|
aab4b6765d | ||
|
|
3539aca1f7 | ||
|
|
81fbe9eaba | ||
|
|
f9dc6de485 | ||
|
|
012022d370 | ||
|
|
993216e157 | ||
|
|
c992f2b582 | ||
|
|
3243adebca | ||
|
|
baccef57d4 | ||
|
|
a823095372 | ||
|
|
172a743de4 | ||
|
|
623eec3689 | ||
|
|
53a033fe35 | ||
|
|
c490ee24f4 | ||
|
|
cc1e49294e | ||
|
|
e6fa46ae26 | ||
|
|
35fe774a1b | ||
|
|
1e6a0f9423 | ||
|
|
cc1877aadb | ||
|
|
f1eb1fa9ba | ||
|
|
bdd01e335d | ||
|
|
4b7baf82cd | ||
|
|
15c88ce1d2 | ||
|
|
8891c388d0 |
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,13 +4,14 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update (dms, dms-git, or all)"
|
||||
required: false
|
||||
default: "all"
|
||||
tag_version:
|
||||
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
||||
required: false
|
||||
default: ""
|
||||
description: "Package to update"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
@@ -56,8 +57,9 @@ jobs:
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
# Sets LATEST_TAG variable in parent scope if update needed
|
||||
check_dms_stable() {
|
||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||
|
||||
@@ -73,8 +75,8 @@ jobs:
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push - always update stable package
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - always update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
@@ -104,7 +106,12 @@ jobs:
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
@@ -129,6 +136,9 @@ jobs:
|
||||
if check_dms_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
@@ -161,12 +171,19 @@ jobs:
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push event - use the pushed tag
|
||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION"
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
# Check if check-updates already determined a version (from auto-detection)
|
||||
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use version from check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - dms-git only
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
@@ -176,22 +193,28 @@ jobs:
|
||||
|
||||
# Determine version for dms stable
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||
# For explicit dms selection, require tag_version
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using specified tag: $VERSION"
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
else
|
||||
echo "ERROR: tag_version is required when package=dms"
|
||||
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
||||
exit 1
|
||||
# Auto-detect latest release for dms
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
# For "all", auto-detect if tag_version not specified
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using specified tag: $VERSION"
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
else
|
||||
# Auto-detect latest release for "all"
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
@@ -206,7 +229,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
else
|
||||
@@ -215,6 +238,9 @@ jobs:
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
|
||||
@@ -199,31 +199,6 @@ func labToHex(L, a, b float64) string {
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||
}
|
||||
|
||||
// Adjust brightness while keeping the same hue
|
||||
func retoneToL(hex string, Ltarget float64) string {
|
||||
rgb := HexToRGB(hex)
|
||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||
L, a, b := col.Lab()
|
||||
L100 := L * 100.0
|
||||
|
||||
scale := 1.0
|
||||
if L100 != 0 {
|
||||
scale = Ltarget / L100
|
||||
}
|
||||
|
||||
a2, b2 := a*scale, b*scale
|
||||
|
||||
// Don't let it get too saturated
|
||||
maxChroma := 0.4
|
||||
if math.Hypot(a2, b2) > maxChroma {
|
||||
k := maxChroma / math.Hypot(a2, b2)
|
||||
a2 *= k
|
||||
b2 *= k
|
||||
}
|
||||
|
||||
return labToHex(Ltarget, a2, b2)
|
||||
}
|
||||
|
||||
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||
Lf := getLstar(hexFg)
|
||||
Lb := getLstar(hexBg)
|
||||
@@ -356,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
||||
return hexColor
|
||||
}
|
||||
|
||||
// Bidirectional contrast - tries both lighter and darker, picks closest to original
|
||||
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||
if current >= minLc {
|
||||
return hexColor
|
||||
}
|
||||
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
origL, af, bf := cf.Lab()
|
||||
|
||||
var darkerResult, lighterResult string
|
||||
darkerL, lighterL := origL, origL
|
||||
darkerFound, lighterFound := false, false
|
||||
|
||||
step := 0.5
|
||||
for i := range 120 {
|
||||
if !darkerFound {
|
||||
darkerL = math.Max(0, origL-float64(i)*step)
|
||||
cand := labToHex(darkerL, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
darkerResult = cand
|
||||
darkerFound = true
|
||||
}
|
||||
}
|
||||
if !lighterFound {
|
||||
lighterL = math.Min(100, origL+float64(i)*step)
|
||||
cand := labToHex(lighterL, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
lighterResult = cand
|
||||
lighterFound = true
|
||||
}
|
||||
}
|
||||
if darkerFound && lighterFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if darkerFound && lighterFound {
|
||||
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
|
||||
return darkerResult
|
||||
}
|
||||
return lighterResult
|
||||
}
|
||||
if darkerFound {
|
||||
return darkerResult
|
||||
}
|
||||
if lighterFound {
|
||||
return lighterResult
|
||||
}
|
||||
return hexColor
|
||||
}
|
||||
|
||||
type PaletteOptions struct {
|
||||
IsLight bool
|
||||
Background string
|
||||
@@ -369,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||
if opts.UseDPS {
|
||||
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func blendHue(base, target, factor float64) float64 {
|
||||
diff := target - base
|
||||
if diff > 0.5 {
|
||||
diff -= 1.0
|
||||
} else if diff < -0.5 {
|
||||
diff += 1.0
|
||||
}
|
||||
result := base + diff*factor
|
||||
if result < 0 {
|
||||
result += 1.0
|
||||
} else if result >= 1.0 {
|
||||
result -= 1.0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func DeriveContainer(primary string, isLight bool) string {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -389,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
rgb := HexToRGB(baseColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
pr := HexToRGB(primaryColor)
|
||||
ph := RGBToHSV(pr)
|
||||
|
||||
var palette Palette
|
||||
|
||||
var normalTextTarget, secondaryTarget float64
|
||||
@@ -410,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
}
|
||||
palette.Color0 = NewColorInfo(bgColor)
|
||||
|
||||
hueShift := (hsv.H - 0.6) * 0.12
|
||||
satBoost := 1.15
|
||||
baseSat := math.Max(ph.S, 0.5)
|
||||
baseVal := math.Max(ph.V, 0.5)
|
||||
|
||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
||||
var redColor string
|
||||
if opts.IsLight {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
redH := blendHue(0.0, ph.H, 0.12)
|
||||
greenH := blendHue(0.33, ph.H, 0.10)
|
||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||
|
||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||
var greenColor string
|
||||
if opts.IsLight {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
||||
var yellowColor string
|
||||
if opts.IsLight {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
var blueColor string
|
||||
if opts.IsLight {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
magH := hsv.H - 0.03
|
||||
if magH < 0 {
|
||||
magH += 1.0
|
||||
}
|
||||
var magColor string
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
if opts.IsLight {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
cyanH := hsv.H + 0.08
|
||||
if cyanH > 1.0 {
|
||||
cyanH -= 1.0
|
||||
}
|
||||
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
||||
accentTarget := secondaryTarget * 0.7
|
||||
|
||||
if opts.IsLight {
|
||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
||||
} else {
|
||||
palette.Color7 = NewColorInfo("#abb2bf")
|
||||
palette.Color8 = NewColorInfo("#5c6370")
|
||||
}
|
||||
redS := math.Min(baseSat*1.2, 1.0)
|
||||
redV := baseVal * 0.95
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
if opts.IsLight {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
||||
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
} else {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
brightBlue := retoneToL(primaryColor, 85.0)
|
||||
palette.Color12 = NewColorInfo(brightBlue)
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyanH := hsv.H + 0.02
|
||||
if brightCyanH > 1.0 {
|
||||
brightCyanH -= 1.0
|
||||
}
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
}
|
||||
greenS := math.Min(baseSat*1.3, 1.0)
|
||||
greenV := baseVal * 0.75
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
if opts.IsLight {
|
||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
||||
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
blueS := math.Min(ph.S*1.05, 1.0)
|
||||
blueV := math.Min(ph.V*1.05, 1.0)
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (light container in light mode)
|
||||
container5 := DeriveContainer(primaryColor, true)
|
||||
palette.Color5 = NewColorInfo(container5)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
gray7S := baseSat * 0.08
|
||||
gray7V := baseVal * 0.28
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.05
|
||||
gray8V := baseVal * 0.85
|
||||
dimTarget := secondaryTarget * 0.5
|
||||
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
|
||||
|
||||
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightGreenS := math.Min(baseSat*1.1, 1.0)
|
||||
brightGreenV := math.Min(baseVal*1.1, 1.0)
|
||||
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightYellowS := math.Min(baseSat*1.4, 1.0)
|
||||
brightYellowV := math.Min(baseVal*1.3, 1.0)
|
||||
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightBlueS := math.Min(ph.S*1.1, 1.0)
|
||||
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
lightContainer := DeriveContainer(primaryColor, true)
|
||||
palette.Color13 = NewColorInfo(lightContainer)
|
||||
|
||||
brightCyanS := ph.S * 0.5
|
||||
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
|
||||
|
||||
white15S := baseSat * 0.04
|
||||
white15V := math.Min(baseVal*1.5, 1.0)
|
||||
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
|
||||
} else {
|
||||
palette.Color15 = NewColorInfo("#ffffff")
|
||||
redS := math.Min(baseSat*1.1, 1.0)
|
||||
redV := math.Min(baseVal*1.15, 1.0)
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
greenS := math.Min(baseSat*1.0, 1.0)
|
||||
greenV := math.Min(baseVal*1.0, 1.0)
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
yellowS := math.Min(baseSat*1.1, 1.0)
|
||||
yellowV := math.Min(baseVal*1.25, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Slightly more saturated variant of primary
|
||||
blueS := math.Min(ph.S*1.2, 1.0)
|
||||
blueV := ph.V * 0.95
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||
darkContainer := DeriveContainer(primaryColor, false)
|
||||
palette.Color5 = NewColorInfo(darkContainer)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
gray7S := baseSat * 0.12
|
||||
gray7V := math.Min(baseVal*1.05, 1.0)
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.15
|
||||
gray8V := baseVal * 0.65
|
||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||
|
||||
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightGreenS := math.Min(baseSat*0.7, 1.0)
|
||||
brightGreenV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightYellowS := math.Min(baseSat*0.7, 1.0)
|
||||
brightYellowV := math.Min(baseVal*1.5, 1.0)
|
||||
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
|
||||
// Color12: Start of the lighter gradient - slightly desaturated
|
||||
brightBlueS := ph.S * 0.85
|
||||
brightBlueV := math.Min(ph.V*1.1, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
// Medium-high saturation pastel primary
|
||||
color13S := ph.S * 0.7
|
||||
color13V := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
|
||||
|
||||
// Lower saturation, lighter variant
|
||||
color14S := ph.S * 0.45
|
||||
color14V := math.Min(ph.V*1.4, 1.0)
|
||||
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
|
||||
|
||||
white15S := baseSat * 0.05
|
||||
white15V := math.Min(baseVal*1.45, 1.0)
|
||||
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
return palette
|
||||
|
||||
@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
|
||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||
}
|
||||
|
||||
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
||||
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
||||
// Color15 is now derived from primary, so just verify it's a valid color
|
||||
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
||||
color15Lum := Luminance(result.Color15.Hex)
|
||||
if tt.opts.IsLight {
|
||||
// Light mode: Color15 should still be relatively light
|
||||
if color15Lum < 0.5 {
|
||||
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||
}
|
||||
} else {
|
||||
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
|
||||
if color15Lum < 0.5 {
|
||||
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -579,6 +588,10 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||
|
||||
bgColor := result.Color0.Hex
|
||||
for i := 1; i < 8; i++ {
|
||||
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||
if i == 5 || i == 6 {
|
||||
continue
|
||||
}
|
||||
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||
minLc := 30.0
|
||||
if lc < minLc && lc > 0 {
|
||||
|
||||
@@ -37,6 +37,14 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
handleSetConfig(conn, req, m)
|
||||
case "clipboard.store":
|
||||
handleStore(conn, req, m)
|
||||
case "clipboard.pinEntry":
|
||||
handlePinEntry(conn, req, m)
|
||||
case "clipboard.unpinEntry":
|
||||
handleUnpinEntry(conn, req, m)
|
||||
case "clipboard.getPinnedEntries":
|
||||
handleGetPinnedEntries(conn, req, m)
|
||||
case "clipboard.getPinnedCount":
|
||||
handleGetPinnedCount(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||
}
|
||||
@@ -205,6 +213,9 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||
cfg.Disabled = v
|
||||
}
|
||||
if v, ok := models.Get[float64](req, "maxPinned"); ok {
|
||||
cfg.MaxPinned = int(v)
|
||||
}
|
||||
|
||||
if err := m.SetConfig(cfg); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -230,3 +241,43 @@ func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
||||
}
|
||||
|
||||
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.PinEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
|
||||
}
|
||||
|
||||
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.UnpinEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
|
||||
}
|
||||
|
||||
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
|
||||
pinned := m.GetPinnedEntries()
|
||||
models.Respond(conn, req.ID, pinned)
|
||||
}
|
||||
|
||||
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||
count := m.GetPinnedCount()
|
||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||
}
|
||||
|
||||
@@ -389,7 +389,11 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
}
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if count < m.config.MaxHistory {
|
||||
count++
|
||||
continue
|
||||
@@ -419,6 +423,11 @@ func encodeEntry(e Entry) ([]byte, error) {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||
if e.Pinned {
|
||||
buf.WriteByte(1)
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -462,6 +471,12 @@ func decodeEntry(data []byte) (Entry, error) {
|
||||
binary.Read(buf, binary.BigEndian, &e.Hash)
|
||||
}
|
||||
|
||||
if buf.Len() >= 1 {
|
||||
var pinnedByte byte
|
||||
binary.Read(buf, binary.BigEndian, &pinnedByte)
|
||||
e.Pinned = pinnedByte == 1
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -735,19 +750,54 @@ func (m *Manager) ClearHistory() {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete only non-pinned entries
|
||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
||||
return err
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := tx.CreateBucket([]byte("clipboard"))
|
||||
return err
|
||||
|
||||
var toDelete [][]byte
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil || !entry.Pinned {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range toDelete {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to clear clipboard history: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b != nil {
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, _ := decodeEntry(v)
|
||||
if entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
if pinnedCount == 0 {
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
@@ -960,6 +1010,10 @@ func (m *Manager) clearOldEntries(days int) error {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Skip pinned entries
|
||||
if entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if entry.Timestamp.Before(cutoff) {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
@@ -1250,3 +1304,153 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) PinEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
// Check pinned count
|
||||
cfg := m.getConfig()
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
if pinnedCount >= cfg.MaxPinned {
|
||||
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Pinned = true
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put(itob(id), encoded)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) UnpinEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Pinned = false
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put(itob(id), encoded)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedEntries() []Entry {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pinned []Entry
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.Pinned {
|
||||
entry.Data = nil
|
||||
pinned = append(pinned, entry)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to get pinned entries: %v", err)
|
||||
}
|
||||
|
||||
return pinned
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedCount() int {
|
||||
if m.db == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Config struct {
|
||||
AutoClearDays int `json:"autoClearDays"`
|
||||
ClearAtStartup bool `json:"clearAtStartup"`
|
||||
Disabled bool `json:"disabled"`
|
||||
MaxPinned int `json:"maxPinned"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
@@ -27,6 +28,7 @@ func DefaultConfig() Config {
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
AutoClearDays: 0,
|
||||
ClearAtStartup: false,
|
||||
MaxPinned: 25,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +102,7 @@ type Entry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsImage bool `json:"isImage"`
|
||||
Hash uint64 `json:"hash,omitempty"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
|
||||
@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -33,7 +33,6 @@ Recommends: cava
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
# Recommended system packages
|
||||
Recommends: NetworkManager
|
||||
|
||||
@@ -24,10 +24,8 @@ Requires: dms-cli = %{version}-%{release}
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: wl-clipboard
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
@@ -20,12 +20,9 @@ Requires: accountsservice
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
@@ -23,12 +23,10 @@ Requires: dgop
|
||||
|
||||
# Core utilities (Highly recommended for DMS functionality)
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Recommends: wl-clipboard
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -1 +1 @@
|
||||
Spicy Miso
|
||||
Saffron Bloom
|
||||
|
||||
@@ -966,6 +966,17 @@ Item {
|
||||
return success ? `PLUGIN_DISABLE_SUCCESS: ${pluginId}` : `PLUGIN_DISABLE_FAILED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function toggle(pluginId: string): string {
|
||||
if (!pluginId)
|
||||
return "ERROR: No plugin ID specified";
|
||||
|
||||
if (!PluginService.availablePlugins[pluginId])
|
||||
return `PLUGIN_NOT_FOUND: ${pluginId}`;
|
||||
|
||||
const success = PluginService.togglePlugin(pluginId);
|
||||
return success ? `PLUGIN_TOGGLE_SUCCESS: ${pluginId}` : `PLUGIN_TOGGLE_FAILED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
const plugins = PluginService.getAvailablePlugins();
|
||||
if (plugins.length === 0)
|
||||
|
||||
@@ -25,7 +25,10 @@ Item {
|
||||
width: parent.width
|
||||
totalCount: modal.totalCount
|
||||
showKeyboardHints: modal.showKeyboardHints
|
||||
activeTab: modal.activeTab
|
||||
pinnedCount: modal.pinnedCount
|
||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
onTabChanged: tabName => modal.activeTab = tabName
|
||||
onClearAllClicked: {
|
||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
||||
modal.clearAll();
|
||||
@@ -70,18 +73,20 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
// Recents Tab
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.clipboardEntries
|
||||
values: clipboardContent.modal.unpinnedEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "recents"
|
||||
|
||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||
spacing: Theme.spacingXS
|
||||
@@ -114,11 +119,11 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No clipboard entries found")
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.clipboardEntries.length === 0
|
||||
visible: clipboardContent.modal.unpinnedEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
@@ -135,11 +140,60 @@ Item {
|
||||
listView: clipboardListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Saved Tab
|
||||
DankListView {
|
||||
id: savedListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.pinnedEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "saved"
|
||||
|
||||
spacing: Theme.spacingXS
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No saved clipboard entries")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.pinnedEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
width: savedListView.width
|
||||
height: ClipboardConstants.itemHeight
|
||||
entry: modelData
|
||||
entryIndex: index + 1
|
||||
itemIndex: index
|
||||
isSelected: false
|
||||
modal: clipboardContent.modal
|
||||
listView: savedListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyboardHintsContainer
|
||||
width: parent.width
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ Rectangle {
|
||||
|
||||
signal copyRequested
|
||||
signal deleteRequested
|
||||
signal pinRequested
|
||||
signal unpinRequested
|
||||
|
||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||
@@ -50,7 +52,7 @@ Rectangle {
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 68
|
||||
width: parent.width - 110
|
||||
spacing: Theme.spacingM
|
||||
|
||||
ClipboardThumbnail {
|
||||
@@ -100,20 +102,32 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: entry.pinned ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: entry.pinned ? Theme.primarySelected : "transparent"
|
||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
anchors.rightMargin: 80
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyRequested()
|
||||
|
||||
@@ -8,10 +8,13 @@ Item {
|
||||
|
||||
property int totalCount: 0
|
||||
property bool showKeyboardHints: false
|
||||
property string activeTab: "recents"
|
||||
property int pinnedCount: 0
|
||||
|
||||
signal keyboardHintsToggled
|
||||
signal clearAllClicked
|
||||
signal closeClicked
|
||||
signal tabChanged(string tabName)
|
||||
|
||||
height: ClipboardConstants.headerHeight
|
||||
|
||||
@@ -41,6 +44,22 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "history"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
|
||||
onClicked: tabChanged("recents")
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
|
||||
opacity: header.pinnedCount > 0 ? 1 : 0
|
||||
enabled: header.pinnedCount > 0
|
||||
onClicked: tabChanged("saved")
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
|
||||
@@ -19,6 +19,8 @@ DankModal {
|
||||
|
||||
property int totalCount: 0
|
||||
property var clipboardEntries: []
|
||||
property var pinnedEntries: []
|
||||
property int pinnedCount: 0
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool keyboardNavigationActive: false
|
||||
@@ -74,22 +76,37 @@ DankModal {
|
||||
|
||||
function updateFilteredModel() {
|
||||
const query = searchText.trim();
|
||||
let filtered = [];
|
||||
|
||||
if (query.length === 0) {
|
||||
clipboardEntries = internalEntries;
|
||||
filtered = internalEntries;
|
||||
} else {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
clipboardEntries = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
||||
filtered = internalEntries.filter(entry =>
|
||||
entry.preview.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort: pinned first, then by ID descending
|
||||
filtered.sort((a, b) => {
|
||||
if (a.pinned !== b.pinned) return b.pinned ? 1 : -1;
|
||||
return b.id - a.id;
|
||||
});
|
||||
|
||||
clipboardEntries = filtered;
|
||||
unpinnedEntries = filtered.filter(e => !e.pinned);
|
||||
totalCount = clipboardEntries.length;
|
||||
if (clipboardEntries.length === 0) {
|
||||
if (unpinnedEntries.length === 0) {
|
||||
keyboardNavigationActive = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex >= clipboardEntries.length) {
|
||||
selectedIndex = clipboardEntries.length - 1;
|
||||
} else if (selectedIndex >= unpinnedEntries.length) {
|
||||
selectedIndex = unpinnedEntries.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
property var internalEntries: []
|
||||
property var unpinnedEntries: []
|
||||
property string activeTab: "recents"
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
@@ -135,6 +152,10 @@ DankModal {
|
||||
return;
|
||||
}
|
||||
internalEntries = response.result || [];
|
||||
|
||||
pinnedEntries = internalEntries.filter(e => e.pinned);
|
||||
pinnedCount = pinnedEntries.length;
|
||||
|
||||
updateFilteredModel();
|
||||
});
|
||||
}
|
||||
@@ -171,18 +192,87 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
function deletePinnedEntry(entry) {
|
||||
clearConfirmDialog.show(
|
||||
I18n.tr("Delete Saved Item?"),
|
||||
I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."),
|
||||
function () {
|
||||
DMSService.sendRequest("clipboard.deleteEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = internalEntries.filter(e => e.id !== entry.id);
|
||||
updateFilteredModel();
|
||||
ToastService.showInfo(I18n.tr("Saved item deleted"));
|
||||
});
|
||||
},
|
||||
function () {}
|
||||
);
|
||||
}
|
||||
|
||||
function pinEntry(entry) {
|
||||
DMSService.sendRequest("clipboard.getPinnedCount", null, function (countResponse) {
|
||||
if (countResponse.error) {
|
||||
ToastService.showError(I18n.tr("Failed to check pin limit"));
|
||||
return;
|
||||
}
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
totalCount = 0;
|
||||
|
||||
const maxPinned = 25; // TODO: Get from config
|
||||
if (countResponse.result.count >= maxPinned) {
|
||||
ToastService.showError(I18n.tr("Maximum pinned entries reached") + " (" + maxPinned + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("clipboard.pinEntry", { "id": entry.id }, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to pin entry"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Entry pinned"));
|
||||
refreshClipboard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unpinEntry(entry) {
|
||||
DMSService.sendRequest("clipboard.unpinEntry", { "id": entry.id }, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to unpin entry"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Entry unpinned"));
|
||||
refreshClipboard();
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const hasPinned = pinnedCount > 0;
|
||||
const message = hasPinned
|
||||
? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount)
|
||||
: I18n.tr("This will permanently delete all clipboard history.");
|
||||
|
||||
clearConfirmDialog.show(
|
||||
I18n.tr("Clear History?"),
|
||||
message,
|
||||
function () {
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
refreshClipboard();
|
||||
if (hasPinned) {
|
||||
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
|
||||
}
|
||||
});
|
||||
},
|
||||
function () {}
|
||||
);
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
return entry.preview || "";
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ FloatingWindow {
|
||||
id: processListModal
|
||||
|
||||
property int currentTab: 0
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
property bool shouldHaveFocus: visible
|
||||
property alias shouldBeVisible: processListModal.visible
|
||||
|
||||
@@ -27,9 +28,8 @@ FloatingWindow {
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
if (processContextMenu.visible) {
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -61,46 +61,39 @@ FloatingWindow {
|
||||
show();
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + " B/s";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(1) + " KB/s";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
objectName: "processListModal"
|
||||
title: I18n.tr("System Monitor", "sysmon window title")
|
||||
minimumSize: Qt.size(650, 400)
|
||||
implicitWidth: 900
|
||||
implicitHeight: 680
|
||||
minimumSize: Qt.size(750, 550)
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 720
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
closingModal();
|
||||
searchText = "";
|
||||
expandedPid = "";
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
|
||||
} else {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk", "system"]);
|
||||
Qt.callLater(() => {
|
||||
if (contentFocusScope) {
|
||||
if (contentFocusScope)
|
||||
contentFocusScope.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
|
||||
PerformanceTab {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
|
||||
SystemTab {}
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: processContextMenu
|
||||
}
|
||||
@@ -128,6 +121,26 @@ FloatingWindow {
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_4:
|
||||
currentTab = 3;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Escape:
|
||||
if (searchText.length > 0) {
|
||||
searchText = "";
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
hide();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_F:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
searchField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +174,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -171,14 +184,14 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
visible: DgopService.dgopAvailable
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -233,166 +246,276 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 52
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
anchors.topMargin: 0
|
||||
spacing: Theme.spacingL
|
||||
Row {
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
text: I18n.tr("Processes"),
|
||||
icon: "list_alt"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Performance"),
|
||||
icon: "analytics"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Disks"),
|
||||
icon: "storage"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("System"),
|
||||
icon: "computer"
|
||||
}
|
||||
]
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Repeater {
|
||||
model: tabNames
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||
return tabIcons[index] || "tab";
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: currentTab = index
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: processesTab
|
||||
DankTextField {
|
||||
id: searchField
|
||||
Layout.preferredWidth: 250
|
||||
Layout.preferredHeight: 40
|
||||
placeholderText: I18n.tr("Search processes...", "process search placeholder")
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
text: searchText
|
||||
visible: currentTab === 0
|
||||
onTextChanged: searchText = text
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: Theme.spacingL
|
||||
Layout.topMargin: Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: processesTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
sourceComponent: ProcessesView {
|
||||
searchText: processListModal.searchText
|
||||
expandedPid: processListModal.expandedPid
|
||||
contextMenu: processContextMenu
|
||||
onExpandedPidChanged: processListModal.expandedPid = expandedPid
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
sourceComponent: PerformanceView {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: disksTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
sourceComponent: DisksView {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 3
|
||||
visible: currentTab === 3
|
||||
sourceComponent: SystemView {}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
Layout.bottomMargin: Theme.spacingM
|
||||
color: "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes:", "process count label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTab
|
||||
StyledText {
|
||||
text: DgopService.processCount.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
opacity: currentTab === 1 ? 1 : 0
|
||||
sourceComponent: performanceTabComponent
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("Uptime:", "uptime label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTab
|
||||
StyledText {
|
||||
text: DgopService.shortUptime || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "swap_horiz"
|
||||
size: 14
|
||||
color: Theme.info
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: 14
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.cpuUsage.toFixed(1) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: DgopService.cpuUsage > 80 ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "sd_card"
|
||||
size: 14
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: DgopService.memoryUsage > 90 ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,7 @@ Item {
|
||||
onContentItemReady: contentItem => {
|
||||
contentItem.widthChanged.connect(() => layoutTimer.restart());
|
||||
contentItem.heightChanged.connect(() => layoutTimer.restart());
|
||||
layoutTimer.restart();
|
||||
}
|
||||
|
||||
onActiveChanged: layoutTimer.restart()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
@@ -51,7 +52,9 @@ Item {
|
||||
if (tabIndex === NotepadStorageService.currentTabIndex && hasUnsavedChanges()) {
|
||||
root.pendingAction = "close_tab_" + tabIndex;
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
} else {
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
@@ -101,7 +104,9 @@ Item {
|
||||
root.pendingFileUrl = fileUrl;
|
||||
root.pendingAction = "load_file";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
} else {
|
||||
performLoadFromFile(fileUrl);
|
||||
}
|
||||
@@ -170,7 +175,9 @@ Item {
|
||||
saveToFile(fileUrl);
|
||||
} else {
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowser.open();
|
||||
saveBrowserLoader.active = true;
|
||||
if (saveBrowserLoader.item)
|
||||
saveBrowserLoader.item.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,10 +185,14 @@ Item {
|
||||
if (hasUnsavedChanges()) {
|
||||
root.pendingAction = "open";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
} else {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +200,9 @@ Item {
|
||||
if (hasUnsavedChanges()) {
|
||||
root.pendingAction = "new";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
} else {
|
||||
createNewTab();
|
||||
}
|
||||
@@ -249,238 +262,259 @@ Item {
|
||||
onLoadFailed: error => {}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: saveBrowser
|
||||
LazyLoader {
|
||||
id: saveBrowserLoader
|
||||
active: false
|
||||
|
||||
browserTitle: I18n.tr("Save Notepad File")
|
||||
browserIcon: "save"
|
||||
browserType: "notepad_save"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
saveMode: true
|
||||
defaultFileName: {
|
||||
if (currentTab && currentTab.title && currentTab.title !== "Untitled") {
|
||||
return currentTab.title;
|
||||
} else if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
|
||||
return currentTab.filePath.split('/').pop();
|
||||
} else {
|
||||
return "note.txt";
|
||||
}
|
||||
}
|
||||
FileBrowserModal {
|
||||
id: saveBrowser
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
if (currentTab) {
|
||||
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
|
||||
browserTitle: I18n.tr("Save Notepad File")
|
||||
browserIcon: "save"
|
||||
browserType: "notepad_save"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
saveMode: true
|
||||
defaultFileName: {
|
||||
if (currentTab && currentTab.title && currentTab.title !== "Untitled") {
|
||||
return currentTab.title;
|
||||
} else if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
|
||||
return currentTab.filePath.split('/').pop();
|
||||
} else {
|
||||
return "note.txt";
|
||||
}
|
||||
}
|
||||
|
||||
saveToFile(fileUrl);
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
if (root.pendingAction === "new") {
|
||||
Qt.callLater(() => {
|
||||
createNewTab();
|
||||
});
|
||||
} else if (root.pendingAction === "open") {
|
||||
Qt.callLater(() => {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
});
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
Qt.callLater(() => {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
});
|
||||
}
|
||||
root.pendingAction = "";
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: loadBrowser
|
||||
|
||||
browserTitle: I18n.tr("Open Notepad File")
|
||||
browserIcon: "folder_open"
|
||||
browserType: "notepad_load"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
loadFromFile(fileUrl);
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
DankModal {
|
||||
id: confirmationDialog
|
||||
|
||||
width: 400
|
||||
height: 180
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
|
||||
onBackgroundClicked: {
|
||||
close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
event.accepted = true;
|
||||
if (currentTab) {
|
||||
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
saveToFile(fileUrl);
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
if (root.pendingAction === "new") {
|
||||
Qt.callLater(() => {
|
||||
createNewTab();
|
||||
});
|
||||
} else if (root.pendingAction === "open") {
|
||||
Qt.callLater(() => {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
});
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
Qt.callLater(() => {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
});
|
||||
}
|
||||
root.pendingAction = "";
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
close();
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
LazyLoader {
|
||||
id: loadBrowserLoader
|
||||
active: false
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
}
|
||||
FileBrowserModal {
|
||||
id: loadBrowser
|
||||
|
||||
browserTitle: I18n.tr("Open Notepad File")
|
||||
browserIcon: "folder_open"
|
||||
browserType: "notepad_load"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
loadFromFile(fileUrl);
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: confirmationDialogLoader
|
||||
active: false
|
||||
|
||||
DankModal {
|
||||
id: confirmationDialog
|
||||
|
||||
width: 400
|
||||
height: 180
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
|
||||
onBackgroundClicked: {
|
||||
close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
width: parent.width
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
id: discardText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Don't Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
if (root.pendingAction === "new") {
|
||||
createNewTab();
|
||||
} else if (root.pendingAction === "open") {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
} else if (root.pendingAction === "load_file") {
|
||||
performLoadFromFile(root.pendingFileUrl);
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
root.pendingAction = "";
|
||||
root.pendingFileUrl = "";
|
||||
}
|
||||
StyledText {
|
||||
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: saveAsText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveAsArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowser.open();
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: discardText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Don't Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
if (root.pendingAction === "new") {
|
||||
createNewTab();
|
||||
} else if (root.pendingAction === "open") {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
} else if (root.pendingAction === "load_file") {
|
||||
performLoadFromFile(root.pendingFileUrl);
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
root.pendingAction = "";
|
||||
root.pendingFileUrl = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
Rectangle {
|
||||
width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: saveAsText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveAsArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowserLoader.active = true;
|
||||
if (saveBrowserLoader.item)
|
||||
saveBrowserLoader.item.open();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
346
quickshell/Modules/ProcessList/DisksView.qml
Normal file
346
quickshell/Modules/ProcessList/DisksView.qml
Normal file
@@ -0,0 +1,346 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
function formatSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024)
|
||||
return bytesPerSec.toFixed(0) + " B/s";
|
||||
if (bytesPerSec < 1024 * 1024)
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
if (bytesPerSec < 1024 * 1024 * 1024)
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["disk", "diskmounts"]);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["disk", "diskmounts"]);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disk I/O", "disk io header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Read:", "disk read label")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.formatSpeed(DgopService.diskReadRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Write:", "disk write label")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.formatSpeed(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.warning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "folder"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Mount Points", "mount points header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.outlineLight
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: mountListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 4
|
||||
|
||||
model: DgopService.diskMounts
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: mountListView.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: mountMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
|
||||
readonly property real usedPct: {
|
||||
const pctStr = modelData?.percent ?? "0%";
|
||||
return parseFloat(pctStr.replace("%", "")) / 100;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mountMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const mp = modelData?.mount ?? "";
|
||||
if (mp === "/")
|
||||
return "home";
|
||||
if (mp === "/home")
|
||||
return "person";
|
||||
if (mp.includes("boot"))
|
||||
return "memory";
|
||||
if (mp.includes("media") || mp.includes("mnt"))
|
||||
return "usb";
|
||||
return "folder";
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.mount ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.device ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.fstype ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.preferredWidth: 200
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.min(1, parent.parent.parent.parent.usedPct)
|
||||
height: parent.height
|
||||
radius: 4
|
||||
color: {
|
||||
const pct = parent.parent.parent.parent.usedPct;
|
||||
if (pct > 0.95)
|
||||
return Theme.error;
|
||||
if (pct > 0.85)
|
||||
return Theme.warning;
|
||||
return Theme.primary;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.used ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "/"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.size ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.preferredWidth: 50
|
||||
text: modelData?.percent ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
const pct = parent.parent.usedPct;
|
||||
if (pct > 0.95)
|
||||
return Theme.error;
|
||||
if (pct > 0.85)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 300
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: DgopService.diskMounts.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: 32
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No mount points found", "empty state in disk mounts list")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
function formatNetworkSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024) {
|
||||
return bytesPerSec.toFixed(0) + " B/s";
|
||||
} else if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
|
||||
}
|
||||
}
|
||||
|
||||
function formatDiskSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk"]);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "CPU"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.cpuUsage.toFixed(1)}%`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 280
|
||||
height: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.cpuCores} cores`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
clip: true
|
||||
width: parent.width
|
||||
height: parent.height - 40
|
||||
contentHeight: coreUsageColumn.implicitHeight
|
||||
|
||||
Column {
|
||||
id: coreUsageColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: DgopService.perCoreCpuUsage
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 20
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: `C${index}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 80
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.min(1, modelData / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
const usage = modelData;
|
||||
if (usage > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (usage > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.primary;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData ? `${modelData.toFixed(0)}%` : "0%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 80
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Memory")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.formatSystemMemory(DgopService.usedMemoryKB)} / ${DgopService.formatSystemMemory(DgopService.totalMemoryKB)}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
width: 120
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: DgopService.totalMemoryKB > 0 ? parent.width * (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
const usage = DgopService.totalMemoryKB > 0 ? (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0;
|
||||
if (usage > 0.9) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (usage > 0.7) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.secondary;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalMemoryKB > 0 ? `${((DgopService.usedMemoryKB / DgopService.totalMemoryKB) * 100).toFixed(1)}% used` : "No data"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Swap")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalSwapKB > 0 ? `${DgopService.formatSystemMemory(DgopService.usedSwapKB)} / ${DgopService.formatSystemMemory(DgopService.totalSwapKB)}` : "No swap"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
width: 120
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: DgopService.totalSwapKB > 0 ? parent.width * (DgopService.usedSwapKB / DgopService.totalSwapKB) : 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (!DgopService.totalSwapKB) {
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
|
||||
}
|
||||
const usage = DgopService.usedSwapKB / DgopService.totalSwapKB;
|
||||
if (usage > 0.9) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (usage > 0.7) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.info;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalSwapKB > 0 ? `${((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(1)}% used` : "N/A"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 80
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Network")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "↓"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.info
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "↑"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disk")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "R"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: formatDiskSpeed(DgopService.diskReadRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "W"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.warning
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: formatDiskSpeed(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
304
quickshell/Modules/ProcessList/PerformanceView.qml
Normal file
304
quickshell/Modules/ProcessList/PerformanceView.qml
Normal file
@@ -0,0 +1,304 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int historySize: 60
|
||||
|
||||
property var cpuHistory: []
|
||||
property var memoryHistory: []
|
||||
property var networkRxHistory: []
|
||||
property var networkTxHistory: []
|
||||
property var diskReadHistory: []
|
||||
property var diskWriteHistory: []
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + " B/s";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(1) + " KB/s";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
function addToHistory(arr, val) {
|
||||
const newArr = arr.slice();
|
||||
newArr.push(val);
|
||||
if (newArr.length > historySize)
|
||||
newArr.shift();
|
||||
return newArr;
|
||||
}
|
||||
|
||||
function sampleData() {
|
||||
cpuHistory = addToHistory(cpuHistory, DgopService.cpuUsage);
|
||||
memoryHistory = addToHistory(memoryHistory, DgopService.memoryUsage);
|
||||
networkRxHistory = addToHistory(networkRxHistory, DgopService.networkRxRate);
|
||||
networkTxHistory = addToHistory(networkTxHistory, DgopService.networkTxRate);
|
||||
diskReadHistory = addToHistory(diskReadHistory, DgopService.diskReadRate);
|
||||
diskWriteHistory = addToHistory(diskWriteHistory, DgopService.diskWriteRate);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: sampleClock
|
||||
precision: SystemClock.Seconds
|
||||
onDateChanged: {
|
||||
if (date.getSeconds() % 1 === 0)
|
||||
root.sampleData();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: "CPU"
|
||||
icon: "memory"
|
||||
value: DgopService.cpuUsage.toFixed(1) + "%"
|
||||
subtitle: DgopService.cpuModel || (DgopService.cpuCores + " cores")
|
||||
accentColor: Theme.primary
|
||||
history: root.cpuHistory
|
||||
maxValue: 100
|
||||
showSecondary: false
|
||||
extraInfo: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°C") : ""
|
||||
extraInfoColor: DgopService.cpuTemperature > 80 ? Theme.error : (DgopService.cpuTemperature > 60 ? Theme.warning : Theme.surfaceVariantText)
|
||||
}
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: I18n.tr("Memory")
|
||||
icon: "sd_card"
|
||||
value: DgopService.memoryUsage.toFixed(1) + "%"
|
||||
subtitle: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
|
||||
accentColor: Theme.secondary
|
||||
history: root.memoryHistory
|
||||
maxValue: 100
|
||||
showSecondary: false
|
||||
extraInfo: DgopService.totalSwapKB > 0 ? ("Swap: " + DgopService.formatSystemMemory(DgopService.usedSwapKB)) : ""
|
||||
extraInfoColor: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: I18n.tr("Network")
|
||||
icon: "swap_horiz"
|
||||
value: "↓ " + root.formatBytes(DgopService.networkRxRate)
|
||||
subtitle: "↑ " + root.formatBytes(DgopService.networkTxRate)
|
||||
accentColor: Theme.info
|
||||
history: root.networkRxHistory
|
||||
history2: root.networkTxHistory
|
||||
maxValue: 0
|
||||
showSecondary: true
|
||||
extraInfo: ""
|
||||
extraInfoColor: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: I18n.tr("Disk")
|
||||
icon: "storage"
|
||||
value: "R: " + root.formatBytes(DgopService.diskReadRate)
|
||||
subtitle: "W: " + root.formatBytes(DgopService.diskWriteRate)
|
||||
accentColor: Theme.warning
|
||||
history: root.diskReadHistory
|
||||
history2: root.diskWriteHistory
|
||||
maxValue: 0
|
||||
showSecondary: true
|
||||
extraInfo: {
|
||||
const rootMount = DgopService.diskMounts.find(m => m.mountpoint === "/");
|
||||
if (rootMount) {
|
||||
const usedPct = ((rootMount.used || 0) / Math.max(1, rootMount.total || 1) * 100).toFixed(0);
|
||||
return "/ " + usedPct + "% used";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
extraInfoColor: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PerformanceCard: Rectangle {
|
||||
id: card
|
||||
|
||||
property string title: ""
|
||||
property string icon: ""
|
||||
property string value: ""
|
||||
property string subtitle: ""
|
||||
property color accentColor: Theme.primary
|
||||
property var history: []
|
||||
property var history2: null
|
||||
property real maxValue: 100
|
||||
property bool showSecondary: false
|
||||
property string extraInfo: ""
|
||||
property color extraInfoColor: Theme.surfaceVariantText
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Canvas {
|
||||
id: graphCanvas
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
property var hist: card.history
|
||||
property var hist2: card.history2
|
||||
|
||||
onHistChanged: requestPaint()
|
||||
onHist2Changed: requestPaint()
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
if (!hist || hist.length < 2)
|
||||
return;
|
||||
|
||||
let max = card.maxValue;
|
||||
if (max <= 0) {
|
||||
max = 1;
|
||||
for (let k = 0; k < hist.length; k++)
|
||||
max = Math.max(max, hist[k]);
|
||||
if (hist2) {
|
||||
for (let l = 0; l < hist2.length; l++)
|
||||
max = Math.max(max, hist2[l]);
|
||||
}
|
||||
max *= 1.1;
|
||||
}
|
||||
|
||||
const c = card.accentColor;
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, height);
|
||||
grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.25));
|
||||
grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0.02));
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, height);
|
||||
for (let i = 0; i < hist.length; i++) {
|
||||
const x = (width / (root.historySize - 1)) * i;
|
||||
const y = height - (hist[i] / max) * height * 0.8;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo((width / (root.historySize - 1)) * (hist.length - 1), height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.8);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
for (let j = 0; j < hist.length; j++) {
|
||||
const px = (width / (root.historySize - 1)) * j;
|
||||
const py = height - (hist[j] / max) * height * 0.8;
|
||||
j === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
if (hist2 && hist2.length >= 2 && card.showSecondary) {
|
||||
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.4);
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
for (let m = 0; m < hist2.length; m++) {
|
||||
const sx = (width / (root.historySize - 1)) * m;
|
||||
const sy = height - (hist2[m] / max) * height * 0.8;
|
||||
m === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: card.icon
|
||||
size: Theme.iconSize
|
||||
color: card.accentColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.extraInfo
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: card.extraInfoColor
|
||||
visible: card.extraInfo.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.value
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.subtitle
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,10 @@ Popup {
|
||||
const menuWidth = processContextMenu.width;
|
||||
const menuHeight = processContextMenu.height;
|
||||
|
||||
if (finalX + menuWidth > parentWidth) {
|
||||
if (finalX + menuWidth > parentWidth)
|
||||
finalX = Math.max(0, parentWidth - menuWidth);
|
||||
}
|
||||
|
||||
if (finalY + menuHeight > parentHeight) {
|
||||
if (finalY + menuHeight > parentHeight)
|
||||
finalY = Math.max(0, parentHeight - menuHeight);
|
||||
}
|
||||
}
|
||||
|
||||
processContextMenu.x = finalX;
|
||||
@@ -33,25 +30,19 @@ Popup {
|
||||
open();
|
||||
}
|
||||
|
||||
width: 180
|
||||
width: 200
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
onClosed: {
|
||||
closePolicy = Popup.CloseOnEscape;
|
||||
}
|
||||
onOpened: {
|
||||
outsideClickTimer.start();
|
||||
}
|
||||
|
||||
onClosed: closePolicy = Popup.CloseOnEscape
|
||||
onOpened: outsideClickTimer.start()
|
||||
|
||||
Timer {
|
||||
id: outsideClickTimer
|
||||
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
|
||||
}
|
||||
onTriggered: processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
@@ -59,8 +50,6 @@ Popup {
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
id: menuContent
|
||||
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
@@ -68,157 +57,140 @@ Popup {
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Copy PID")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: copyPidArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (processContextMenu.processData) {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", processContextMenu.processData.pid.toString()]);
|
||||
}
|
||||
processContextMenu.close();
|
||||
}
|
||||
MenuItem {
|
||||
text: I18n.tr("Copy PID")
|
||||
iconName: "tag"
|
||||
onClicked: {
|
||||
if (processContextMenu.processData)
|
||||
Quickshell.execDetached(["dms", "cl", "copy", processContextMenu.processData.pid.toString()]);
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Copy Process Name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: copyNameArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (processContextMenu.processData) {
|
||||
const processName = processContextMenu.processData.displayName || processContextMenu.processData.command;
|
||||
Quickshell.execDetached(["dms", "cl", "copy", processName]);
|
||||
}
|
||||
processContextMenu.close();
|
||||
MenuItem {
|
||||
text: I18n.tr("Copy Name")
|
||||
iconName: "content_copy"
|
||||
onClicked: {
|
||||
if (processContextMenu.processData) {
|
||||
const name = processContextMenu.processData.command || "";
|
||||
Quickshell.execDetached(["dms", "cl", "copy", name]);
|
||||
}
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: I18n.tr("Copy Full Command")
|
||||
iconName: "code"
|
||||
onClicked: {
|
||||
if (processContextMenu.processData) {
|
||||
const fullCmd = processContextMenu.processData.fullCommand || processContextMenu.processData.command || "";
|
||||
Quickshell.execDetached(["dms", "cl", "copy", fullCmd]);
|
||||
}
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
height: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
MenuItem {
|
||||
text: I18n.tr("Kill Process")
|
||||
iconName: "close"
|
||||
dangerous: true
|
||||
enabled: processContextMenu.processData
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Kill Process")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: killArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
if (processContextMenu.processData) {
|
||||
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
|
||||
}
|
||||
|
||||
processContextMenu.close();
|
||||
}
|
||||
onClicked: {
|
||||
if (processContextMenu.processData)
|
||||
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
MenuItem {
|
||||
text: I18n.tr("Force Kill (SIGKILL)")
|
||||
iconName: "dangerous"
|
||||
dangerous: true
|
||||
enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Force Kill Process")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forceKillArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
if (processContextMenu.processData) {
|
||||
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
|
||||
}
|
||||
|
||||
processContextMenu.close();
|
||||
}
|
||||
onClicked: {
|
||||
if (processContextMenu.processData)
|
||||
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component MenuItem: Rectangle {
|
||||
id: menuItem
|
||||
|
||||
property string text: ""
|
||||
property string iconName: ""
|
||||
property bool dangerous: false
|
||||
property bool enabled: true
|
||||
|
||||
signal clicked
|
||||
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!enabled)
|
||||
return "transparent";
|
||||
if (dangerous)
|
||||
return menuItemArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent";
|
||||
return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
|
||||
}
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: menuItem.iconName
|
||||
size: 16
|
||||
color: {
|
||||
if (!menuItem.enabled)
|
||||
return Theme.surfaceVariantText;
|
||||
if (menuItem.dangerous && menuItemArea.containsMouse)
|
||||
return Theme.error;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: menuItem.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Normal
|
||||
color: {
|
||||
if (!menuItem.enabled)
|
||||
return Theme.surfaceVariantText;
|
||||
if (menuItem.dangerous && menuItemArea.containsMouse)
|
||||
return Theme.error;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: menuItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: menuItem.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: menuItem.enabled
|
||||
onClicked: menuItem.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: processItem
|
||||
|
||||
property var process: null
|
||||
property var contextMenu: null
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.primary, 0)
|
||||
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: processMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (process && process.pid > 0 && contextMenu) {
|
||||
contextMenu.processData = process;
|
||||
const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
|
||||
const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
|
||||
contextMenu.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
onPressAndHold: {
|
||||
if (process && process.pid > 0 && contextMenu) {
|
||||
contextMenu.processData = process;
|
||||
const globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2);
|
||||
contextMenu.show(globalPos.x, globalPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
DankIcon {
|
||||
id: processIcon
|
||||
|
||||
name: DgopService.getProcessIcon(process ? process.command : "")
|
||||
size: Theme.iconSize - 4
|
||||
color: {
|
||||
if (process && process.cpu > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
opacity: 0.8
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: process ? process.displayName : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: 250
|
||||
elide: Text.ElideRight
|
||||
anchors.left: processIcon.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cpuBadge
|
||||
|
||||
width: 80
|
||||
height: 20
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (process && process.cpu > 80) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 194
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatCpuUsage(process ? process.cpu : 0)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (process && process.cpu > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: memoryBadge
|
||||
|
||||
width: 80
|
||||
height: 20
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (process && process.memoryKB > 1024 * 1024) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 102
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatMemoryUsage(process ? process.memoryKB : 0)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (process && process.memoryKB > 1024 * 1024) {
|
||||
return Theme.error;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: process ? process.pid.toString() : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
width: 50
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 40
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuButton
|
||||
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "more_vert"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: menuButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (process && process.pid > 0 && contextMenu) {
|
||||
contextMenu.processData = process;
|
||||
const globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height);
|
||||
const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
|
||||
contextMenu.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,32 +12,39 @@ DankPopout {
|
||||
|
||||
property var parentWidget: null
|
||||
property var triggerScreen: null
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
if (processContextMenu.visible) {
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
open();
|
||||
}
|
||||
|
||||
popupWidth: 600
|
||||
popupHeight: 600
|
||||
popupWidth: 650
|
||||
popupHeight: 550
|
||||
triggerWidth: 55
|
||||
positioning: ""
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
|
||||
onBackgroundClicked: {
|
||||
if (processContextMenu.visible) {
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
searchText = "";
|
||||
expandedPid = "";
|
||||
}
|
||||
}
|
||||
|
||||
Ref {
|
||||
service: DgopService
|
||||
}
|
||||
@@ -55,55 +62,247 @@ DankPopout {
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
clip: true
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (processListPopout.shouldBeVisible) {
|
||||
if (processListPopout.shouldBeVisible)
|
||||
forceActiveFocus();
|
||||
}
|
||||
processContextMenu.parent = processListContent;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
if (processListPopout.searchText.length > 0) {
|
||||
processListPopout.searchText = "";
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
processListPopout.close();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_F:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
searchField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: processListPopout
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (processListPopout.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
processListContent.forceActiveFocus();
|
||||
});
|
||||
Qt.callLater(() => processListContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
target: processListPopout
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
height: systemOverview.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SystemOverview {
|
||||
id: systemOverview
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
DankIcon {
|
||||
name: "analytics"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
Layout.preferredWidth: Theme.fontSizeMedium * 14
|
||||
Layout.preferredHeight: Theme.fontSizeMedium * 2.5
|
||||
placeholderText: I18n.tr("Search...")
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
text: processListPopout.searchText
|
||||
onTextChanged: processListPopout.searchText = text
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: statsContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(leftInfo.height, gaugesRow.height) + Theme.spacingS
|
||||
|
||||
function compactMem(kb) {
|
||||
if (kb < 1024 * 1024) {
|
||||
const mb = kb / 1024;
|
||||
return mb >= 100 ? mb.toFixed(0) + " MB" : mb.toFixed(1) + " MB";
|
||||
}
|
||||
const gb = kb / (1024 * 1024);
|
||||
return gb >= 10 ? gb.toFixed(0) + " GB" : gb.toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
readonly property real gaugeSize: Theme.fontSizeMedium * 6.5
|
||||
|
||||
readonly property var enabledGpusWithTemp: {
|
||||
if (!SessionData.enabledGpuPciIds || SessionData.enabledGpuPciIds.length === 0)
|
||||
return [];
|
||||
const result = [];
|
||||
for (const gpu of DgopService.availableGpus) {
|
||||
if (SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 && gpu.temperature > 0)
|
||||
result.push(gpu);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
readonly property bool hasGpu: enabledGpusWithTemp.length > 0
|
||||
|
||||
Row {
|
||||
id: leftInfo
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Theme.fontSizeMedium * 3
|
||||
height: width
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
|
||||
SystemLogo {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width * 0.7
|
||||
height: width
|
||||
colorOverride: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS / 2
|
||||
|
||||
StyledText {
|
||||
text: DgopService.hostname || "localhost"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.distribution || "Linux"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "schedule"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.shortUptime || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.processCount + " " + I18n.tr("procs", "short for processes")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: gaugesRow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
CircleGauge {
|
||||
width: statsContainer.gaugeSize
|
||||
height: statsContainer.gaugeSize
|
||||
value: DgopService.cpuUsage / 100
|
||||
label: DgopService.cpuUsage.toFixed(0) + "%"
|
||||
sublabel: "CPU"
|
||||
detail: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°") : ""
|
||||
accentColor: DgopService.cpuUsage > 80 ? Theme.error : (DgopService.cpuUsage > 50 ? Theme.warning : Theme.primary)
|
||||
detailColor: DgopService.cpuTemperature > 85 ? Theme.error : (DgopService.cpuTemperature > 70 ? Theme.warning : Theme.surfaceVariantText)
|
||||
}
|
||||
|
||||
CircleGauge {
|
||||
width: statsContainer.gaugeSize
|
||||
height: statsContainer.gaugeSize
|
||||
value: DgopService.memoryUsage / 100
|
||||
label: statsContainer.compactMem(DgopService.usedMemoryKB)
|
||||
sublabel: I18n.tr("Memory")
|
||||
detail: DgopService.totalSwapKB > 0 ? ("+" + statsContainer.compactMem(DgopService.usedSwapKB)) : ""
|
||||
accentColor: DgopService.memoryUsage > 90 ? Theme.error : (DgopService.memoryUsage > 70 ? Theme.warning : Theme.secondary)
|
||||
}
|
||||
|
||||
CircleGauge {
|
||||
width: statsContainer.gaugeSize
|
||||
height: statsContainer.gaugeSize
|
||||
visible: statsContainer.hasGpu
|
||||
|
||||
readonly property var gpu: statsContainer.enabledGpusWithTemp[0] ?? null
|
||||
readonly property color vendorColor: {
|
||||
const vendor = (gpu?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Theme.success;
|
||||
if (vendor.includes("amd"))
|
||||
return Theme.error;
|
||||
if (vendor.includes("intel"))
|
||||
return Theme.info;
|
||||
return Theme.info;
|
||||
}
|
||||
|
||||
value: Math.min(1, (gpu?.temperature ?? 0) / 100)
|
||||
label: (gpu?.temperature ?? 0) > 0 ? ((gpu?.temperature ?? 0).toFixed(0) + "°C") : "--"
|
||||
sublabel: "GPU"
|
||||
accentColor: {
|
||||
const temp = gpu?.temperature ?? 0;
|
||||
if (temp > 85)
|
||||
return Theme.error;
|
||||
if (temp > 70)
|
||||
return Theme.warning;
|
||||
return vendorColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,16 +311,176 @@ DankPopout {
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||
border.width: 0
|
||||
clip: true
|
||||
|
||||
ProcessListView {
|
||||
ProcessesView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
searchText: processListPopout.searchText
|
||||
expandedPid: processListPopout.expandedPid
|
||||
contextMenu: processContextMenu
|
||||
onExpandedPidChanged: processListPopout.expandedPid = expandedPid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component CircleGauge: Item {
|
||||
id: gaugeRoot
|
||||
|
||||
property real value: 0
|
||||
property string label: ""
|
||||
property string sublabel: ""
|
||||
property string detail: ""
|
||||
property color accentColor: Theme.primary
|
||||
property color detailColor: Theme.surfaceVariantText
|
||||
|
||||
readonly property real thickness: Math.max(4, Math.min(width, height) / 15)
|
||||
readonly property real glowExtra: thickness * 1.4
|
||||
readonly property real arcPadding: thickness / 1.3
|
||||
|
||||
readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2
|
||||
readonly property real maxTextWidth: innerDiameter * 0.9
|
||||
readonly property real baseLabelSize: Math.round(width * 0.18)
|
||||
readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65)))
|
||||
readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7)))
|
||||
readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65)))
|
||||
|
||||
property real animValue: 0
|
||||
|
||||
onValueChanged: animValue = Math.min(1, Math.max(0, value))
|
||||
|
||||
Behavior on animValue {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: animValue = Math.min(1, Math.max(0, value))
|
||||
|
||||
Canvas {
|
||||
id: glowCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
|
||||
const startAngle = -Math.PI * 0.5;
|
||||
const endAngle = Math.PI * 1.5;
|
||||
|
||||
ctx.lineCap = "round";
|
||||
|
||||
if (gaugeRoot.animValue > 0) {
|
||||
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, startAngle, prog);
|
||||
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2);
|
||||
ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: gaugeRoot
|
||||
function onAnimValueChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
function onAccentColorChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
function onWidthChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: arcCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
|
||||
const startAngle = -Math.PI * 0.5;
|
||||
const endAngle = Math.PI * 1.5;
|
||||
|
||||
ctx.lineCap = "round";
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, startAngle, endAngle);
|
||||
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1);
|
||||
ctx.lineWidth = gaugeRoot.thickness;
|
||||
ctx.stroke();
|
||||
|
||||
if (gaugeRoot.animValue > 0) {
|
||||
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, startAngle, prog);
|
||||
ctx.strokeStyle = gaugeRoot.accentColor;
|
||||
ctx.lineWidth = gaugeRoot.thickness;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: gaugeRoot
|
||||
function onAnimValueChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
function onAccentColorChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
function onWidthChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 1
|
||||
|
||||
StyledText {
|
||||
text: gaugeRoot.label
|
||||
font.pixelSize: gaugeRoot.labelSize
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: gaugeRoot.sublabel
|
||||
font.pixelSize: gaugeRoot.sublabelSize
|
||||
font.weight: Font.Medium
|
||||
color: gaugeRoot.accentColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: gaugeRoot.detail
|
||||
font.pixelSize: gaugeRoot.detailSize
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: gaugeRoot.detailColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: gaugeRoot.detail.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property var contextMenu: null
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["processes"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["processes"]);
|
||||
}
|
||||
|
||||
Item {
|
||||
id: columnHeaders
|
||||
|
||||
width: parent.width
|
||||
anchors.leftMargin: 8
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
width: 60
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "name") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Process")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "name" ? 1 : 0.7
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: processHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("name");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "cpu") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 200
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "CPU"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "cpu" ? 1 : 0.7
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cpuHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("cpu");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "memory") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 112
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "RAM"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "memory" ? 1 : 0.7
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: memoryHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("memory");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 50
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "pid") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 53
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "PID"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "pid" ? 1 : 0.7
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pidHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("pid");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: DgopService.sortDescending ? "↓" : "↑"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// TODO: Re-implement sort order toggle
|
||||
|
||||
id: sortOrderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: processListView
|
||||
|
||||
property string keyRoleName: "pid"
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - columnHeaders.height
|
||||
clip: true
|
||||
spacing: 4
|
||||
model: ScriptModel {
|
||||
values: DgopService.processes
|
||||
objectProp: "pid"
|
||||
}
|
||||
|
||||
delegate: ProcessListItem {
|
||||
process: modelData
|
||||
contextMenu: root.contextMenu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: processesTab
|
||||
|
||||
property var contextMenu: null
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SystemOverview {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ProcessListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contextMenu: processesTab.contextMenu
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: localContextMenu
|
||||
}
|
||||
}
|
||||
607
quickshell/Modules/ProcessList/ProcessesView.qml
Normal file
607
quickshell/Modules/ProcessList/ProcessesView.qml
Normal file
@@ -0,0 +1,607 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
property var contextMenu: null
|
||||
property bool hoveringExpandedItem: false
|
||||
|
||||
readonly property bool pauseUpdates: hoveringExpandedItem || (contextMenu?.visible ?? false)
|
||||
property var cachedProcesses: []
|
||||
|
||||
onFilteredProcessesChanged: {
|
||||
if (!pauseUpdates)
|
||||
cachedProcesses = filteredProcesses;
|
||||
}
|
||||
|
||||
onPauseUpdatesChanged: {
|
||||
if (!pauseUpdates)
|
||||
cachedProcesses = filteredProcesses;
|
||||
}
|
||||
|
||||
readonly property var filteredProcesses: {
|
||||
if (!DgopService.allProcesses || DgopService.allProcesses.length === 0)
|
||||
return [];
|
||||
|
||||
let procs = DgopService.allProcesses.slice();
|
||||
|
||||
if (searchText.length > 0) {
|
||||
const search = searchText.toLowerCase();
|
||||
procs = procs.filter(p => {
|
||||
const cmd = (p.command || "").toLowerCase();
|
||||
const fullCmd = (p.fullCommand || "").toLowerCase();
|
||||
const pid = p.pid.toString();
|
||||
return cmd.includes(search) || fullCmd.includes(search) || pid.includes(search);
|
||||
});
|
||||
}
|
||||
|
||||
const asc = DgopService.sortAscending;
|
||||
procs.sort((a, b) => {
|
||||
let valueA, valueB, result;
|
||||
switch (DgopService.currentSort) {
|
||||
case "cpu":
|
||||
valueA = a.cpu || 0;
|
||||
valueB = b.cpu || 0;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
case "memory":
|
||||
valueA = a.memoryKB || 0;
|
||||
valueB = b.memoryKB || 0;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
case "name":
|
||||
valueA = (a.command || "").toLowerCase();
|
||||
valueB = (b.command || "").toLowerCase();
|
||||
result = valueA.localeCompare(valueB);
|
||||
break;
|
||||
case "pid":
|
||||
valueA = a.pid || 0;
|
||||
valueB = b.pid || 0;
|
||||
result = valueA - valueB;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return asc ? -result : result;
|
||||
});
|
||||
|
||||
return procs;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["processes", "cpu", "memory", "system"]);
|
||||
cachedProcesses = filteredProcesses;
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["processes", "cpu", "memory", "system"]);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: 0
|
||||
|
||||
SortableHeader {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 200
|
||||
text: I18n.tr("Name")
|
||||
sortKey: "name"
|
||||
currentSort: DgopService.currentSort
|
||||
sortAscending: DgopService.sortAscending
|
||||
onClicked: DgopService.toggleSort("name")
|
||||
alignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
SortableHeader {
|
||||
Layout.preferredWidth: 100
|
||||
text: "CPU"
|
||||
sortKey: "cpu"
|
||||
currentSort: DgopService.currentSort
|
||||
sortAscending: DgopService.sortAscending
|
||||
onClicked: DgopService.toggleSort("cpu")
|
||||
}
|
||||
|
||||
SortableHeader {
|
||||
Layout.preferredWidth: 100
|
||||
text: I18n.tr("Memory")
|
||||
sortKey: "memory"
|
||||
currentSort: DgopService.currentSort
|
||||
sortAscending: DgopService.sortAscending
|
||||
onClicked: DgopService.toggleSort("memory")
|
||||
}
|
||||
|
||||
SortableHeader {
|
||||
Layout.preferredWidth: 80
|
||||
text: "PID"
|
||||
sortKey: "pid"
|
||||
currentSort: DgopService.currentSort
|
||||
sortAscending: DgopService.sortAscending
|
||||
onClicked: DgopService.toggleSort("pid")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.outlineLight
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: processListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 2
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.cachedProcesses
|
||||
objectProp: "pid"
|
||||
}
|
||||
|
||||
delegate: ProcessItem {
|
||||
required property var modelData
|
||||
|
||||
width: processListView.width
|
||||
process: modelData
|
||||
isExpanded: root.expandedPid === (modelData?.pid ?? -1).toString()
|
||||
contextMenu: root.contextMenu
|
||||
onToggleExpand: {
|
||||
const pidStr = (modelData?.pid ?? -1).toString();
|
||||
root.expandedPid = (root.expandedPid === pidStr) ? "" : pidStr;
|
||||
}
|
||||
onHoveringExpandedChanged: {
|
||||
if (hoveringExpanded)
|
||||
root.hoveringExpandedItem = true;
|
||||
else
|
||||
Qt.callLater(() => {
|
||||
root.hoveringExpandedItem = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 300
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: root.cachedProcesses.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: root.searchText.length > 0 ? "search_off" : "hourglass_empty"
|
||||
size: 32
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No matching processes", "empty state in process list")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.searchText.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SortableHeader: Item {
|
||||
id: headerItem
|
||||
|
||||
property string text: ""
|
||||
property string sortKey: ""
|
||||
property string currentSort: ""
|
||||
property bool sortAscending: false
|
||||
property int alignment: Text.AlignHCenter
|
||||
|
||||
signal clicked
|
||||
|
||||
readonly property bool isActive: sortKey === currentSort
|
||||
|
||||
height: 36
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: Theme.cornerRadius
|
||||
color: headerItem.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (headerMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06) : "transparent")
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: 4
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: headerItem.alignment === Text.AlignLeft
|
||||
visible: headerItem.alignment !== Text.AlignLeft
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: headerItem.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: headerItem.isActive ? Font.Bold : Font.Medium
|
||||
color: headerItem.isActive ? Theme.primary : Theme.surfaceText
|
||||
opacity: headerItem.isActive ? 1 : 0.8
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: headerItem.sortAscending ? "arrow_upward" : "arrow_downward"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
visible: headerItem.isActive
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: headerItem.alignment !== Text.AlignLeft
|
||||
visible: headerItem.alignment === Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: headerMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: headerItem.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
component ProcessItem: Rectangle {
|
||||
id: processItemRoot
|
||||
|
||||
property var process: null
|
||||
property bool isExpanded: false
|
||||
property var contextMenu: null
|
||||
readonly property bool hoveringExpanded: (isExpanded && processMouseArea.containsMouse) || copyMouseArea.containsMouse
|
||||
|
||||
signal toggleExpand
|
||||
|
||||
readonly property int processPid: process?.pid ?? 0
|
||||
readonly property real processCpu: process?.cpu ?? 0
|
||||
readonly property int processMemKB: process?.memoryKB ?? 0
|
||||
readonly property string processCmd: process?.command ?? ""
|
||||
readonly property string processFullCmd: process?.fullCommand ?? processCmd
|
||||
|
||||
height: isExpanded ? (44 + expandedRect.height + Theme.spacingXS) : 44
|
||||
radius: Theme.cornerRadius
|
||||
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: processMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (processItemRoot.processPid > 0 && processItemRoot.contextMenu) {
|
||||
processItemRoot.contextMenu.processData = processItemRoot.process;
|
||||
const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
|
||||
const localPos = processItemRoot.contextMenu.parent ? processItemRoot.contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
|
||||
processItemRoot.contextMenu.show(localPos.x, localPos.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
processItemRoot.toggleExpand();
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 44
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 200
|
||||
height: parent.height
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: DgopService.getProcessIcon(processItemRoot.processCmd)
|
||||
size: Theme.iconSize - 4
|
||||
color: {
|
||||
if (processItemRoot.processCpu > 80)
|
||||
return Theme.error;
|
||||
if (processItemRoot.processCpu > 50)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
opacity: 0.8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: processItemRoot.processCmd
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 280)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 100
|
||||
height: parent.height
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 70
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (processItemRoot.processCpu > 80)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
|
||||
if (processItemRoot.processCpu > 50)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: DgopService.formatCpuUsage(processItemRoot.processCpu)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (processItemRoot.processCpu > 80)
|
||||
return Theme.error;
|
||||
if (processItemRoot.processCpu > 50)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 100
|
||||
height: parent.height
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 70
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
|
||||
if (processItemRoot.processMemKB > 1024 * 1024)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: DgopService.formatMemoryUsage(processItemRoot.processMemKB)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
|
||||
return Theme.error;
|
||||
if (processItemRoot.processMemKB > 1024 * 1024)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 80
|
||||
height: parent.height
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: processItemRoot.processPid > 0 ? processItemRoot.processPid.toString() : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 40
|
||||
height: parent.height
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: processItemRoot.isExpanded ? "expand_less" : "expand_more"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: expandedRect
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: processItemRoot.isExpanded ? (expandedContent.implicitHeight + Theme.spacingS * 2) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
radius: Theme.cornerRadius - 2
|
||||
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6)
|
||||
clip: true
|
||||
visible: processItemRoot.isExpanded
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: expandedContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
id: cmdLabel
|
||||
text: I18n.tr("Full Command:", "process detail label")
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceVariantText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: cmdText
|
||||
text: processItemRoot.processFullCmd
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: copyBtn
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Theme.cornerRadius - 2
|
||||
color: copyMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "content_copy"
|
||||
size: 14
|
||||
color: copyMouseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: copyMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", processItemRoot.processFullCmd]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "PPID:"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (processItemRoot.process?.ppid ?? 0) > 0 ? processItemRoot.process.ppid.toString() : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Mem:"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (processItemRoot.process?.memoryPercent ?? 0).toFixed(1) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "system"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "system"]);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (DgopService.sortBy === "cpu") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
|
||||
} else if (cpuCardMouseArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
border.width: DgopService.sortBy === "cpu" ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: cpuCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("cpu");
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("CPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary
|
||||
opacity: DgopService.sortBy === "cpu" ? 1 : 0.8
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null) {
|
||||
return "--%";
|
||||
}
|
||||
return DgopService.cpuUsage.toFixed(1) + "%";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 20
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature <= 0) {
|
||||
return "--°";
|
||||
}
|
||||
return Math.round(DgopService.cpuTemperature) + "°";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: {
|
||||
if (DgopService.cpuTemperature > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (DgopService.cpuTemperature > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.cpuCores} cores`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (DgopService.sortBy === "memory") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
|
||||
} else if (memoryCardMouseArea.containsMouse) {
|
||||
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12);
|
||||
} else {
|
||||
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: DgopService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2)
|
||||
border.width: DgopService.sortBy === "memory" ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: memoryCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("memory");
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Memory")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary
|
||||
opacity: DgopService.sortBy === "memory" ? 1 : 0.8
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB)
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 20
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: DgopService.totalSwapKB > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(DgopService.usedSwapKB) : ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: DgopService.totalSwapKB > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (DgopService.totalSwapKB > 0) {
|
||||
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + " + swap";
|
||||
}
|
||||
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.vendor.toLowerCase();
|
||||
if (vendor.includes("nvidia")) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
|
||||
} else {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12);
|
||||
}
|
||||
} else if (vendor.includes("amd")) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
|
||||
} else {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
}
|
||||
} else if (vendor.includes("intel")) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
|
||||
} else {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.12);
|
||||
}
|
||||
}
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.vendor.toLowerCase();
|
||||
if (vendor.includes("nvidia")) {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
|
||||
} else if (vendor.includes("amd")) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
|
||||
} else if (vendor.includes("intel")) {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3);
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: gpuCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (DgopService.availableGpus.length > 1) {
|
||||
const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length;
|
||||
SessionData.setSelectedGpuIndex(nextIndex);
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
gpuContextMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("GPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.secondary
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "No GPU";
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
// Check if temperature monitoring is enabled for this GPU
|
||||
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
|
||||
const temp = gpu.temperature;
|
||||
const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0;
|
||||
if (hasTemp) {
|
||||
return Math.round(temp) + "°";
|
||||
} else {
|
||||
return gpu.vendor;
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
|
||||
const temp = gpu.temperature || 0;
|
||||
if (tempEnabled && temp > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (tempEnabled && temp > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "No GPUs detected";
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
|
||||
const temp = gpu.temperature;
|
||||
const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0;
|
||||
if (hasTemp) {
|
||||
return gpu.vendor + " " + gpu.displayName;
|
||||
} else {
|
||||
return gpu.displayName;
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
width: parent.parent.width - Theme.spacingM * 2
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: gpuContextMenu
|
||||
|
||||
MenuItem {
|
||||
text: I18n.tr("Enable GPU Temperature")
|
||||
checkable: true
|
||||
checked: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
if (!gpu.pciId) {
|
||||
return false;
|
||||
}
|
||||
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 : false;
|
||||
}
|
||||
onTriggered: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return;
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
if (!gpu.pciId) {
|
||||
return;
|
||||
}
|
||||
const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
|
||||
const index = enabledIds.indexOf(gpu.pciId);
|
||||
if (checked && index === -1) {
|
||||
enabledIds.push(gpu.pciId);
|
||||
DgopService.addGpuPciId(gpu.pciId);
|
||||
} else if (!checked && index !== -1) {
|
||||
enabledIds.splice(index, 1);
|
||||
DgopService.removeGpuPciId(gpu.pciId);
|
||||
}
|
||||
SessionData.setEnabledGpuPciIds(enabledIds);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,591 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: systemColumn.implicitHeight
|
||||
clip: true
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["system", "diskmounts"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["system", "diskmounts"]);
|
||||
}
|
||||
|
||||
Column {
|
||||
id: systemColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: systemInfoColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
SystemLogo {
|
||||
width: 80
|
||||
height: 80
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 80 - Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: DgopService.hostname
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Light
|
||||
color: Theme.surfaceText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.distribution} • ${DgopService.architecture} • ${DgopService.kernelVersion}`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.uptime} • Boot: ${DgopService.bootTime}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `Load: ${DgopService.loadAverage} • ${DgopService.processCount} processes, ${DgopService.threadCount} threads`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXL) / 2
|
||||
height: hardwareColumn.implicitHeight + Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
|
||||
Column {
|
||||
id: hardwareColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.cpuModel
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.motherboard
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `BIOS ${DgopService.biosVersion}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.formatSystemMemory(DgopService.totalMemoryKB)} RAM`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXL) / 2
|
||||
height: gpuColumn.implicitHeight + Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const baseColor = Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
|
||||
const hoverColor = Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.popupTransparency * 1.5);
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor;
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.fullName.split(' ')[0].toLowerCase();
|
||||
let tintColor;
|
||||
if (vendor.includes("nvidia")) {
|
||||
tintColor = Theme.success;
|
||||
} else if (vendor.includes("amd")) {
|
||||
tintColor = Theme.error;
|
||||
} else if (vendor.includes("intel")) {
|
||||
tintColor = Theme.info;
|
||||
} else {
|
||||
return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor;
|
||||
}
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba((hoverColor.r + tintColor.r * 0.1) / 1.1, (hoverColor.g + tintColor.g * 0.1) / 1.1, (hoverColor.b + tintColor.b * 0.1) / 1.1, 0.6);
|
||||
} else {
|
||||
return Qt.rgba((baseColor.r + tintColor.r * 0.08) / 1.08, (baseColor.g + tintColor.g * 0.08) / 1.08, (baseColor.b + tintColor.b * 0.08) / 1.08, 0.4);
|
||||
}
|
||||
}
|
||||
border.width: 1
|
||||
border.color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1);
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.fullName.split(' ')[0].toLowerCase();
|
||||
if (vendor.includes("nvidia")) {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
|
||||
} else if (vendor.includes("amd")) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
|
||||
} else if (vendor.includes("intel")) {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3);
|
||||
}
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gpuCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (DgopService.availableGpus.length > 1) {
|
||||
const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length;
|
||||
SessionData.setSelectedGpuIndex(nextIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: gpuColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "auto_awesome_mosaic"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "GPU"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "No GPUs detected";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
return gpu.fullName;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "Device: N/A";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
return `Device: ${gpu.pciId}`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "Driver: N/A";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
return `Driver: ${gpu.driver}`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "Temp: --°";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const temp = gpu.temperature;
|
||||
return `Temp: ${(temp === undefined || temp === null || temp === 0) ? '--°' : `${Math.round(temp)}°C`}`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const temp = gpu.temperature || 0;
|
||||
if (temp > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
|
||||
if (temp > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: storageColumn.implicitHeight + 2 * Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: storageColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Storage & Disks")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 24
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Device")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.25
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Mount")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.2
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Size")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Used")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Available")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Use%")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.1
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: diskMountRepeater
|
||||
|
||||
model: DgopService.diskMounts
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: diskMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData.device
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.25
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.mount
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.2
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.size
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.used
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.avail
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.percent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: {
|
||||
const percent = parseInt(modelData.percent);
|
||||
if (percent > 90) {
|
||||
return Theme.error;
|
||||
}
|
||||
|
||||
if (percent > 75) {
|
||||
return Theme.warning;
|
||||
}
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
width: parent.width * 0.1
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
386
quickshell/Modules/ProcessList/SystemView.qml
Normal file
386
quickshell/Modules/ProcessList/SystemView.qml
Normal file
@@ -0,0 +1,386 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["system", "cpu"]);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["system", "cpu"]);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
ColumnLayout {
|
||||
id: systemInfoColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "computer"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Information", "system info header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
rowSpacing: Theme.spacingS
|
||||
columnSpacing: Theme.spacingXL
|
||||
|
||||
InfoRow {
|
||||
label: I18n.tr("Hostname", "system info label")
|
||||
value: DgopService.hostname || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Distribution", "system info label")
|
||||
value: DgopService.distribution || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Kernel", "system info label")
|
||||
value: DgopService.kernelVersion || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Architecture", "system info label")
|
||||
value: DgopService.architecture || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("CPU")
|
||||
value: DgopService.cpuModel || ("" + DgopService.cpuCores + " cores")
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Uptime")
|
||||
value: DgopService.uptime || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Load Average", "system info label")
|
||||
value: DgopService.loadAverage || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Processes")
|
||||
value: DgopService.processCount > 0 ? DgopService.processCount.toString() : "--"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board"
|
||||
size: Theme.iconSize
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("GPU Monitoring", "gpu section header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.outlineLight
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: gpuListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 8
|
||||
|
||||
model: DgopService.availableGpus
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: gpuListView.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const vendor = (modelData?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.08);
|
||||
if (vendor.includes("amd"))
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||
if (vendor.includes("intel"))
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.08);
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
border.color: {
|
||||
const vendor = (modelData?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
|
||||
if (vendor.includes("amd"))
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
|
||||
if (vendor.includes("intel"))
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
readonly property bool tempEnabled: {
|
||||
const pciId = modelData?.pciId ?? "";
|
||||
if (!pciId)
|
||||
return false;
|
||||
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(pciId) !== -1 : false;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board"
|
||||
size: Theme.iconSize + 4
|
||||
color: {
|
||||
const vendor = (modelData?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Theme.success;
|
||||
if (vendor.includes("amd"))
|
||||
return Theme.error;
|
||||
if (vendor.includes("intel"))
|
||||
return Theme.info;
|
||||
return Theme.surfaceVariantText;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.displayName ?? I18n.tr("Unknown GPU", "fallback gpu name")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.vendor ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: (modelData?.driver ?? "").length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.driver ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: (modelData?.driver ?? "").length > 0
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.pciId ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 70
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: parent.parent.tempEnabled ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
|
||||
border.color: tempMouseArea.containsMouse ? Theme.outline : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: tempRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "thermostat"
|
||||
size: 16
|
||||
color: {
|
||||
if (!parent.parent.parent.parent.tempEnabled)
|
||||
return Theme.surfaceVariantText;
|
||||
const temp = modelData?.temperature ?? 0;
|
||||
if (temp > 85)
|
||||
return Theme.error;
|
||||
if (temp > 70)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
opacity: parent.parent.parent.parent.tempEnabled ? 1 : 0.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!parent.parent.parent.parent.tempEnabled)
|
||||
return I18n.tr("Off");
|
||||
const temp = modelData?.temperature ?? 0;
|
||||
return temp > 0 ? (temp.toFixed(0) + "°C") : "--";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: parent.parent.parent.parent.tempEnabled ? SettingsData.monoFontFamily : ""
|
||||
font.weight: parent.parent.parent.parent.tempEnabled ? Font.Bold : Font.Normal
|
||||
color: {
|
||||
if (!parent.parent.parent.parent.tempEnabled)
|
||||
return Theme.surfaceVariantText;
|
||||
const temp = modelData?.temperature ?? 0;
|
||||
if (temp > 85)
|
||||
return Theme.error;
|
||||
if (temp > 70)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tempMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const pciId = modelData?.pciId;
|
||||
if (!pciId)
|
||||
return;
|
||||
|
||||
const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
|
||||
const idx = enabledIds.indexOf(pciId);
|
||||
const wasEnabled = idx !== -1;
|
||||
|
||||
if (!wasEnabled) {
|
||||
enabledIds.push(pciId);
|
||||
DgopService.addGpuPciId(pciId);
|
||||
} else {
|
||||
enabledIds.splice(idx, 1);
|
||||
DgopService.removeGpuPciId(pciId);
|
||||
}
|
||||
|
||||
SessionData.setEnabledGpuPciIds(enabledIds);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 300
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: DgopService.availableGpus.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board_off"
|
||||
size: 32
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No GPUs detected", "empty state in gpu list")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoRow: RowLayout {
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
Layout.preferredWidth: 100
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,33 @@ Item {
|
||||
}
|
||||
]
|
||||
|
||||
readonly property var maxPinnedOptions: [
|
||||
{
|
||||
text: "5",
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
text: "10",
|
||||
value: 10
|
||||
},
|
||||
{
|
||||
text: "15",
|
||||
value: 15
|
||||
},
|
||||
{
|
||||
text: "25",
|
||||
value: 25
|
||||
},
|
||||
{
|
||||
text: "50",
|
||||
value: 50
|
||||
},
|
||||
{
|
||||
text: "100",
|
||||
value: 100
|
||||
}
|
||||
]
|
||||
|
||||
function getMaxHistoryText(value) {
|
||||
if (value <= 0)
|
||||
return "∞";
|
||||
@@ -152,6 +179,14 @@ Item {
|
||||
return value + " " + I18n.tr("days");
|
||||
}
|
||||
|
||||
function getMaxPinnedText(value) {
|
||||
for (let opt of maxPinnedOptions) {
|
||||
if (opt.value === value)
|
||||
return opt.text;
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
configLoaded = false;
|
||||
configError = false;
|
||||
@@ -295,6 +330,24 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "pinned", "max", "limit"]
|
||||
settingKey: "maxPinned"
|
||||
text: I18n.tr("Maximum Pinned Entries")
|
||||
description: I18n.tr("Maximum number of entries that can be saved")
|
||||
currentValue: root.getMaxPinnedText(root.config.maxPinned ?? 25)
|
||||
options: root.maxPinnedOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let opt of root.maxPinnedOptions) {
|
||||
if (opt.text === value) {
|
||||
root.saveConfig("maxPinned", opt.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -11,7 +11,7 @@ Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string currentVersion: "1.2"
|
||||
readonly property bool changelogEnabled: true
|
||||
readonly property bool changelogEnabled: false
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
||||
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
||||
|
||||
@@ -21,6 +21,7 @@ Singleton {
|
||||
property int processLimit: 20
|
||||
property string processSort: "cpu"
|
||||
property bool noCpu: false
|
||||
property int dgopProcessPid: 0
|
||||
|
||||
// Cursor data for accurate CPU calculations
|
||||
property string cpuCursor: ""
|
||||
@@ -59,6 +60,7 @@ Singleton {
|
||||
property var processes: []
|
||||
property var allProcesses: []
|
||||
property string currentSort: "cpu"
|
||||
property bool sortAscending: false
|
||||
property var availableGpus: []
|
||||
|
||||
property string kernelVersion: ""
|
||||
@@ -93,10 +95,8 @@ Singleton {
|
||||
if (modules) {
|
||||
const modulesToAdd = Array.isArray(modules) ? modules : [modules];
|
||||
for (const module of modulesToAdd) {
|
||||
// Increment reference count for this module
|
||||
const currentCount = moduleRefCounts[module] || 0;
|
||||
moduleRefCounts[module] = currentCount + 1;
|
||||
console.log("Adding ref for module:", module, "count:", moduleRefCounts[module]);
|
||||
|
||||
// Add to enabled modules if not already there
|
||||
if (enabledModules.indexOf(module) === -1) {
|
||||
@@ -126,17 +126,13 @@ Singleton {
|
||||
for (const module of modulesToRemove) {
|
||||
const currentCount = moduleRefCounts[module] || 0;
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
moduleRefCounts[module] = currentCount - 1;
|
||||
console.log("Removing ref for module:", module, "count:", moduleRefCounts[module]);
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete moduleRefCounts[module];
|
||||
const index = enabledModules.indexOf(module);
|
||||
if (index > -1) {
|
||||
enabledModules.splice(index, 1);
|
||||
modulesChanged = true;
|
||||
console.log("Disabling module:", module, "(no more refs)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,17 +167,13 @@ Singleton {
|
||||
gpuPciIds = gpuPciIds.concat([pciId]);
|
||||
}
|
||||
|
||||
console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]);
|
||||
// Force property change notification
|
||||
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts);
|
||||
}
|
||||
|
||||
function removeGpuPciId(pciId) {
|
||||
const currentCount = gpuPciIdRefCounts[pciId] || 0;
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
gpuPciIdRefCounts[pciId] = currentCount - 1;
|
||||
console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]);
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete gpuPciIdRefCounts[pciId];
|
||||
@@ -203,8 +195,6 @@ Singleton {
|
||||
}
|
||||
availableGpus = updatedGpus;
|
||||
}
|
||||
|
||||
console.log("Removing GPU PCI ID completely:", pciId);
|
||||
}
|
||||
|
||||
// Force property change notification
|
||||
@@ -389,8 +379,12 @@ Singleton {
|
||||
if (data.processes && Array.isArray(data.processes)) {
|
||||
const newProcesses = [];
|
||||
processSampleCount++;
|
||||
const ourPid = dgopProcessPid;
|
||||
|
||||
for (const proc of data.processes) {
|
||||
if (ourPid > 0 && proc.pid === ourPid)
|
||||
continue;
|
||||
|
||||
const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0;
|
||||
|
||||
newProcesses.push({
|
||||
@@ -577,39 +571,55 @@ Singleton {
|
||||
function setSortBy(newSortBy) {
|
||||
if (newSortBy !== currentSort) {
|
||||
currentSort = newSortBy;
|
||||
sortAscending = false;
|
||||
applySorting();
|
||||
}
|
||||
}
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0) {
|
||||
return;
|
||||
function toggleSort(column) {
|
||||
if (column === currentSort) {
|
||||
sortAscending = !sortAscending;
|
||||
} else {
|
||||
currentSort = column;
|
||||
sortAscending = false;
|
||||
}
|
||||
applySorting();
|
||||
}
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0)
|
||||
return;
|
||||
|
||||
const asc = sortAscending;
|
||||
const sorted = allProcesses.slice();
|
||||
sorted.sort((a, b) => {
|
||||
let valueA, valueB;
|
||||
let valueA, valueB, result;
|
||||
|
||||
switch (currentSort) {
|
||||
case "cpu":
|
||||
valueA = a.cpu || 0;
|
||||
valueB = b.cpu || 0;
|
||||
return valueB - valueA;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
case "memory":
|
||||
valueA = a.memoryKB || 0;
|
||||
valueB = b.memoryKB || 0;
|
||||
return valueB - valueA;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
case "name":
|
||||
valueA = (a.command || "").toLowerCase();
|
||||
valueB = (b.command || "").toLowerCase();
|
||||
return valueA.localeCompare(valueB);
|
||||
result = valueA.localeCompare(valueB);
|
||||
break;
|
||||
case "pid":
|
||||
valueA = a.pid || 0;
|
||||
valueB = b.pid || 0;
|
||||
return valueA - valueB;
|
||||
result = valueA - valueB;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return asc ? -result : result;
|
||||
});
|
||||
|
||||
processes = sorted.slice(0, processLimit);
|
||||
@@ -628,10 +638,7 @@ Singleton {
|
||||
id: dgopProcess
|
||||
command: root.buildDgopCommand()
|
||||
running: false
|
||||
onCommandChanged:
|
||||
|
||||
//console.log("DgopService command:", JSON.stringify(command))
|
||||
{}
|
||||
onStarted: dgopProcessPid = processId ?? 0
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:", exitCode);
|
||||
|
||||
@@ -556,6 +556,33 @@ Singleton {
|
||||
return loadPlugin(pluginId, true);
|
||||
}
|
||||
|
||||
function togglePlugin(pluginId) {
|
||||
let instance = pluginInstances[pluginId];
|
||||
|
||||
// Lazy instantiate daemon plugins on first toggle
|
||||
// This respects the daemon lifecycle (not instantiated on load)
|
||||
// while supporting toggle functionality for slideout-capable daemons
|
||||
if (!instance && pluginDaemonComponents[pluginId]) {
|
||||
const comp = pluginDaemonComponents[pluginId];
|
||||
const newInstance = comp.createObject(root, {
|
||||
"pluginId": pluginId,
|
||||
"pluginService": root
|
||||
});
|
||||
if (newInstance) {
|
||||
const newInstances = Object.assign({}, pluginInstances);
|
||||
newInstances[pluginId] = newInstance;
|
||||
pluginInstances = newInstances;
|
||||
instance = newInstance;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance && typeof instance.toggle === "function") {
|
||||
instance.toggle();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function savePluginData(pluginId, key, value) {
|
||||
SettingsData.setPluginSetting(pluginId, key, value);
|
||||
pluginDataChanged(pluginId);
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.2.3
|
||||
v1.4-unstable
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
{
|
||||
"scope": ["keyword"],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color5.dark.hex}}"
|
||||
"foreground": "{{dank16.color6.dark.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -320,7 +320,7 @@
|
||||
"foreground": "{{dank16.color12.dark.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color5.dark.hex}}"
|
||||
"foreground": "{{dank16.color6.dark.hex}}"
|
||||
},
|
||||
"comment": {
|
||||
"foreground": "{{dank16.color8.dark.hex}}"
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
{
|
||||
"scope": ["keyword"],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color5.default.hex}}"
|
||||
"foreground": "{{dank16.color6.default.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -272,7 +272,7 @@
|
||||
"foreground": "{{dank16.color12.default.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color5.default.hex}}"
|
||||
"foreground": "{{dank16.color6.default.hex}}"
|
||||
},
|
||||
"comment": {
|
||||
"foreground": "{{colors.outline.default.hex}}"
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"storage.type"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color5.light.hex}}"
|
||||
"foreground": "{{dank16.color6.light.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -444,7 +444,7 @@
|
||||
"foreground": "{{colors.outline.light.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color5.light.hex}}"
|
||||
"foreground": "{{dank16.color6.light.hex}}"
|
||||
},
|
||||
"operator": {
|
||||
"foreground": "{{colors.on_surface.light.hex}}"
|
||||
|
||||
Reference in New Issue
Block a user