mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
Compare commits
33 Commits
f2be6cfeb1
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7cdb39b0b | ||
|
|
0ceba92a23 | ||
|
|
4daa7a4c88 | ||
|
|
cc4a6a5899 | ||
|
|
994947477c | ||
|
|
311817ee97 | ||
|
|
b80c73f9b9 | ||
|
|
a85101c099 | ||
|
|
3513d57e06 | ||
|
|
1234847abb | ||
|
|
0ed595b43d | ||
|
|
060cbefc79 | ||
|
|
e022c04519 | ||
|
|
f534384e5e | ||
|
|
a25cdb43d5 | ||
|
|
4e9b4ca400 | ||
|
|
5bab1c98b1 | ||
|
|
2284bb002f | ||
|
|
b0611d6104 | ||
|
|
27965862d6 | ||
|
|
e74a901e05 | ||
|
|
77794deb2c | ||
|
|
1c10746e50 | ||
|
|
8ecb7282b9 | ||
|
|
9b3fa804ab | ||
|
|
b2ad31a27e | ||
|
|
db17e4cb14 | ||
|
|
1b7dcf56a8 | ||
|
|
502bb88e92 | ||
|
|
b76d0ce97d | ||
|
|
fa66d330cf | ||
|
|
157eab2d07 | ||
|
|
f50ad2dc22 |
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,14 +4,13 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
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: ""
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
@@ -57,9 +56,8 @@ jobs:
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
# Sets LATEST_TAG variable in parent scope if update needed
|
||||
check_dms_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\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 "")
|
||||
|
||||
@@ -75,8 +73,8 @@ jobs:
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - always update stable package
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push - always update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
@@ -106,12 +104,7 @@ jobs:
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
@@ -136,9 +129,6 @@ 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
|
||||
@@ -171,19 +161,12 @@ jobs:
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
# 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
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push event - use the pushed tag
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
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 }}"
|
||||
echo "Triggered by tag: $VERSION"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - dms-git only
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
@@ -193,28 +176,22 @@ jobs:
|
||||
|
||||
# Determine version for dms stable
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
# For explicit dms selection, require tag_version
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
echo "Using specified tag: $VERSION"
|
||||
else
|
||||
# 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
|
||||
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
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
# For "all", auto-detect if tag_version not specified
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
echo "Using specified tag: $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 "")
|
||||
@@ -229,7 +206,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 }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
else
|
||||
@@ -238,9 +215,6 @@ 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
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Overhauled system monitor, graphs, styling
|
||||
- dbus API for plugins, KDEConnect
|
||||
- new dank16 algorithm
|
||||
- launcher actions, customize env, args, name, icon
|
||||
|
||||
# 1.2.0
|
||||
|
||||
- Added clipboard and clipboard history integration
|
||||
|
||||
1
Makefile
1
Makefile
@@ -43,6 +43,7 @@ install-shell:
|
||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
||||
@echo "Shell files installed"
|
||||
|
||||
install-completions:
|
||||
|
||||
@@ -511,8 +511,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
colorCmd,
|
||||
screenshotCmd,
|
||||
notifyActionCmd,
|
||||
notifyCmd,
|
||||
genericNotifyActionCmd,
|
||||
matugenCmd,
|
||||
clipboardCmd,
|
||||
doctorCmd,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
notifyAppName string
|
||||
notifyIcon string
|
||||
notifyFile string
|
||||
notifyTimeout int
|
||||
)
|
||||
|
||||
var notifyCmd = &cobra.Command{
|
||||
Use: "notify <summary> [body]",
|
||||
Short: "Send a desktop notification",
|
||||
Long: `Send a desktop notification with optional actions.
|
||||
|
||||
If --file is provided, the notification will have "Open" and "Open Folder" actions.
|
||||
|
||||
Examples:
|
||||
dms notify "Hello" "World"
|
||||
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone
|
||||
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: runNotify,
|
||||
}
|
||||
|
||||
var genericNotifyActionCmd = &cobra.Command{
|
||||
Use: "notify-action-generic",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
notify.RunActionListener(args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
notifyCmd.Flags().StringVar(¬ifyAppName, "app", "DMS", "Application name")
|
||||
notifyCmd.Flags().StringVar(¬ifyIcon, "icon", "", "Icon name or path")
|
||||
notifyCmd.Flags().StringVar(¬ifyFile, "file", "", "File path (enables Open/Open Folder actions)")
|
||||
notifyCmd.Flags().IntVar(¬ifyTimeout, "timeout", 5000, "Timeout in milliseconds")
|
||||
}
|
||||
|
||||
func runNotify(cmd *cobra.Command, args []string) {
|
||||
summary := args[0]
|
||||
body := ""
|
||||
if len(args) > 1 {
|
||||
body = args[1]
|
||||
}
|
||||
|
||||
n := notify.Notification{
|
||||
AppName: notifyAppName,
|
||||
Icon: notifyIcon,
|
||||
Summary: summary,
|
||||
Body: body,
|
||||
FilePath: notifyFile,
|
||||
Timeout: int32(notifyTimeout),
|
||||
}
|
||||
|
||||
if err := notify.Send(n); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
20
core/go.mod
20
core/go.mod
@@ -16,21 +16,21 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||
golang.org/x/image v0.35.0
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||
golang.org/x/image v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.7.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.2 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
@@ -38,8 +38,8 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -47,12 +47,12 @@ require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -66,7 +66,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
55
core/go.sum
55
core/go.sum
@@ -16,6 +16,8 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
@@ -24,22 +26,24 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
||||
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -48,6 +52,8 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
@@ -58,19 +64,22 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
@@ -118,12 +127,16 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -140,37 +153,33 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -199,6 +199,31 @@ 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)
|
||||
@@ -331,59 +356,6 @@ 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
|
||||
@@ -397,29 +369,6 @@ 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)
|
||||
@@ -440,9 +389,6 @@ 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
|
||||
@@ -464,136 +410,115 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
}
|
||||
palette.Color0 = NewColorInfo(bgColor)
|
||||
|
||||
baseSat := math.Max(ph.S, 0.5)
|
||||
baseVal := math.Max(ph.V, 0.5)
|
||||
hueShift := (hsv.H - 0.6) * 0.12
|
||||
satBoost := 1.15
|
||||
|
||||
redH := blendHue(0.0, ph.H, 0.12)
|
||||
greenH := blendHue(0.33, ph.H, 0.10)
|
||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||
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))
|
||||
}
|
||||
|
||||
accentTarget := secondaryTarget * 0.7
|
||||
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))
|
||||
|
||||
if opts.IsLight {
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
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})))
|
||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
||||
} else {
|
||||
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))
|
||||
palette.Color7 = NewColorInfo("#abb2bf")
|
||||
palette.Color8 = NewColorInfo("#5c6370")
|
||||
}
|
||||
|
||||
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))
|
||||
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))
|
||||
}
|
||||
|
||||
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))
|
||||
if opts.IsLight {
|
||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
||||
} else {
|
||||
palette.Color15 = NewColorInfo("#ffffff")
|
||||
}
|
||||
|
||||
return palette
|
||||
|
||||
@@ -366,19 +366,10 @@ func TestGeneratePalette(t *testing.T) {
|
||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -588,10 +579,6 @@ 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 {
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyDest = "org.freedesktop.Notifications"
|
||||
notifyPath = "/org/freedesktop/Notifications"
|
||||
notifyInterface = "org.freedesktop.Notifications"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
AppName string
|
||||
Icon string
|
||||
Summary string
|
||||
Body string
|
||||
FilePath string
|
||||
Timeout int32
|
||||
}
|
||||
|
||||
func Send(n Notification) error {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dbus session failed: %w", err)
|
||||
}
|
||||
|
||||
if n.AppName == "" {
|
||||
n.AppName = "DMS"
|
||||
}
|
||||
if n.Timeout == 0 {
|
||||
n.Timeout = 5000
|
||||
}
|
||||
|
||||
var actions []string
|
||||
if n.FilePath != "" {
|
||||
actions = []string{
|
||||
"open", "Open",
|
||||
"folder", "Open Folder",
|
||||
}
|
||||
}
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
if n.FilePath != "" {
|
||||
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
||||
}
|
||||
|
||||
obj := conn.Object(notifyDest, notifyPath)
|
||||
call := obj.Call(
|
||||
notifyInterface+".Notify",
|
||||
0,
|
||||
n.AppName,
|
||||
uint32(0),
|
||||
n.Icon,
|
||||
n.Summary,
|
||||
n.Body,
|
||||
actions,
|
||||
hints,
|
||||
n.Timeout,
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("notify call failed: %w", call.Err)
|
||||
}
|
||||
|
||||
var notificationID uint32
|
||||
if err := call.Store(¬ificationID); err != nil {
|
||||
return fmt.Errorf("failed to get notification id: %w", err)
|
||||
}
|
||||
|
||||
if len(actions) > 0 && n.FilePath != "" {
|
||||
spawnActionListener(notificationID, n.FilePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func spawnActionListener(notificationID uint32, filePath string) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, "notify-action-generic", fmt.Sprintf("%d", notificationID), filePath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
|
||||
func RunActionListener(args []string) {
|
||||
if len(args) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
notificationID, err := strconv.ParseUint(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := args[1]
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(notifyPath),
|
||||
dbus.WithMatchInterface(notifyInterface),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
signals := make(chan *dbus.Signal, 10)
|
||||
conn.Signal(signals)
|
||||
|
||||
for sig := range signals {
|
||||
switch sig.Name {
|
||||
case notifyInterface + ".ActionInvoked":
|
||||
if len(sig.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
id, ok := sig.Body[0].(uint32)
|
||||
if !ok || id != uint32(notificationID) {
|
||||
continue
|
||||
}
|
||||
action, ok := sig.Body[1].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
handleAction(action, filePath)
|
||||
return
|
||||
|
||||
case notifyInterface + ".NotificationClosed":
|
||||
if len(sig.Body) < 1 {
|
||||
continue
|
||||
}
|
||||
id, ok := sig.Body[0].(uint32)
|
||||
if !ok || id != uint32(notificationID) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAction(action, filePath string) {
|
||||
switch action {
|
||||
case "open", "default":
|
||||
openPath(filePath)
|
||||
case "folder":
|
||||
openPath(filepath.Dir(filePath))
|
||||
}
|
||||
}
|
||||
|
||||
func openPath(path string) {
|
||||
cmd := exec.Command("xdg-open", path)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -111,15 +110,17 @@ func (m *Manager) updateAdapterState() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
powered, _ := poweredVar.Value().(bool)
|
||||
|
||||
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discovering, _ := discoveringVar.Value().(bool)
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Powered = dbusutil.AsOr(poweredVar, false)
|
||||
m.state.Discovering = dbusutil.AsOr(discoveringVar, false)
|
||||
m.state.Powered = powered
|
||||
m.state.Discovering = discovering
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -168,20 +169,65 @@ func (m *Manager) updateDevices() error {
|
||||
}
|
||||
|
||||
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
||||
return Device{
|
||||
Path: path,
|
||||
Address: dbusutil.GetOr(props, "Address", ""),
|
||||
Name: dbusutil.GetOr(props, "Name", ""),
|
||||
Alias: dbusutil.GetOr(props, "Alias", ""),
|
||||
Paired: dbusutil.GetOr(props, "Paired", false),
|
||||
Trusted: dbusutil.GetOr(props, "Trusted", false),
|
||||
Blocked: dbusutil.GetOr(props, "Blocked", false),
|
||||
Connected: dbusutil.GetOr(props, "Connected", false),
|
||||
Class: dbusutil.GetOr(props, "Class", uint32(0)),
|
||||
Icon: dbusutil.GetOr(props, "Icon", ""),
|
||||
RSSI: dbusutil.GetOr(props, "RSSI", int16(0)),
|
||||
LegacyPairing: dbusutil.GetOr(props, "LegacyPairing", false),
|
||||
dev := Device{Path: path}
|
||||
|
||||
if v, ok := props["Address"]; ok {
|
||||
if addr, ok := v.Value().(string); ok {
|
||||
dev.Address = addr
|
||||
}
|
||||
}
|
||||
if v, ok := props["Name"]; ok {
|
||||
if name, ok := v.Value().(string); ok {
|
||||
dev.Name = name
|
||||
}
|
||||
}
|
||||
if v, ok := props["Alias"]; ok {
|
||||
if alias, ok := v.Value().(string); ok {
|
||||
dev.Alias = alias
|
||||
}
|
||||
}
|
||||
if v, ok := props["Paired"]; ok {
|
||||
if paired, ok := v.Value().(bool); ok {
|
||||
dev.Paired = paired
|
||||
}
|
||||
}
|
||||
if v, ok := props["Trusted"]; ok {
|
||||
if trusted, ok := v.Value().(bool); ok {
|
||||
dev.Trusted = trusted
|
||||
}
|
||||
}
|
||||
if v, ok := props["Blocked"]; ok {
|
||||
if blocked, ok := v.Value().(bool); ok {
|
||||
dev.Blocked = blocked
|
||||
}
|
||||
}
|
||||
if v, ok := props["Connected"]; ok {
|
||||
if connected, ok := v.Value().(bool); ok {
|
||||
dev.Connected = connected
|
||||
}
|
||||
}
|
||||
if v, ok := props["Class"]; ok {
|
||||
if class, ok := v.Value().(uint32); ok {
|
||||
dev.Class = class
|
||||
}
|
||||
}
|
||||
if v, ok := props["Icon"]; ok {
|
||||
if icon, ok := v.Value().(string); ok {
|
||||
dev.Icon = icon
|
||||
}
|
||||
}
|
||||
if v, ok := props["RSSI"]; ok {
|
||||
if rssi, ok := v.Value().(int16); ok {
|
||||
dev.RSSI = rssi
|
||||
}
|
||||
}
|
||||
if v, ok := props["LegacyPairing"]; ok {
|
||||
if legacy, ok := v.Value().(bool); ok {
|
||||
dev.LegacyPairing = legacy
|
||||
}
|
||||
}
|
||||
|
||||
return dev
|
||||
}
|
||||
|
||||
func (m *Manager) startAgent() error {
|
||||
@@ -282,13 +328,17 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
||||
m.stateMutex.Lock()
|
||||
dirty := false
|
||||
|
||||
if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
if v, ok := changed["Powered"]; ok {
|
||||
if powered, ok := v.Value().(bool); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
if v, ok := changed["Discovering"]; ok {
|
||||
if discovering, ok := v.Value().(bool); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
m.stateMutex.Unlock()
|
||||
@@ -299,28 +349,31 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
||||
}
|
||||
|
||||
func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) {
|
||||
paired, hasPaired := dbusutil.Get[bool](changed, "Paired")
|
||||
pairedVar, hasPaired := changed["Paired"]
|
||||
_, hasConnected := changed["Connected"]
|
||||
_, hasTrusted := changed["Trusted"]
|
||||
|
||||
if hasPaired {
|
||||
devicePath := string(path)
|
||||
if paired {
|
||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||
if wasPending {
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
if paired, ok := pairedVar.Value().(bool); ok {
|
||||
if paired {
|
||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||
|
||||
if wasPending {
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
}
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
m.pendingPairings.Delete(devicePath)
|
||||
}
|
||||
} else {
|
||||
m.pendingPairings.Delete(devicePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,14 +37,6 @@ 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)
|
||||
}
|
||||
@@ -213,9 +205,6 @@ 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())
|
||||
@@ -241,43 +230,3 @@ 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,11 +389,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
}
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
if count < m.config.MaxHistory {
|
||||
count++
|
||||
continue
|
||||
@@ -423,11 +419,6 @@ 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
|
||||
}
|
||||
@@ -471,12 +462,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -750,54 +735,19 @@ func (m *Manager) ClearHistory() {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete only non-pinned entries
|
||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
||||
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 := tx.CreateBucket([]byte("clipboard"))
|
||||
return err
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to clear clipboard history: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
@@ -1010,10 +960,6 @@ 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)
|
||||
}
|
||||
@@ -1304,153 +1250,3 @@ 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,7 +19,6 @@ type Config struct {
|
||||
AutoClearDays int `json:"autoClearDays"`
|
||||
ClearAtStartup bool `json:"clearAtStartup"`
|
||||
Disabled bool `json:"disabled"`
|
||||
MaxPinned int `json:"maxPinned"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
@@ -28,7 +27,6 @@ func DefaultConfig() Config {
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
AutoClearDays: 0,
|
||||
ClearAtStartup: false,
|
||||
MaxPinned: 25,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +100,6 @@ type Entry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsImage bool `json:"isImage"`
|
||||
Hash uint64 `json:"hash,omitempty"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
type objectParams struct {
|
||||
bus string
|
||||
dest string
|
||||
path string
|
||||
iface string
|
||||
}
|
||||
|
||||
func extractObjectParams(p map[string]any, requirePath bool) (objectParams, error) {
|
||||
bus, err := params.String(p, "bus")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
dest, err := params.String(p, "dest")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
|
||||
var path string
|
||||
if requirePath {
|
||||
path, err = params.String(p, "path")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
} else {
|
||||
path = params.StringOpt(p, "path", "/")
|
||||
}
|
||||
|
||||
iface, err := params.String(p, "interface")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
|
||||
return objectParams{bus: bus, dest: dest, path: path, iface: iface}, nil
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||
switch req.Method {
|
||||
case "dbus.call":
|
||||
handleCall(conn, req, m)
|
||||
case "dbus.getProperty":
|
||||
handleGetProperty(conn, req, m)
|
||||
case "dbus.setProperty":
|
||||
handleSetProperty(conn, req, m)
|
||||
case "dbus.getAllProperties":
|
||||
handleGetAllProperties(conn, req, m)
|
||||
case "dbus.introspect":
|
||||
handleIntrospect(conn, req, m)
|
||||
case "dbus.listNames":
|
||||
handleListNames(conn, req, m)
|
||||
case "dbus.subscribe":
|
||||
handleSubscribe(conn, req, m, clientID)
|
||||
case "dbus.unsubscribe":
|
||||
handleUnsubscribe(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleCall(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
method, err := params.String(req.Params, "method")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var args []any
|
||||
if argsRaw, ok := params.Any(req.Params, "args"); ok {
|
||||
if argsSlice, ok := argsRaw.([]any); ok {
|
||||
args = argsSlice
|
||||
}
|
||||
}
|
||||
|
||||
result, err := m.Call(op.bus, op.dest, op.path, op.iface, method, args)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
property, err := params.String(req.Params, "property")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.GetProperty(op.bus, op.dest, op.path, op.iface, property)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
property, err := params.String(req.Params, "property")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
value, ok := params.Any(req.Params, "value")
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing 'value' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.SetProperty(op.bus, op.dest, op.path, op.iface, property, value); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||
}
|
||||
|
||||
func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.GetAllProperties(op.bus, op.dest, op.path, op.iface)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dest, err := params.String(req.Params, "dest")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
path := params.StringOpt(req.Params, "path", "/")
|
||||
|
||||
result, err := m.Introspect(bus, dest, path)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleListNames(conn net.Conn, req models.Request, m *Manager) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.ListNames(bus)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sender := params.StringOpt(req.Params, "sender", "")
|
||||
path := params.StringOpt(req.Params, "path", "")
|
||||
iface := params.StringOpt(req.Params, "interface", "")
|
||||
member := params.StringOpt(req.Params, "member", "")
|
||||
|
||||
result, err := m.Subscribe(clientID, bus, sender, path, iface, member)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) {
|
||||
subID, err := params.String(req.Params, "subscriptionId")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Unsubscribe(subID); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
systemConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
|
||||
sessionConn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
systemConn.Close()
|
||||
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
systemConn: systemConn,
|
||||
sessionConn: sessionConn,
|
||||
}
|
||||
|
||||
go m.processSystemSignals()
|
||||
go m.processSessionSignals()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) getConn(bus string) (*dbus.Conn, error) {
|
||||
switch bus {
|
||||
case "system":
|
||||
if m.systemConn == nil {
|
||||
return nil, fmt.Errorf("system bus not connected")
|
||||
}
|
||||
return m.systemConn, nil
|
||||
case "session":
|
||||
if m.sessionConn == nil {
|
||||
return nil, fmt.Errorf("session bus not connected")
|
||||
}
|
||||
return m.sessionConn, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid bus: %s (must be 'system' or 'session')", bus)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Call(bus, dest, path, iface, method string, args []any) (*CallResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
fullMethod := iface + "." + method
|
||||
|
||||
call := obj.Call(fullMethod, 0, args...)
|
||||
if call.Err != nil {
|
||||
return nil, fmt.Errorf("dbus call failed: %w", call.Err)
|
||||
}
|
||||
|
||||
return &CallResult{Values: call.Body}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
var variant dbus.Variant
|
||||
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, iface, property).Store(&variant)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get property: %w", err)
|
||||
}
|
||||
|
||||
return &PropertyResult{Value: dbusutil.Normalize(variant.Value())}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetProperty(bus, dest, path, iface, property string, value any) error {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
call := obj.Call("org.freedesktop.DBus.Properties.Set", 0, iface, property, dbus.MakeVariant(value))
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to set property: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetAllProperties(bus, dest, path, iface string) (map[string]any, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
var props map[string]dbus.Variant
|
||||
err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface).Store(&props)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get properties: %w", err)
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
for k, v := range props {
|
||||
result[k] = dbusutil.Normalize(v.Value())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Introspect(bus, dest, path string) (*IntrospectResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
var xml string
|
||||
err = obj.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&xml)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to introspect: %w", err)
|
||||
}
|
||||
|
||||
return &IntrospectResult{XML: xml}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListNames(bus string) (*ListNamesResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var names []string
|
||||
err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list names: %w", err)
|
||||
}
|
||||
|
||||
return &ListNamesResult{Names: names}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(clientID, bus, sender, path, iface, member string) (*SubscribeResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subID := generateSubscriptionID()
|
||||
|
||||
parts := []string{"type='signal'"}
|
||||
if sender != "" {
|
||||
parts = append(parts, fmt.Sprintf("sender='%s'", sender))
|
||||
}
|
||||
if path != "" {
|
||||
parts = append(parts, fmt.Sprintf("path='%s'", path))
|
||||
}
|
||||
if iface != "" {
|
||||
parts = append(parts, fmt.Sprintf("interface='%s'", iface))
|
||||
}
|
||||
if member != "" {
|
||||
parts = append(parts, fmt.Sprintf("member='%s'", member))
|
||||
}
|
||||
matchRule := strings.Join(parts, ",")
|
||||
|
||||
call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule)
|
||||
if call.Err != nil {
|
||||
return nil, fmt.Errorf("failed to add match rule: %w", call.Err)
|
||||
}
|
||||
|
||||
sub := &signalSubscription{
|
||||
Bus: bus,
|
||||
Sender: sender,
|
||||
Path: path,
|
||||
Interface: iface,
|
||||
Member: member,
|
||||
ClientID: clientID,
|
||||
}
|
||||
m.subscriptions.Store(subID, sub)
|
||||
|
||||
log.Debugf("dbus: subscribed %s to %s", subID, matchRule)
|
||||
|
||||
return &SubscribeResult{SubscriptionID: subID}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(subID string) error {
|
||||
sub, ok := m.subscriptions.LoadAndDelete(subID)
|
||||
if !ok {
|
||||
return fmt.Errorf("subscription not found: %s", subID)
|
||||
}
|
||||
|
||||
conn, err := m.getConn(sub.Bus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := []string{"type='signal'"}
|
||||
if sub.Sender != "" {
|
||||
parts = append(parts, fmt.Sprintf("sender='%s'", sub.Sender))
|
||||
}
|
||||
if sub.Path != "" {
|
||||
parts = append(parts, fmt.Sprintf("path='%s'", sub.Path))
|
||||
}
|
||||
if sub.Interface != "" {
|
||||
parts = append(parts, fmt.Sprintf("interface='%s'", sub.Interface))
|
||||
}
|
||||
if sub.Member != "" {
|
||||
parts = append(parts, fmt.Sprintf("member='%s'", sub.Member))
|
||||
}
|
||||
matchRule := strings.Join(parts, ",")
|
||||
|
||||
call := conn.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule)
|
||||
if call.Err != nil {
|
||||
log.Warnf("dbus: failed to remove match rule: %v", call.Err)
|
||||
}
|
||||
|
||||
log.Debugf("dbus: unsubscribed %s", subID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) UnsubscribeClient(clientID string) {
|
||||
var toDelete []string
|
||||
m.subscriptions.Range(func(subID string, sub *signalSubscription) bool {
|
||||
if sub.ClientID == clientID {
|
||||
toDelete = append(toDelete, subID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, subID := range toDelete {
|
||||
if err := m.Unsubscribe(subID); err != nil {
|
||||
log.Warnf("dbus: failed to unsubscribe %s: %v", subID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SubscribeSignals(clientID string) chan SignalEvent {
|
||||
ch := make(chan SignalEvent, 64)
|
||||
existing, loaded := m.signalSubscribers.LoadOrStore(clientID, ch)
|
||||
if loaded {
|
||||
return existing
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) UnsubscribeSignals(clientID string) {
|
||||
if ch, ok := m.signalSubscribers.LoadAndDelete(clientID); ok {
|
||||
close(ch)
|
||||
}
|
||||
m.UnsubscribeClient(clientID)
|
||||
}
|
||||
|
||||
func (m *Manager) processSystemSignals() {
|
||||
if m.systemConn == nil {
|
||||
return
|
||||
}
|
||||
ch := make(chan *dbus.Signal, 256)
|
||||
m.systemConn.Signal(ch)
|
||||
|
||||
for sig := range ch {
|
||||
m.dispatchSignal("system", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) processSessionSignals() {
|
||||
if m.sessionConn == nil {
|
||||
return
|
||||
}
|
||||
ch := make(chan *dbus.Signal, 256)
|
||||
m.sessionConn.Signal(ch)
|
||||
|
||||
for sig := range ch {
|
||||
m.dispatchSignal("session", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) dispatchSignal(bus string, sig *dbus.Signal) {
|
||||
path := string(sig.Path)
|
||||
iface := ""
|
||||
member := sig.Name
|
||||
|
||||
if idx := strings.LastIndex(sig.Name, "."); idx != -1 {
|
||||
iface = sig.Name[:idx]
|
||||
member = sig.Name[idx+1:]
|
||||
}
|
||||
|
||||
m.subscriptions.Range(func(subID string, sub *signalSubscription) bool {
|
||||
if sub.Bus != bus {
|
||||
return true
|
||||
}
|
||||
if sub.Path != "" && sub.Path != path && !strings.HasPrefix(path, sub.Path) {
|
||||
return true
|
||||
}
|
||||
if sub.Interface != "" && sub.Interface != iface {
|
||||
return true
|
||||
}
|
||||
if sub.Member != "" && sub.Member != member {
|
||||
return true
|
||||
}
|
||||
|
||||
event := SignalEvent{
|
||||
SubscriptionID: subID,
|
||||
Sender: sig.Sender,
|
||||
Path: path,
|
||||
Interface: iface,
|
||||
Member: member,
|
||||
Body: dbusutil.NormalizeSlice(sig.Body),
|
||||
}
|
||||
|
||||
ch, ok := m.signalSubscribers.Load(sub.ClientID)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- event:
|
||||
default:
|
||||
log.Warnf("dbus: channel full for %s, dropping signal", subID)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
m.signalSubscribers.Range(func(clientID string, ch chan SignalEvent) bool {
|
||||
close(ch)
|
||||
m.signalSubscribers.Delete(clientID)
|
||||
return true
|
||||
})
|
||||
|
||||
if m.systemConn != nil {
|
||||
m.systemConn.Close()
|
||||
}
|
||||
if m.sessionConn != nil {
|
||||
m.sessionConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func generateSubscriptionID() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Warnf("dbus: failed to generate random subscription ID: %v", err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
|
||||
subscriptions syncmap.Map[string, *signalSubscription]
|
||||
signalSubscribers syncmap.Map[string, chan SignalEvent]
|
||||
}
|
||||
|
||||
type signalSubscription struct {
|
||||
Bus string
|
||||
Sender string
|
||||
Path string
|
||||
Interface string
|
||||
Member string
|
||||
ClientID string
|
||||
}
|
||||
|
||||
type SignalEvent struct {
|
||||
SubscriptionID string `json:"subscriptionId"`
|
||||
Sender string `json:"sender"`
|
||||
Path string `json:"path"`
|
||||
Interface string `json:"interface"`
|
||||
Member string `json:"member"`
|
||||
Body []any `json:"body"`
|
||||
}
|
||||
|
||||
type CallResult struct {
|
||||
Values []any `json:"values"`
|
||||
}
|
||||
|
||||
type PropertyResult struct {
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
type IntrospectResult struct {
|
||||
XML string `json:"xml"`
|
||||
}
|
||||
|
||||
type ListNamesResult struct {
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
|
||||
type SubscribeResult struct {
|
||||
SubscriptionID string `json:"subscriptionId"`
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -111,17 +110,61 @@ func (m *Manager) updateAccountsState() error {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Accounts.IconFile = dbusutil.GetOr(props, "IconFile", "")
|
||||
m.state.Accounts.RealName = dbusutil.GetOr(props, "RealName", "")
|
||||
m.state.Accounts.UserName = dbusutil.GetOr(props, "UserName", "")
|
||||
m.state.Accounts.AccountType = dbusutil.GetOr(props, "AccountType", int32(0))
|
||||
m.state.Accounts.HomeDirectory = dbusutil.GetOr(props, "HomeDirectory", "")
|
||||
m.state.Accounts.Shell = dbusutil.GetOr(props, "Shell", "")
|
||||
m.state.Accounts.Email = dbusutil.GetOr(props, "Email", "")
|
||||
m.state.Accounts.Language = dbusutil.GetOr(props, "Language", "")
|
||||
m.state.Accounts.Location = dbusutil.GetOr(props, "Location", "")
|
||||
m.state.Accounts.Locked = dbusutil.GetOr(props, "Locked", false)
|
||||
m.state.Accounts.PasswordMode = dbusutil.GetOr(props, "PasswordMode", int32(0))
|
||||
if v, ok := props["IconFile"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.IconFile = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["RealName"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.RealName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["UserName"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.UserName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["AccountType"]; ok {
|
||||
if val, ok := v.Value().(int32); ok {
|
||||
m.state.Accounts.AccountType = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["HomeDirectory"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.HomeDirectory = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Shell"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Shell = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Email"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Email = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Language"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Language = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Location"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Location = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Locked"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Accounts.Locked = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["PasswordMode"]; ok {
|
||||
if val, ok := v.Value().(int32); ok {
|
||||
m.state.Accounts.PasswordMode = val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -137,7 +180,7 @@ func (m *Manager) updateSettingsState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if colorScheme, ok := dbusutil.As[uint32](variant); ok {
|
||||
if colorScheme, ok := variant.Value().(uint32); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.ColorScheme = colorScheme
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -133,15 +132,37 @@ func (m *Manager) updateSessionState() error {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Active = dbusutil.GetOr(props, "Active", m.state.Active)
|
||||
m.state.IdleHint = dbusutil.GetOr(props, "IdleHint", m.state.IdleHint)
|
||||
m.state.IdleSinceHint = dbusutil.GetOr(props, "IdleSinceHint", m.state.IdleSinceHint)
|
||||
if lockedHint, ok := dbusutil.Get[bool](props, "LockedHint"); ok {
|
||||
m.state.LockedHint = lockedHint
|
||||
m.state.Locked = lockedHint
|
||||
if v, ok := props["Active"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Active = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["IdleHint"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.IdleHint = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["IdleSinceHint"]; ok {
|
||||
if val, ok := v.Value().(uint64); ok {
|
||||
m.state.IdleSinceHint = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["LockedHint"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.LockedHint = val
|
||||
m.state.Locked = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Type"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.SessionType = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Class"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.SessionClass = val
|
||||
}
|
||||
}
|
||||
m.state.SessionType = dbusutil.GetOr(props, "Type", m.state.SessionType)
|
||||
m.state.SessionClass = dbusutil.GetOr(props, "Class", m.state.SessionClass)
|
||||
if v, ok := props["User"]; ok {
|
||||
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
||||
if uid, ok := userArr[0].(uint32); ok {
|
||||
@@ -149,12 +170,36 @@ func (m *Manager) updateSessionState() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
m.state.UserName = dbusutil.GetOr(props, "Name", m.state.UserName)
|
||||
m.state.RemoteHost = dbusutil.GetOr(props, "RemoteHost", m.state.RemoteHost)
|
||||
m.state.Service = dbusutil.GetOr(props, "Service", m.state.Service)
|
||||
m.state.TTY = dbusutil.GetOr(props, "TTY", m.state.TTY)
|
||||
m.state.Display = dbusutil.GetOr(props, "Display", m.state.Display)
|
||||
m.state.Remote = dbusutil.GetOr(props, "Remote", m.state.Remote)
|
||||
if v, ok := props["Name"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.UserName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["RemoteHost"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.RemoteHost = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Service"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Service = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["TTY"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.TTY = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Display"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Display = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Remote"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Remote = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Seat"]; ok {
|
||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||
if seatID, ok := seatArr[0].(string); ok {
|
||||
@@ -162,7 +207,11 @@ func (m *Manager) updateSessionState() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr)
|
||||
if v, ok := props["VTNr"]; ok {
|
||||
if val, ok := v.Value().(uint32); ok {
|
||||
m.state.VTNr = val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package loginctl
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -118,28 +117,31 @@ func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) {
|
||||
for key, variant := range changes {
|
||||
switch key {
|
||||
case "Active":
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Active = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "IdleHint":
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.IdleHint = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "IdleSinceHint":
|
||||
if val, ok := dbusutil.As[uint64](variant); ok {
|
||||
if val, ok := variant.Value().(uint64); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.IdleSinceHint = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "LockedHint":
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.LockedHint = val
|
||||
m.state.Locked = val
|
||||
|
||||
@@ -150,13 +150,21 @@ func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority),
|
||||
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric),
|
||||
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||
log.Warnf("Failed to set priority for %s: %v", connName, err)
|
||||
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority)).Run(); err != nil {
|
||||
log.Warnf("Failed to set autoconnect-priority for %v: %v", connName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||
log.Warnf("Failed to set ipv4.route-metric for %v: %v", connName, err)
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||
log.Warnf("Failed to set ipv6.route-metric for %v: %v", connName, err)
|
||||
}
|
||||
|
||||
log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
@@ -155,15 +154,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "dbus.") {
|
||||
if dbusManager == nil {
|
||||
models.RespondError(conn, req.ID, "dbus manager not initialized")
|
||||
return
|
||||
}
|
||||
serverDbus.HandleRequest(conn, req, dbusManager, dbusClientID)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "clipboard.") {
|
||||
switch req.Method {
|
||||
case "clipboard.getConfig":
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
@@ -66,11 +65,8 @@ var brightnessManager *brightness.Manager
|
||||
var wlrOutputManager *wlroutput.Manager
|
||||
var evdevManager *evdev.Manager
|
||||
var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||
var cupsSubscribers syncmap.Map[string, bool]
|
||||
var cupsSubscriberCount atomic.Int32
|
||||
@@ -367,19 +363,6 @@ func InitializeClipboardManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeDbusManager() error {
|
||||
manager, err := serverDbus.NewManager()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize dbus manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
dbusManager = manager
|
||||
|
||||
log.Info("DBus manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -457,10 +440,6 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
return Capabilities{Capabilities: caps}
|
||||
}
|
||||
|
||||
@@ -519,10 +498,6 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
return ServerInfo{
|
||||
APIVersion: APIVersion,
|
||||
CLIVersion: CLIVersion,
|
||||
@@ -1158,31 +1133,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
||||
}()
|
||||
}
|
||||
|
||||
if shouldSubscribe("dbus") && dbusManager != nil {
|
||||
wg.Add(1)
|
||||
dbusChan := dbusManager.SubscribeSignals(dbusClientID)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer dbusManager.UnsubscribeSignals(dbusClientID)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-dbusChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "dbus", Data: event}:
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(eventChan)
|
||||
@@ -1248,9 +1198,6 @@ func cleanupManagers() {
|
||||
if clipboardManager != nil {
|
||||
clipboardManager.Close()
|
||||
}
|
||||
if dbusManager != nil {
|
||||
dbusManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
@@ -1543,14 +1490,6 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := InitializeDbusManager(); err != nil {
|
||||
log.Warnf("DBus manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("")
|
||||
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package dbusutil
|
||||
|
||||
import "github.com/godbus/dbus/v5"
|
||||
|
||||
func As[T any](v dbus.Variant) (T, bool) {
|
||||
val, ok := v.Value().(T)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func AsOr[T any](v dbus.Variant, def T) T {
|
||||
if val, ok := v.Value().(T); ok {
|
||||
return val
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func Get[T any](m map[string]dbus.Variant, key string) (T, bool) {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
return As[T](v)
|
||||
}
|
||||
|
||||
func GetOr[T any](m map[string]dbus.Variant, key string, def T) T {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
return AsOr(v, def)
|
||||
}
|
||||
|
||||
func Normalize(v any) any {
|
||||
switch val := v.(type) {
|
||||
case dbus.Variant:
|
||||
return Normalize(val.Value())
|
||||
case dbus.ObjectPath:
|
||||
return string(val)
|
||||
case []dbus.ObjectPath:
|
||||
result := make([]string, len(val))
|
||||
for i, p := range val {
|
||||
result[i] = string(p)
|
||||
}
|
||||
return result
|
||||
case map[string]dbus.Variant:
|
||||
result := make(map[string]any)
|
||||
for k, vv := range val {
|
||||
result[k] = Normalize(vv.Value())
|
||||
}
|
||||
return result
|
||||
case []any:
|
||||
result := make([]any, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = Normalize(item)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeSlice(values []any) []any {
|
||||
result := make([]any, len(values))
|
||||
for i, v := range values {
|
||||
result[i] = Normalize(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package dbusutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAs(t *testing.T) {
|
||||
t.Run("string", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
val, ok := As[string](v)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "hello", val)
|
||||
})
|
||||
|
||||
t.Run("bool", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(true)
|
||||
val, ok := As[bool](v)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, val)
|
||||
})
|
||||
|
||||
t.Run("int32", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(int32(42))
|
||||
val, ok := As[int32](v)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, int32(42), val)
|
||||
})
|
||||
|
||||
t.Run("wrong type", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
_, ok := As[int](v)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsOr(t *testing.T) {
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
val := AsOr(v, "default")
|
||||
assert.Equal(t, "hello", val)
|
||||
})
|
||||
|
||||
t.Run("wrong type uses default", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(123)
|
||||
val := AsOr(v, "default")
|
||||
assert.Equal(t, "default", val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
m := map[string]dbus.Variant{
|
||||
"name": dbus.MakeVariant("test"),
|
||||
"enabled": dbus.MakeVariant(true),
|
||||
"count": dbus.MakeVariant(int32(5)),
|
||||
}
|
||||
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
val, ok := Get[string](m, "name")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test", val)
|
||||
})
|
||||
|
||||
t.Run("missing key", func(t *testing.T) {
|
||||
_, ok := Get[string](m, "missing")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("wrong type", func(t *testing.T) {
|
||||
_, ok := Get[int](m, "name")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOr(t *testing.T) {
|
||||
m := map[string]dbus.Variant{
|
||||
"name": dbus.MakeVariant("test"),
|
||||
}
|
||||
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
val := GetOr(m, "name", "default")
|
||||
assert.Equal(t, "test", val)
|
||||
})
|
||||
|
||||
t.Run("missing uses default", func(t *testing.T) {
|
||||
val := GetOr(m, "missing", "default")
|
||||
assert.Equal(t, "default", val)
|
||||
})
|
||||
|
||||
t.Run("wrong type uses default", func(t *testing.T) {
|
||||
val := GetOr(m, "name", 42)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalize(t *testing.T) {
|
||||
t.Run("variant unwrap", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("nested variant", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(dbus.MakeVariant("nested"))
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, "nested", result)
|
||||
})
|
||||
|
||||
t.Run("object path", func(t *testing.T) {
|
||||
v := dbus.ObjectPath("/org/test")
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, "/org/test", result)
|
||||
})
|
||||
|
||||
t.Run("object path slice", func(t *testing.T) {
|
||||
v := []dbus.ObjectPath{"/org/a", "/org/b"}
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, []string{"/org/a", "/org/b"}, result)
|
||||
})
|
||||
|
||||
t.Run("variant map", func(t *testing.T) {
|
||||
v := map[string]dbus.Variant{
|
||||
"key": dbus.MakeVariant("value"),
|
||||
}
|
||||
result := Normalize(v)
|
||||
expected := map[string]any{"key": "value"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("any slice", func(t *testing.T) {
|
||||
v := []any{dbus.MakeVariant("a"), dbus.ObjectPath("/b")}
|
||||
result := Normalize(v)
|
||||
expected := []any{"a", "/b"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("passthrough primitives", func(t *testing.T) {
|
||||
assert.Equal(t, "hello", Normalize("hello"))
|
||||
assert.Equal(t, 42, Normalize(42))
|
||||
assert.Equal(t, true, Normalize(true))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeSlice(t *testing.T) {
|
||||
input := []any{
|
||||
dbus.MakeVariant("a"),
|
||||
dbus.ObjectPath("/b"),
|
||||
"c",
|
||||
}
|
||||
result := NormalizeSlice(input)
|
||||
expected := []any{"a", "/b", "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,7 +29,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,6 +14,7 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -27,7 +28,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -33,6 +33,7 @@ Recommends: cava
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
# Recommended system packages
|
||||
Recommends: NetworkManager
|
||||
|
||||
@@ -24,8 +24,10 @@ 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,9 +20,12 @@ 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,10 +23,12 @@ 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,6 +15,7 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,7 +29,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,6 +14,7 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -27,7 +28,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-lXqOJ0yNlOcXuR3vcuVjFI02Hskmavcasb1Ntf3UlPM=";
|
||||
vendorHash = "sha256-9CnZFtjXXWYELRiBX2UbZvWopnl9Y1ILuK+xP6YQZ9U=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
Saffron Bloom
|
||||
Spicy Miso
|
||||
|
||||
@@ -21,7 +21,6 @@ Singleton {
|
||||
property bool _isReadOnly: false
|
||||
property bool _hasUnsavedChanges: false
|
||||
property var _loadedSessionSnapshot: null
|
||||
readonly property var _hooks: ({})
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
@@ -103,10 +102,6 @@ Singleton {
|
||||
property string weatherLocation: "New York, NY"
|
||||
property string weatherCoordinates: "40.7128,-74.0060"
|
||||
|
||||
property var hiddenApps: []
|
||||
property var appOverrides: ({})
|
||||
property bool searchAppActions: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadSettings();
|
||||
@@ -266,10 +261,6 @@ Singleton {
|
||||
_checkSessionWritable();
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
Spec.set(root, key, value, saveSettings, _hooks);
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(settings) {
|
||||
console.info("SessionData: Migrating configuration from undefined to version 1");
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
@@ -915,61 +906,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function hideApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const current = [...hiddenApps];
|
||||
if (current.indexOf(appId) === -1) {
|
||||
current.push(appId);
|
||||
hiddenApps = current;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function showApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
hiddenApps = hiddenApps.filter(id => id !== appId);
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function isAppHidden(appId) {
|
||||
return appId && hiddenApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function setAppOverride(appId, overrides) {
|
||||
if (!appId)
|
||||
return;
|
||||
const newOverrides = Object.assign({}, appOverrides);
|
||||
if (!overrides || Object.keys(overrides).length === 0) {
|
||||
delete newOverrides[appId];
|
||||
} else {
|
||||
newOverrides[appId] = overrides;
|
||||
}
|
||||
appOverrides = newOverrides;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getAppOverride(appId) {
|
||||
if (!appId)
|
||||
return null;
|
||||
return appOverrides[appId] || null;
|
||||
}
|
||||
|
||||
function clearAppOverride(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const newOverrides = Object.assign({}, appOverrides);
|
||||
delete newOverrides[appId];
|
||||
appOverrides = newOverrides;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setSearchAppActions(enabled) {
|
||||
searchAppActions = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
|
||||
@@ -206,7 +206,6 @@ Singleton {
|
||||
property bool reverseScrolling: false
|
||||
property bool dwlShowAllTags: false
|
||||
property string workspaceColorMode: "default"
|
||||
property string workspaceOccupiedColorMode: "none"
|
||||
property string workspaceUnfocusedColorMode: "default"
|
||||
property string workspaceUrgentColorMode: "default"
|
||||
property bool workspaceFocusedBorderEnabled: false
|
||||
@@ -367,7 +366,6 @@ Singleton {
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
property bool dockSmartAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property bool dockOpenOnOverview: false
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
@@ -395,7 +393,6 @@ Singleton {
|
||||
property bool lockScreenShowDate: true
|
||||
property bool lockScreenShowProfileImage: true
|
||||
property bool lockScreenShowPasswordField: true
|
||||
property bool lockScreenPowerOffMonitorsOnLock: false
|
||||
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 15
|
||||
@@ -495,8 +492,7 @@ Singleton {
|
||||
"shadowIntensity": 0,
|
||||
"shadowOpacity": 60,
|
||||
"shadowColorMode": "text",
|
||||
"shadowCustomColor": "#000000",
|
||||
"clickThrough": false
|
||||
"shadowCustomColor": "#000000"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -55,23 +55,9 @@ var SPEC = {
|
||||
enabledGpuPciIds: { def: [] },
|
||||
|
||||
wifiDeviceOverride: { def: "" },
|
||||
weatherHourlyDetailed: { def: true },
|
||||
|
||||
hiddenApps: { def: [] },
|
||||
appOverrides: { def: {} },
|
||||
searchAppActions: { def: true }
|
||||
weatherHourlyDetailed: { def: true }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
return Object.keys(SPEC).concat(["configVersion"]);
|
||||
}
|
||||
|
||||
function set(root, key, value, saveFn, hooks) {
|
||||
if (!(key in SPEC)) return;
|
||||
root[key] = value;
|
||||
var hookName = SPEC[key].onChange;
|
||||
if (hookName && hooks && hooks[hookName]) {
|
||||
hooks[hookName](root);
|
||||
}
|
||||
saveFn();
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ var SPEC = {
|
||||
reverseScrolling: { def: false },
|
||||
dwlShowAllTags: { def: false },
|
||||
workspaceColorMode: { def: "default" },
|
||||
workspaceOccupiedColorMode: { def: "none" },
|
||||
workspaceUnfocusedColorMode: { def: "default" },
|
||||
workspaceUrgentColorMode: { def: "default" },
|
||||
workspaceFocusedBorderEnabled: { def: false },
|
||||
@@ -232,7 +231,6 @@ var SPEC = {
|
||||
|
||||
showDock: { def: false },
|
||||
dockAutoHide: { def: false },
|
||||
dockSmartAutoHide: { def: false },
|
||||
dockGroupByApp: { def: false },
|
||||
dockOpenOnOverview: { def: false },
|
||||
dockPosition: { def: 1 },
|
||||
@@ -260,7 +258,6 @@ var SPEC = {
|
||||
lockScreenShowDate: { def: true },
|
||||
lockScreenShowProfileImage: { def: true },
|
||||
lockScreenShowPasswordField: { def: true },
|
||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
fprintdAvailable: { def: false, persist: false },
|
||||
@@ -358,8 +355,7 @@ var SPEC = {
|
||||
shadowIntensity: 0,
|
||||
shadowOpacity: 60,
|
||||
shadowColorMode: "text",
|
||||
shadowCustomColor: "#000000",
|
||||
clickThrough: false
|
||||
shadowCustomColor: "#000000"
|
||||
}], onChange: "updateBarConfigs" },
|
||||
|
||||
desktopClockEnabled: { def: false },
|
||||
|
||||
@@ -966,17 +966,6 @@ 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,10 +25,7 @@ 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();
|
||||
@@ -73,20 +70,18 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
|
||||
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
// Recents Tab
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.unpinnedEntries
|
||||
values: clipboardContent.modal.clipboardEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "recents"
|
||||
|
||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||
spacing: Theme.spacingXS
|
||||
@@ -119,11 +114,11 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
text: I18n.tr("No clipboard entries found")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.unpinnedEntries.length === 0
|
||||
visible: clipboardContent.modal.clipboardEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
@@ -140,60 +135,11 @@ 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,8 +14,6 @@ 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) : ""
|
||||
@@ -52,7 +50,7 @@ Rectangle {
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 110
|
||||
width: parent.width - 68
|
||||
spacing: Theme.spacingM
|
||||
|
||||
ClipboardThumbnail {
|
||||
@@ -102,32 +100,20 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
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()
|
||||
}
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 80
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyRequested()
|
||||
|
||||
@@ -8,13 +8,10 @@ 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
|
||||
|
||||
@@ -44,22 +41,6 @@ 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,8 +19,6 @@ 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
|
||||
@@ -76,37 +74,22 @@ DankModal {
|
||||
|
||||
function updateFilteredModel() {
|
||||
const query = searchText.trim();
|
||||
let filtered = [];
|
||||
|
||||
if (query.length === 0) {
|
||||
filtered = internalEntries;
|
||||
clipboardEntries = internalEntries;
|
||||
} else {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
filtered = internalEntries.filter(entry =>
|
||||
entry.preview.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
clipboardEntries = 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 (unpinnedEntries.length === 0) {
|
||||
if (clipboardEntries.length === 0) {
|
||||
keyboardNavigationActive = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex >= unpinnedEntries.length) {
|
||||
selectedIndex = unpinnedEntries.length - 1;
|
||||
} else if (selectedIndex >= clipboardEntries.length) {
|
||||
selectedIndex = clipboardEntries.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
property var internalEntries: []
|
||||
property var unpinnedEntries: []
|
||||
property string activeTab: "recents"
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
@@ -152,10 +135,6 @@ DankModal {
|
||||
return;
|
||||
}
|
||||
internalEntries = response.result || [];
|
||||
|
||||
pinnedEntries = internalEntries.filter(e => e.pinned);
|
||||
pinnedCount = pinnedEntries.length;
|
||||
|
||||
updateFilteredModel();
|
||||
});
|
||||
}
|
||||
@@ -192,85 +171,16 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 () {}
|
||||
);
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
totalCount = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
|
||||
@@ -11,8 +11,7 @@ FloatingWindow {
|
||||
id: processListModal
|
||||
|
||||
property int currentTab: 0
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
property bool shouldHaveFocus: visible
|
||||
property alias shouldBeVisible: processListModal.visible
|
||||
|
||||
@@ -28,8 +27,9 @@ FloatingWindow {
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
if (processContextMenu.visible)
|
||||
if (processContextMenu.visible) {
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -61,63 +61,48 @@ 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";
|
||||
}
|
||||
|
||||
function nextTab() {
|
||||
currentTab = (currentTab + 1) % 4;
|
||||
}
|
||||
|
||||
function previousTab() {
|
||||
currentTab = (currentTab - 1 + 4) % 4;
|
||||
}
|
||||
|
||||
objectName: "processListModal"
|
||||
title: I18n.tr("System Monitor", "sysmon window title")
|
||||
minimumSize: Qt.size(750, 550)
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 720
|
||||
minimumSize: Qt.size(650, 400)
|
||||
implicitWidth: 900
|
||||
implicitHeight: 680
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
onCurrentTabChanged: {
|
||||
if (visible && currentTab === 0 && searchField.visible)
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
closingModal();
|
||||
searchText = "";
|
||||
expandedPid = "";
|
||||
if (processesTabLoader.item)
|
||||
processesTabLoader.item.reset();
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
|
||||
} else {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk", "system"]);
|
||||
Qt.callLater(() => {
|
||||
if (currentTab === 0 && searchField.visible)
|
||||
searchField.forceActiveFocus();
|
||||
else if (contentFocusScope)
|
||||
if (contentFocusScope) {
|
||||
contentFocusScope.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
|
||||
PerformanceTab {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
|
||||
SystemTab {}
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: processContextMenu
|
||||
parentFocusItem: contentFocusScope
|
||||
onProcessKilled: {
|
||||
if (processesTabLoader.item)
|
||||
processesTabLoader.item.forceRefresh(3);
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
@@ -130,9 +115,6 @@ FloatingWindow {
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (processContextMenu.visible)
|
||||
return;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_1:
|
||||
currentTab = 0;
|
||||
@@ -146,43 +128,7 @@ FloatingWindow {
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_4:
|
||||
currentTab = 3;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
nextTab();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
previousTab();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Escape:
|
||||
if (searchText.length > 0) {
|
||||
searchText = "";
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (currentTab === 0 && processesTabLoader.item?.keyboardNavigationActive) {
|
||||
processesTabLoader.item.reset();
|
||||
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;
|
||||
}
|
||||
|
||||
if (currentTab === 0 && processesTabLoader.item)
|
||||
processesTabLoader.item.handleKey(event);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -215,7 +161,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message")
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -225,14 +171,14 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
visible: DgopService.dgopAvailable
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 48
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -287,278 +233,166 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 52
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
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
|
||||
ignoreUpDownKeys: true
|
||||
keyForwardTargets: [contentFocusScope]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Loader {
|
||||
id: processesTabLoader
|
||||
ColumnLayout {
|
||||
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
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
anchors.topMargin: 0
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes:", "process count label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: DgopService.processCount.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
Repeater {
|
||||
model: tabNames
|
||||
|
||||
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
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Uptime:", "uptime label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
Loader {
|
||||
id: processesTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.shortUptime || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: performanceTab
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
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
|
||||
|
||||
DankIcon {
|
||||
name: "swap_horiz"
|
||||
size: 14
|
||||
color: Theme.info
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: systemTab
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
|
||||
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
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
@@ -20,10 +19,6 @@ Item {
|
||||
property string searchMode: "apps"
|
||||
property bool usePopupContextMenu: false
|
||||
|
||||
property bool editMode: false
|
||||
property var editingApp: null
|
||||
property string editAppId: ""
|
||||
|
||||
function resetScroll() {
|
||||
if (searchMode === "apps") {
|
||||
resultsView.resetScroll();
|
||||
@@ -48,49 +43,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function openEditMode(app) {
|
||||
if (!app)
|
||||
return;
|
||||
editingApp = app;
|
||||
editAppId = app.id || app.execString || app.exec || "";
|
||||
const existing = SessionData.getAppOverride(editAppId);
|
||||
editNameField.text = existing?.name || "";
|
||||
editIconField.text = existing?.icon || "";
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
editMode = false;
|
||||
editingApp = null;
|
||||
editAppId = "";
|
||||
Qt.callLater(() => searchField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function saveAppOverride() {
|
||||
const override = {};
|
||||
if (editNameField.text.trim())
|
||||
override.name = editNameField.text.trim();
|
||||
if (editIconField.text.trim())
|
||||
override.icon = editIconField.text.trim();
|
||||
if (editCommentField.text.trim())
|
||||
override.comment = editCommentField.text.trim();
|
||||
if (editEnvVarsField.text.trim())
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function resetAppOverride() {
|
||||
SessionData.clearAppOverride(editAppId);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
onSearchModeChanged: {
|
||||
if (searchMode === "files") {
|
||||
appLauncher.keyboardNavigationActive = false;
|
||||
@@ -103,16 +55,10 @@ Item {
|
||||
focus: true
|
||||
clip: false
|
||||
Keys.onPressed: event => {
|
||||
if (editMode) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
closeEditMode();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide();
|
||||
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (searchMode === "apps") {
|
||||
@@ -209,6 +155,7 @@ Item {
|
||||
if (searchMode === "apps" && appLauncher.model.count > 0) {
|
||||
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
|
||||
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
|
||||
|
||||
if (selectedApp && menu && resultsView) {
|
||||
const itemPos = resultsView.getSelectedItemPosition();
|
||||
const contentPos = resultsView.mapToItem(spotlightKeyHandler, itemPos.x, itemPos.y);
|
||||
@@ -221,6 +168,7 @@ Item {
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncher
|
||||
|
||||
viewMode: SettingsData.spotlightModalViewMode
|
||||
gridColumns: SettingsData.appLauncherGridColumns
|
||||
onAppLaunched: () => {
|
||||
@@ -237,6 +185,7 @@ Item {
|
||||
|
||||
FileSearchController {
|
||||
id: fileSearchController
|
||||
|
||||
onFileOpened: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide();
|
||||
@@ -248,6 +197,7 @@ Item {
|
||||
|
||||
SpotlightContextMenuPopup {
|
||||
id: popupContextMenu
|
||||
|
||||
parent: spotlightKeyHandler
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
@@ -281,37 +231,20 @@ Item {
|
||||
target: parentModal
|
||||
function onSpotlightOpenChanged() {
|
||||
if (parentModal && !parentModal.spotlightOpen) {
|
||||
if (layerContextMenuLoader.item)
|
||||
if (layerContextMenuLoader.item) {
|
||||
layerContextMenuLoader.item.hide();
|
||||
}
|
||||
popupContextMenu.hide();
|
||||
if (editMode)
|
||||
closeEditMode();
|
||||
}
|
||||
}
|
||||
enabled: parentModal !== null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: popupContextMenu
|
||||
function onEditAppRequested(app) {
|
||||
spotlightKeyHandler.openEditMode(app);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: layerContextMenuLoader.item
|
||||
function onEditAppRequested(app) {
|
||||
spotlightKeyHandler.openEditMode(app);
|
||||
}
|
||||
enabled: layerContextMenuLoader.item !== null
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
clip: false
|
||||
visible: !editMode
|
||||
|
||||
Item {
|
||||
id: searchRow
|
||||
@@ -342,14 +275,18 @@ Item {
|
||||
ignoreTabKeys: true
|
||||
keyForwardTargets: [spotlightKeyHandler]
|
||||
onTextChanged: {
|
||||
if (searchMode === "apps")
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.searchQuery = text;
|
||||
}
|
||||
}
|
||||
onTextEdited: {
|
||||
updateSearchMode();
|
||||
}
|
||||
onTextEdited: updateSearchMode()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide();
|
||||
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||
if (searchMode === "apps") {
|
||||
@@ -397,10 +334,13 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: appLauncher.setViewMode("list")
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,10 +359,13 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: appLauncher.setViewMode("grid")
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("grid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,6 +379,7 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: filenameFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
@@ -450,10 +394,13 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
id: filenameFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: fileSearchController.searchField = "filename"
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "filename";
|
||||
}
|
||||
onEntered: {
|
||||
filenameTooltipLoader.active = true;
|
||||
Qt.callLater(() => {
|
||||
@@ -466,6 +413,7 @@ Item {
|
||||
onExited: {
|
||||
if (filenameTooltipLoader.item)
|
||||
filenameTooltipLoader.item.hide();
|
||||
|
||||
filenameTooltipLoader.active = false;
|
||||
}
|
||||
}
|
||||
@@ -473,6 +421,7 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: contentFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
@@ -487,10 +436,13 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
id: contentFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: fileSearchController.searchField = "body"
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "body";
|
||||
}
|
||||
onEntered: {
|
||||
contentTooltipLoader.active = true;
|
||||
Qt.callLater(() => {
|
||||
@@ -503,6 +455,7 @@ Item {
|
||||
onExited: {
|
||||
if (contentTooltipLoader.item)
|
||||
contentTooltipLoader.item.hide();
|
||||
|
||||
contentTooltipLoader.active = false;
|
||||
}
|
||||
}
|
||||
@@ -521,10 +474,13 @@ Item {
|
||||
anchors.fill: parent
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
visible: searchMode === "apps"
|
||||
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
|
||||
|
||||
if (menu?.show) {
|
||||
const isPopup = menu.contentItem !== undefined;
|
||||
|
||||
if (isPopup) {
|
||||
const localPos = popupContextMenu.parent.mapFromItem(null, mouseX, mouseY);
|
||||
menu.show(localPos.x, localPos.y, modelData, false);
|
||||
@@ -544,320 +500,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: editView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: editMode
|
||||
focus: editMode
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
closeEditMode();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_S:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_R:
|
||||
if ((event.modifiers & Qt.ControlModifier) && SessionData.getAppOverride(editAppId) !== null) {
|
||||
resetAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 40
|
||||
height: 40
|
||||
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Edit App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: editingApp?.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - y - buttonsRow.height - Theme.spacingM
|
||||
contentHeight: editFieldsColumn.height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: editFieldsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
keyNavigationBacktab: editExtraFlagsField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Description")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Environment Variables")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "KEY=value KEY2=value2"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Extra Arguments")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonsRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
id: resetButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
visible: SessionData.getAppOverride(editAppId) !== null
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reset")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resetButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: resetAppOverride()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cancelButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: saveButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: saveAppOverride()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: filenameTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
|
||||
@@ -18,8 +19,6 @@ PanelWindow {
|
||||
property real menuPositionX: 0
|
||||
property real menuPositionY: 0
|
||||
|
||||
signal editAppRequested(var app)
|
||||
|
||||
readonly property real shadowBuffer: 5
|
||||
|
||||
screen: parentModal?.effectiveScreen
|
||||
@@ -107,7 +106,6 @@ PanelWindow {
|
||||
}
|
||||
|
||||
onHideRequested: root.hide()
|
||||
onEditAppRequested: app => root.editAppRequested(app)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -68,27 +68,6 @@ Item {
|
||||
hideRequested();
|
||||
}
|
||||
|
||||
readonly property bool isRegularApp: desktopEntry && !currentApp?.isPlugin && !currentApp?.isCore && !currentApp?.isAction && !currentApp?.isBuiltInLauncher
|
||||
|
||||
signal editAppRequested(var app)
|
||||
|
||||
function hideCurrentApp() {
|
||||
if (!desktopEntry)
|
||||
return;
|
||||
const appId = desktopEntry.id || desktopEntry.execString || "";
|
||||
SessionData.hideApp(appId);
|
||||
if (appLauncher)
|
||||
appLauncher.updateFilteredModel();
|
||||
hideRequested();
|
||||
}
|
||||
|
||||
function editCurrentApp() {
|
||||
if (!desktopEntry)
|
||||
return;
|
||||
editAppRequested(desktopEntry);
|
||||
hideRequested();
|
||||
}
|
||||
|
||||
readonly property var menuItems: {
|
||||
const items = [];
|
||||
|
||||
@@ -124,21 +103,6 @@ Item {
|
||||
action: togglePin
|
||||
});
|
||||
|
||||
if (isRegularApp) {
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "visibility_off",
|
||||
text: I18n.tr("Hide App"),
|
||||
action: hideCurrentApp
|
||||
});
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "edit",
|
||||
text: I18n.tr("Edit App"),
|
||||
action: editCurrentApp
|
||||
});
|
||||
}
|
||||
|
||||
if (desktopEntry && desktopEntry.actions) {
|
||||
items.push({
|
||||
type: "separator"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
|
||||
@@ -10,8 +11,6 @@ Popup {
|
||||
property var parentHandler: null
|
||||
property var searchField: null
|
||||
|
||||
signal editAppRequested(var app)
|
||||
|
||||
function show(x, y, app, fromKeyboard) {
|
||||
fromKeyboard = fromKeyboard || false;
|
||||
menuContent.currentApp = app;
|
||||
@@ -54,7 +53,7 @@ Popup {
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = true;
|
||||
}
|
||||
if (searchField?.visible) {
|
||||
if (searchField) {
|
||||
Qt.callLater(() => {
|
||||
searchField.forceActiveFocus();
|
||||
});
|
||||
@@ -85,6 +84,5 @@ Popup {
|
||||
id: menuContent
|
||||
appLauncher: root.appLauncher
|
||||
onHideRequested: root.hide()
|
||||
onEditAppRequested: app => root.editAppRequested(app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,20 +65,6 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
function showWithEditApp(app) {
|
||||
openedFromOverview = false;
|
||||
isClosing = false;
|
||||
resetContent();
|
||||
spotlightOpen = true;
|
||||
open();
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent?.appLauncher)
|
||||
spotlightContent.appLauncher.ensureInitialized();
|
||||
if (spotlightContent?.openEditMode)
|
||||
spotlightContent.openEditMode(app);
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankPopout {
|
||||
@@ -11,63 +15,6 @@ DankPopout {
|
||||
|
||||
property string searchMode: "apps"
|
||||
property alias fileSearch: fileSearchController
|
||||
property bool editMode: false
|
||||
property var editingApp: null
|
||||
property string editAppId: ""
|
||||
|
||||
function openEditMode(app) {
|
||||
if (!app)
|
||||
return;
|
||||
editingApp = app;
|
||||
editAppId = app.id || app.execString || app.exec || "";
|
||||
const existing = SessionData.getAppOverride(editAppId);
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.searchField.focus = false;
|
||||
contentLoader.item.editNameField.text = existing?.name || "";
|
||||
contentLoader.item.editIconField.text = existing?.icon || "";
|
||||
contentLoader.item.editCommentField.text = existing?.comment || "";
|
||||
contentLoader.item.editEnvVarsField.text = existing?.envVars || "";
|
||||
contentLoader.item.editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
}
|
||||
editMode = true;
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item?.editNameField)
|
||||
contentLoader.item.editNameField.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
editMode = false;
|
||||
editingApp = null;
|
||||
editAppId = "";
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item?.searchField)
|
||||
contentLoader.item.searchField.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
function saveAppOverride() {
|
||||
const override = {};
|
||||
if (contentLoader.item) {
|
||||
if (contentLoader.item.editNameField.text.trim())
|
||||
override.name = contentLoader.item.editNameField.text.trim();
|
||||
if (contentLoader.item.editIconField.text.trim())
|
||||
override.icon = contentLoader.item.editIconField.text.trim();
|
||||
if (contentLoader.item.editCommentField.text.trim())
|
||||
override.comment = contentLoader.item.editCommentField.text.trim();
|
||||
if (contentLoader.item.editEnvVarsField.text.trim())
|
||||
override.envVars = contentLoader.item.editEnvVarsField.text.trim();
|
||||
if (contentLoader.item.editExtraFlagsField.text.trim())
|
||||
override.extraFlags = contentLoader.item.editExtraFlagsField.text.trim();
|
||||
}
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function resetAppOverride() {
|
||||
SessionData.clearAppOverride(editAppId);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function updateSearchMode(text) {
|
||||
if (text.startsWith("/")) {
|
||||
@@ -95,25 +42,16 @@ DankPopout {
|
||||
popupHeight: 600
|
||||
triggerWidth: 40
|
||||
positioning: ""
|
||||
contentHandlesKeys: editMode
|
||||
|
||||
onBackgroundClicked: {
|
||||
if (contextMenu.visible) {
|
||||
contextMenu.close();
|
||||
return;
|
||||
}
|
||||
if (editMode) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
searchMode = "apps";
|
||||
editMode = false;
|
||||
editingApp = null;
|
||||
editAppId = "";
|
||||
appLauncher.ensureInitialized();
|
||||
appLauncher.searchQuery = "";
|
||||
appLauncher.selectedIndex = 0;
|
||||
@@ -162,45 +100,8 @@ DankPopout {
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property alias searchField: searchField
|
||||
property alias keyHandler: keyHandler
|
||||
property alias editNameField: editNameField
|
||||
property alias editIconField: editIconField
|
||||
property alias editCommentField: editCommentField
|
||||
property alias editEnvVarsField: editEnvVarsField
|
||||
property alias editExtraFlagsField: editExtraFlagsField
|
||||
|
||||
focus: true
|
||||
color: "transparent"
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (appDrawerPopout.editMode) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
appDrawerPopout.closeEditMode();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
appDrawerPopout.saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_S:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
appDrawerPopout.saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_R:
|
||||
if ((event.modifiers & Qt.ControlModifier) && SessionData.getAppOverride(appDrawerPopout.editAppId) !== null) {
|
||||
appDrawerPopout.resetAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
@@ -239,7 +140,7 @@ DankPopout {
|
||||
id: keyHandler
|
||||
|
||||
anchors.fill: parent
|
||||
focus: !appDrawerPopout.editMode
|
||||
focus: true
|
||||
|
||||
function selectNext() {
|
||||
switch (appDrawerPopout.searchMode) {
|
||||
@@ -271,29 +172,6 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedItemPosition() {
|
||||
const index = appLauncher.selectedIndex;
|
||||
if (appLauncher.viewMode === "list") {
|
||||
const y = index * (appList.itemHeight + appList.itemSpacing) - appList.contentY;
|
||||
return Qt.point(appList.width / 2, y + appList.itemHeight / 2 + appList.y);
|
||||
}
|
||||
const row = Math.floor(index / appGrid.actualColumns);
|
||||
const col = index % appGrid.actualColumns;
|
||||
const x = col * appGrid.cellWidth + appGrid.cellWidth / 2;
|
||||
const y = row * appGrid.cellHeight - appGrid.contentY + appGrid.cellHeight / 2 + appGrid.y;
|
||||
return Qt.point(x, y);
|
||||
}
|
||||
|
||||
function openContextMenuForSelected() {
|
||||
if (appDrawerPopout.searchMode !== "apps" || appLauncher.model.count === 0)
|
||||
return;
|
||||
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
|
||||
if (!selectedApp)
|
||||
return;
|
||||
const pos = getSelectedItemPosition();
|
||||
contextMenu.show(pos.x, pos.y, selectedApp, true);
|
||||
}
|
||||
|
||||
readonly property var keyMappings: {
|
||||
const mappings = {};
|
||||
mappings[Qt.Key_Escape] = () => appDrawerPopout.close();
|
||||
@@ -303,8 +181,6 @@ DankPopout {
|
||||
mappings[Qt.Key_Enter] = () => keyHandler.activateSelected();
|
||||
mappings[Qt.Key_Tab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : keyHandler.selectNext();
|
||||
mappings[Qt.Key_Backtab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : keyHandler.selectPrevious();
|
||||
mappings[Qt.Key_Menu] = () => keyHandler.openContextMenuForSelected();
|
||||
mappings[Qt.Key_F10] = () => keyHandler.openContextMenuForSelected();
|
||||
|
||||
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
mappings[Qt.Key_Right] = () => I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
|
||||
@@ -315,9 +191,6 @@ DankPopout {
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (appDrawerPopout.editMode)
|
||||
return;
|
||||
|
||||
if (keyMappings[event.key]) {
|
||||
keyMappings[event.key]();
|
||||
event.accepted = true;
|
||||
@@ -325,8 +198,9 @@ DankPopout {
|
||||
}
|
||||
|
||||
const hasCtrl = event.modifiers & Qt.ControlModifier;
|
||||
if (!hasCtrl)
|
||||
if (!hasCtrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_N:
|
||||
@@ -360,7 +234,6 @@ DankPopout {
|
||||
x: Theme.spacingS
|
||||
y: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
visible: !appDrawerPopout.editMode
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
@@ -614,7 +487,7 @@ DankPopout {
|
||||
appLauncher.launchApp(modelData);
|
||||
}
|
||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||
contextMenu.show(mouseX, mouseY, modelData, false);
|
||||
contextMenu.show(mouseX, mouseY, modelData);
|
||||
}
|
||||
onKeyboardNavigationReset: {
|
||||
appLauncher.keyboardNavigationActive = false;
|
||||
@@ -701,7 +574,7 @@ DankPopout {
|
||||
appLauncher.launchApp(modelData);
|
||||
}
|
||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||
contextMenu.show(mouseX, mouseY, modelData, false);
|
||||
contextMenu.show(mouseX, mouseY, modelData);
|
||||
}
|
||||
onKeyboardNavigationReset: {
|
||||
appLauncher.keyboardNavigationActive = false;
|
||||
@@ -742,281 +615,6 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: editView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appDrawerPopout.editMode
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: appDrawerPopout.closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 40
|
||||
height: 40
|
||||
source: appDrawerPopout.editingApp?.icon ? "image://icon/" + appDrawerPopout.editingApp.icon : "image://icon/application-x-executable"
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Edit App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: appDrawerPopout.editingApp?.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - y - editButtonsRow.height - Theme.spacingM
|
||||
contentHeight: editFieldsColumn.height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: editFieldsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
height: 44
|
||||
focus: true
|
||||
placeholderText: appDrawerPopout.editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
keyNavigationBacktab: editExtraFlagsField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: appDrawerPopout.editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Description")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: appDrawerPopout.editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Environment Variables")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "KEY=value KEY2=value2"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Extra Arguments")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: editButtonsRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
visible: SessionData.getAppOverride(appDrawerPopout.editAppId) !== null
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reset")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resetButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: appDrawerPopout.resetAppOverride()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: appDrawerPopout.closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.onPrimary
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: appDrawerPopout.saveAppOverride()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: contextMenu.visible
|
||||
@@ -1026,21 +624,338 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightContextMenuPopup {
|
||||
Popup {
|
||||
id: contextMenu
|
||||
|
||||
parent: contentLoader.item
|
||||
appLauncher: appLauncher
|
||||
parentHandler: contentLoader.item?.keyHandler ?? null
|
||||
searchField: contentLoader.item?.searchField ?? null
|
||||
visible: false
|
||||
z: 1000
|
||||
}
|
||||
property var currentApp: null
|
||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
||||
readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""
|
||||
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
|
||||
|
||||
Connections {
|
||||
target: contextMenu
|
||||
function onEditAppRequested(app) {
|
||||
appDrawerPopout.openEditMode(app);
|
||||
function show(x, y, app) {
|
||||
currentApp = app;
|
||||
let finalX = x + 4;
|
||||
let finalY = y + 4;
|
||||
|
||||
if (contextMenu.parent) {
|
||||
const parentWidth = contextMenu.parent.width;
|
||||
const parentHeight = contextMenu.parent.height;
|
||||
const menuWidth = contextMenu.width;
|
||||
const menuHeight = contextMenu.height;
|
||||
|
||||
if (finalX + menuWidth > parentWidth) {
|
||||
finalX = Math.max(0, parentWidth - menuWidth);
|
||||
}
|
||||
|
||||
if (finalY + menuHeight > parentHeight) {
|
||||
finalY = Math.max(0, parentHeight - menuHeight);
|
||||
}
|
||||
}
|
||||
|
||||
contextMenu.x = finalX;
|
||||
contextMenu.y = finalY;
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
contextMenu.close();
|
||||
}
|
||||
|
||||
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
modal: false
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: contextMenu.isPinned ? "keep_off" : "push_pin"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: contextMenu.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!contextMenu.desktopEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextMenu.isPinned) {
|
||||
SessionData.removePinnedApp(contextMenu.appId);
|
||||
} else {
|
||||
SessionData.addPinnedApp(contextMenu.appId);
|
||||
}
|
||||
contextMenu.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: actionRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.iconSize - 2
|
||||
height: Theme.iconSize - 2
|
||||
visible: modelData.icon && modelData.icon !== ""
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && contextMenu.desktopEntry) {
|
||||
SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData);
|
||||
if (contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp);
|
||||
}
|
||||
}
|
||||
contextMenu.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "launch"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launchMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (contextMenu.currentApp)
|
||||
appLauncher.launchApp(contextMenu.currentApp);
|
||||
|
||||
contextMenu.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.nvidiaCommand
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.nvidiaCommand
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: nvidiaMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch on dGPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nvidiaMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (contextMenu.desktopEntry) {
|
||||
SessionService.launchDesktopEntry(contextMenu.desktopEntry, true);
|
||||
if (contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp);
|
||||
}
|
||||
}
|
||||
contextMenu.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,16 +72,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onHiddenAppsChanged() {
|
||||
updateFilteredModel();
|
||||
}
|
||||
function onAppOverridesChanged() {
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilteredModel() {
|
||||
if (suppressUpdatesWhileLaunching) {
|
||||
suppressUpdatesWhileLaunching = false;
|
||||
@@ -134,7 +124,7 @@ Item {
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
||||
});
|
||||
const coreItems = AppSearchService.getCoreApps("");
|
||||
apps = AppSearchService.getVisibleApplications().concat(emptyTriggerItems).concat(coreItems);
|
||||
apps = AppSearchService.applications.concat(emptyTriggerItems).concat(coreItems);
|
||||
} else {
|
||||
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
|
||||
const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory));
|
||||
@@ -215,7 +205,6 @@ Item {
|
||||
"isPlugin": isPluginItem,
|
||||
"isCore": app.isCore === true,
|
||||
"isBuiltInLauncher": app.isBuiltInLauncher === true,
|
||||
"isAction": app.isAction === true,
|
||||
"appIndex": uniqueApps.length - 1,
|
||||
"pinned": app._pinned === true
|
||||
});
|
||||
@@ -294,13 +283,6 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appData.isAction && actualApp.parentApp && actualApp.actionData) {
|
||||
SessionService.launchDesktopAction(actualApp.parentApp, actualApp.actionData);
|
||||
appLaunched(appData);
|
||||
AppUsageHistoryData.addAppUsage(actualApp.parentApp);
|
||||
return;
|
||||
}
|
||||
|
||||
SessionService.launchDesktopEntry(actualApp);
|
||||
appLaunched(appData);
|
||||
AppUsageHistoryData.addAppUsage(actualApp);
|
||||
|
||||
@@ -296,9 +296,6 @@ Item {
|
||||
width: parent.width
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: isVertical ? widgetThickness : totalSize
|
||||
implicitHeight: isVertical ? totalSize : widgetThickness
|
||||
|
||||
Timer {
|
||||
id: layoutTimer
|
||||
interval: 0
|
||||
@@ -368,7 +365,6 @@ Item {
|
||||
onContentItemReady: contentItem => {
|
||||
contentItem.widthChanged.connect(() => layoutTimer.restart());
|
||||
contentItem.heightChanged.connect(() => layoutTimer.restart());
|
||||
layoutTimer.restart();
|
||||
}
|
||||
|
||||
onActiveChanged: layoutTimer.restart()
|
||||
|
||||
@@ -20,13 +20,6 @@ Item {
|
||||
|
||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||
|
||||
property alias hLeftSection: hLeftSection
|
||||
property alias hCenterSection: hCenterSection
|
||||
property alias hRightSection: hRightSection
|
||||
property alias vLeftSection: vLeftSection
|
||||
property alias vCenterSection: vCenterSection
|
||||
property alias vRightSection: vRightSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
|
||||
@@ -500,78 +500,8 @@ PanelWindow {
|
||||
height: axis.isVertical ? parent.height : maskThickness
|
||||
}
|
||||
|
||||
readonly property bool clickThroughEnabled: barConfig?.clickThrough ?? false
|
||||
|
||||
readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null
|
||||
readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null
|
||||
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
|
||||
|
||||
function sectionRect(section, isCenter) {
|
||||
if (!section)
|
||||
return {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
"h": 0
|
||||
};
|
||||
|
||||
const pos = section.mapToItem(barWindow.contentItem, 0, 0);
|
||||
const implW = section.implicitWidth || 0;
|
||||
const implH = section.implicitHeight || 0;
|
||||
|
||||
const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
|
||||
const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
|
||||
|
||||
const edgePad = 2;
|
||||
return {
|
||||
"x": pos.x + offsetX - edgePad,
|
||||
"y": pos.y + offsetY - edgePad,
|
||||
"w": implW + edgePad * 2,
|
||||
"h": implH + edgePad * 2
|
||||
};
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: clickThroughEnabled ? null : inputMask
|
||||
|
||||
Region {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false) : {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
"h": 0
|
||||
}
|
||||
x: r.x
|
||||
y: r.y
|
||||
width: r.w
|
||||
height: r.h
|
||||
}
|
||||
|
||||
Region {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true) : {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
"h": 0
|
||||
}
|
||||
x: r.x
|
||||
y: r.y
|
||||
width: r.w
|
||||
height: r.h
|
||||
}
|
||||
|
||||
Region {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false) : {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
"h": 0
|
||||
}
|
||||
x: r.x
|
||||
y: r.y
|
||||
width: r.w
|
||||
height: r.h
|
||||
}
|
||||
item: inputMask
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
@@ -731,8 +731,7 @@ Item {
|
||||
Flow {
|
||||
id: workspaceRow
|
||||
|
||||
x: isVertical ? visualBackground.x : (parent.width - implicitWidth) / 2
|
||||
y: isVertical ? (parent.height - implicitHeight) / 2 : visualBackground.y
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
@@ -755,17 +754,6 @@ Item {
|
||||
return !!(modelData && modelData.num === root.currentWorkspace);
|
||||
return modelData === root.currentWorkspace;
|
||||
}
|
||||
property bool isOccupied: {
|
||||
if (CompositorService.isHyprland)
|
||||
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
|
||||
if (CompositorService.isDwl)
|
||||
return modelData.clients > 0;
|
||||
if (CompositorService.isNiri) {
|
||||
const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
|
||||
return workspace ? (NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false) : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
property bool isPlaceholder: {
|
||||
if (root.useExtWorkspace)
|
||||
return !!(modelData && modelData.hidden);
|
||||
@@ -847,23 +835,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property color occupiedColor: {
|
||||
switch (SettingsData.workspaceOccupiedColorMode) {
|
||||
case "sec":
|
||||
return Theme.secondary;
|
||||
case "s":
|
||||
return Theme.surface;
|
||||
case "sc":
|
||||
return Theme.surfaceContainer;
|
||||
case "sch":
|
||||
return Theme.surfaceContainerHigh;
|
||||
case "schh":
|
||||
return Theme.surfaceContainerHighest;
|
||||
default:
|
||||
return unfocusedColor;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property color urgentColor: {
|
||||
switch (SettingsData.workspaceUrgentColorMode) {
|
||||
case "primary":
|
||||
@@ -997,13 +968,12 @@ Item {
|
||||
dataUpdateTimer.restart();
|
||||
}
|
||||
|
||||
width: root.isVertical ? root.widgetHeight : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.widgetHeight
|
||||
width: root.isVertical ? root.barThickness : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.barThickness
|
||||
|
||||
Rectangle {
|
||||
id: focusedBorderRing
|
||||
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
|
||||
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
|
||||
anchors.centerIn: parent
|
||||
width: {
|
||||
const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0;
|
||||
return delegateRoot.visualWidth + borderWidth * 2;
|
||||
@@ -1050,10 +1020,9 @@ Item {
|
||||
id: visualContent
|
||||
width: delegateRoot.visualWidth
|
||||
height: delegateRoot.visualHeight
|
||||
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
|
||||
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor
|
||||
color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : unfocusedColor
|
||||
|
||||
border.width: isUrgent ? 2 : 0
|
||||
border.color: isUrgent ? urgentColor : "transparent"
|
||||
|
||||
@@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -29,7 +28,7 @@ Variants {
|
||||
}
|
||||
|
||||
property var modelData: item
|
||||
property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide
|
||||
property bool autoHide: SettingsData.dockAutoHide
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
|
||||
@@ -112,133 +111,6 @@ Variants {
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
||||
property bool revealSticky: false
|
||||
|
||||
readonly property bool shouldHideForWindows: {
|
||||
if (!SettingsData.dockSmartAutoHide)
|
||||
return false;
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland)
|
||||
return false;
|
||||
|
||||
const screenName = dock.modelData?.name ?? "";
|
||||
const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
|
||||
const screenWidth = dock.screen?.width ?? 0;
|
||||
const screenHeight = dock.screen?.height ?? 0;
|
||||
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.windows;
|
||||
|
||||
let currentWorkspaceId = null;
|
||||
for (let i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
const ws = NiriService.allWorkspaces[i];
|
||||
if (ws.output === screenName && ws.is_active) {
|
||||
currentWorkspaceId = ws.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < NiriService.windows.length; i++) {
|
||||
const win = NiriService.windows[i];
|
||||
if (win.workspace_id !== currentWorkspaceId)
|
||||
continue;
|
||||
|
||||
// Get window position and size from layout data
|
||||
const tilePos = win.layout?.tile_pos_in_workspace_view;
|
||||
const winSize = win.layout?.window_size || win.layout?.tile_size;
|
||||
|
||||
if (tilePos && winSize) {
|
||||
const winX = tilePos[0];
|
||||
const winY = tilePos[1];
|
||||
const winW = winSize[0];
|
||||
const winH = winSize[1];
|
||||
|
||||
switch (SettingsData.dockPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
if (winY < dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Bottom:
|
||||
if (winY + winH > screenHeight - dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Left:
|
||||
if (winX < dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Right:
|
||||
if (winX + winW > screenWidth - dockThickness)
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
} else if (!win.is_floating) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hyprland implementation
|
||||
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
|
||||
if (filtered.length === 0)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
const toplevel = filtered[i];
|
||||
|
||||
let hyprToplevel = null;
|
||||
if (Hyprland.toplevels) {
|
||||
const hyprToplevels = Array.from(Hyprland.toplevels.values);
|
||||
for (let j = 0; j < hyprToplevels.length; j++) {
|
||||
if (hyprToplevels[j].wayland === toplevel) {
|
||||
hyprToplevel = hyprToplevels[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hyprToplevel?.lastIpcObject)
|
||||
continue;
|
||||
|
||||
const ipc = hyprToplevel.lastIpcObject;
|
||||
const at = ipc.at;
|
||||
const size = ipc.size;
|
||||
if (!at || !size)
|
||||
continue;
|
||||
|
||||
const monX = hyprToplevel.monitor?.x ?? 0;
|
||||
const monY = hyprToplevel.monitor?.y ?? 0;
|
||||
|
||||
const winX = at[0] - monX;
|
||||
const winY = at[1] - monY;
|
||||
const winW = size[0];
|
||||
const winH = size[1];
|
||||
|
||||
switch (SettingsData.dockPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
if (winY < dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Bottom:
|
||||
if (winY + winH > screenHeight - dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Left:
|
||||
if (winX < dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Right:
|
||||
if (winX + winW > screenWidth - dockThickness)
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: revealHold
|
||||
interval: 250
|
||||
@@ -250,15 +122,6 @@ Variants {
|
||||
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Smart auto-hide: show dock when no windows overlap, hide when they do
|
||||
if (SettingsData.dockSmartAutoHide) {
|
||||
if (shouldHideForWindows)
|
||||
return dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky;
|
||||
return true; // No overlapping windows - show dock
|
||||
}
|
||||
|
||||
// Regular auto-hide: always hide unless hovering
|
||||
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,34 +12,8 @@ Scope {
|
||||
|
||||
property string sharedPasswordBuffer: ""
|
||||
property bool shouldLock: false
|
||||
|
||||
onShouldLockChanged: {
|
||||
if (shouldLock && lockPowerOffArmed) {
|
||||
lockStateCheck.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: lockStateCheck
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (sessionLock.locked && lockPowerOffArmed) {
|
||||
pendingLock = false;
|
||||
IdleService.monitorsOff = true;
|
||||
CompositorService.powerOffMonitors();
|
||||
lockWakeAllowed = false;
|
||||
lockWakeDebounce.restart();
|
||||
lockPowerOffArmed = false;
|
||||
dpmsReapplyTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool lockInitiatedLocally: false
|
||||
property bool pendingLock: false
|
||||
property bool lockPowerOffArmed: false
|
||||
property bool lockWakeAllowed: false
|
||||
|
||||
Component.onCompleted: {
|
||||
IdleService.lockComponent = this;
|
||||
@@ -63,7 +37,6 @@ Scope {
|
||||
return;
|
||||
|
||||
lockInitiatedLocally = true;
|
||||
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
|
||||
|
||||
if (!SessionService.active && SessionService.loginctlAvailable) {
|
||||
pendingLock = true;
|
||||
@@ -105,7 +78,6 @@ Scope {
|
||||
return;
|
||||
}
|
||||
lockInitiatedLocally = false;
|
||||
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
|
||||
shouldLock = true;
|
||||
}
|
||||
|
||||
@@ -124,13 +96,11 @@ Scope {
|
||||
if (SessionService.active && pendingLock) {
|
||||
pendingLock = false;
|
||||
lockInitiatedLocally = true;
|
||||
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
|
||||
shouldLock = true;
|
||||
return;
|
||||
}
|
||||
if (SessionService.locked && !shouldLock && !pendingLock) {
|
||||
lockInitiatedLocally = false;
|
||||
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
|
||||
shouldLock = true;
|
||||
}
|
||||
}
|
||||
@@ -149,6 +119,13 @@ Scope {
|
||||
|
||||
locked: shouldLock
|
||||
|
||||
onLockedChanged: {
|
||||
if (locked) {
|
||||
pendingLock = false;
|
||||
dpmsReapplyTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: lockSurface
|
||||
|
||||
@@ -178,33 +155,7 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: sessionLock
|
||||
|
||||
function onLockedChanged() {
|
||||
if (sessionLock.locked) {
|
||||
pendingLock = false;
|
||||
if (lockPowerOffArmed && SettingsData.lockScreenPowerOffMonitorsOnLock) {
|
||||
IdleService.monitorsOff = true;
|
||||
CompositorService.powerOffMonitors();
|
||||
lockWakeAllowed = false;
|
||||
lockWakeDebounce.restart();
|
||||
}
|
||||
lockPowerOffArmed = false;
|
||||
dpmsReapplyTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
lockWakeAllowed = false;
|
||||
if (IdleService.monitorsOff && SettingsData.lockScreenPowerOffMonitorsOnLock) {
|
||||
IdleService.monitorsOff = false;
|
||||
CompositorService.powerOnMonitors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LockScreenDemo {
|
||||
|
||||
id: demoWindow
|
||||
}
|
||||
|
||||
@@ -249,46 +200,4 @@ Scope {
|
||||
repeat: false
|
||||
onTriggered: IdleService.reapplyDpmsIfNeeded()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: lockWakeDebounce
|
||||
interval: 200
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!sessionLock.locked)
|
||||
return;
|
||||
if (!SettingsData.lockScreenPowerOffMonitorsOnLock)
|
||||
return;
|
||||
if (!IdleService.monitorsOff) {
|
||||
lockWakeAllowed = true;
|
||||
return;
|
||||
}
|
||||
if (lockWakeAllowed) {
|
||||
IdleService.monitorsOff = false;
|
||||
CompositorService.powerOnMonitors();
|
||||
} else {
|
||||
lockWakeAllowed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: sessionLock.locked
|
||||
hoverEnabled: enabled
|
||||
onPressed: lockWakeDebounce.restart()
|
||||
onPositionChanged: lockWakeDebounce.restart()
|
||||
onWheel: lockWakeDebounce.restart()
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: sessionLock.locked
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!sessionLock.locked)
|
||||
return;
|
||||
lockWakeDebounce.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
@@ -52,9 +51,7 @@ Item {
|
||||
if (tabIndex === NotepadStorageService.currentTabIndex && hasUnsavedChanges()) {
|
||||
root.pendingAction = "close_tab_" + tabIndex;
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
confirmationDialog.open();
|
||||
} else {
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
@@ -104,9 +101,7 @@ Item {
|
||||
root.pendingFileUrl = fileUrl;
|
||||
root.pendingAction = "load_file";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
confirmationDialog.open();
|
||||
} else {
|
||||
performLoadFromFile(fileUrl);
|
||||
}
|
||||
@@ -175,27 +170,29 @@ Item {
|
||||
saveToFile(fileUrl);
|
||||
} else {
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowserLoader.active = true;
|
||||
if (saveBrowserLoader.item)
|
||||
saveBrowserLoader.item.open();
|
||||
saveBrowser.open();
|
||||
}
|
||||
}
|
||||
|
||||
onOpenRequested: {
|
||||
textEditor.autoSaveToSession();
|
||||
if (textEditor.text.length > 0) {
|
||||
createNewTab();
|
||||
if (hasUnsavedChanges()) {
|
||||
root.pendingAction = "open";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
} else {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
}
|
||||
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
}
|
||||
|
||||
onNewRequested: {
|
||||
textEditor.autoSaveToSession();
|
||||
createNewTab();
|
||||
if (hasUnsavedChanges()) {
|
||||
root.pendingAction = "new";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
} else {
|
||||
createNewTab();
|
||||
}
|
||||
}
|
||||
|
||||
onEscapePressed: {
|
||||
@@ -252,259 +249,238 @@ Item {
|
||||
onLoadFailed: error => {}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: saveBrowserLoader
|
||||
active: false
|
||||
FileBrowserModal {
|
||||
id: saveBrowser
|
||||
|
||||
FileBrowserSurfaceModal {
|
||||
id: saveBrowser
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
saveToFile(fileUrl);
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
if (currentTab) {
|
||||
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
|
||||
}
|
||||
|
||||
saveToFile(fileUrl);
|
||||
|
||||
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 = "";
|
||||
|
||||
close();
|
||||
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 = "";
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: loadBrowserLoader
|
||||
active: false
|
||||
FileBrowserModal {
|
||||
id: loadBrowser
|
||||
|
||||
FileBrowserSurfaceModal {
|
||||
id: loadBrowser
|
||||
browserTitle: I18n.tr("Open Notepad File")
|
||||
browserIcon: "folder_open"
|
||||
browserType: "notepad_load"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
loadFromFile(fileUrl);
|
||||
close();
|
||||
}
|
||||
|
||||
loadFromFile(fileUrl);
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: confirmationDialogLoader
|
||||
active: false
|
||||
DankModal {
|
||||
id: confirmationDialog
|
||||
|
||||
DankModal {
|
||||
id: confirmationDialog
|
||||
width: 400
|
||||
height: 180
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
|
||||
width: 400
|
||||
height: 180
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
onBackgroundClicked: {
|
||||
close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
|
||||
onBackgroundClicked: {
|
||||
close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: event => {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
event.accepted = true;
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
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 {
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
id: discardText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Don't Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
saveBrowserLoader.active = true;
|
||||
if (saveBrowserLoader.item)
|
||||
saveBrowserLoader.item.open();
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
saveBrowser.open();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +52,20 @@ Column {
|
||||
|
||||
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
||||
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
|
||||
readonly property real tabWidth: 128
|
||||
readonly property real calculatedWidth: {
|
||||
const textWidth = tabText.paintedWidth || 100;
|
||||
const closeButtonWidth = NotepadStorageService.tabs.length > 1 ? 20 : 0;
|
||||
const spacing = Theme.spacingXS;
|
||||
const padding = Theme.spacingM * 2;
|
||||
return Math.max(120, Math.min(200, textWidth + closeButtonWidth + spacing + padding));
|
||||
}
|
||||
|
||||
width: tabWidth
|
||||
width: calculatedWidth
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
border.width: isActive ? 0 : 1
|
||||
border.color: Theme.outlineMedium
|
||||
clip: true
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
@@ -74,14 +79,11 @@ Column {
|
||||
|
||||
Row {
|
||||
id: tabContent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
id: tabText
|
||||
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
||||
text: {
|
||||
var prefix = "";
|
||||
if (hasUnsavedChangesForTab(modelData)) {
|
||||
@@ -94,7 +96,6 @@ Column {
|
||||
font.weight: isActive ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ Column {
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: textArea
|
||||
placeholderText: ""
|
||||
placeholderText: I18n.tr("Start typing your notes here...")
|
||||
placeholderTextColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily)
|
||||
font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale
|
||||
@@ -404,22 +404,6 @@ Column {
|
||||
topPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
bottomPadding: Theme.spacingM
|
||||
cursorDelegate: Rectangle {
|
||||
width: 1.5
|
||||
radius: 1
|
||||
color: Theme.surfaceText
|
||||
x: textArea.cursorRectangle.x
|
||||
y: textArea.cursorRectangle.y
|
||||
height: textArea.cursorRectangle.height
|
||||
opacity: 1.0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: textArea.activeFocus
|
||||
loops: Animation.Infinite
|
||||
PropertyAnimation { from: 1.0; to: 0.0; duration: 650; easing.type: Easing.InOutQuad }
|
||||
PropertyAnimation { from: 0.0; to: 1.0; duration: 650; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadCurrentTabContent()
|
||||
@@ -496,20 +480,6 @@ Column {
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholderOverlay
|
||||
text: I18n.tr("Start typing your notes here...")
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.family: textArea.font.family
|
||||
font.pixelSize: textArea.font.pixelSize
|
||||
visible: textArea.text.length === 0
|
||||
anchors.left: textArea.left
|
||||
anchors.top: textArea.top
|
||||
anchors.leftMargin: textArea.leftPadding
|
||||
anchors.topMargin: textArea.topPadding
|
||||
z: textArea.z + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ Column {
|
||||
property string detailsText: ""
|
||||
property bool showCloseButton: false
|
||||
property var closePopout: null
|
||||
property alias headerActions: headerActionsLoader.sourceComponent
|
||||
|
||||
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
|
||||
readonly property int detailsHeight: popoutDetails.visible ? popoutDetails.implicitHeight : 0
|
||||
@@ -32,40 +31,31 @@ Column {
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Row {
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
visible: root.showCloseButton
|
||||
|
||||
Loader {
|
||||
id: headerActionsLoader
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
visible: root.showCloseButton
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (root.closePopout) {
|
||||
root.closePopout();
|
||||
}
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (root.closePopout) {
|
||||
root.closePopout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
451
quickshell/Modules/ProcessList/PerformanceTab.qml
Normal file
451
quickshell/Modules/ProcessList/PerformanceTab.qml
Normal file
@@ -0,0 +1,451 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,61 +8,8 @@ Popup {
|
||||
id: processContextMenu
|
||||
|
||||
property var processData: null
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigation: false
|
||||
property var parentFocusItem: null
|
||||
|
||||
signal menuClosed
|
||||
signal processKilled
|
||||
|
||||
readonly property var menuItems: [
|
||||
{
|
||||
text: I18n.tr("Copy PID"),
|
||||
icon: "tag",
|
||||
action: copyPid,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Copy Name"),
|
||||
icon: "content_copy",
|
||||
action: copyName,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Copy Full Command"),
|
||||
icon: "code",
|
||||
action: copyFullCommand,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Kill Process"),
|
||||
icon: "close",
|
||||
action: killProcess,
|
||||
enabled: true,
|
||||
dangerous: true
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Force Kill (SIGKILL)"),
|
||||
icon: "dangerous",
|
||||
action: forceKillProcess,
|
||||
enabled: processData && processData.pid > 1000,
|
||||
dangerous: true
|
||||
}
|
||||
]
|
||||
|
||||
readonly property int visibleItemCount: {
|
||||
let count = 0;
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].type !== "separator")
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function show(x, y, fromKeyboard) {
|
||||
function show(x, y) {
|
||||
let finalX = x;
|
||||
let finalY = y;
|
||||
|
||||
@@ -72,111 +19,39 @@ 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;
|
||||
processContextMenu.y = finalY;
|
||||
keyboardNavigation = fromKeyboard || false;
|
||||
selectedIndex = fromKeyboard ? 0 : -1;
|
||||
open();
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (visibleItemCount === 0)
|
||||
return;
|
||||
let current = selectedIndex;
|
||||
let next = current;
|
||||
do {
|
||||
next = (next + 1) % menuItems.length;
|
||||
} while (menuItems[next].type === "separator" && next !== current)
|
||||
selectedIndex = next;
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (visibleItemCount === 0)
|
||||
return;
|
||||
let current = selectedIndex;
|
||||
let prev = current;
|
||||
do {
|
||||
prev = (prev - 1 + menuItems.length) % menuItems.length;
|
||||
} while (menuItems[prev].type === "separator" && prev !== current)
|
||||
selectedIndex = prev;
|
||||
}
|
||||
|
||||
function activateSelected() {
|
||||
if (selectedIndex < 0 || selectedIndex >= menuItems.length)
|
||||
return;
|
||||
const item = menuItems[selectedIndex];
|
||||
if (item.type === "separator" || !item.enabled)
|
||||
return;
|
||||
item.action();
|
||||
}
|
||||
|
||||
function copyPid() {
|
||||
if (processData)
|
||||
Quickshell.execDetached(["dms", "cl", "copy", processData.pid.toString()]);
|
||||
close();
|
||||
}
|
||||
|
||||
function copyName() {
|
||||
if (processData) {
|
||||
const name = processData.command || "";
|
||||
Quickshell.execDetached(["dms", "cl", "copy", name]);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
function copyFullCommand() {
|
||||
if (processData) {
|
||||
const fullCmd = processData.fullCommand || processData.command || "";
|
||||
Quickshell.execDetached(["dms", "cl", "copy", fullCmd]);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
function killProcess() {
|
||||
if (processData)
|
||||
Quickshell.execDetached(["kill", processData.pid.toString()]);
|
||||
processKilled();
|
||||
close();
|
||||
}
|
||||
|
||||
function forceKillProcess() {
|
||||
if (processData)
|
||||
Quickshell.execDetached(["kill", "-9", processData.pid.toString()]);
|
||||
processKilled();
|
||||
close();
|
||||
}
|
||||
|
||||
width: 200
|
||||
width: 180
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
onClosed: {
|
||||
closePolicy = Popup.CloseOnEscape;
|
||||
keyboardNavigation = false;
|
||||
selectedIndex = -1;
|
||||
menuClosed();
|
||||
if (parentFocusItem)
|
||||
Qt.callLater(() => parentFocusItem.forceActiveFocus());
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
outsideClickTimer.start();
|
||||
if (keyboardNavigation)
|
||||
Qt.callLater(() => keyboardHandler.forceActiveFocus());
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: outsideClickTimer
|
||||
|
||||
interval: 100
|
||||
onTriggered: processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
onTriggered: {
|
||||
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
@@ -184,147 +59,163 @@ 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)
|
||||
border.width: 1
|
||||
|
||||
Item {
|
||||
id: keyboardHandler
|
||||
anchors.fill: parent
|
||||
focus: keyboardNavigation
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_J:
|
||||
keyboardNavigation = true;
|
||||
selectNext();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_K:
|
||||
keyboardNavigation = true;
|
||||
selectPrevious();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Space:
|
||||
activateSelected();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Escape:
|
||||
case Qt.Key_Left:
|
||||
case Qt.Key_H:
|
||||
close();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: menuItems
|
||||
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"
|
||||
|
||||
Item {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: modelData.type === "separator" ? 5 : 32
|
||||
visible: modelData.type !== "separator" || index > 0
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
property int itemVisibleIndex: {
|
||||
let count = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (menuItems[i].type !== "separator")
|
||||
count++;
|
||||
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"
|
||||
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()]);
|
||||
}
|
||||
return count;
|
||||
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.type === "separator"
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15)
|
||||
}
|
||||
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"
|
||||
enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
Rectangle {
|
||||
id: menuItem
|
||||
visible: modelData.type !== "separator"
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!modelData.enabled)
|
||||
return "transparent";
|
||||
const isSelected = keyboardNavigation && selectedIndex === index;
|
||||
if (modelData.dangerous) {
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
|
||||
return menuItemArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent";
|
||||
}
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
|
||||
return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
|
||||
}
|
||||
opacity: modelData.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
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
MouseArea {
|
||||
id: forceKillArea
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
size: 16
|
||||
color: {
|
||||
if (!modelData.enabled)
|
||||
return Theme.surfaceVariantText;
|
||||
const isSelected = keyboardNavigation && selectedIndex === index;
|
||||
if (modelData.dangerous && (menuItemArea.containsMouse || isSelected))
|
||||
return Theme.error;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Normal
|
||||
color: {
|
||||
if (!modelData.enabled)
|
||||
return Theme.surfaceVariantText;
|
||||
const isSelected = keyboardNavigation && selectedIndex === index;
|
||||
if (modelData.dangerous && (menuItemArea.containsMouse || isSelected))
|
||||
return Theme.error;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
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()]);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: menuItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: modelData.enabled
|
||||
onEntered: {
|
||||
keyboardNavigation = false;
|
||||
selectedIndex = index;
|
||||
}
|
||||
onClicked: modelData.action()
|
||||
}
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
200
quickshell/Modules/ProcessList/ProcessListItem.qml
Normal file
200
quickshell/Modules/ProcessList/ProcessListItem.qml
Normal file
@@ -0,0 +1,200 @@
|
||||
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,39 +12,30 @@ 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: 650
|
||||
popupHeight: 550
|
||||
popupWidth: 600
|
||||
popupHeight: 600
|
||||
triggerWidth: 55
|
||||
positioning: ""
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
|
||||
onBackgroundClicked: {
|
||||
if (processContextMenu.visible)
|
||||
if (processContextMenu.visible) {
|
||||
processContextMenu.close();
|
||||
close();
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
searchText = "";
|
||||
expandedPid = "";
|
||||
if (processesView)
|
||||
processesView.reset();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
Ref {
|
||||
@@ -64,259 +55,55 @@ 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;
|
||||
processContextMenu.parentFocusItem = processListContent;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (processContextMenu.visible)
|
||||
return;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
if (processListPopout.searchText.length > 0) {
|
||||
processListPopout.searchText = "";
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (processesView.keyboardNavigationActive) {
|
||||
processesView.reset();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
processListPopout.close();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_F:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
searchField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
processesView.handleKey(event);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: processListPopout
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (processListPopout.shouldBeVisible)
|
||||
Qt.callLater(() => processListContent.forceActiveFocus());
|
||||
if (processListPopout.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
processListContent.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
target: processListPopout
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingM
|
||||
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
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
SystemOverview {
|
||||
id: systemOverview
|
||||
|
||||
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
|
||||
ignoreUpDownKeys: true
|
||||
keyForwardTargets: [processListContent]
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,177 +112,16 @@ DankPopout {
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
clip: true
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||
border.width: 0
|
||||
|
||||
ProcessesView {
|
||||
id: processesView
|
||||
ProcessListView {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
264
quickshell/Modules/ProcessList/ProcessListView.qml
Normal file
264
quickshell/Modules/ProcessList/ProcessListView.qml
Normal file
@@ -0,0 +1,264 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
quickshell/Modules/ProcessList/ProcessesTab.qml
Normal file
28
quickshell/Modules/ProcessList/ProcessesTab.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,757 +0,0 @@
|
||||
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 int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property int forceRefreshCount: 0
|
||||
|
||||
readonly property bool pauseUpdates: (contextMenu?.visible ?? false) || expandedPid.length > 0
|
||||
readonly property bool shouldUpdate: !pauseUpdates || forceRefreshCount > 0
|
||||
property var cachedProcesses: []
|
||||
|
||||
signal openContextMenuRequested(int index, real x, real y, bool fromKeyboard)
|
||||
|
||||
onFilteredProcessesChanged: {
|
||||
if (!shouldUpdate)
|
||||
return;
|
||||
cachedProcesses = filteredProcesses;
|
||||
if (forceRefreshCount > 0)
|
||||
forceRefreshCount--;
|
||||
}
|
||||
|
||||
onShouldUpdateChanged: {
|
||||
if (shouldUpdate)
|
||||
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;
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (cachedProcesses.length === 0)
|
||||
return;
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = Math.min(selectedIndex + 1, cachedProcesses.length - 1);
|
||||
ensureVisible();
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (cachedProcesses.length === 0)
|
||||
return;
|
||||
keyboardNavigationActive = true;
|
||||
if (selectedIndex <= 0) {
|
||||
selectedIndex = -1;
|
||||
keyboardNavigationActive = false;
|
||||
return;
|
||||
}
|
||||
selectedIndex = selectedIndex - 1;
|
||||
ensureVisible();
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
if (cachedProcesses.length === 0)
|
||||
return;
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = 0;
|
||||
ensureVisible();
|
||||
}
|
||||
|
||||
function selectLast() {
|
||||
if (cachedProcesses.length === 0)
|
||||
return;
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = cachedProcesses.length - 1;
|
||||
ensureVisible();
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
if (selectedIndex < 0 || selectedIndex >= cachedProcesses.length)
|
||||
return;
|
||||
const process = cachedProcesses[selectedIndex];
|
||||
const pidStr = (process?.pid ?? -1).toString();
|
||||
expandedPid = (expandedPid === pidStr) ? "" : pidStr;
|
||||
}
|
||||
|
||||
function openContextMenu() {
|
||||
if (selectedIndex < 0 || selectedIndex >= cachedProcesses.length)
|
||||
return;
|
||||
const delegate = processListView.itemAtIndex(selectedIndex);
|
||||
if (!delegate)
|
||||
return;
|
||||
const process = cachedProcesses[selectedIndex];
|
||||
if (!process || !contextMenu)
|
||||
return;
|
||||
contextMenu.processData = process;
|
||||
const itemPos = delegate.mapToItem(contextMenu.parent, delegate.width / 2, delegate.height / 2);
|
||||
contextMenu.parentFocusItem = root;
|
||||
contextMenu.show(itemPos.x, itemPos.y, true);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selectedIndex = -1;
|
||||
keyboardNavigationActive = false;
|
||||
expandedPid = "";
|
||||
}
|
||||
|
||||
function forceRefresh(count) {
|
||||
forceRefreshCount = count || 3;
|
||||
}
|
||||
|
||||
function ensureVisible() {
|
||||
if (selectedIndex < 0)
|
||||
return;
|
||||
processListView.positionViewAtIndex(selectedIndex, ListView.Contain);
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Down:
|
||||
selectNext();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
selectPrevious();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectNext();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectPrevious();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Home:
|
||||
selectFirst();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_End:
|
||||
selectLast();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Space:
|
||||
if (keyboardNavigationActive) {
|
||||
toggleExpand();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (keyboardNavigationActive) {
|
||||
toggleExpand();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Menu:
|
||||
case Qt.Key_F10:
|
||||
if (keyboardNavigationActive && selectedIndex >= 0) {
|
||||
openContextMenu();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
required property int index
|
||||
|
||||
width: processListView.width
|
||||
process: modelData
|
||||
isExpanded: root.expandedPid === (modelData?.pid ?? -1).toString()
|
||||
isSelected: root.keyboardNavigationActive && root.selectedIndex === index
|
||||
contextMenu: root.contextMenu
|
||||
onToggleExpand: {
|
||||
const pidStr = (modelData?.pid ?? -1).toString();
|
||||
root.expandedPid = (root.expandedPid === pidStr) ? "" : pidStr;
|
||||
}
|
||||
onClicked: {
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = index;
|
||||
}
|
||||
onContextMenuRequested: (mouseX, mouseY) => {
|
||||
if (root.contextMenu) {
|
||||
root.contextMenu.processData = modelData;
|
||||
root.contextMenu.parentFocusItem = root;
|
||||
const globalPos = mapToItem(root.contextMenu.parent, mouseX, mouseY);
|
||||
root.contextMenu.show(globalPos.x, globalPos.y, 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 bool isSelected: false
|
||||
property var contextMenu: null
|
||||
|
||||
signal toggleExpand
|
||||
signal clicked
|
||||
signal contextMenuRequested(real mouseX, real mouseY)
|
||||
|
||||
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: {
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
|
||||
return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent";
|
||||
}
|
||||
border.color: {
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
|
||||
return 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) {
|
||||
processItemRoot.contextMenuRequested(mouse.x, mouse.y);
|
||||
return;
|
||||
}
|
||||
processItemRoot.clicked();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
435
quickshell/Modules/ProcessList/SystemOverview.qml
Normal file
435
quickshell/Modules/ProcessList/SystemOverview.qml
Normal file
@@ -0,0 +1,435 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
591
quickshell/Modules/ProcessList/SystemTab.qml
Normal file
591
quickshell/Modules/ProcessList/SystemTab.qml
Normal file
@@ -0,0 +1,591 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
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,33 +125,6 @@ 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 "∞";
|
||||
@@ -179,14 +152,6 @@ 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;
|
||||
@@ -330,24 +295,6 @@ 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 {
|
||||
|
||||
@@ -373,9 +373,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
switch (cfg?.position ?? SettingsData.Position.Top) {
|
||||
switch (barCard.modelData.position) {
|
||||
case SettingsData.Position.Top:
|
||||
return I18n.tr("Top");
|
||||
case SettingsData.Position.Bottom:
|
||||
@@ -400,9 +398,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
const prefs = cfg?.screenPreferences || ["all"];
|
||||
const prefs = barCard.modelData.screenPreferences || ["all"];
|
||||
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"))
|
||||
return I18n.tr("All displays");
|
||||
return I18n.tr("%1 display(s)").replace("%1", prefs.length);
|
||||
@@ -419,11 +415,9 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
const left = cfg?.leftWidgets?.length || 0;
|
||||
const center = cfg?.centerWidgets?.length || 0;
|
||||
const right = cfg?.rightWidgets?.length || 0;
|
||||
const left = barCard.modelData.leftWidgets?.length || 0;
|
||||
const center = barCard.modelData.centerWidgets?.length || 0;
|
||||
const right = barCard.modelData.rightWidgets?.length || 0;
|
||||
return I18n.tr("%1 widgets").replace("%1", left + center + right);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -434,22 +428,14 @@ Item {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
return !cfg?.enabled && barCard.modelData.id !== "default";
|
||||
}
|
||||
visible: !barCard.modelData.enabled && barCard.modelData.id !== "default"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disabled")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
visible: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
return !cfg?.enabled && barCard.modelData.id !== "default";
|
||||
}
|
||||
visible: !barCard.modelData.enabled && barCard.modelData.id !== "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -759,21 +745,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Click Through")
|
||||
checked: selectedBarConfig?.clickThrough ?? false
|
||||
onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, {
|
||||
clickThrough: toggled
|
||||
})
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
@@ -1086,7 +1057,6 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: shadowCard
|
||||
iconName: "layers"
|
||||
title: I18n.tr("Shadow", "bar shadow settings card")
|
||||
visible: selectedBarConfig?.enabled
|
||||
@@ -1106,7 +1076,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
visible: shadowCard.shadowActive
|
||||
visible: parent.shadowActive
|
||||
text: I18n.tr("Opacity")
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
@@ -1118,7 +1088,7 @@ Item {
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: shadowCard.shadowActive
|
||||
visible: parent.shadowActive
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
|
||||
@@ -86,30 +86,10 @@ Item {
|
||||
settingKey: "dockAutoHide"
|
||||
tags: ["dock", "autohide", "hide", "hover"]
|
||||
text: I18n.tr("Auto-hide Dock")
|
||||
description: I18n.tr("Always hide the dock and reveal it when hovering near the dock area")
|
||||
description: I18n.tr("Hide the dock when not in use and reveal it when hovering near the dock area")
|
||||
checked: SettingsData.dockAutoHide
|
||||
visible: SettingsData.showDock
|
||||
onToggled: checked => {
|
||||
if (checked && SettingsData.dockSmartAutoHide) {
|
||||
SettingsData.set("dockSmartAutoHide", false);
|
||||
}
|
||||
SettingsData.set("dockAutoHide", checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dockSmartAutoHide"
|
||||
tags: ["dock", "smart", "autohide", "windows", "overlap", "intelligent"]
|
||||
text: I18n.tr("Intelligent Auto-hide")
|
||||
description: I18n.tr("Show dock when floating windows don't overlap its area")
|
||||
checked: SettingsData.dockSmartAutoHide
|
||||
visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland)
|
||||
onToggled: checked => {
|
||||
if (checked && SettingsData.dockAutoHide) {
|
||||
SettingsData.set("dockAutoHide", false);
|
||||
}
|
||||
SettingsData.set("dockSmartAutoHide", checked);
|
||||
}
|
||||
onToggled: checked => SettingsData.set("dockAutoHide", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
|
||||
@@ -462,250 +462,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "search"
|
||||
title: I18n.tr("Search Options")
|
||||
settingKey: "searchOptions"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "searchAppActions"
|
||||
tags: ["launcher", "search", "actions", "shortcuts"]
|
||||
text: I18n.tr("Search App Actions")
|
||||
description: I18n.tr("Include desktop actions (shortcuts) in search results.")
|
||||
checked: SessionData.searchAppActions
|
||||
onToggled: checked => SessionData.setSearchAppActions(checked)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: hiddenAppsCard
|
||||
width: parent.width
|
||||
iconName: "visibility_off"
|
||||
title: I18n.tr("Hidden Apps")
|
||||
settingKey: "hiddenApps"
|
||||
|
||||
property var hiddenAppsModel: {
|
||||
SessionData.hiddenApps;
|
||||
const apps = [];
|
||||
const allApps = AppSearchService.applications || [];
|
||||
for (const hiddenId of SessionData.hiddenApps) {
|
||||
const app = allApps.find(a => (a.id || a.execString || a.exec) === hiddenId);
|
||||
if (app) {
|
||||
apps.push({
|
||||
id: hiddenId,
|
||||
name: app.name || hiddenId,
|
||||
icon: app.icon || "",
|
||||
comment: app.comment || ""
|
||||
});
|
||||
} else {
|
||||
apps.push({
|
||||
id: hiddenId,
|
||||
name: hiddenId,
|
||||
icon: "",
|
||||
comment: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
return apps.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Hidden apps won't appear in the launcher. Right-click an app and select 'Hide App' to hide it.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Column {
|
||||
id: hiddenAppsList
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: hiddenAppsCard.hiddenAppsModel
|
||||
|
||||
delegate: Rectangle {
|
||||
width: hiddenAppsList.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.comment || modelData.id
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "visibility"
|
||||
iconSize: 18
|
||||
iconColor: Theme.primary
|
||||
onClicked: SessionData.showApp(modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("No hidden apps.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: hiddenAppsCard.hiddenAppsModel.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: appOverridesCard
|
||||
width: parent.width
|
||||
iconName: "edit"
|
||||
title: I18n.tr("App Customizations")
|
||||
settingKey: "appOverrides"
|
||||
|
||||
property var overridesModel: {
|
||||
SessionData.appOverrides;
|
||||
const items = [];
|
||||
const allApps = AppSearchService.applications || [];
|
||||
for (const appId in SessionData.appOverrides) {
|
||||
const override = SessionData.appOverrides[appId];
|
||||
const app = allApps.find(a => (a.id || a.execString || a.exec) === appId);
|
||||
items.push({
|
||||
id: appId,
|
||||
name: override.name || app?.name || appId,
|
||||
originalName: app?.name || appId,
|
||||
icon: override.icon || app?.icon || "",
|
||||
hasOverride: true
|
||||
});
|
||||
}
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Apps with custom display name, icon, or launch options. Right-click an app and select 'Edit App' to customize.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Column {
|
||||
id: overridesList
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: appOverridesCard.overridesModel
|
||||
|
||||
delegate: Rectangle {
|
||||
width: overridesList.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = "image://icon/application-x-executable";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.originalName !== modelData.name ? modelData.originalName : modelData.id
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "delete"
|
||||
iconSize: 18
|
||||
iconColor: Theme.error
|
||||
onClicked: SessionData.clearAppOverride(modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("No app customizations.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: appOverridesCard.overridesModel.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: recentAppsCard
|
||||
width: parent.width
|
||||
|
||||
@@ -131,15 +131,6 @@ Item {
|
||||
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "lockScreenPowerOffMonitorsOnLock"
|
||||
tags: ["lock", "screen", "monitor", "display", "dpms", "power"]
|
||||
text: I18n.tr("Power off monitors on lock")
|
||||
description: I18n.tr("Turn off all displays immediately when the lock screen activates")
|
||||
checked: SettingsData.lockScreenPowerOffMonitorsOnLock
|
||||
onToggled: checked => SettingsData.set("lockScreenPowerOffMonitorsOnLock", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "enableFprint"
|
||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||
|
||||
@@ -199,48 +199,6 @@ Item {
|
||||
opacity: 0.15
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("Occupied Color")
|
||||
model: ["none", "sec", "s", "sc", "sch", "schh"]
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
buttonHeight: 22
|
||||
minButtonWidth: 36
|
||||
buttonPadding: Theme.spacingS
|
||||
checkIconSize: Theme.iconSizeSmall - 2
|
||||
textSize: Theme.fontSizeSmall - 1
|
||||
spacing: 1
|
||||
currentIndex: {
|
||||
switch (SettingsData.workspaceOccupiedColorMode) {
|
||||
case "sec":
|
||||
return 1;
|
||||
case "s":
|
||||
return 2;
|
||||
case "sc":
|
||||
return 3;
|
||||
case "sch":
|
||||
return 4;
|
||||
case "schh":
|
||||
return 5;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
const modes = ["none", "sec", "s", "sc", "sch", "schh"];
|
||||
SettingsData.set("workspaceOccupiedColorMode", modes[index]);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("Unfocused Color")
|
||||
model: ["def", "s", "sc", "sch"]
|
||||
|
||||
@@ -10,8 +10,6 @@ Singleton {
|
||||
|
||||
property var applications: []
|
||||
property var _cachedCategories: null
|
||||
property var _cachedVisibleApps: null
|
||||
property var _hiddenAppsSet: new Set()
|
||||
|
||||
readonly property int maxResults: 10
|
||||
readonly property int frecencySampleSize: 10
|
||||
@@ -42,51 +40,6 @@ Singleton {
|
||||
function refreshApplications() {
|
||||
applications = DesktopEntries.applications.values;
|
||||
_cachedCategories = null;
|
||||
_cachedVisibleApps = null;
|
||||
}
|
||||
|
||||
function _rebuildHiddenSet() {
|
||||
_hiddenAppsSet = new Set(SessionData.hiddenApps || []);
|
||||
_cachedVisibleApps = null;
|
||||
}
|
||||
|
||||
function isAppHidden(app) {
|
||||
if (!app)
|
||||
return false;
|
||||
const appId = app.id || app.execString || app.exec || "";
|
||||
return _hiddenAppsSet.has(appId);
|
||||
}
|
||||
|
||||
function getVisibleApplications() {
|
||||
if (_cachedVisibleApps === null) {
|
||||
_cachedVisibleApps = applications.filter(app => !isAppHidden(app));
|
||||
}
|
||||
return _cachedVisibleApps.map(app => applyAppOverride(app));
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onHiddenAppsChanged() {
|
||||
root._rebuildHiddenSet();
|
||||
}
|
||||
function onAppOverridesChanged() {
|
||||
root._cachedVisibleApps = null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyAppOverride(app) {
|
||||
if (!app)
|
||||
return app;
|
||||
const appId = app.id || app.execString || app.exec || "";
|
||||
const override = SessionData.getAppOverride(appId);
|
||||
if (!override)
|
||||
return app;
|
||||
return Object.assign({}, app, {
|
||||
name: override.name || app.name,
|
||||
icon: override.icon || app.icon,
|
||||
comment: override.comment || app.comment,
|
||||
_override: override
|
||||
});
|
||||
}
|
||||
|
||||
readonly property string dmsLogoPath: Qt.resolvedUrl("../assets/danklogo2.svg")
|
||||
@@ -273,10 +226,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
_rebuildHiddenSet();
|
||||
refreshApplications();
|
||||
}
|
||||
Component.onCompleted: refreshApplications()
|
||||
|
||||
function tokenize(text) {
|
||||
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 0);
|
||||
@@ -395,17 +345,17 @@ Singleton {
|
||||
}
|
||||
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0)
|
||||
return getVisibleApplications();
|
||||
if (!query || query.length === 0) {
|
||||
return applications;
|
||||
}
|
||||
if (applications.length === 0)
|
||||
return [];
|
||||
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
const scoredApps = [];
|
||||
const results = [];
|
||||
const visibleApps = getVisibleApplications();
|
||||
|
||||
for (const app of visibleApps) {
|
||||
for (const app of applications) {
|
||||
const name = (app.name || "").toLowerCase();
|
||||
const genericName = (app.genericName || "").toLowerCase();
|
||||
const comment = (app.comment || "").toLowerCase();
|
||||
@@ -490,58 +440,10 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
if (SessionData.searchAppActions) {
|
||||
const actionResults = searchAppActions(queryLower, visibleApps);
|
||||
for (const actionResult of actionResults) {
|
||||
scoredApps.push({
|
||||
app: actionResult.app,
|
||||
score: actionResult.score
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scoredApps.sort((a, b) => b.score - a.score);
|
||||
return scoredApps.slice(0, maxResults).map(item => item.app);
|
||||
}
|
||||
|
||||
function searchAppActions(query, apps) {
|
||||
const results = [];
|
||||
for (const app of apps) {
|
||||
if (!app.actions || app.actions.length === 0)
|
||||
continue;
|
||||
for (const action of app.actions) {
|
||||
const actionName = (action.name || "").toLowerCase();
|
||||
if (!actionName)
|
||||
continue;
|
||||
|
||||
let score = 0;
|
||||
if (actionName === query) {
|
||||
score = 8000;
|
||||
} else if (actionName.startsWith(query)) {
|
||||
score = 4000;
|
||||
} else if (actionName.includes(query)) {
|
||||
score = 400;
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
app: {
|
||||
name: action.name,
|
||||
icon: action.icon || app.icon,
|
||||
comment: app.name,
|
||||
categories: app.categories || [],
|
||||
isAction: true,
|
||||
parentApp: app,
|
||||
actionData: action
|
||||
},
|
||||
score: score
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function getCategoriesForApp(app) {
|
||||
if (!app?.categories)
|
||||
return [];
|
||||
@@ -623,15 +525,17 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
const visibleApps = getVisibleApplications();
|
||||
if (category === I18n.tr("All"))
|
||||
return visibleApps;
|
||||
if (category === I18n.tr("All")) {
|
||||
return applications;
|
||||
}
|
||||
|
||||
// Check if it's a plugin category
|
||||
const pluginItems = getPluginItems(category, "");
|
||||
if (pluginItems.length > 0)
|
||||
if (pluginItems.length > 0) {
|
||||
return pluginItems;
|
||||
}
|
||||
|
||||
return visibleApps.filter(app => {
|
||||
return applications.filter(app => {
|
||||
const appCategories = getCategoriesForApp(app);
|
||||
return appCategories.includes(category);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string currentVersion: "1.2"
|
||||
readonly property bool changelogEnabled: false
|
||||
readonly property bool changelogEnabled: true
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
||||
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
||||
|
||||
@@ -64,7 +64,7 @@ Singleton {
|
||||
property bool screensaverInhibited: false
|
||||
property var screensaverInhibitors: []
|
||||
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"]
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
@@ -191,19 +191,17 @@ Singleton {
|
||||
if (!line || line.length === 0)
|
||||
return;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(line);
|
||||
const response = JSON.parse(line);
|
||||
const isClipboard = clipboardRequestIds[response.id];
|
||||
if (isClipboard)
|
||||
delete clipboardRequestIds[response.id];
|
||||
else
|
||||
console.log("DMSService: Request socket <<", line);
|
||||
handleResponse(response);
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse request response:", line.substring(0, 100));
|
||||
return;
|
||||
console.warn("DMSService: Failed to parse request response");
|
||||
}
|
||||
const isClipboard = clipboardRequestIds[response.id];
|
||||
if (isClipboard)
|
||||
delete clipboardRequestIds[response.id];
|
||||
else
|
||||
console.log("DMSService: Request socket <<", line);
|
||||
handleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,16 +223,14 @@ Singleton {
|
||||
if (!line || line.length === 0)
|
||||
return;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(line);
|
||||
const response = JSON.parse(line);
|
||||
if (!line.includes("clipboard"))
|
||||
console.log("DMSService: Subscribe socket <<", line);
|
||||
handleSubscriptionEvent(response);
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse subscription event:", line.substring(0, 100));
|
||||
return;
|
||||
console.warn("DMSService: Failed to parse subscription event");
|
||||
}
|
||||
if (!line.includes("clipboard"))
|
||||
console.log("DMSService: Subscribe socket <<", line);
|
||||
handleSubscriptionEvent(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +300,7 @@ Singleton {
|
||||
excludeServices = [excludeServices];
|
||||
}
|
||||
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser"];
|
||||
const filtered = allServices.filter(s => !excludeServices.includes(s));
|
||||
subscribe(filtered);
|
||||
}
|
||||
@@ -387,8 +383,6 @@ Singleton {
|
||||
screensaverInhibited = data.inhibited || false;
|
||||
screensaverInhibitors = data.inhibitors || [];
|
||||
screensaverStateUpdate(data);
|
||||
} else if (service === "dbus") {
|
||||
dbusSignalReceived(data.subscriptionId || "", data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,89 +646,4 @@ Singleton {
|
||||
"token": token
|
||||
}, callback);
|
||||
}
|
||||
|
||||
signal dbusSignalReceived(string subscriptionId, var data)
|
||||
|
||||
property var dbusSubscriptions: ({})
|
||||
|
||||
function dbusCall(bus, dest, path, iface, method, args, callback) {
|
||||
sendRequest("dbus.call", {
|
||||
"bus": bus,
|
||||
"dest": dest,
|
||||
"path": path,
|
||||
"interface": iface,
|
||||
"method": method,
|
||||
"args": args || []
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dbusGetProperty(bus, dest, path, iface, property, callback) {
|
||||
sendRequest("dbus.getProperty", {
|
||||
"bus": bus,
|
||||
"dest": dest,
|
||||
"path": path,
|
||||
"interface": iface,
|
||||
"property": property
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dbusSetProperty(bus, dest, path, iface, property, value, callback) {
|
||||
sendRequest("dbus.setProperty", {
|
||||
"bus": bus,
|
||||
"dest": dest,
|
||||
"path": path,
|
||||
"interface": iface,
|
||||
"property": property,
|
||||
"value": value
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dbusGetAllProperties(bus, dest, path, iface, callback) {
|
||||
sendRequest("dbus.getAllProperties", {
|
||||
"bus": bus,
|
||||
"dest": dest,
|
||||
"path": path,
|
||||
"interface": iface
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dbusIntrospect(bus, dest, path, callback) {
|
||||
sendRequest("dbus.introspect", {
|
||||
"bus": bus,
|
||||
"dest": dest,
|
||||
"path": path || "/"
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dbusListNames(bus, callback) {
|
||||
sendRequest("dbus.listNames", {
|
||||
"bus": bus
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dbusSubscribe(bus, sender, path, iface, member, callback) {
|
||||
sendRequest("dbus.subscribe", {
|
||||
"bus": bus,
|
||||
"sender": sender || "",
|
||||
"path": path || "",
|
||||
"interface": iface || "",
|
||||
"member": member || ""
|
||||
}, response => {
|
||||
if (!response.error && response.result?.subscriptionId) {
|
||||
dbusSubscriptions[response.result.subscriptionId] = true;
|
||||
}
|
||||
if (callback) callback(response);
|
||||
});
|
||||
}
|
||||
|
||||
function dbusUnsubscribe(subscriptionId, callback) {
|
||||
sendRequest("dbus.unsubscribe", {
|
||||
"subscriptionId": subscriptionId
|
||||
}, response => {
|
||||
if (!response.error) {
|
||||
delete dbusSubscriptions[subscriptionId];
|
||||
}
|
||||
if (callback) callback(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ 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: ""
|
||||
@@ -60,7 +59,6 @@ Singleton {
|
||||
property var processes: []
|
||||
property var allProcesses: []
|
||||
property string currentSort: "cpu"
|
||||
property bool sortAscending: false
|
||||
property var availableGpus: []
|
||||
|
||||
property string kernelVersion: ""
|
||||
@@ -95,8 +93,10 @@ 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,13 +126,17 @@ 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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,13 +171,17 @@ 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];
|
||||
@@ -195,6 +203,8 @@ Singleton {
|
||||
}
|
||||
availableGpus = updatedGpus;
|
||||
}
|
||||
|
||||
console.log("Removing GPU PCI ID completely:", pciId);
|
||||
}
|
||||
|
||||
// Force property change notification
|
||||
@@ -379,12 +389,8 @@ 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({
|
||||
@@ -571,55 +577,39 @@ Singleton {
|
||||
function setSortBy(newSortBy) {
|
||||
if (newSortBy !== currentSort) {
|
||||
currentSort = newSortBy;
|
||||
sortAscending = false;
|
||||
applySorting();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSort(column) {
|
||||
if (column === currentSort) {
|
||||
sortAscending = !sortAscending;
|
||||
} else {
|
||||
currentSort = column;
|
||||
sortAscending = false;
|
||||
}
|
||||
applySorting();
|
||||
}
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0)
|
||||
if (!allProcesses || allProcesses.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const asc = sortAscending;
|
||||
const sorted = allProcesses.slice();
|
||||
sorted.sort((a, b) => {
|
||||
let valueA, valueB, result;
|
||||
let valueA, valueB;
|
||||
|
||||
switch (currentSort) {
|
||||
case "cpu":
|
||||
valueA = a.cpu || 0;
|
||||
valueB = b.cpu || 0;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
return valueB - valueA;
|
||||
case "memory":
|
||||
valueA = a.memoryKB || 0;
|
||||
valueB = b.memoryKB || 0;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
return valueB - valueA;
|
||||
case "name":
|
||||
valueA = (a.command || "").toLowerCase();
|
||||
valueB = (b.command || "").toLowerCase();
|
||||
result = valueA.localeCompare(valueB);
|
||||
break;
|
||||
return valueA.localeCompare(valueB);
|
||||
case "pid":
|
||||
valueA = a.pid || 0;
|
||||
valueB = b.pid || 0;
|
||||
result = valueA - valueB;
|
||||
break;
|
||||
return valueA - valueB;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return asc ? -result : result;
|
||||
});
|
||||
|
||||
processes = sorted.slice(0, processLimit);
|
||||
@@ -638,7 +628,10 @@ Singleton {
|
||||
id: dgopProcess
|
||||
command: root.buildDgopCommand()
|
||||
running: false
|
||||
onStarted: dgopProcessPid = processId ?? 0
|
||||
onCommandChanged:
|
||||
|
||||
//console.log("DgopService command:", JSON.stringify(command))
|
||||
{}
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:", exitCode);
|
||||
|
||||
@@ -556,33 +556,6 @@ 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);
|
||||
|
||||
@@ -182,44 +182,17 @@ Singleton {
|
||||
return /[;&|<>()$`\\"']/.test(prefix);
|
||||
}
|
||||
|
||||
function parseEnvVars(envVarsStr) {
|
||||
if (!envVarsStr || envVarsStr.trim().length === 0)
|
||||
return {};
|
||||
const envObj = {};
|
||||
const pairs = envVarsStr.trim().split(/\s+/);
|
||||
for (const pair of pairs) {
|
||||
const eqIndex = pair.indexOf("=");
|
||||
if (eqIndex > 0) {
|
||||
const key = pair.substring(0, eqIndex);
|
||||
const value = pair.substring(eqIndex + 1);
|
||||
envObj[key] = value;
|
||||
}
|
||||
}
|
||||
return envObj;
|
||||
}
|
||||
|
||||
function launchDesktopEntry(desktopEntry, useNvidia) {
|
||||
let cmd = desktopEntry.command;
|
||||
if (useNvidia && nvidiaCommand)
|
||||
cmd = [nvidiaCommand].concat(cmd);
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
|
||||
const override = SessionData.getAppOverride(appId);
|
||||
|
||||
if (override?.extraFlags) {
|
||||
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
|
||||
cmd = cmd.concat(extraArgs);
|
||||
}
|
||||
|
||||
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
||||
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
|
||||
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
|
||||
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
|
||||
const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {};
|
||||
|
||||
const overrideEnv = override?.envVars ? parseEnvVars(override.envVars) : {};
|
||||
const finalEnv = Object.assign({}, cursorEnv, overrideEnv);
|
||||
|
||||
if (desktopEntry.runInTerminal) {
|
||||
const terminal = Quickshell.env("TERMINAL") || "xterm";
|
||||
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
|
||||
@@ -227,7 +200,7 @@ Singleton {
|
||||
Quickshell.execDetached({
|
||||
command: [terminal, "-e", "sh", "-c", shellCmd],
|
||||
workingDirectory: workDir,
|
||||
environment: finalEnv
|
||||
environment: cursorEnv
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -237,7 +210,7 @@ Singleton {
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", `${prefix} ${escapedCmd}`],
|
||||
workingDirectory: workDir,
|
||||
environment: finalEnv
|
||||
environment: cursorEnv
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -248,7 +221,7 @@ Singleton {
|
||||
Quickshell.execDetached({
|
||||
command: cmd,
|
||||
workingDirectory: workDir,
|
||||
environment: finalEnv
|
||||
environment: cursorEnv
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.4-unstable
|
||||
v1.2.3
|
||||
|
||||
@@ -13,7 +13,6 @@ StyledRect {
|
||||
property bool enabled: true
|
||||
property int buttonSize: 32
|
||||
property var tooltipText: null
|
||||
property string tooltipSide: "bottom"
|
||||
|
||||
signal clicked
|
||||
signal entered
|
||||
@@ -39,6 +38,5 @@ StyledRect {
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
tooltipText: root.tooltipText
|
||||
tooltipSide: root.tooltipSide
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ Flow {
|
||||
animationTimer.restart();
|
||||
} else {
|
||||
const oldIndex = currentIndex;
|
||||
currentIndex = index;
|
||||
selectionChanged(index, true);
|
||||
if (oldIndex !== index && oldIndex >= 0) {
|
||||
selectionChanged(oldIndex, false);
|
||||
|
||||
@@ -29,7 +29,6 @@ Item {
|
||||
property bool shouldBeVisible: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool backgroundInteractive: true
|
||||
property bool contentHandlesKeys: false
|
||||
|
||||
property real storedBarThickness: Theme.barHeight - 4
|
||||
property real storedBarSpacing: 4
|
||||
@@ -462,12 +461,8 @@ Item {
|
||||
id: focusHelper
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
visible: !root.contentHandlesKeys
|
||||
enabled: !root.contentHandlesKeys
|
||||
focus: !root.contentHandlesKeys
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (root.contentHandlesKeys)
|
||||
return;
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close();
|
||||
event.accepted = true;
|
||||
|
||||
@@ -53,7 +53,6 @@ StyledRect {
|
||||
property real topPadding: Theme.spacingM
|
||||
property real bottomPadding: Theme.spacingM
|
||||
property bool ignoreLeftRightKeys: false
|
||||
property bool ignoreUpDownKeys: false
|
||||
property bool ignoreTabKeys: false
|
||||
property var keyForwardTargets: []
|
||||
property Item keyNavigationTab: null
|
||||
@@ -146,16 +145,9 @@ StyledRect {
|
||||
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
|
||||
event.accepted = false;
|
||||
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||
if (root.keyForwardTargets[i])
|
||||
root.keyForwardTargets[i].Keys.pressed(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (root.ignoreUpDownKeys && (event.key === Qt.Key_Up || event.key === Qt.Key_Down)) {
|
||||
event.accepted = false;
|
||||
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||
if (root.keyForwardTargets[i])
|
||||
if (root.keyForwardTargets[i]) {
|
||||
root.keyForwardTargets[i].Keys.pressed(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ MouseArea {
|
||||
property color stateColor: Theme.surfaceText
|
||||
property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
|
||||
property var tooltipText: null
|
||||
property string tooltipSide: "bottom"
|
||||
|
||||
readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
|
||||
|
||||
@@ -27,7 +26,7 @@ MouseArea {
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
tooltip.show(root.tooltipText, root, 0, 0, root.tooltipSide);
|
||||
tooltip.show(root.tooltipText, root, 0, 0, "bottom");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
{
|
||||
"scope": ["keyword"],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color6.dark.hex}}"
|
||||
"foreground": "{{dank16.color5.dark.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -320,7 +320,7 @@
|
||||
"foreground": "{{dank16.color12.dark.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color6.dark.hex}}"
|
||||
"foreground": "{{dank16.color5.dark.hex}}"
|
||||
},
|
||||
"comment": {
|
||||
"foreground": "{{dank16.color8.dark.hex}}"
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
{
|
||||
"scope": ["keyword"],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color6.default.hex}}"
|
||||
"foreground": "{{dank16.color5.default.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -272,7 +272,7 @@
|
||||
"foreground": "{{dank16.color12.default.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color6.default.hex}}"
|
||||
"foreground": "{{dank16.color5.default.hex}}"
|
||||
},
|
||||
"comment": {
|
||||
"foreground": "{{colors.outline.default.hex}}"
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"storage.type"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color6.light.hex}}"
|
||||
"foreground": "{{dank16.color5.light.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -444,7 +444,7 @@
|
||||
"foreground": "{{colors.outline.light.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color6.light.hex}}"
|
||||
"foreground": "{{dank16.color5.light.hex}}"
|
||||
},
|
||||
"operator": {
|
||||
"foreground": "{{colors.on_surface.light.hex}}"
|
||||
|
||||
@@ -24,8 +24,7 @@ LANGUAGES = {
|
||||
"es": "es.json",
|
||||
"he": "he.json",
|
||||
"hu": "hu.json",
|
||||
"fa": "fa.json",
|
||||
"fr": "fr.json"
|
||||
"fa": "fa.json"
|
||||
}
|
||||
|
||||
def error(msg):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -251,9 +251,6 @@
|
||||
"Always Show Percentage": {
|
||||
"Always Show Percentage": "Mostrar siempre el porcentaje"
|
||||
},
|
||||
"Always hide the dock and reveal it when hovering near the dock area": {
|
||||
"Always hide the dock and reveal it when hovering near the dock area": ""
|
||||
},
|
||||
"Always on icons": {
|
||||
"Always on icons": "Iconos fijos"
|
||||
},
|
||||
@@ -710,9 +707,6 @@
|
||||
"Clear All Jobs": {
|
||||
"Clear All Jobs": "Borrar todos los trabajos"
|
||||
},
|
||||
"Clear History?": {
|
||||
"Clear History?": ""
|
||||
},
|
||||
"Clear all history when server starts": {
|
||||
"Clear all history when server starts": "Limpiar todo el historial cuando el servidor inicia"
|
||||
},
|
||||
@@ -923,12 +917,6 @@
|
||||
"Copy": {
|
||||
"Copy": ""
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "Copiar ID del Proceso"
|
||||
},
|
||||
@@ -1136,9 +1124,6 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "Eliminar impresora"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "Eliminar VPN"
|
||||
},
|
||||
@@ -1223,9 +1208,6 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "Uso de disco"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "Descartar"
|
||||
},
|
||||
@@ -1460,12 +1442,6 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "Empresa"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "Error"
|
||||
},
|
||||
@@ -1508,9 +1484,6 @@
|
||||
"Failed to cancel selected job": {
|
||||
"Failed to cancel selected job": "Error al cancelar el trabajo seleccionado"
|
||||
},
|
||||
"Failed to check pin limit": {
|
||||
"Failed to check pin limit": ""
|
||||
},
|
||||
"Failed to connect VPN": {
|
||||
"Failed to connect VPN": "Hubo un error al conectar con la VPN"
|
||||
},
|
||||
@@ -1586,9 +1559,6 @@
|
||||
"Failed to pause printer": {
|
||||
"Failed to pause printer": "Error al pausar la impresora"
|
||||
},
|
||||
"Failed to pin entry": {
|
||||
"Failed to pin entry": ""
|
||||
},
|
||||
"Failed to print test page": {
|
||||
"Failed to print test page": "Error al imprimir la página de prueba"
|
||||
},
|
||||
@@ -1634,9 +1604,6 @@
|
||||
"Failed to start connection to %1": {
|
||||
"Failed to start connection to %1": "Error al iniciar la conexión con %1"
|
||||
},
|
||||
"Failed to unpin entry": {
|
||||
"Failed to unpin entry": ""
|
||||
},
|
||||
"Failed to update VPN": {
|
||||
"Failed to update VPN": "Error al actualizar VPN"
|
||||
},
|
||||
@@ -1742,9 +1709,6 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "Forzar HDR"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "Forzar terminar el proceso"
|
||||
},
|
||||
@@ -1799,9 +1763,6 @@
|
||||
"Gamma control not available. Requires DMS API v6+.": {
|
||||
"Gamma control not available. Requires DMS API v6+.": "Control de gamma no disponible. Requiere DMS API v6+."
|
||||
},
|
||||
"Generic device name | Generic device name fallback": {
|
||||
"device": ""
|
||||
},
|
||||
"GitHub": {
|
||||
"GitHub": "GitHub"
|
||||
},
|
||||
@@ -1919,9 +1880,6 @@
|
||||
"History Settings": {
|
||||
"History Settings": "Ajustes de historial"
|
||||
},
|
||||
"History cleared. %1 pinned entries kept.": {
|
||||
"History cleared. %1 pinned entries kept.": ""
|
||||
},
|
||||
"Hold Duration": {
|
||||
"Hold Duration": "Duración de pulsación"
|
||||
},
|
||||
@@ -2036,9 +1994,6 @@
|
||||
"Install plugins from the DMS plugin registry": {
|
||||
"Install plugins from the DMS plugin registry": "Instalar complementos de los registros de complementos DMS"
|
||||
},
|
||||
"Intelligent Auto-hide": {
|
||||
"Intelligent Auto-hide": ""
|
||||
},
|
||||
"Interface:": {
|
||||
"Interface:": "Interfaz:"
|
||||
},
|
||||
@@ -2066,166 +2021,6 @@
|
||||
"Jobs: ": {
|
||||
"Jobs: ": "Trabajos:"
|
||||
},
|
||||
"KDE Connect SMS action": {
|
||||
"Opening SMS": "",
|
||||
"Opening SMS app": ""
|
||||
},
|
||||
"KDE Connect SMS dialog title": {
|
||||
"Send SMS": ""
|
||||
},
|
||||
"KDE Connect SMS message input placeholder": {
|
||||
"Message": ""
|
||||
},
|
||||
"KDE Connect SMS phone input placeholder": {
|
||||
"Phone number": ""
|
||||
},
|
||||
"KDE Connect SMS send button": {
|
||||
"Send": ""
|
||||
},
|
||||
"KDE Connect SMS tooltip": {
|
||||
"SMS": ""
|
||||
},
|
||||
"KDE Connect accept pairing button": {
|
||||
"Accept": ""
|
||||
},
|
||||
"KDE Connect browse action": {
|
||||
"Opening file browser": "",
|
||||
"Opening files": ""
|
||||
},
|
||||
"KDE Connect browse tooltip": {
|
||||
"Browse Files": ""
|
||||
},
|
||||
"KDE Connect clipboard action": {
|
||||
"Clipboard sent": ""
|
||||
},
|
||||
"KDE Connect clipboard tooltip": {
|
||||
"Send Clipboard": ""
|
||||
},
|
||||
"KDE Connect connected status": {
|
||||
"Connected": ""
|
||||
},
|
||||
"KDE Connect daemon hint": {
|
||||
"Start kdeconnectd to use this plugin": ""
|
||||
},
|
||||
"KDE Connect error": {
|
||||
"Failed to accept pairing": "",
|
||||
"Failed to browse device": "",
|
||||
"Failed to launch SMS app": "",
|
||||
"Failed to reject pairing": "",
|
||||
"Failed to ring device": "",
|
||||
"Failed to send clipboard": "",
|
||||
"Failed to send ping": "",
|
||||
"Failed to share": "",
|
||||
"Pairing failed": "",
|
||||
"Unpair failed": ""
|
||||
},
|
||||
"KDE Connect file share notification": {
|
||||
"File received from": ""
|
||||
},
|
||||
"KDE Connect hint message": {
|
||||
"Make sure KDE Connect is running on your other devices": ""
|
||||
},
|
||||
"KDE Connect no devices message": {
|
||||
"No devices found": ""
|
||||
},
|
||||
"KDE Connect no devices status": {
|
||||
"No devices": ""
|
||||
},
|
||||
"KDE Connect not paired status": {
|
||||
"Not paired": ""
|
||||
},
|
||||
"KDE Connect offline status": {
|
||||
"Offline": ""
|
||||
},
|
||||
"KDE Connect open SMS app button": {
|
||||
"Open App": ""
|
||||
},
|
||||
"KDE Connect open app hint": {
|
||||
"Open KDE Connect on your phone": ""
|
||||
},
|
||||
"KDE Connect pair button": {
|
||||
"Pair": ""
|
||||
},
|
||||
"KDE Connect pairing action": {
|
||||
"Device paired": "",
|
||||
"Pairing request sent": ""
|
||||
},
|
||||
"KDE Connect pairing in progress status": {
|
||||
"Pairing": ""
|
||||
},
|
||||
"KDE Connect pairing request notification": {
|
||||
"Pairing request from": ""
|
||||
},
|
||||
"KDE Connect pairing requested status": {
|
||||
"Pairing requested": ""
|
||||
},
|
||||
"KDE Connect pairing verification key label": {
|
||||
"Verification": ""
|
||||
},
|
||||
"KDE Connect ping action": {
|
||||
"Ping sent": "",
|
||||
"Ping sent to": ""
|
||||
},
|
||||
"KDE Connect ping tooltip": {
|
||||
"Ping": ""
|
||||
},
|
||||
"KDE Connect refresh button | KDE Connect refresh tooltip": {
|
||||
"Refresh": ""
|
||||
},
|
||||
"KDE Connect reject pairing button": {
|
||||
"Reject": ""
|
||||
},
|
||||
"KDE Connect request pairing button": {
|
||||
"Request Pairing": ""
|
||||
},
|
||||
"KDE Connect ring action": {
|
||||
"Ringing": ""
|
||||
},
|
||||
"KDE Connect ring tooltip": {
|
||||
"Ring": ""
|
||||
},
|
||||
"KDE Connect service unavailable message": {
|
||||
"KDE Connect unavailable": ""
|
||||
},
|
||||
"KDE Connect share URL button": {
|
||||
"Share URL": ""
|
||||
},
|
||||
"KDE Connect share button | KDE Connect share dialog title | KDE Connect share tooltip": {
|
||||
"Share": ""
|
||||
},
|
||||
"KDE Connect share input placeholder": {
|
||||
"Enter URL or text to share": ""
|
||||
},
|
||||
"KDE Connect share success": {
|
||||
"Shared": ""
|
||||
},
|
||||
"KDE Connect start daemon hint": {
|
||||
"Start kdeconnectd to connect devices": ""
|
||||
},
|
||||
"KDE Connect status": {
|
||||
"No devices connected": ""
|
||||
},
|
||||
"KDE Connect status multiple devices": {
|
||||
"devices connected": ""
|
||||
},
|
||||
"KDE Connect status single device": {
|
||||
"1 device connected": ""
|
||||
},
|
||||
"KDE Connect unavailable error title": {
|
||||
"KDE Connect Not Available": ""
|
||||
},
|
||||
"KDE Connect unavailable status": {
|
||||
"Unavailable": ""
|
||||
},
|
||||
"KDE Connect unknown device status | unknown author": {
|
||||
"Unknown": ""
|
||||
},
|
||||
"KDE Connect unpair action": {
|
||||
"Device unpaired": ""
|
||||
},
|
||||
"KDE Connect unpair tooltip": {
|
||||
"Unpair": ""
|
||||
},
|
||||
"Keep Awake": {
|
||||
"Keep Awake": "Mantener despierto"
|
||||
},
|
||||
@@ -2460,18 +2255,9 @@
|
||||
"Maximum History": {
|
||||
"Maximum History": "Historial máxima"
|
||||
},
|
||||
"Maximum Pinned Entries": {
|
||||
"Maximum Pinned Entries": ""
|
||||
},
|
||||
"Maximum number of clipboard entries to keep": {
|
||||
"Maximum number of clipboard entries to keep": "Número máximo de entradas del portapapeles a conservar"
|
||||
},
|
||||
"Maximum number of entries that can be saved": {
|
||||
"Maximum number of entries that can be saved": ""
|
||||
},
|
||||
"Maximum pinned entries reached": {
|
||||
"Maximum pinned entries reached": ""
|
||||
},
|
||||
"Maximum size per clipboard entry": {
|
||||
"Maximum size per clipboard entry": "Tamaño máximo por entrada en el portapapeles"
|
||||
},
|
||||
@@ -2763,12 +2549,6 @@
|
||||
"No printers found": {
|
||||
"No printers found": "No se encontraron impresoras"
|
||||
},
|
||||
"No recent clipboard entries found": {
|
||||
"No recent clipboard entries found": ""
|
||||
},
|
||||
"No saved clipboard entries": {
|
||||
"No saved clipboard entries": ""
|
||||
},
|
||||
"No variants created. Click Add to create a new monitor widget.": {
|
||||
"No variants created. Click Add to create a new monitor widget.": "No hay variantes creadas. Haga clic en Añadir para crear un nuevo widget de monitor."
|
||||
},
|
||||
@@ -2844,9 +2624,6 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "Posición OSD"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "Apagado"
|
||||
},
|
||||
@@ -2994,9 +2771,6 @@
|
||||
"Percentage": {
|
||||
"Percentage": "Porcentaje"
|
||||
},
|
||||
"Performance": {
|
||||
"Performance": ""
|
||||
},
|
||||
"Permission denied to set profile image.": {
|
||||
"Permission denied to set profile image.": "Permiso denegado para poner la imagen de perfil."
|
||||
},
|
||||
@@ -3096,9 +2870,6 @@
|
||||
"Power Profile Degradation": {
|
||||
"Power Profile Degradation": "Perfil de Energía Degradada"
|
||||
},
|
||||
"Power off monitors on lock": {
|
||||
"Power off monitors on lock": ""
|
||||
},
|
||||
"Power profile management available": {
|
||||
"Power profile management available": "Gestión del perfil de potencia disponible"
|
||||
},
|
||||
@@ -3168,9 +2939,6 @@
|
||||
"Process Count": {
|
||||
"Process Count": "Conteo de procesos"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "Procesando"
|
||||
},
|
||||
@@ -3369,9 +3137,6 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "Configuraciones guardadas"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "Escala"
|
||||
},
|
||||
@@ -3393,9 +3158,6 @@
|
||||
"Science": {
|
||||
"Science": "Ciencia"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "Compartir pantalla"
|
||||
},
|
||||
@@ -3675,9 +3437,6 @@
|
||||
"Show darkened overlay behind modal dialogs": {
|
||||
"Show darkened overlay behind modal dialogs": "Oscurecer el fondo al abrir un modal"
|
||||
},
|
||||
"Show dock when floating windows don't overlap its area": {
|
||||
"Show dock when floating windows don't overlap its area": ""
|
||||
},
|
||||
"Show launcher overlay when typing in Niri overview. Disable to use another launcher.": {
|
||||
"Show launcher overlay when typing in Niri overview. Disable to use another launcher.": "Mostrar la superposición del lanzador al escribir en la vista general de Niri. Desactivar para utilizar otro lanzador."
|
||||
},
|
||||
@@ -3993,15 +3752,9 @@
|
||||
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": {
|
||||
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": "Este widget evita que la GPU entre en estados de apagado y puede afectar significativamente la batería en portátiles. No se recomienda usarlo en portátiles con gráficos híbridos."
|
||||
},
|
||||
"This will delete all unpinned entries. %1 pinned entries will be kept.": {
|
||||
"This will delete all unpinned entries. %1 pinned entries will be kept.": ""
|
||||
},
|
||||
"This will permanently delete all clipboard history.": {
|
||||
"This will permanently delete all clipboard history.": "Esto eliminará permanentemente todo el historial del portapapeles."
|
||||
},
|
||||
"This will permanently remove this saved clipboard item. This action cannot be undone.": {
|
||||
"This will permanently remove this saved clipboard item. This action cannot be undone.": ""
|
||||
},
|
||||
"Tiling": {
|
||||
"Tiling": "Mosaico"
|
||||
},
|
||||
@@ -4101,9 +3854,6 @@
|
||||
"Trigger Prefix": {
|
||||
"Trigger Prefix": ""
|
||||
},
|
||||
"Turn off all displays immediately when the lock screen activates": {
|
||||
"Turn off all displays immediately when the lock screen activates": ""
|
||||
},
|
||||
"Turn off monitors after": {
|
||||
"Turn off monitors after": "Apagar monitores después de"
|
||||
},
|
||||
@@ -4170,9 +3920,6 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "Actualizar complemento"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": ""
|
||||
},
|
||||
@@ -4556,18 +4303,6 @@
|
||||
"dgop not available": {
|
||||
"dgop not available": "dgop no disponible"
|
||||
},
|
||||
"dgop unavailable error message": {
|
||||
"The 'dgop' tool is required for system monitoring.\\nPlease install dgop to use this feature.": ""
|
||||
},
|
||||
"disk io header in system monitor": {
|
||||
"Disk I/O": ""
|
||||
},
|
||||
"disk read label": {
|
||||
"Read:": ""
|
||||
},
|
||||
"disk write label": {
|
||||
"Write:": ""
|
||||
},
|
||||
"dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": {
|
||||
"dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": "dms/binds.kdl existe pero no está incluido en config.kdl. Las combinaciones de teclas personalizadas no funcionarán hasta que esto se solucione."
|
||||
},
|
||||
@@ -4601,33 +4336,18 @@
|
||||
"empty plugin list": {
|
||||
"No plugins found": "No se han encontrado plugins"
|
||||
},
|
||||
"empty state in disk mounts list": {
|
||||
"No mount points found": ""
|
||||
},
|
||||
"empty state in gpu list": {
|
||||
"No GPUs detected": ""
|
||||
},
|
||||
"empty state in process list": {
|
||||
"No matching processes": ""
|
||||
},
|
||||
"empty theme list": {
|
||||
"No themes found": "No se han encontrado temas"
|
||||
},
|
||||
"events": {
|
||||
"events": "eventos"
|
||||
},
|
||||
"fallback gpu name": {
|
||||
"Unknown GPU": ""
|
||||
},
|
||||
"files": {
|
||||
"files": "archivos"
|
||||
},
|
||||
"generic theme description": {
|
||||
"Material Design inspired color themes": "Temas de color inspirados en Material Design"
|
||||
},
|
||||
"gpu section header in system monitor": {
|
||||
"GPU Monitoring": ""
|
||||
},
|
||||
"greeter back button": {
|
||||
"Back": ""
|
||||
},
|
||||
@@ -4820,9 +4540,6 @@
|
||||
"minutes": {
|
||||
"minutes": "minutos"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "ms"
|
||||
},
|
||||
@@ -4897,15 +4614,6 @@
|
||||
"plugin search placeholder": {
|
||||
"Search plugins...": "Buscar complementos..."
|
||||
},
|
||||
"process count label in footer": {
|
||||
"Processes:": ""
|
||||
},
|
||||
"process detail label": {
|
||||
"Full Command:": ""
|
||||
},
|
||||
"process search placeholder": {
|
||||
"Search processes...": ""
|
||||
},
|
||||
"profile image file browser title": {
|
||||
"Select Profile Image": "Elegir foto de perfil"
|
||||
},
|
||||
@@ -4931,25 +4639,12 @@
|
||||
"shadow intensity slider": {
|
||||
"Intensity": ""
|
||||
},
|
||||
"short for processes": {
|
||||
"procs": ""
|
||||
},
|
||||
"source code link": {
|
||||
"source": "fuente"
|
||||
},
|
||||
"sysmon window title": {
|
||||
"System Monitor": "Monitor del sistema"
|
||||
},
|
||||
"system info header in system monitor": {
|
||||
"System Information": ""
|
||||
},
|
||||
"system info label": {
|
||||
"Architecture": "",
|
||||
"Distribution": "",
|
||||
"Hostname": "",
|
||||
"Kernel": "",
|
||||
"Load Average": ""
|
||||
},
|
||||
"theme browser description": {
|
||||
"Install color themes from the DMS theme registry": "Instalar tema de colores desde el repositorio de temas de DMS"
|
||||
},
|
||||
@@ -4980,9 +4675,6 @@
|
||||
"update dms for NM integration.": {
|
||||
"update dms for NM integration.": "Actualizar dms para integración con NM."
|
||||
},
|
||||
"uptime label in footer": {
|
||||
"Uptime:": ""
|
||||
},
|
||||
"version requirement": {
|
||||
"Requires %1": ""
|
||||
},
|
||||
|
||||
@@ -251,9 +251,6 @@
|
||||
"Always Show Percentage": {
|
||||
"Always Show Percentage": "همیشه درصد را نشان بده"
|
||||
},
|
||||
"Always hide the dock and reveal it when hovering near the dock area": {
|
||||
"Always hide the dock and reveal it when hovering near the dock area": ""
|
||||
},
|
||||
"Always on icons": {
|
||||
"Always on icons": "آیکونها همیشه فعال"
|
||||
},
|
||||
@@ -710,9 +707,6 @@
|
||||
"Clear All Jobs": {
|
||||
"Clear All Jobs": "پاککردن همه کارهای چاپ"
|
||||
},
|
||||
"Clear History?": {
|
||||
"Clear History?": ""
|
||||
},
|
||||
"Clear all history when server starts": {
|
||||
"Clear all history when server starts": "هنگام راهاندازی سرور تمام تاریخچه را پاک کن"
|
||||
},
|
||||
@@ -923,12 +917,6 @@
|
||||
"Copy": {
|
||||
"Copy": ""
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "کپی PID"
|
||||
},
|
||||
@@ -1136,9 +1124,6 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "حذف چاپگر"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "حذف VPN"
|
||||
},
|
||||
@@ -1223,9 +1208,6 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "میزان مصرف دیسک"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "رد کردن"
|
||||
},
|
||||
@@ -1460,12 +1442,6 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "شرکت"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "خطا"
|
||||
},
|
||||
@@ -1508,9 +1484,6 @@
|
||||
"Failed to cancel selected job": {
|
||||
"Failed to cancel selected job": "لغو کار چاپ انتخاب شده ناموفق بود"
|
||||
},
|
||||
"Failed to check pin limit": {
|
||||
"Failed to check pin limit": ""
|
||||
},
|
||||
"Failed to connect VPN": {
|
||||
"Failed to connect VPN": "اتصال به VPN ناموفق بود"
|
||||
},
|
||||
@@ -1586,9 +1559,6 @@
|
||||
"Failed to pause printer": {
|
||||
"Failed to pause printer": "توقف چاپگر ناموفق بود"
|
||||
},
|
||||
"Failed to pin entry": {
|
||||
"Failed to pin entry": ""
|
||||
},
|
||||
"Failed to print test page": {
|
||||
"Failed to print test page": "چاپ صفحه تست ناموفق بود"
|
||||
},
|
||||
@@ -1634,9 +1604,6 @@
|
||||
"Failed to start connection to %1": {
|
||||
"Failed to start connection to %1": "شروع اتصال به %1 ناموفق بود"
|
||||
},
|
||||
"Failed to unpin entry": {
|
||||
"Failed to unpin entry": ""
|
||||
},
|
||||
"Failed to update VPN": {
|
||||
"Failed to update VPN": "بروزرسانی VPN ناموفق بود"
|
||||
},
|
||||
@@ -1742,9 +1709,6 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "اجبار HDR"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "بستن اجباری فرایند"
|
||||
},
|
||||
@@ -1799,9 +1763,6 @@
|
||||
"Gamma control not available. Requires DMS API v6+.": {
|
||||
"Gamma control not available. Requires DMS API v6+.": "کنترل گاما در دسترس نیست. نیاز به DMS API نسخه ۶ به بالا دارد."
|
||||
},
|
||||
"Generic device name | Generic device name fallback": {
|
||||
"device": ""
|
||||
},
|
||||
"GitHub": {
|
||||
"GitHub": "گیتهاب"
|
||||
},
|
||||
@@ -1919,9 +1880,6 @@
|
||||
"History Settings": {
|
||||
"History Settings": "تنظیمات تاریخچه"
|
||||
},
|
||||
"History cleared. %1 pinned entries kept.": {
|
||||
"History cleared. %1 pinned entries kept.": ""
|
||||
},
|
||||
"Hold Duration": {
|
||||
"Hold Duration": "مدت زمان نگهداشتن"
|
||||
},
|
||||
@@ -2036,9 +1994,6 @@
|
||||
"Install plugins from the DMS plugin registry": {
|
||||
"Install plugins from the DMS plugin registry": "نصب افزونهها از مخزن افزونه DMS"
|
||||
},
|
||||
"Intelligent Auto-hide": {
|
||||
"Intelligent Auto-hide": ""
|
||||
},
|
||||
"Interface:": {
|
||||
"Interface:": "اینترفیس:"
|
||||
},
|
||||
@@ -2066,166 +2021,6 @@
|
||||
"Jobs: ": {
|
||||
"Jobs: ": "کارهای چاپ: "
|
||||
},
|
||||
"KDE Connect SMS action": {
|
||||
"Opening SMS": "",
|
||||
"Opening SMS app": ""
|
||||
},
|
||||
"KDE Connect SMS dialog title": {
|
||||
"Send SMS": ""
|
||||
},
|
||||
"KDE Connect SMS message input placeholder": {
|
||||
"Message": ""
|
||||
},
|
||||
"KDE Connect SMS phone input placeholder": {
|
||||
"Phone number": ""
|
||||
},
|
||||
"KDE Connect SMS send button": {
|
||||
"Send": ""
|
||||
},
|
||||
"KDE Connect SMS tooltip": {
|
||||
"SMS": ""
|
||||
},
|
||||
"KDE Connect accept pairing button": {
|
||||
"Accept": ""
|
||||
},
|
||||
"KDE Connect browse action": {
|
||||
"Opening file browser": "",
|
||||
"Opening files": ""
|
||||
},
|
||||
"KDE Connect browse tooltip": {
|
||||
"Browse Files": ""
|
||||
},
|
||||
"KDE Connect clipboard action": {
|
||||
"Clipboard sent": ""
|
||||
},
|
||||
"KDE Connect clipboard tooltip": {
|
||||
"Send Clipboard": ""
|
||||
},
|
||||
"KDE Connect connected status": {
|
||||
"Connected": ""
|
||||
},
|
||||
"KDE Connect daemon hint": {
|
||||
"Start kdeconnectd to use this plugin": ""
|
||||
},
|
||||
"KDE Connect error": {
|
||||
"Failed to accept pairing": "",
|
||||
"Failed to browse device": "",
|
||||
"Failed to launch SMS app": "",
|
||||
"Failed to reject pairing": "",
|
||||
"Failed to ring device": "",
|
||||
"Failed to send clipboard": "",
|
||||
"Failed to send ping": "",
|
||||
"Failed to share": "",
|
||||
"Pairing failed": "",
|
||||
"Unpair failed": ""
|
||||
},
|
||||
"KDE Connect file share notification": {
|
||||
"File received from": ""
|
||||
},
|
||||
"KDE Connect hint message": {
|
||||
"Make sure KDE Connect is running on your other devices": ""
|
||||
},
|
||||
"KDE Connect no devices message": {
|
||||
"No devices found": ""
|
||||
},
|
||||
"KDE Connect no devices status": {
|
||||
"No devices": ""
|
||||
},
|
||||
"KDE Connect not paired status": {
|
||||
"Not paired": ""
|
||||
},
|
||||
"KDE Connect offline status": {
|
||||
"Offline": ""
|
||||
},
|
||||
"KDE Connect open SMS app button": {
|
||||
"Open App": ""
|
||||
},
|
||||
"KDE Connect open app hint": {
|
||||
"Open KDE Connect on your phone": ""
|
||||
},
|
||||
"KDE Connect pair button": {
|
||||
"Pair": ""
|
||||
},
|
||||
"KDE Connect pairing action": {
|
||||
"Device paired": "",
|
||||
"Pairing request sent": ""
|
||||
},
|
||||
"KDE Connect pairing in progress status": {
|
||||
"Pairing": ""
|
||||
},
|
||||
"KDE Connect pairing request notification": {
|
||||
"Pairing request from": ""
|
||||
},
|
||||
"KDE Connect pairing requested status": {
|
||||
"Pairing requested": ""
|
||||
},
|
||||
"KDE Connect pairing verification key label": {
|
||||
"Verification": ""
|
||||
},
|
||||
"KDE Connect ping action": {
|
||||
"Ping sent": "",
|
||||
"Ping sent to": ""
|
||||
},
|
||||
"KDE Connect ping tooltip": {
|
||||
"Ping": ""
|
||||
},
|
||||
"KDE Connect refresh button | KDE Connect refresh tooltip": {
|
||||
"Refresh": ""
|
||||
},
|
||||
"KDE Connect reject pairing button": {
|
||||
"Reject": ""
|
||||
},
|
||||
"KDE Connect request pairing button": {
|
||||
"Request Pairing": ""
|
||||
},
|
||||
"KDE Connect ring action": {
|
||||
"Ringing": ""
|
||||
},
|
||||
"KDE Connect ring tooltip": {
|
||||
"Ring": ""
|
||||
},
|
||||
"KDE Connect service unavailable message": {
|
||||
"KDE Connect unavailable": ""
|
||||
},
|
||||
"KDE Connect share URL button": {
|
||||
"Share URL": ""
|
||||
},
|
||||
"KDE Connect share button | KDE Connect share dialog title | KDE Connect share tooltip": {
|
||||
"Share": ""
|
||||
},
|
||||
"KDE Connect share input placeholder": {
|
||||
"Enter URL or text to share": ""
|
||||
},
|
||||
"KDE Connect share success": {
|
||||
"Shared": ""
|
||||
},
|
||||
"KDE Connect start daemon hint": {
|
||||
"Start kdeconnectd to connect devices": ""
|
||||
},
|
||||
"KDE Connect status": {
|
||||
"No devices connected": ""
|
||||
},
|
||||
"KDE Connect status multiple devices": {
|
||||
"devices connected": ""
|
||||
},
|
||||
"KDE Connect status single device": {
|
||||
"1 device connected": ""
|
||||
},
|
||||
"KDE Connect unavailable error title": {
|
||||
"KDE Connect Not Available": ""
|
||||
},
|
||||
"KDE Connect unavailable status": {
|
||||
"Unavailable": ""
|
||||
},
|
||||
"KDE Connect unknown device status | unknown author": {
|
||||
"Unknown": ""
|
||||
},
|
||||
"KDE Connect unpair action": {
|
||||
"Device unpaired": ""
|
||||
},
|
||||
"KDE Connect unpair tooltip": {
|
||||
"Unpair": ""
|
||||
},
|
||||
"Keep Awake": {
|
||||
"Keep Awake": "بیدار نگه دار"
|
||||
},
|
||||
@@ -2460,18 +2255,9 @@
|
||||
"Maximum History": {
|
||||
"Maximum History": "حداکثر تاریخچه"
|
||||
},
|
||||
"Maximum Pinned Entries": {
|
||||
"Maximum Pinned Entries": ""
|
||||
},
|
||||
"Maximum number of clipboard entries to keep": {
|
||||
"Maximum number of clipboard entries to keep": "بیشینه تعداد ورودیهای کلیپبورد برای نگهداری"
|
||||
},
|
||||
"Maximum number of entries that can be saved": {
|
||||
"Maximum number of entries that can be saved": ""
|
||||
},
|
||||
"Maximum pinned entries reached": {
|
||||
"Maximum pinned entries reached": ""
|
||||
},
|
||||
"Maximum size per clipboard entry": {
|
||||
"Maximum size per clipboard entry": "بیشینه اندازه برای هر ورودی کلیپبورد"
|
||||
},
|
||||
@@ -2763,12 +2549,6 @@
|
||||
"No printers found": {
|
||||
"No printers found": "چاپگری یافت نشد"
|
||||
},
|
||||
"No recent clipboard entries found": {
|
||||
"No recent clipboard entries found": ""
|
||||
},
|
||||
"No saved clipboard entries": {
|
||||
"No saved clipboard entries": ""
|
||||
},
|
||||
"No variants created. Click Add to create a new monitor widget.": {
|
||||
"No variants created. Click Add to create a new monitor widget.": "هیچ گونهی دیگری ایجاد نشد. برای ایجاد یک ابزارک مانیتور جدید، روی افزودن کلیک کنید."
|
||||
},
|
||||
@@ -2844,9 +2624,6 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "موقعیت OSD"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "خاموش"
|
||||
},
|
||||
@@ -2994,9 +2771,6 @@
|
||||
"Percentage": {
|
||||
"Percentage": "درصد"
|
||||
},
|
||||
"Performance": {
|
||||
"Performance": ""
|
||||
},
|
||||
"Permission denied to set profile image.": {
|
||||
"Permission denied to set profile image.": "اجازه تنظیم تصویر نمایه داده نشد."
|
||||
},
|
||||
@@ -3096,9 +2870,6 @@
|
||||
"Power Profile Degradation": {
|
||||
"Power Profile Degradation": "تنزلدادن پروفایل پاور"
|
||||
},
|
||||
"Power off monitors on lock": {
|
||||
"Power off monitors on lock": ""
|
||||
},
|
||||
"Power profile management available": {
|
||||
"Power profile management available": "مدیریت پروفایل پاور در دسترس"
|
||||
},
|
||||
@@ -3168,9 +2939,6 @@
|
||||
"Process Count": {
|
||||
"Process Count": "تعداد فرایندها"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "درحال پردازش"
|
||||
},
|
||||
@@ -3369,9 +3137,6 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "پیکربندیهای ذخیره شده"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "بزرگنمایی"
|
||||
},
|
||||
@@ -3393,9 +3158,6 @@
|
||||
"Science": {
|
||||
"Science": "علمی"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "اشتراکگذاری صفحه"
|
||||
},
|
||||
@@ -3675,9 +3437,6 @@
|
||||
"Show darkened overlay behind modal dialogs": {
|
||||
"Show darkened overlay behind modal dialogs": "نمایش overlay تیره پشت پنجره مودال"
|
||||
},
|
||||
"Show dock when floating windows don't overlap its area": {
|
||||
"Show dock when floating windows don't overlap its area": ""
|
||||
},
|
||||
"Show launcher overlay when typing in Niri overview. Disable to use another launcher.": {
|
||||
"Show launcher overlay when typing in Niri overview. Disable to use another launcher.": "overlay لانچر را هنگام تایپ در نمای کلی نیری نمایش بده. برای استفاده از لانچر دیگری غیرفعال کنید."
|
||||
},
|
||||
@@ -3993,15 +3752,9 @@
|
||||
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": {
|
||||
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": "این ابزارک از حالتهای خاموش شدن GPU جلوگیری میکند، که میتواند به طور قابل توجهی بر عمر باتری لپتاپها تأثیر بگذارد. استفاده از این ابزارک در لپتاپهایی با گرافیک هیبریدی توصیه نمیشود."
|
||||
},
|
||||
"This will delete all unpinned entries. %1 pinned entries will be kept.": {
|
||||
"This will delete all unpinned entries. %1 pinned entries will be kept.": ""
|
||||
},
|
||||
"This will permanently delete all clipboard history.": {
|
||||
"This will permanently delete all clipboard history.": "این کار تمام تاریخچه کلیپبورد را برای همیشه حذف میکند."
|
||||
},
|
||||
"This will permanently remove this saved clipboard item. This action cannot be undone.": {
|
||||
"This will permanently remove this saved clipboard item. This action cannot be undone.": ""
|
||||
},
|
||||
"Tiling": {
|
||||
"Tiling": "تایلینگ"
|
||||
},
|
||||
@@ -4101,9 +3854,6 @@
|
||||
"Trigger Prefix": {
|
||||
"Trigger Prefix": ""
|
||||
},
|
||||
"Turn off all displays immediately when the lock screen activates": {
|
||||
"Turn off all displays immediately when the lock screen activates": ""
|
||||
},
|
||||
"Turn off monitors after": {
|
||||
"Turn off monitors after": "خاموشکردن مانیتور پس از"
|
||||
},
|
||||
@@ -4170,9 +3920,6 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "بروزرسانی افزونه"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": ""
|
||||
},
|
||||
@@ -4556,18 +4303,6 @@
|
||||
"dgop not available": {
|
||||
"dgop not available": "dgop در دسترس نیست"
|
||||
},
|
||||
"dgop unavailable error message": {
|
||||
"The 'dgop' tool is required for system monitoring.\\nPlease install dgop to use this feature.": ""
|
||||
},
|
||||
"disk io header in system monitor": {
|
||||
"Disk I/O": ""
|
||||
},
|
||||
"disk read label": {
|
||||
"Read:": ""
|
||||
},
|
||||
"disk write label": {
|
||||
"Write:": ""
|
||||
},
|
||||
"dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": {
|
||||
"dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": "فایل dms/binds.kdl وجود دارد اما در config.kdl گنجانده نشده است. کلیدهای ترکیبی سفارشی تا زمانی که این مشکل برطرف نشود، کار نخواهند کرد."
|
||||
},
|
||||
@@ -4601,33 +4336,18 @@
|
||||
"empty plugin list": {
|
||||
"No plugins found": "هیچ افزونهای یافت نشد"
|
||||
},
|
||||
"empty state in disk mounts list": {
|
||||
"No mount points found": ""
|
||||
},
|
||||
"empty state in gpu list": {
|
||||
"No GPUs detected": ""
|
||||
},
|
||||
"empty state in process list": {
|
||||
"No matching processes": ""
|
||||
},
|
||||
"empty theme list": {
|
||||
"No themes found": "هیچ تمی یافت نشد"
|
||||
},
|
||||
"events": {
|
||||
"events": "رویدادها"
|
||||
},
|
||||
"fallback gpu name": {
|
||||
"Unknown GPU": ""
|
||||
},
|
||||
"files": {
|
||||
"files": "فایل"
|
||||
},
|
||||
"generic theme description": {
|
||||
"Material Design inspired color themes": "رنگهای تم الهام گرفته شده از متریال دیزاین"
|
||||
},
|
||||
"gpu section header in system monitor": {
|
||||
"GPU Monitoring": ""
|
||||
},
|
||||
"greeter back button": {
|
||||
"Back": "بازگشت"
|
||||
},
|
||||
@@ -4820,9 +4540,6 @@
|
||||
"minutes": {
|
||||
"minutes": "دقیقه"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "ms"
|
||||
},
|
||||
@@ -4897,15 +4614,6 @@
|
||||
"plugin search placeholder": {
|
||||
"Search plugins...": "جستجوی افزونهها..."
|
||||
},
|
||||
"process count label in footer": {
|
||||
"Processes:": ""
|
||||
},
|
||||
"process detail label": {
|
||||
"Full Command:": ""
|
||||
},
|
||||
"process search placeholder": {
|
||||
"Search processes...": ""
|
||||
},
|
||||
"profile image file browser title": {
|
||||
"Select Profile Image": "انتخاب تصویر نمایه"
|
||||
},
|
||||
@@ -4931,25 +4639,12 @@
|
||||
"shadow intensity slider": {
|
||||
"Intensity": ""
|
||||
},
|
||||
"short for processes": {
|
||||
"procs": ""
|
||||
},
|
||||
"source code link": {
|
||||
"source": "منبع"
|
||||
},
|
||||
"sysmon window title": {
|
||||
"System Monitor": "ناظر سیستم"
|
||||
},
|
||||
"system info header in system monitor": {
|
||||
"System Information": ""
|
||||
},
|
||||
"system info label": {
|
||||
"Architecture": "",
|
||||
"Distribution": "",
|
||||
"Hostname": "",
|
||||
"Kernel": "",
|
||||
"Load Average": ""
|
||||
},
|
||||
"theme browser description": {
|
||||
"Install color themes from the DMS theme registry": "نصب تم رنگها از مخزن تم DMS"
|
||||
},
|
||||
@@ -4980,9 +4675,6 @@
|
||||
"update dms for NM integration.": {
|
||||
"update dms for NM integration.": "DMS را برای یکپارچهسازی NM بروز کنید."
|
||||
},
|
||||
"uptime label in footer": {
|
||||
"Uptime:": ""
|
||||
},
|
||||
"version requirement": {
|
||||
"Requires %1": ""
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user