mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
24 Commits
d7b7086b21
...
chroma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ecb5af70 | ||
|
|
f2be6cfeb1 | ||
|
|
65486ed3cf | ||
|
|
cc30e2a9e4 | ||
|
|
ac68451cdf | ||
|
|
0f6ae11c3d | ||
|
|
7cb39f00ad | ||
|
|
f313d03348 | ||
|
|
1adbf3937b | ||
|
|
a685d9da52 | ||
|
|
13dededcc9 | ||
|
|
3bed2d9feb | ||
|
|
7241877995 | ||
|
|
340d79000c | ||
|
|
162ec909da | ||
|
|
53f5240d41 | ||
|
|
27f0df07af | ||
|
|
ad940b5884 | ||
|
|
ec8ab47462 | ||
|
|
35cbfeb008 | ||
|
|
7036362b9b | ||
|
|
2bcb33e85c | ||
|
|
76ac036f85 | ||
|
|
581073394a |
@@ -1,5 +1,12 @@
|
||||
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,7 +43,6 @@ 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:
|
||||
|
||||
193
core/cmd/dms/commands_chroma.go
Normal file
193
core/cmd/dms/commands_chroma.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yuin/goldmark"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
ghtml "github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
var (
|
||||
chromaLanguage string
|
||||
chromaStyle string
|
||||
chromaInline bool
|
||||
chromaMarkdown bool
|
||||
)
|
||||
|
||||
var chromaCmd = &cobra.Command{
|
||||
Use: "chroma [file]",
|
||||
Short: "Syntax highlight source code",
|
||||
Long: `Generate syntax-highlighted HTML from source code.
|
||||
|
||||
Reads from file or stdin, outputs HTML with syntax highlighting.
|
||||
Language is auto-detected from filename or can be specified with --language.
|
||||
|
||||
Examples:
|
||||
dms chroma main.go
|
||||
dms chroma --language python script.py
|
||||
echo "def foo(): pass" | dms chroma -l python
|
||||
cat code.rs | dms chroma -l rust --style dracula
|
||||
dms chroma --markdown README.md
|
||||
dms chroma --markdown --style github-dark notes.md
|
||||
dms chroma list-languages
|
||||
dms chroma list-styles`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: runChroma,
|
||||
}
|
||||
|
||||
var chromaListLanguagesCmd = &cobra.Command{
|
||||
Use: "list-languages",
|
||||
Short: "List all supported languages",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, name := range lexers.Names(true) {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var chromaListStylesCmd = &cobra.Command{
|
||||
Use: "list-styles",
|
||||
Short: "List all available color styles",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, name := range styles.Names() {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
|
||||
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
|
||||
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
|
||||
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
|
||||
|
||||
chromaCmd.AddCommand(chromaListLanguagesCmd)
|
||||
chromaCmd.AddCommand(chromaListStylesCmd)
|
||||
}
|
||||
|
||||
func runChroma(cmd *cobra.Command, args []string) {
|
||||
var source string
|
||||
var filename string
|
||||
|
||||
// Read from file or stdin
|
||||
if len(args) > 0 {
|
||||
filename = args[0]
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source = string(content)
|
||||
} else {
|
||||
content, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source = string(content)
|
||||
}
|
||||
|
||||
// Handle empty input
|
||||
if strings.TrimSpace(source) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Markdown rendering
|
||||
if chromaMarkdown {
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle(chromaStyle),
|
||||
highlighting.WithFormatOptions(
|
||||
html.WithClasses(!chromaInline),
|
||||
),
|
||||
),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
ghtml.WithHardWraps(),
|
||||
ghtml.WithXHTML(),
|
||||
),
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Detect or use specified lexer
|
||||
var lexer chroma.Lexer
|
||||
if chromaLanguage != "" {
|
||||
lexer = lexers.Get(chromaLanguage)
|
||||
if lexer == nil {
|
||||
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if filename != "" {
|
||||
lexer = lexers.Match(filename)
|
||||
}
|
||||
|
||||
// Try content analysis if no lexer found
|
||||
if lexer == nil {
|
||||
lexer = lexers.Analyse(source)
|
||||
}
|
||||
|
||||
// Fallback to plaintext
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
// Get style
|
||||
style := styles.Get(chromaStyle)
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
// Create HTML formatter
|
||||
var formatter *html.Formatter
|
||||
if chromaInline {
|
||||
formatter = html.New(
|
||||
html.WithClasses(false),
|
||||
html.TabWidth(4),
|
||||
)
|
||||
} else {
|
||||
formatter = html.New(
|
||||
html.WithClasses(true),
|
||||
html.TabWidth(4),
|
||||
)
|
||||
}
|
||||
|
||||
// Tokenize
|
||||
iterator, err := lexer.Tokenise(nil, source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Format and output
|
||||
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -511,9 +511,12 @@ func getCommonCommands() []*cobra.Command {
|
||||
colorCmd,
|
||||
screenshotCmd,
|
||||
notifyActionCmd,
|
||||
notifyCmd,
|
||||
genericNotifyActionCmd,
|
||||
matugenCmd,
|
||||
clipboardCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
chromaCmd,
|
||||
}
|
||||
}
|
||||
|
||||
68
core/cmd/dms/commands_notify.go
Normal file
68
core/cmd/dms/commands_notify.go
Normal file
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
24
core/go.mod
24
core/go.mod
@@ -4,6 +4,7 @@ go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.17.2
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
@@ -16,21 +17,22 @@ 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-20251219203646-944ab1f22d93
|
||||
golang.org/x/image v0.34.0
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||
golang.org/x/image v0.35.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.6.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.7.0 // 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/dlclark/regexp2 v1.11.5 // 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-20251217170237-e9738f50a3cd // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // 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 +40,10 @@ 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.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -47,12 +51,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.3 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.4 // 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-20251231065035-29ae690a9f19
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6
|
||||
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 +70,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.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
97
core/go.sum
97
core/go.sum
@@ -4,6 +4,14 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
@@ -16,8 +24,6 @@ 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=
|
||||
@@ -26,24 +32,18 @@ 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=
|
||||
@@ -52,8 +52,10 @@ 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/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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=
|
||||
@@ -64,22 +66,15 @@ 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-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-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.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
||||
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-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||
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=
|
||||
@@ -87,6 +82,8 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -127,16 +124,12 @@ 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=
|
||||
@@ -146,45 +139,43 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
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/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/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
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/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-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.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
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/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/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
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.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -199,31 +199,6 @@ func labToHex(L, a, b float64) string {
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||
}
|
||||
|
||||
// Adjust brightness while keeping the same hue
|
||||
func retoneToL(hex string, Ltarget float64) string {
|
||||
rgb := HexToRGB(hex)
|
||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||
L, a, b := col.Lab()
|
||||
L100 := L * 100.0
|
||||
|
||||
scale := 1.0
|
||||
if L100 != 0 {
|
||||
scale = Ltarget / L100
|
||||
}
|
||||
|
||||
a2, b2 := a*scale, b*scale
|
||||
|
||||
// Don't let it get too saturated
|
||||
maxChroma := 0.4
|
||||
if math.Hypot(a2, b2) > maxChroma {
|
||||
k := maxChroma / math.Hypot(a2, b2)
|
||||
a2 *= k
|
||||
b2 *= k
|
||||
}
|
||||
|
||||
return labToHex(Ltarget, a2, b2)
|
||||
}
|
||||
|
||||
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||
Lf := getLstar(hexFg)
|
||||
Lb := getLstar(hexBg)
|
||||
@@ -356,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
||||
return hexColor
|
||||
}
|
||||
|
||||
// Bidirectional contrast - tries both lighter and darker, picks closest to original
|
||||
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||
if current >= minLc {
|
||||
return hexColor
|
||||
}
|
||||
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
origL, af, bf := cf.Lab()
|
||||
|
||||
var darkerResult, lighterResult string
|
||||
darkerL, lighterL := origL, origL
|
||||
darkerFound, lighterFound := false, false
|
||||
|
||||
step := 0.5
|
||||
for i := range 120 {
|
||||
if !darkerFound {
|
||||
darkerL = math.Max(0, origL-float64(i)*step)
|
||||
cand := labToHex(darkerL, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
darkerResult = cand
|
||||
darkerFound = true
|
||||
}
|
||||
}
|
||||
if !lighterFound {
|
||||
lighterL = math.Min(100, origL+float64(i)*step)
|
||||
cand := labToHex(lighterL, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
lighterResult = cand
|
||||
lighterFound = true
|
||||
}
|
||||
}
|
||||
if darkerFound && lighterFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if darkerFound && lighterFound {
|
||||
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
|
||||
return darkerResult
|
||||
}
|
||||
return lighterResult
|
||||
}
|
||||
if darkerFound {
|
||||
return darkerResult
|
||||
}
|
||||
if lighterFound {
|
||||
return lighterResult
|
||||
}
|
||||
return hexColor
|
||||
}
|
||||
|
||||
type PaletteOptions struct {
|
||||
IsLight bool
|
||||
Background string
|
||||
@@ -369,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||
if opts.UseDPS {
|
||||
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func blendHue(base, target, factor float64) float64 {
|
||||
diff := target - base
|
||||
if diff > 0.5 {
|
||||
diff -= 1.0
|
||||
} else if diff < -0.5 {
|
||||
diff += 1.0
|
||||
}
|
||||
result := base + diff*factor
|
||||
if result < 0 {
|
||||
result += 1.0
|
||||
} else if result >= 1.0 {
|
||||
result -= 1.0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func DeriveContainer(primary string, isLight bool) string {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -389,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
rgb := HexToRGB(baseColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
pr := HexToRGB(primaryColor)
|
||||
ph := RGBToHSV(pr)
|
||||
|
||||
var palette Palette
|
||||
|
||||
var normalTextTarget, secondaryTarget float64
|
||||
@@ -410,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
}
|
||||
palette.Color0 = NewColorInfo(bgColor)
|
||||
|
||||
hueShift := (hsv.H - 0.6) * 0.12
|
||||
satBoost := 1.15
|
||||
baseSat := math.Max(ph.S, 0.5)
|
||||
baseVal := math.Max(ph.V, 0.5)
|
||||
|
||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
||||
var redColor string
|
||||
if opts.IsLight {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
redH := blendHue(0.0, ph.H, 0.12)
|
||||
greenH := blendHue(0.33, ph.H, 0.10)
|
||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||
|
||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||
var greenColor string
|
||||
if opts.IsLight {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
||||
var yellowColor string
|
||||
if opts.IsLight {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
var blueColor string
|
||||
if opts.IsLight {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
magH := hsv.H - 0.03
|
||||
if magH < 0 {
|
||||
magH += 1.0
|
||||
}
|
||||
var magColor string
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
if opts.IsLight {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
cyanH := hsv.H + 0.08
|
||||
if cyanH > 1.0 {
|
||||
cyanH -= 1.0
|
||||
}
|
||||
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
||||
accentTarget := secondaryTarget * 0.7
|
||||
|
||||
if opts.IsLight {
|
||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
||||
} else {
|
||||
palette.Color7 = NewColorInfo("#abb2bf")
|
||||
palette.Color8 = NewColorInfo("#5c6370")
|
||||
}
|
||||
redS := math.Min(baseSat*1.2, 1.0)
|
||||
redV := baseVal * 0.95
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
if opts.IsLight {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
||||
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
} else {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
brightBlue := retoneToL(primaryColor, 85.0)
|
||||
palette.Color12 = NewColorInfo(brightBlue)
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyanH := hsv.H + 0.02
|
||||
if brightCyanH > 1.0 {
|
||||
brightCyanH -= 1.0
|
||||
}
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
}
|
||||
greenS := math.Min(baseSat*1.3, 1.0)
|
||||
greenV := baseVal * 0.75
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
if opts.IsLight {
|
||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
||||
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
blueS := math.Min(ph.S*1.05, 1.0)
|
||||
blueV := math.Min(ph.V*1.05, 1.0)
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (light container in light mode)
|
||||
container5 := DeriveContainer(primaryColor, true)
|
||||
palette.Color5 = NewColorInfo(container5)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
gray7S := baseSat * 0.08
|
||||
gray7V := baseVal * 0.28
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.05
|
||||
gray8V := baseVal * 0.85
|
||||
dimTarget := secondaryTarget * 0.5
|
||||
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
|
||||
|
||||
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightGreenS := math.Min(baseSat*1.1, 1.0)
|
||||
brightGreenV := math.Min(baseVal*1.1, 1.0)
|
||||
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightYellowS := math.Min(baseSat*1.4, 1.0)
|
||||
brightYellowV := math.Min(baseVal*1.3, 1.0)
|
||||
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightBlueS := math.Min(ph.S*1.1, 1.0)
|
||||
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
lightContainer := DeriveContainer(primaryColor, true)
|
||||
palette.Color13 = NewColorInfo(lightContainer)
|
||||
|
||||
brightCyanS := ph.S * 0.5
|
||||
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
|
||||
|
||||
white15S := baseSat * 0.04
|
||||
white15V := math.Min(baseVal*1.5, 1.0)
|
||||
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
|
||||
} else {
|
||||
palette.Color15 = NewColorInfo("#ffffff")
|
||||
redS := math.Min(baseSat*1.1, 1.0)
|
||||
redV := math.Min(baseVal*1.15, 1.0)
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
greenS := math.Min(baseSat*1.0, 1.0)
|
||||
greenV := math.Min(baseVal*1.0, 1.0)
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
yellowS := math.Min(baseSat*1.1, 1.0)
|
||||
yellowV := math.Min(baseVal*1.25, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Slightly more saturated variant of primary
|
||||
blueS := math.Min(ph.S*1.2, 1.0)
|
||||
blueV := ph.V * 0.95
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||
darkContainer := DeriveContainer(primaryColor, false)
|
||||
palette.Color5 = NewColorInfo(darkContainer)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
gray7S := baseSat * 0.12
|
||||
gray7V := math.Min(baseVal*1.05, 1.0)
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.15
|
||||
gray8V := baseVal * 0.65
|
||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||
|
||||
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightGreenS := math.Min(baseSat*0.7, 1.0)
|
||||
brightGreenV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightYellowS := math.Min(baseSat*0.7, 1.0)
|
||||
brightYellowV := math.Min(baseVal*1.5, 1.0)
|
||||
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
|
||||
// Color12: Start of the lighter gradient - slightly desaturated
|
||||
brightBlueS := ph.S * 0.85
|
||||
brightBlueV := math.Min(ph.V*1.1, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
// Medium-high saturation pastel primary
|
||||
color13S := ph.S * 0.7
|
||||
color13V := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
|
||||
|
||||
// Lower saturation, lighter variant
|
||||
color14S := ph.S * 0.45
|
||||
color14V := math.Min(ph.V*1.4, 1.0)
|
||||
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
|
||||
|
||||
white15S := baseSat * 0.05
|
||||
white15V := math.Min(baseVal*1.45, 1.0)
|
||||
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
return palette
|
||||
|
||||
@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
|
||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||
}
|
||||
|
||||
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
||||
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
||||
// Color15 is now derived from primary, so just verify it's a valid color
|
||||
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
||||
color15Lum := Luminance(result.Color15.Hex)
|
||||
if tt.opts.IsLight {
|
||||
// Light mode: Color15 should still be relatively light
|
||||
if color15Lum < 0.5 {
|
||||
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||
}
|
||||
} else {
|
||||
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
|
||||
if color15Lum < 0.5 {
|
||||
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -579,6 +588,10 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||
|
||||
bgColor := result.Color0.Hex
|
||||
for i := 1; i < 8; i++ {
|
||||
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||
if i == 5 || i == 6 {
|
||||
continue
|
||||
}
|
||||
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||
minLc := 30.0
|
||||
if lc < minLc && lc > 0 {
|
||||
|
||||
170
core/internal/notify/notify.go
Normal file
170
core/internal/notify/notify.go
Normal file
@@ -0,0 +1,170 @@
|
||||
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,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -110,17 +111,15 @@ 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 = powered
|
||||
m.state.Discovering = discovering
|
||||
m.state.Powered = dbusutil.AsOr(poweredVar, false)
|
||||
m.state.Discovering = dbusutil.AsOr(discoveringVar, false)
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -169,65 +168,20 @@ func (m *Manager) updateDevices() error {
|
||||
}
|
||||
|
||||
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
||||
dev := Device{Path: path}
|
||||
|
||||
if v, ok := props["Address"]; ok {
|
||||
if addr, ok := v.Value().(string); ok {
|
||||
dev.Address = addr
|
||||
}
|
||||
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),
|
||||
}
|
||||
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 {
|
||||
@@ -328,17 +282,13 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
||||
m.stateMutex.Lock()
|
||||
dirty := false
|
||||
|
||||
if v, ok := changed["Powered"]; ok {
|
||||
if powered, ok := v.Value().(bool); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
}
|
||||
if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
}
|
||||
if v, ok := changed["Discovering"]; ok {
|
||||
if discovering, ok := v.Value().(bool); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
}
|
||||
if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
}
|
||||
|
||||
m.stateMutex.Unlock()
|
||||
@@ -349,31 +299,28 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
||||
}
|
||||
|
||||
func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) {
|
||||
pairedVar, hasPaired := changed["Paired"]
|
||||
paired, hasPaired := dbusutil.Get[bool](changed, "Paired")
|
||||
_, hasConnected := changed["Connected"]
|
||||
_, hasTrusted := changed["Trusted"]
|
||||
|
||||
if hasPaired {
|
||||
devicePath := string(path)
|
||||
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:
|
||||
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:
|
||||
}
|
||||
} else {
|
||||
m.pendingPairings.Delete(devicePath)
|
||||
}
|
||||
} else {
|
||||
m.pendingPairings.Delete(devicePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,14 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
handleSetConfig(conn, req, m)
|
||||
case "clipboard.store":
|
||||
handleStore(conn, req, m)
|
||||
case "clipboard.pinEntry":
|
||||
handlePinEntry(conn, req, m)
|
||||
case "clipboard.unpinEntry":
|
||||
handleUnpinEntry(conn, req, m)
|
||||
case "clipboard.getPinnedEntries":
|
||||
handleGetPinnedEntries(conn, req, m)
|
||||
case "clipboard.getPinnedCount":
|
||||
handleGetPinnedCount(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||
}
|
||||
@@ -205,6 +213,9 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||
cfg.Disabled = v
|
||||
}
|
||||
if v, ok := models.Get[float64](req, "maxPinned"); ok {
|
||||
cfg.MaxPinned = int(v)
|
||||
}
|
||||
|
||||
if err := m.SetConfig(cfg); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -230,3 +241,43 @@ func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
||||
}
|
||||
|
||||
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.PinEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
|
||||
}
|
||||
|
||||
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.UnpinEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
|
||||
}
|
||||
|
||||
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
|
||||
pinned := m.GetPinnedEntries()
|
||||
models.Respond(conn, req.ID, pinned)
|
||||
}
|
||||
|
||||
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||
count := m.GetPinnedCount()
|
||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||
}
|
||||
|
||||
@@ -389,7 +389,11 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
}
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if count < m.config.MaxHistory {
|
||||
count++
|
||||
continue
|
||||
@@ -419,6 +423,11 @@ func encodeEntry(e Entry) ([]byte, error) {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||
if e.Pinned {
|
||||
buf.WriteByte(1)
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -462,6 +471,12 @@ func decodeEntry(data []byte) (Entry, error) {
|
||||
binary.Read(buf, binary.BigEndian, &e.Hash)
|
||||
}
|
||||
|
||||
if buf.Len() >= 1 {
|
||||
var pinnedByte byte
|
||||
binary.Read(buf, binary.BigEndian, &pinnedByte)
|
||||
e.Pinned = pinnedByte == 1
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -735,19 +750,54 @@ func (m *Manager) ClearHistory() {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete only non-pinned entries
|
||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
||||
return err
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := tx.CreateBucket([]byte("clipboard"))
|
||||
return err
|
||||
|
||||
var toDelete [][]byte
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil || !entry.Pinned {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range toDelete {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to clear clipboard history: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b != nil {
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, _ := decodeEntry(v)
|
||||
if entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
if pinnedCount == 0 {
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
@@ -960,6 +1010,10 @@ func (m *Manager) clearOldEntries(days int) error {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Skip pinned entries
|
||||
if entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if entry.Timestamp.Before(cutoff) {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
@@ -1250,3 +1304,153 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) PinEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
// Check pinned count
|
||||
cfg := m.getConfig()
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
if pinnedCount >= cfg.MaxPinned {
|
||||
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Pinned = true
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put(itob(id), encoded)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) UnpinEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Pinned = false
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put(itob(id), encoded)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedEntries() []Entry {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pinned []Entry
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.Pinned {
|
||||
entry.Data = nil
|
||||
pinned = append(pinned, entry)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to get pinned entries: %v", err)
|
||||
}
|
||||
|
||||
return pinned
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedCount() int {
|
||||
if m.db == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Config struct {
|
||||
AutoClearDays int `json:"autoClearDays"`
|
||||
ClearAtStartup bool `json:"clearAtStartup"`
|
||||
Disabled bool `json:"disabled"`
|
||||
MaxPinned int `json:"maxPinned"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
@@ -27,6 +28,7 @@ func DefaultConfig() Config {
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
AutoClearDays: 0,
|
||||
ClearAtStartup: false,
|
||||
MaxPinned: 25,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +102,7 @@ type Entry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsImage bool `json:"isImage"`
|
||||
Hash uint64 `json:"hash,omitempty"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
|
||||
237
core/internal/server/dbus/handlers.go
Normal file
237
core/internal/server/dbus/handlers.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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})
|
||||
}
|
||||
362
core/internal/server/dbus/manager.go
Normal file
362
core/internal/server/dbus/manager.go
Normal file
@@ -0,0 +1,362 @@
|
||||
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)
|
||||
}
|
||||
52
core/internal/server/dbus/types.go
Normal file
52
core/internal/server/dbus/types.go
Normal file
@@ -0,0 +1,52 @@
|
||||
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,6 +6,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -110,61 +111,17 @@ func (m *Manager) updateAccountsState() error {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -180,7 +137,7 @@ func (m *Manager) updateSettingsState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if colorScheme, ok := variant.Value().(uint32); ok {
|
||||
if colorScheme, ok := dbusutil.As[uint32](variant); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.ColorScheme = colorScheme
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -132,37 +133,15 @@ func (m *Manager) updateSessionState() error {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
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.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
|
||||
}
|
||||
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 {
|
||||
@@ -170,36 +149,12 @@ func (m *Manager) updateSessionState() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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["Seat"]; ok {
|
||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||
if seatID, ok := seatArr[0].(string); ok {
|
||||
@@ -207,11 +162,7 @@ func (m *Manager) updateSessionState() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := props["VTNr"]; ok {
|
||||
if val, ok := v.Value().(uint32); ok {
|
||||
m.state.VTNr = val
|
||||
}
|
||||
}
|
||||
m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package loginctl
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -117,31 +118,28 @@ func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) {
|
||||
for key, variant := range changes {
|
||||
switch key {
|
||||
case "Active":
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Active = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "IdleHint":
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.IdleHint = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "IdleSinceHint":
|
||||
if val, ok := variant.Value().(uint64); ok {
|
||||
if val, ok := dbusutil.As[uint64](variant); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.IdleSinceHint = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "LockedHint":
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.LockedHint = val
|
||||
m.state.Locked = val
|
||||
|
||||
@@ -150,19 +150,11 @@ func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"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,
|
||||
"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 ipv6.route-metric for %v: %v", connName, err)
|
||||
log.Warnf("Failed to set priority for %s: %v", connName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric)
|
||||
|
||||
@@ -10,6 +10,7 @@ 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"
|
||||
@@ -154,6 +155,15 @@ 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,6 +20,7 @@ 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"
|
||||
@@ -65,8 +66,11 @@ 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
|
||||
@@ -363,6 +367,19 @@ 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()
|
||||
|
||||
@@ -440,6 +457,10 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
return Capabilities{Capabilities: caps}
|
||||
}
|
||||
|
||||
@@ -498,6 +519,10 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
return ServerInfo{
|
||||
APIVersion: APIVersion,
|
||||
CLIVersion: CLIVersion,
|
||||
@@ -1133,6 +1158,31 @@ 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)
|
||||
@@ -1198,6 +1248,9 @@ func cleanupManagers() {
|
||||
if clipboardManager != nil {
|
||||
clipboardManager.Close()
|
||||
}
|
||||
if dbusManager != nil {
|
||||
dbusManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
@@ -1490,6 +1543,14 @@ 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)
|
||||
|
||||
|
||||
69
core/pkg/dbusutil/variant.go
Normal file
69
core/pkg/dbusutil/variant.go
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
}
|
||||
155
core/pkg/dbusutil/variant_test.go
Normal file
155
core/pkg/dbusutil/variant_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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,7 +15,6 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -33,7 +33,6 @@ Recommends: cava
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
# Recommended system packages
|
||||
Recommends: NetworkManager
|
||||
|
||||
@@ -24,10 +24,8 @@ Requires: dms-cli = %{version}-%{release}
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: wl-clipboard
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
@@ -20,12 +20,9 @@ Requires: accountsservice
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
@@ -23,12 +23,10 @@ Requires: dgop
|
||||
|
||||
# Core utilities (Highly recommended for DMS functionality)
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Recommends: wl-clipboard
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
qt6ct
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-9CnZFtjXXWYELRiBX2UbZvWopnl9Y1ILuK+xP6YQZ9U=";
|
||||
vendorHash = "sha256-lXqOJ0yNlOcXuR3vcuVjFI02Hskmavcasb1Ntf3UlPM=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ 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)
|
||||
|
||||
@@ -102,6 +103,10 @@ 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();
|
||||
@@ -261,6 +266,10 @@ 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") {
|
||||
@@ -906,6 +915,61 @@ 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,6 +206,7 @@ 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
|
||||
@@ -366,6 +367,7 @@ 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
|
||||
@@ -393,6 +395,7 @@ 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
|
||||
@@ -492,7 +495,8 @@ Singleton {
|
||||
"shadowIntensity": 0,
|
||||
"shadowOpacity": 60,
|
||||
"shadowColorMode": "text",
|
||||
"shadowCustomColor": "#000000"
|
||||
"shadowCustomColor": "#000000",
|
||||
"clickThrough": false
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -55,9 +55,23 @@ var SPEC = {
|
||||
enabledGpuPciIds: { def: [] },
|
||||
|
||||
wifiDeviceOverride: { def: "" },
|
||||
weatherHourlyDetailed: { def: true }
|
||||
weatherHourlyDetailed: { def: true },
|
||||
|
||||
hiddenApps: { def: [] },
|
||||
appOverrides: { def: {} },
|
||||
searchAppActions: { 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,6 +100,7 @@ var SPEC = {
|
||||
reverseScrolling: { def: false },
|
||||
dwlShowAllTags: { def: false },
|
||||
workspaceColorMode: { def: "default" },
|
||||
workspaceOccupiedColorMode: { def: "none" },
|
||||
workspaceUnfocusedColorMode: { def: "default" },
|
||||
workspaceUrgentColorMode: { def: "default" },
|
||||
workspaceFocusedBorderEnabled: { def: false },
|
||||
@@ -231,6 +232,7 @@ var SPEC = {
|
||||
|
||||
showDock: { def: false },
|
||||
dockAutoHide: { def: false },
|
||||
dockSmartAutoHide: { def: false },
|
||||
dockGroupByApp: { def: false },
|
||||
dockOpenOnOverview: { def: false },
|
||||
dockPosition: { def: 1 },
|
||||
@@ -258,6 +260,7 @@ 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 },
|
||||
@@ -355,7 +358,8 @@ var SPEC = {
|
||||
shadowIntensity: 0,
|
||||
shadowOpacity: 60,
|
||||
shadowColorMode: "text",
|
||||
shadowCustomColor: "#000000"
|
||||
shadowCustomColor: "#000000",
|
||||
clickThrough: false
|
||||
}], onChange: "updateBarConfigs" },
|
||||
|
||||
desktopClockEnabled: { def: false },
|
||||
|
||||
@@ -966,6 +966,17 @@ Item {
|
||||
return success ? `PLUGIN_DISABLE_SUCCESS: ${pluginId}` : `PLUGIN_DISABLE_FAILED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function toggle(pluginId: string): string {
|
||||
if (!pluginId)
|
||||
return "ERROR: No plugin ID specified";
|
||||
|
||||
if (!PluginService.availablePlugins[pluginId])
|
||||
return `PLUGIN_NOT_FOUND: ${pluginId}`;
|
||||
|
||||
const success = PluginService.togglePlugin(pluginId);
|
||||
return success ? `PLUGIN_TOGGLE_SUCCESS: ${pluginId}` : `PLUGIN_TOGGLE_FAILED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
const plugins = PluginService.getAvailablePlugins();
|
||||
if (plugins.length === 0)
|
||||
|
||||
@@ -25,7 +25,10 @@ Item {
|
||||
width: parent.width
|
||||
totalCount: modal.totalCount
|
||||
showKeyboardHints: modal.showKeyboardHints
|
||||
activeTab: modal.activeTab
|
||||
pinnedCount: modal.pinnedCount
|
||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
onTabChanged: tabName => modal.activeTab = tabName
|
||||
onClearAllClicked: {
|
||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
||||
modal.clearAll();
|
||||
@@ -70,18 +73,20 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
// Recents Tab
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.clipboardEntries
|
||||
values: clipboardContent.modal.unpinnedEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "recents"
|
||||
|
||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||
spacing: Theme.spacingXS
|
||||
@@ -114,11 +119,11 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No clipboard entries found")
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.clipboardEntries.length === 0
|
||||
visible: clipboardContent.modal.unpinnedEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
@@ -135,11 +140,60 @@ Item {
|
||||
listView: clipboardListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Saved Tab
|
||||
DankListView {
|
||||
id: savedListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.pinnedEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "saved"
|
||||
|
||||
spacing: Theme.spacingXS
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No saved clipboard entries")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.pinnedEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
width: savedListView.width
|
||||
height: ClipboardConstants.itemHeight
|
||||
entry: modelData
|
||||
entryIndex: index + 1
|
||||
itemIndex: index
|
||||
isSelected: false
|
||||
modal: clipboardContent.modal
|
||||
listView: savedListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyboardHintsContainer
|
||||
width: parent.width
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ Rectangle {
|
||||
|
||||
signal copyRequested
|
||||
signal deleteRequested
|
||||
signal pinRequested
|
||||
signal unpinRequested
|
||||
|
||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||
@@ -50,7 +52,7 @@ Rectangle {
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 68
|
||||
width: parent.width - 110
|
||||
spacing: Theme.spacingM
|
||||
|
||||
ClipboardThumbnail {
|
||||
@@ -100,20 +102,32 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: entry.pinned ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: entry.pinned ? Theme.primarySelected : "transparent"
|
||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
anchors.rightMargin: 80
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyRequested()
|
||||
|
||||
@@ -8,10 +8,13 @@ Item {
|
||||
|
||||
property int totalCount: 0
|
||||
property bool showKeyboardHints: false
|
||||
property string activeTab: "recents"
|
||||
property int pinnedCount: 0
|
||||
|
||||
signal keyboardHintsToggled
|
||||
signal clearAllClicked
|
||||
signal closeClicked
|
||||
signal tabChanged(string tabName)
|
||||
|
||||
height: ClipboardConstants.headerHeight
|
||||
|
||||
@@ -41,6 +44,22 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "history"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
|
||||
onClicked: tabChanged("recents")
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
|
||||
opacity: header.pinnedCount > 0 ? 1 : 0
|
||||
enabled: header.pinnedCount > 0
|
||||
onClicked: tabChanged("saved")
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
|
||||
@@ -19,6 +19,8 @@ DankModal {
|
||||
|
||||
property int totalCount: 0
|
||||
property var clipboardEntries: []
|
||||
property var pinnedEntries: []
|
||||
property int pinnedCount: 0
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool keyboardNavigationActive: false
|
||||
@@ -74,22 +76,37 @@ DankModal {
|
||||
|
||||
function updateFilteredModel() {
|
||||
const query = searchText.trim();
|
||||
let filtered = [];
|
||||
|
||||
if (query.length === 0) {
|
||||
clipboardEntries = internalEntries;
|
||||
filtered = internalEntries;
|
||||
} else {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
clipboardEntries = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
||||
filtered = internalEntries.filter(entry =>
|
||||
entry.preview.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort: pinned first, then by ID descending
|
||||
filtered.sort((a, b) => {
|
||||
if (a.pinned !== b.pinned) return b.pinned ? 1 : -1;
|
||||
return b.id - a.id;
|
||||
});
|
||||
|
||||
clipboardEntries = filtered;
|
||||
unpinnedEntries = filtered.filter(e => !e.pinned);
|
||||
totalCount = clipboardEntries.length;
|
||||
if (clipboardEntries.length === 0) {
|
||||
if (unpinnedEntries.length === 0) {
|
||||
keyboardNavigationActive = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex >= clipboardEntries.length) {
|
||||
selectedIndex = clipboardEntries.length - 1;
|
||||
} else if (selectedIndex >= unpinnedEntries.length) {
|
||||
selectedIndex = unpinnedEntries.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
property var internalEntries: []
|
||||
property var unpinnedEntries: []
|
||||
property string activeTab: "recents"
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
@@ -135,6 +152,10 @@ DankModal {
|
||||
return;
|
||||
}
|
||||
internalEntries = response.result || [];
|
||||
|
||||
pinnedEntries = internalEntries.filter(e => e.pinned);
|
||||
pinnedCount = pinnedEntries.length;
|
||||
|
||||
updateFilteredModel();
|
||||
});
|
||||
}
|
||||
@@ -171,18 +192,87 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
function deletePinnedEntry(entry) {
|
||||
clearConfirmDialog.show(
|
||||
I18n.tr("Delete Saved Item?"),
|
||||
I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."),
|
||||
function () {
|
||||
DMSService.sendRequest("clipboard.deleteEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = internalEntries.filter(e => e.id !== entry.id);
|
||||
updateFilteredModel();
|
||||
ToastService.showInfo(I18n.tr("Saved item deleted"));
|
||||
});
|
||||
},
|
||||
function () {}
|
||||
);
|
||||
}
|
||||
|
||||
function pinEntry(entry) {
|
||||
DMSService.sendRequest("clipboard.getPinnedCount", null, function (countResponse) {
|
||||
if (countResponse.error) {
|
||||
ToastService.showError(I18n.tr("Failed to check pin limit"));
|
||||
return;
|
||||
}
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
totalCount = 0;
|
||||
|
||||
const maxPinned = 25; // TODO: Get from config
|
||||
if (countResponse.result.count >= maxPinned) {
|
||||
ToastService.showError(I18n.tr("Maximum pinned entries reached") + " (" + maxPinned + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("clipboard.pinEntry", { "id": entry.id }, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to pin entry"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Entry pinned"));
|
||||
refreshClipboard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unpinEntry(entry) {
|
||||
DMSService.sendRequest("clipboard.unpinEntry", { "id": entry.id }, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to unpin entry"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Entry unpinned"));
|
||||
refreshClipboard();
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const hasPinned = pinnedCount > 0;
|
||||
const message = hasPinned
|
||||
? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount)
|
||||
: I18n.tr("This will permanently delete all clipboard history.");
|
||||
|
||||
clearConfirmDialog.show(
|
||||
I18n.tr("Clear History?"),
|
||||
message,
|
||||
function () {
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
refreshClipboard();
|
||||
if (hasPinned) {
|
||||
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
|
||||
}
|
||||
});
|
||||
},
|
||||
function () {}
|
||||
);
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
return entry.preview || "";
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ FloatingWindow {
|
||||
id: processListModal
|
||||
|
||||
property int currentTab: 0
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
property bool shouldHaveFocus: visible
|
||||
property alias shouldBeVisible: processListModal.visible
|
||||
|
||||
@@ -27,9 +28,8 @@ FloatingWindow {
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
if (processContextMenu.visible) {
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -61,48 +61,63 @@ 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(650, 400)
|
||||
implicitWidth: 900
|
||||
implicitHeight: 680
|
||||
minimumSize: Qt.size(750, 550)
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 720
|
||||
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 (contentFocusScope) {
|
||||
if (currentTab === 0 && searchField.visible)
|
||||
searchField.forceActiveFocus();
|
||||
else 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 {
|
||||
@@ -115,6 +130,9 @@ FloatingWindow {
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (processContextMenu.visible)
|
||||
return;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_1:
|
||||
currentTab = 0;
|
||||
@@ -128,7 +146,43 @@ 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 {
|
||||
@@ -161,7 +215,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -171,14 +225,14 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
visible: DgopService.dgopAvailable
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -233,166 +287,278 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 52
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
anchors.topMargin: 0
|
||||
spacing: Theme.spacingL
|
||||
Row {
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
text: I18n.tr("Processes"),
|
||||
icon: "list_alt"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Performance"),
|
||||
icon: "analytics"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Disks"),
|
||||
icon: "storage"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("System"),
|
||||
icon: "computer"
|
||||
}
|
||||
]
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Repeater {
|
||||
model: tabNames
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||
return tabIcons[index] || "tab";
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: currentTab = index
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: processesTab
|
||||
DankTextField {
|
||||
id: searchField
|
||||
Layout.preferredWidth: 250
|
||||
Layout.preferredHeight: 40
|
||||
placeholderText: I18n.tr("Search processes...", "process search placeholder")
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
text: searchText
|
||||
visible: currentTab === 0
|
||||
onTextChanged: searchText = text
|
||||
ignoreUpDownKeys: true
|
||||
keyForwardTargets: [contentFocusScope]
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: Theme.spacingL
|
||||
Layout.topMargin: Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: processesTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
sourceComponent: ProcessesView {
|
||||
searchText: processListModal.searchText
|
||||
expandedPid: processListModal.expandedPid
|
||||
contextMenu: processContextMenu
|
||||
onExpandedPidChanged: processListModal.expandedPid = expandedPid
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
sourceComponent: PerformanceView {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: disksTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
sourceComponent: DisksView {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 3
|
||||
visible: currentTab === 3
|
||||
sourceComponent: SystemView {}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
Layout.bottomMargin: Theme.spacingM
|
||||
color: "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes:", "process count label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTab
|
||||
StyledText {
|
||||
text: DgopService.processCount.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
opacity: currentTab === 1 ? 1 : 0
|
||||
sourceComponent: performanceTabComponent
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("Uptime:", "uptime label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTab
|
||||
StyledText {
|
||||
text: DgopService.shortUptime || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "swap_horiz"
|
||||
size: 14
|
||||
color: Theme.info
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: 14
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.cpuUsage.toFixed(1) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: DgopService.cpuUsage > 80 ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "sd_card"
|
||||
size: 14
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: DgopService.memoryUsage > 90 ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
@@ -19,6 +20,10 @@ 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();
|
||||
@@ -43,6 +48,49 @@ 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;
|
||||
@@ -55,10 +103,16 @@ 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") {
|
||||
@@ -155,7 +209,6 @@ 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);
|
||||
@@ -168,7 +221,6 @@ Item {
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncher
|
||||
|
||||
viewMode: SettingsData.spotlightModalViewMode
|
||||
gridColumns: SettingsData.appLauncherGridColumns
|
||||
onAppLaunched: () => {
|
||||
@@ -185,7 +237,6 @@ Item {
|
||||
|
||||
FileSearchController {
|
||||
id: fileSearchController
|
||||
|
||||
onFileOpened: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide();
|
||||
@@ -197,7 +248,6 @@ Item {
|
||||
|
||||
SpotlightContextMenuPopup {
|
||||
id: popupContextMenu
|
||||
|
||||
parent: spotlightKeyHandler
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
@@ -231,20 +281,37 @@ 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
|
||||
@@ -275,18 +342,14 @@ 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") {
|
||||
@@ -334,13 +397,10 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("list");
|
||||
}
|
||||
onClicked: appLauncher.setViewMode("list")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,13 +419,10 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("grid");
|
||||
}
|
||||
onClicked: appLauncher.setViewMode("grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,7 +436,6 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: filenameFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
@@ -394,13 +450,10 @@ 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(() => {
|
||||
@@ -413,7 +466,6 @@ Item {
|
||||
onExited: {
|
||||
if (filenameTooltipLoader.item)
|
||||
filenameTooltipLoader.item.hide();
|
||||
|
||||
filenameTooltipLoader.active = false;
|
||||
}
|
||||
}
|
||||
@@ -421,7 +473,6 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: contentFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
@@ -436,13 +487,10 @@ 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(() => {
|
||||
@@ -455,7 +503,6 @@ Item {
|
||||
onExited: {
|
||||
if (contentTooltipLoader.item)
|
||||
contentTooltipLoader.item.hide();
|
||||
|
||||
contentTooltipLoader.active = false;
|
||||
}
|
||||
}
|
||||
@@ -474,13 +521,10 @@ 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);
|
||||
@@ -500,16 +544,320 @@ 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,7 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
|
||||
@@ -19,6 +18,8 @@ PanelWindow {
|
||||
property real menuPositionX: 0
|
||||
property real menuPositionY: 0
|
||||
|
||||
signal editAppRequested(var app)
|
||||
|
||||
readonly property real shadowBuffer: 5
|
||||
|
||||
screen: parentModal?.effectiveScreen
|
||||
@@ -106,6 +107,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
onHideRequested: root.hide()
|
||||
onEditAppRequested: app => root.editAppRequested(app)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -68,6 +68,27 @@ 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 = [];
|
||||
|
||||
@@ -103,6 +124,21 @@ 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,6 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
|
||||
@@ -11,6 +10,8 @@ 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;
|
||||
@@ -53,7 +54,7 @@ Popup {
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = true;
|
||||
}
|
||||
if (searchField) {
|
||||
if (searchField?.visible) {
|
||||
Qt.callLater(() => {
|
||||
searchField.forceActiveFocus();
|
||||
});
|
||||
@@ -84,5 +85,6 @@ Popup {
|
||||
id: menuContent
|
||||
appLauncher: root.appLauncher
|
||||
onHideRequested: root.hide()
|
||||
onEditAppRequested: app => root.editAppRequested(app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,20 @@ 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,11 +1,7 @@
|
||||
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 {
|
||||
@@ -15,6 +11,63 @@ 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("/")) {
|
||||
@@ -42,16 +95,25 @@ 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;
|
||||
@@ -100,8 +162,45 @@ 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
|
||||
@@ -140,7 +239,7 @@ DankPopout {
|
||||
id: keyHandler
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
focus: !appDrawerPopout.editMode
|
||||
|
||||
function selectNext() {
|
||||
switch (appDrawerPopout.searchMode) {
|
||||
@@ -172,6 +271,29 @@ 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();
|
||||
@@ -181,6 +303,8 @@ 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();
|
||||
@@ -191,6 +315,9 @@ DankPopout {
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (appDrawerPopout.editMode)
|
||||
return;
|
||||
|
||||
if (keyMappings[event.key]) {
|
||||
keyMappings[event.key]();
|
||||
event.accepted = true;
|
||||
@@ -198,9 +325,8 @@ DankPopout {
|
||||
}
|
||||
|
||||
const hasCtrl = event.modifiers & Qt.ControlModifier;
|
||||
if (!hasCtrl) {
|
||||
if (!hasCtrl)
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_N:
|
||||
@@ -234,6 +360,7 @@ DankPopout {
|
||||
x: Theme.spacingS
|
||||
y: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
visible: !appDrawerPopout.editMode
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
@@ -487,7 +614,7 @@ DankPopout {
|
||||
appLauncher.launchApp(modelData);
|
||||
}
|
||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||
contextMenu.show(mouseX, mouseY, modelData);
|
||||
contextMenu.show(mouseX, mouseY, modelData, false);
|
||||
}
|
||||
onKeyboardNavigationReset: {
|
||||
appLauncher.keyboardNavigationActive = false;
|
||||
@@ -574,7 +701,7 @@ DankPopout {
|
||||
appLauncher.launchApp(modelData);
|
||||
}
|
||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||
contextMenu.show(mouseX, mouseY, modelData);
|
||||
contextMenu.show(mouseX, mouseY, modelData, false);
|
||||
}
|
||||
onKeyboardNavigationReset: {
|
||||
appLauncher.keyboardNavigationActive = false;
|
||||
@@ -615,6 +742,281 @@ 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
|
||||
@@ -624,338 +1026,21 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
SpotlightContextMenuPopup {
|
||||
id: contextMenu
|
||||
|
||||
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)
|
||||
parent: contentLoader.item
|
||||
appLauncher: appLauncher
|
||||
parentHandler: contentLoader.item?.keyHandler ?? null
|
||||
searchField: contentLoader.item?.searchField ?? null
|
||||
visible: false
|
||||
z: 1000
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: contextMenu
|
||||
function onEditAppRequested(app) {
|
||||
appDrawerPopout.openEditMode(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onHiddenAppsChanged() {
|
||||
updateFilteredModel();
|
||||
}
|
||||
function onAppOverridesChanged() {
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilteredModel() {
|
||||
if (suppressUpdatesWhileLaunching) {
|
||||
suppressUpdatesWhileLaunching = false;
|
||||
@@ -124,7 +134,7 @@ Item {
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
||||
});
|
||||
const coreItems = AppSearchService.getCoreApps("");
|
||||
apps = AppSearchService.applications.concat(emptyTriggerItems).concat(coreItems);
|
||||
apps = AppSearchService.getVisibleApplications().concat(emptyTriggerItems).concat(coreItems);
|
||||
} else {
|
||||
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
|
||||
const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory));
|
||||
@@ -205,6 +215,7 @@ Item {
|
||||
"isPlugin": isPluginItem,
|
||||
"isCore": app.isCore === true,
|
||||
"isBuiltInLauncher": app.isBuiltInLauncher === true,
|
||||
"isAction": app.isAction === true,
|
||||
"appIndex": uniqueApps.length - 1,
|
||||
"pinned": app._pinned === true
|
||||
});
|
||||
@@ -283,6 +294,13 @@ 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,6 +296,9 @@ Item {
|
||||
width: parent.width
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: isVertical ? widgetThickness : totalSize
|
||||
implicitHeight: isVertical ? totalSize : widgetThickness
|
||||
|
||||
Timer {
|
||||
id: layoutTimer
|
||||
interval: 0
|
||||
@@ -365,6 +368,7 @@ Item {
|
||||
onContentItemReady: contentItem => {
|
||||
contentItem.widthChanged.connect(() => layoutTimer.restart());
|
||||
contentItem.heightChanged.connect(() => layoutTimer.restart());
|
||||
layoutTimer.restart();
|
||||
}
|
||||
|
||||
onActiveChanged: layoutTimer.restart()
|
||||
|
||||
@@ -20,6 +20,13 @@ 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,8 +500,78 @@ 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: inputMask
|
||||
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 {
|
||||
|
||||
@@ -731,7 +731,8 @@ Item {
|
||||
Flow {
|
||||
id: workspaceRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
x: isVertical ? visualBackground.x : (parent.width - implicitWidth) / 2
|
||||
y: isVertical ? (parent.height - implicitHeight) / 2 : visualBackground.y
|
||||
spacing: Theme.spacingS
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
@@ -754,6 +755,17 @@ 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);
|
||||
@@ -835,6 +847,23 @@ 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":
|
||||
@@ -968,12 +997,13 @@ Item {
|
||||
dataUpdateTimer.restart();
|
||||
}
|
||||
|
||||
width: root.isVertical ? root.barThickness : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.barThickness
|
||||
width: root.isVertical ? root.widgetHeight : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.widgetHeight
|
||||
|
||||
Rectangle {
|
||||
id: focusedBorderRing
|
||||
anchors.centerIn: parent
|
||||
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
|
||||
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
|
||||
width: {
|
||||
const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0;
|
||||
return delegateRoot.visualWidth + borderWidth * 2;
|
||||
@@ -1020,9 +1050,10 @@ Item {
|
||||
id: visualContent
|
||||
width: delegateRoot.visualWidth
|
||||
height: delegateRoot.visualHeight
|
||||
anchors.centerIn: parent
|
||||
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
|
||||
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : unfocusedColor
|
||||
color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor
|
||||
|
||||
border.width: isUrgent ? 2 : 0
|
||||
border.color: isUrgent ? urgentColor : "transparent"
|
||||
|
||||
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -28,7 +29,7 @@ Variants {
|
||||
}
|
||||
|
||||
property var modelData: item
|
||||
property bool autoHide: SettingsData.dockAutoHide
|
||||
property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
|
||||
@@ -111,6 +112,133 @@ 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
|
||||
@@ -122,6 +250,15 @@ 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,8 +12,34 @@ 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;
|
||||
@@ -37,6 +63,7 @@ Scope {
|
||||
return;
|
||||
|
||||
lockInitiatedLocally = true;
|
||||
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
|
||||
|
||||
if (!SessionService.active && SessionService.loginctlAvailable) {
|
||||
pendingLock = true;
|
||||
@@ -78,6 +105,7 @@ Scope {
|
||||
return;
|
||||
}
|
||||
lockInitiatedLocally = false;
|
||||
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
|
||||
shouldLock = true;
|
||||
}
|
||||
|
||||
@@ -96,11 +124,13 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -119,13 +149,6 @@ Scope {
|
||||
|
||||
locked: shouldLock
|
||||
|
||||
onLockedChanged: {
|
||||
if (locked) {
|
||||
pendingLock = false;
|
||||
dpmsReapplyTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: lockSurface
|
||||
|
||||
@@ -155,7 +178,33 @@ 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
|
||||
}
|
||||
|
||||
@@ -200,4 +249,46 @@ 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,5 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
@@ -51,7 +52,9 @@ Item {
|
||||
if (tabIndex === NotepadStorageService.currentTabIndex && hasUnsavedChanges()) {
|
||||
root.pendingAction = "close_tab_" + tabIndex;
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
} else {
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
@@ -101,7 +104,9 @@ Item {
|
||||
root.pendingFileUrl = fileUrl;
|
||||
root.pendingAction = "load_file";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
confirmationDialogLoader.active = true;
|
||||
if (confirmationDialogLoader.item)
|
||||
confirmationDialogLoader.item.open();
|
||||
} else {
|
||||
performLoadFromFile(fileUrl);
|
||||
}
|
||||
@@ -170,29 +175,27 @@ Item {
|
||||
saveToFile(fileUrl);
|
||||
} else {
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowser.open();
|
||||
saveBrowserLoader.active = true;
|
||||
if (saveBrowserLoader.item)
|
||||
saveBrowserLoader.item.open();
|
||||
}
|
||||
}
|
||||
|
||||
onOpenRequested: {
|
||||
if (hasUnsavedChanges()) {
|
||||
root.pendingAction = "open";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
} else {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
textEditor.autoSaveToSession();
|
||||
if (textEditor.text.length > 0) {
|
||||
createNewTab();
|
||||
}
|
||||
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
}
|
||||
|
||||
onNewRequested: {
|
||||
if (hasUnsavedChanges()) {
|
||||
root.pendingAction = "new";
|
||||
root.confirmationDialogOpen = true;
|
||||
confirmationDialog.open();
|
||||
} else {
|
||||
createNewTab();
|
||||
}
|
||||
textEditor.autoSaveToSession();
|
||||
createNewTab();
|
||||
}
|
||||
|
||||
onEscapePressed: {
|
||||
@@ -249,238 +252,259 @@ Item {
|
||||
onLoadFailed: error => {}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: saveBrowser
|
||||
LazyLoader {
|
||||
id: saveBrowserLoader
|
||||
active: false
|
||||
|
||||
browserTitle: I18n.tr("Save Notepad File")
|
||||
browserIcon: "save"
|
||||
browserType: "notepad_save"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
saveMode: true
|
||||
defaultFileName: {
|
||||
if (currentTab && currentTab.title && currentTab.title !== "Untitled") {
|
||||
return currentTab.title;
|
||||
} else if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
|
||||
return currentTab.filePath.split('/').pop();
|
||||
} else {
|
||||
return "note.txt";
|
||||
}
|
||||
}
|
||||
FileBrowserSurfaceModal {
|
||||
id: saveBrowser
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
if (currentTab) {
|
||||
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
|
||||
browserTitle: I18n.tr("Save Notepad File")
|
||||
browserIcon: "save"
|
||||
browserType: "notepad_save"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
saveMode: true
|
||||
defaultFileName: {
|
||||
if (currentTab && currentTab.title && currentTab.title !== "Untitled") {
|
||||
return currentTab.title;
|
||||
} else if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
|
||||
return currentTab.filePath.split('/').pop();
|
||||
} else {
|
||||
return "note.txt";
|
||||
}
|
||||
}
|
||||
|
||||
saveToFile(fileUrl);
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
if (root.pendingAction === "new") {
|
||||
Qt.callLater(() => {
|
||||
createNewTab();
|
||||
});
|
||||
} else if (root.pendingAction === "open") {
|
||||
Qt.callLater(() => {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
});
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
Qt.callLater(() => {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
});
|
||||
}
|
||||
root.pendingAction = "";
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: loadBrowser
|
||||
|
||||
browserTitle: I18n.tr("Open Notepad File")
|
||||
browserIcon: "folder_open"
|
||||
browserType: "notepad_load"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
loadFromFile(fileUrl);
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
DankModal {
|
||||
id: confirmationDialog
|
||||
|
||||
width: 400
|
||||
height: 180
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
|
||||
onBackgroundClicked: {
|
||||
close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
event.accepted = true;
|
||||
if (currentTab) {
|
||||
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
saveToFile(fileUrl);
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
if (root.pendingAction === "new") {
|
||||
Qt.callLater(() => {
|
||||
createNewTab();
|
||||
});
|
||||
} else if (root.pendingAction === "open") {
|
||||
Qt.callLater(() => {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
});
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
Qt.callLater(() => {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
});
|
||||
}
|
||||
root.pendingAction = "";
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
close();
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
LazyLoader {
|
||||
id: loadBrowserLoader
|
||||
active: false
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
}
|
||||
FileBrowserSurfaceModal {
|
||||
id: loadBrowser
|
||||
|
||||
browserTitle: I18n.tr("Open Notepad File")
|
||||
browserIcon: "folder_open"
|
||||
browserType: "notepad_load"
|
||||
fileExtensions: ["*.txt", "*.md", "*.*"]
|
||||
allowStacking: true
|
||||
|
||||
onFileSelected: path => {
|
||||
root.fileDialogOpen = false;
|
||||
const cleanPath = path.toString().replace(/^file:\/\//, '');
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
const fileUrl = "file://" + cleanPath;
|
||||
|
||||
root.currentFileName = fileName;
|
||||
root.currentFileUrl = fileUrl;
|
||||
|
||||
loadFromFile(fileUrl);
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
root.fileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: confirmationDialogLoader
|
||||
active: false
|
||||
|
||||
DankModal {
|
||||
id: confirmationDialog
|
||||
|
||||
width: 400
|
||||
height: 180
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
|
||||
onBackgroundClicked: {
|
||||
close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
width: parent.width
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
id: discardText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Don't Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
if (root.pendingAction === "new") {
|
||||
createNewTab();
|
||||
} else if (root.pendingAction === "open") {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowser.open();
|
||||
} else if (root.pendingAction === "load_file") {
|
||||
performLoadFromFile(root.pendingFileUrl);
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
root.pendingAction = "";
|
||||
root.pendingFileUrl = "";
|
||||
}
|
||||
StyledText {
|
||||
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: saveAsText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveAsArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowser.open();
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: discardText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Don't Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
if (root.pendingAction === "new") {
|
||||
createNewTab();
|
||||
} else if (root.pendingAction === "open") {
|
||||
root.fileDialogOpen = true;
|
||||
loadBrowserLoader.active = true;
|
||||
if (loadBrowserLoader.item)
|
||||
loadBrowserLoader.item.open();
|
||||
} else if (root.pendingAction === "load_file") {
|
||||
performLoadFromFile(root.pendingFileUrl);
|
||||
} else if (root.pendingAction.startsWith("close_tab_")) {
|
||||
var tabIndex = parseInt(root.pendingAction.split("_")[2]);
|
||||
performCloseTab(tabIndex);
|
||||
}
|
||||
root.pendingAction = "";
|
||||
root.pendingFileUrl = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
Rectangle {
|
||||
width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: saveAsText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveAsArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmationDialog.close();
|
||||
root.confirmationDialogOpen = false;
|
||||
root.fileDialogOpen = true;
|
||||
saveBrowserLoader.active = true;
|
||||
if (saveBrowserLoader.item)
|
||||
saveBrowserLoader.item.open();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,20 +52,15 @@ Column {
|
||||
|
||||
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
||||
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
|
||||
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));
|
||||
}
|
||||
readonly property real tabWidth: 128
|
||||
|
||||
width: calculatedWidth
|
||||
width: tabWidth
|
||||
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
|
||||
@@ -79,11 +74,14 @@ Column {
|
||||
|
||||
Row {
|
||||
id: tabContent
|
||||
anchors.centerIn: parent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
id: tabText
|
||||
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
||||
text: {
|
||||
var prefix = "";
|
||||
if (hasUnsavedChangesForTab(modelData)) {
|
||||
@@ -96,6 +94,7 @@ Column {
|
||||
font.weight: isActive ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
@@ -6,8 +7,6 @@ import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
@@ -22,55 +21,181 @@ Column {
|
||||
property int currentMatchIndex: -1
|
||||
property int matchCount: 0
|
||||
|
||||
signal saveRequested()
|
||||
signal openRequested()
|
||||
signal newRequested()
|
||||
signal escapePressed()
|
||||
signal contentChanged()
|
||||
signal settingsRequested()
|
||||
// Plugin-provided markdown/syntax highlighting (via builtInPluginSettings)
|
||||
property bool pluginInstalled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "enabled", false)
|
||||
property bool pluginMarkdownEnabled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "markdownPreview", false)
|
||||
property bool pluginSyntaxEnabled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "syntaxHighlighting", false)
|
||||
property string pluginHighlightedHtml: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "highlightedHtml", "")
|
||||
property string pluginFileExtension: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", "")
|
||||
|
||||
// Local toggle for markdown preview (can be toggled from UI)
|
||||
property bool markdownPreviewActive: pluginMarkdownEnabled
|
||||
|
||||
// Toggle markdown preview
|
||||
function toggleMarkdownPreview() {
|
||||
if (!markdownPreviewActive) {
|
||||
// Entering preview mode
|
||||
syncContentToPlugin();
|
||||
markdownPreviewActive = true;
|
||||
} else {
|
||||
// Exiting preview mode
|
||||
markdownPreviewActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Local toggle for syntax highlighting preview (read-only view with colors)
|
||||
property bool syntaxPreviewActive: false
|
||||
|
||||
// Store original text when entering syntax preview mode
|
||||
property string syntaxPreviewOriginalText: ""
|
||||
|
||||
// Function to refresh plugin settings (called from Connections inside TextArea)
|
||||
function refreshPluginSettings() {
|
||||
pluginInstalled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "enabled", false);
|
||||
pluginMarkdownEnabled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "markdownPreview", false);
|
||||
pluginSyntaxEnabled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "syntaxHighlighting", false);
|
||||
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "highlightedHtml", "");
|
||||
pluginFileExtension = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", "");
|
||||
|
||||
console.warn("NotepadTextEditor: Plugin settings refreshed. MdEnabled:", pluginMarkdownEnabled, "HtmlLength:", pluginHighlightedHtml.length);
|
||||
}
|
||||
|
||||
// Toggle syntax preview mode
|
||||
function toggleSyntaxPreview() {
|
||||
if (!syntaxPreviewActive) {
|
||||
// Entering preview mode
|
||||
syncContentToPlugin();
|
||||
syntaxPreviewActive = true;
|
||||
} else {
|
||||
// Exiting preview mode
|
||||
syntaxPreviewActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// File extension detection for current tab
|
||||
readonly property string currentFilePath: currentTab?.filePath || ""
|
||||
readonly property string currentFileExtension: {
|
||||
if (!currentFilePath)
|
||||
return "";
|
||||
var parts = currentFilePath.split('.');
|
||||
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
|
||||
}
|
||||
|
||||
onCurrentTabChanged: handleCurrentTabChanged()
|
||||
|
||||
Component.onCompleted: handleCurrentTabChanged()
|
||||
|
||||
function handleCurrentTabChanged() {
|
||||
if (!currentTab)
|
||||
return;
|
||||
|
||||
// Reset preview state ONLY when tab actually changes
|
||||
markdownPreviewActive = false;
|
||||
syntaxPreviewActive = false;
|
||||
syntaxPreviewOriginalText = "";
|
||||
textArea.readOnly = false;
|
||||
|
||||
syncContentToPlugin();
|
||||
}
|
||||
|
||||
function syncContentToPlugin() {
|
||||
if (!currentTab)
|
||||
return;
|
||||
|
||||
// Notify plugin of content update
|
||||
// console.warn("NotepadTextEditor: Pushing content to plugin. Length:", textArea.text.length, "Path:", currentFilePath);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentFilePath", currentFilePath);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", currentFileExtension);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "sourceContent", textArea.text);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentTabChanged", Date.now());
|
||||
}
|
||||
|
||||
// Debounce content updates to plugin to keep preview ready
|
||||
Timer {
|
||||
id: syncTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: syncContentToPlugin()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: textArea
|
||||
function onTextChanged() {
|
||||
if (!markdownPreviewActive && !syntaxPreviewActive) {
|
||||
syncTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string fileExtension: {
|
||||
if (!currentFilePath)
|
||||
return "";
|
||||
var parts = currentFilePath.split('.');
|
||||
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
|
||||
}
|
||||
readonly property bool isMarkdownFile: fileExtension === "md" || fileExtension === "markdown" || fileExtension === "mdown"
|
||||
readonly property bool isCodeFile: fileExtension !== "" && fileExtension !== "txt" && !isMarkdownFile
|
||||
|
||||
signal saveRequested
|
||||
signal openRequested
|
||||
signal newRequested
|
||||
signal escapePressed
|
||||
signal contentChanged
|
||||
signal settingsRequested
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
if (!currentTab || !contentLoaded) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentTab.isTemporary) {
|
||||
return textArea.text.length > 0
|
||||
return textArea.text.length > 0;
|
||||
}
|
||||
return textArea.text !== lastSavedContent
|
||||
|
||||
// If in preview mode, compare original text
|
||||
if (markdownPreviewActive || syntaxPreviewActive) {
|
||||
return syntaxPreviewOriginalText !== lastSavedContent;
|
||||
}
|
||||
|
||||
return textArea.text !== lastSavedContent;
|
||||
}
|
||||
|
||||
function loadCurrentTabContent() {
|
||||
if (!currentTab) return
|
||||
if (!currentTab)
|
||||
return;
|
||||
contentLoaded = false;
|
||||
// Reset preview states on load
|
||||
markdownPreviewActive = false;
|
||||
syntaxPreviewActive = false;
|
||||
syntaxPreviewOriginalText = "";
|
||||
|
||||
contentLoaded = false
|
||||
NotepadStorageService.loadTabContent(
|
||||
NotepadStorageService.currentTabIndex,
|
||||
(content) => {
|
||||
lastSavedContent = content
|
||||
textArea.text = content
|
||||
contentLoaded = true
|
||||
}
|
||||
)
|
||||
NotepadStorageService.loadTabContent(NotepadStorageService.currentTabIndex, content => {
|
||||
lastSavedContent = content;
|
||||
textArea.text = content;
|
||||
contentLoaded = true;
|
||||
textArea.readOnly = false;
|
||||
});
|
||||
}
|
||||
|
||||
function saveCurrentTabContent() {
|
||||
if (!currentTab || !contentLoaded) return
|
||||
if (!currentTab || !contentLoaded)
|
||||
return;
|
||||
|
||||
NotepadStorageService.saveTabContent(
|
||||
NotepadStorageService.currentTabIndex,
|
||||
textArea.text
|
||||
)
|
||||
lastSavedContent = textArea.text
|
||||
// If in preview mode, save the original text, NOT the HTML
|
||||
var contentToSave = (markdownPreviewActive || syntaxPreviewActive) ? syntaxPreviewOriginalText : textArea.text;
|
||||
|
||||
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, contentToSave);
|
||||
lastSavedContent = contentToSave;
|
||||
}
|
||||
|
||||
function autoSaveToSession() {
|
||||
if (!currentTab || !contentLoaded) return
|
||||
saveCurrentTabContent()
|
||||
if (!currentTab || !contentLoaded)
|
||||
return;
|
||||
saveCurrentTabContent();
|
||||
}
|
||||
|
||||
function setTextDocumentLineHeight() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
property string lastTextForLineModel: ""
|
||||
@@ -78,100 +203,102 @@ Column {
|
||||
|
||||
function updateLineModel() {
|
||||
if (!SettingsData.notepadShowLineNumbers) {
|
||||
lineModel = []
|
||||
lastTextForLineModel = ""
|
||||
return
|
||||
lineModel = [];
|
||||
lastTextForLineModel = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// In preview mode, line numbers might not match visual lines correctly due to wrapping/HTML
|
||||
// But for now let's use the current text (plain or HTML)
|
||||
if (textArea.text !== lastTextForLineModel || lineModel.length === 0) {
|
||||
lastTextForLineModel = textArea.text
|
||||
lineModel = textArea.text.split('\n')
|
||||
lastTextForLineModel = textArea.text;
|
||||
lineModel = textArea.text.split('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
let matches = []
|
||||
currentMatchIndex = -1
|
||||
let matches = [];
|
||||
currentMatchIndex = -1;
|
||||
|
||||
if (!searchQuery || searchQuery.length === 0) {
|
||||
searchMatches = []
|
||||
matchCount = 0
|
||||
textArea.select(0, 0)
|
||||
return
|
||||
searchMatches = [];
|
||||
matchCount = 0;
|
||||
textArea.select(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = textArea.text
|
||||
const query = searchQuery.toLowerCase()
|
||||
let index = 0
|
||||
const text = textArea.text;
|
||||
const query = searchQuery.toLowerCase();
|
||||
let index = 0;
|
||||
|
||||
while (index < text.length) {
|
||||
const foundIndex = text.toLowerCase().indexOf(query, index)
|
||||
if (foundIndex === -1) break
|
||||
|
||||
const foundIndex = text.toLowerCase().indexOf(query, index);
|
||||
if (foundIndex === -1)
|
||||
break;
|
||||
matches.push({
|
||||
start: foundIndex,
|
||||
end: foundIndex + searchQuery.length
|
||||
})
|
||||
index = foundIndex + 1
|
||||
});
|
||||
index = foundIndex + 1;
|
||||
}
|
||||
|
||||
searchMatches = matches
|
||||
matchCount = matches.length
|
||||
searchMatches = matches;
|
||||
matchCount = matches.length;
|
||||
|
||||
if (matchCount > 0) {
|
||||
currentMatchIndex = 0
|
||||
highlightCurrentMatch()
|
||||
currentMatchIndex = 0;
|
||||
highlightCurrentMatch();
|
||||
} else {
|
||||
textArea.select(0, 0)
|
||||
textArea.select(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightCurrentMatch() {
|
||||
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
|
||||
const match = searchMatches[currentMatchIndex]
|
||||
const match = searchMatches[currentMatchIndex];
|
||||
|
||||
textArea.cursorPosition = match.start
|
||||
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters)
|
||||
textArea.cursorPosition = match.start;
|
||||
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters);
|
||||
|
||||
const flickable = textArea.parent
|
||||
const flickable = textArea.parent;
|
||||
if (flickable && flickable.contentY !== undefined) {
|
||||
const lineHeight = textArea.font.pixelSize * 1.5
|
||||
const approxLine = textArea.text.substring(0, match.start).split('\n').length
|
||||
const targetY = approxLine * lineHeight - flickable.height / 2
|
||||
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height))
|
||||
const lineHeight = textArea.font.pixelSize * 1.5;
|
||||
const approxLine = textArea.text.substring(0, match.start).split('\n').length;
|
||||
const targetY = approxLine * lineHeight - flickable.height / 2;
|
||||
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findNext() {
|
||||
if (matchCount === 0 || searchMatches.length === 0) return
|
||||
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matchCount
|
||||
highlightCurrentMatch()
|
||||
if (matchCount === 0 || searchMatches.length === 0)
|
||||
return;
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matchCount;
|
||||
highlightCurrentMatch();
|
||||
}
|
||||
|
||||
function findPrevious() {
|
||||
if (matchCount === 0 || searchMatches.length === 0) return
|
||||
|
||||
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1
|
||||
highlightCurrentMatch()
|
||||
if (matchCount === 0 || searchMatches.length === 0)
|
||||
return;
|
||||
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1;
|
||||
highlightCurrentMatch();
|
||||
}
|
||||
|
||||
function showSearch() {
|
||||
searchVisible = true
|
||||
searchVisible = true;
|
||||
Qt.callLater(() => {
|
||||
searchField.forceActiveFocus()
|
||||
})
|
||||
searchField.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
searchVisible = false
|
||||
searchQuery = ""
|
||||
searchMatches = []
|
||||
matchCount = 0
|
||||
currentMatchIndex = -1
|
||||
textArea.select(0, 0)
|
||||
textArea.forceActiveFocus()
|
||||
searchVisible = false;
|
||||
searchQuery = "";
|
||||
searchMatches = [];
|
||||
matchCount = 0;
|
||||
currentMatchIndex = -1;
|
||||
textArea.select(0, 0);
|
||||
textArea.forceActiveFocus();
|
||||
}
|
||||
|
||||
spacing: Theme.spacingM
|
||||
@@ -221,43 +348,43 @@ Column {
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: {
|
||||
text = root.searchQuery
|
||||
text = root.searchQuery;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onSearchQueryChanged() {
|
||||
if (searchField.text !== root.searchQuery) {
|
||||
searchField.text = root.searchQuery
|
||||
searchField.text = root.searchQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (root.searchQuery !== text) {
|
||||
root.searchQuery = text
|
||||
root.performSearch()
|
||||
root.searchQuery = text;
|
||||
root.performSearch();
|
||||
}
|
||||
}
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hideSearch()
|
||||
event.accepted = true
|
||||
root.hideSearch();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onReturnPressed: event => {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
root.findPrevious()
|
||||
root.findPrevious();
|
||||
} else {
|
||||
root.findNext()
|
||||
root.findNext();
|
||||
}
|
||||
event.accepted = true
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
root.findPrevious()
|
||||
root.findPrevious();
|
||||
} else {
|
||||
root.findNext()
|
||||
root.findNext();
|
||||
}
|
||||
event.accepted = true
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +452,7 @@ Column {
|
||||
|
||||
DankFlickable {
|
||||
id: flickable
|
||||
visible: !root.markdownPreviewActive && !root.syntaxPreviewActive
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
clip: true
|
||||
@@ -383,7 +511,7 @@ Column {
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: textArea
|
||||
placeholderText: I18n.tr("Start typing your notes here...")
|
||||
placeholderText: ""
|
||||
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
|
||||
@@ -397,6 +525,7 @@ Column {
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
textFormat: TextEdit.PlainText
|
||||
// readOnly: root.syntaxPreviewActive || root.markdownPreviewActive // Handled by visibility now
|
||||
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
|
||||
persistentSelection: true
|
||||
tabStopDistance: 40
|
||||
@@ -404,27 +533,57 @@ 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()
|
||||
setTextDocumentLineHeight()
|
||||
root.updateLineModel()
|
||||
loadCurrentTabContent();
|
||||
setTextDocumentLineHeight();
|
||||
root.updateLineModel();
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus()
|
||||
})
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NotepadStorageService
|
||||
function onCurrentTabIndexChanged() {
|
||||
loadCurrentTabContent()
|
||||
// Exit syntax preview mode when switching tabs
|
||||
if (root.syntaxPreviewActive) {
|
||||
root.syntaxPreviewActive = false;
|
||||
}
|
||||
loadCurrentTabContent();
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus()
|
||||
})
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
function onTabsChanged() {
|
||||
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
|
||||
loadCurrentTabContent()
|
||||
loadCurrentTabContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,46 +591,49 @@ Column {
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onNotepadShowLineNumbersChanged() {
|
||||
root.updateLineModel()
|
||||
root.updateLineModel();
|
||||
}
|
||||
function onBuiltInPluginSettingsChanged() {
|
||||
root.refreshPluginSettings();
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (contentLoaded && text !== lastSavedContent) {
|
||||
autoSaveTimer.restart()
|
||||
autoSaveTimer.restart();
|
||||
}
|
||||
root.contentChanged()
|
||||
root.updateLineModel()
|
||||
root.contentChanged();
|
||||
root.updateLineModel();
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: (event) => {
|
||||
root.escapePressed()
|
||||
event.accepted = true
|
||||
Keys.onEscapePressed: event => {
|
||||
root.escapePressed();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
Keys.onPressed: event => {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_S:
|
||||
event.accepted = true
|
||||
root.saveRequested()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.saveRequested();
|
||||
break;
|
||||
case Qt.Key_O:
|
||||
event.accepted = true
|
||||
root.openRequested()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.openRequested();
|
||||
break;
|
||||
case Qt.Key_N:
|
||||
event.accepted = true
|
||||
root.newRequested()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.newRequested();
|
||||
break;
|
||||
case Qt.Key_A:
|
||||
event.accepted = true
|
||||
selectAll()
|
||||
break
|
||||
event.accepted = true;
|
||||
selectAll();
|
||||
break;
|
||||
case Qt.Key_F:
|
||||
event.accepted = true
|
||||
root.showSearch()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.showSearch();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,6 +641,66 @@ Column {
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
// Make links clickable in markdown preview mode
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated Flickable for Preview Mode
|
||||
DankFlickable {
|
||||
id: previewFlickable
|
||||
visible: root.markdownPreviewActive || root.syntaxPreviewActive
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
clip: true
|
||||
contentWidth: width - 11
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: previewAreaReal
|
||||
text: root.pluginHighlightedHtml
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true
|
||||
|
||||
// Copy styling from main textArea
|
||||
placeholderText: ""
|
||||
font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily)
|
||||
font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale
|
||||
font.letterSpacing: 0
|
||||
color: Theme.surfaceText
|
||||
selectedTextColor: Theme.background
|
||||
selectionColor: Theme.primary
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
wrapMode: TextArea.Wrap
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
|
||||
leftPadding: Theme.spacingM
|
||||
topPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
bottomPadding: Theme.spacingM
|
||||
|
||||
// Make links clickable
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,6 +767,44 @@ Column {
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown preview toggle (only visible when plugin installed and viewing .md file)
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
visible: root.pluginInstalled && root.isMarkdownFile
|
||||
|
||||
DankActionButton {
|
||||
iconName: root.markdownPreviewActive ? "visibility" : "visibility_off"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: root.markdownPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
onClicked: root.toggleMarkdownPreview()
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Preview")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.markdownPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
// Syntax highlighting toggle (only visible when plugin installed and viewing code file)
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
visible: root.pluginInstalled && root.pluginSyntaxEnabled && root.isCodeFile
|
||||
|
||||
DankActionButton {
|
||||
iconName: root.syntaxPreviewActive ? "code" : "code_off"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: root.syntaxPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
onClicked: root.toggleSyntaxPreview()
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.syntaxPreviewActive ? I18n.tr("Edit") : I18n.tr("Highlight")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.syntaxPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
@@ -578,29 +838,29 @@ Column {
|
||||
StyledText {
|
||||
text: {
|
||||
if (autoSaveTimer.running) {
|
||||
return I18n.tr("Auto-saving...")
|
||||
return I18n.tr("Auto-saving...");
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
if (currentTab && currentTab.isTemporary) {
|
||||
return I18n.tr("Unsaved note...")
|
||||
return I18n.tr("Unsaved note...");
|
||||
} else {
|
||||
return I18n.tr("Unsaved changes")
|
||||
return I18n.tr("Unsaved changes");
|
||||
}
|
||||
} else {
|
||||
return I18n.tr("Saved")
|
||||
return I18n.tr("Saved");
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (autoSaveTimer.running) {
|
||||
return Theme.primary
|
||||
return Theme.primary;
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
return Theme.warning
|
||||
return Theme.warning;
|
||||
} else {
|
||||
return Theme.success
|
||||
return Theme.success;
|
||||
}
|
||||
}
|
||||
opacity: textArea.text.length > 0 ? 1.0 : 0.0
|
||||
@@ -613,7 +873,7 @@ Column {
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
autoSaveToSession()
|
||||
autoSaveToSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -31,31 +32,40 @@ Column {
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
visible: root.showCloseButton
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
Loader {
|
||||
id: headerActionsLoader
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (root.closePopout) {
|
||||
root.closePopout();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
346
quickshell/Modules/ProcessList/DisksView.qml
Normal file
346
quickshell/Modules/ProcessList/DisksView.qml
Normal file
@@ -0,0 +1,346 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
function formatSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024)
|
||||
return bytesPerSec.toFixed(0) + " B/s";
|
||||
if (bytesPerSec < 1024 * 1024)
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
if (bytesPerSec < 1024 * 1024 * 1024)
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["disk", "diskmounts"]);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["disk", "diskmounts"]);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disk I/O", "disk io header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Read:", "disk read label")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.formatSpeed(DgopService.diskReadRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Write:", "disk write label")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.formatSpeed(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.warning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "folder"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Mount Points", "mount points header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.outlineLight
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: mountListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 4
|
||||
|
||||
model: DgopService.diskMounts
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: mountListView.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: mountMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
|
||||
readonly property real usedPct: {
|
||||
const pctStr = modelData?.percent ?? "0%";
|
||||
return parseFloat(pctStr.replace("%", "")) / 100;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mountMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const mp = modelData?.mount ?? "";
|
||||
if (mp === "/")
|
||||
return "home";
|
||||
if (mp === "/home")
|
||||
return "person";
|
||||
if (mp.includes("boot"))
|
||||
return "memory";
|
||||
if (mp.includes("media") || mp.includes("mnt"))
|
||||
return "usb";
|
||||
return "folder";
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.mount ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.device ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.fstype ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.preferredWidth: 200
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.min(1, parent.parent.parent.parent.usedPct)
|
||||
height: parent.height
|
||||
radius: 4
|
||||
color: {
|
||||
const pct = parent.parent.parent.parent.usedPct;
|
||||
if (pct > 0.95)
|
||||
return Theme.error;
|
||||
if (pct > 0.85)
|
||||
return Theme.warning;
|
||||
return Theme.primary;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.used ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "/"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.size ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.preferredWidth: 50
|
||||
text: modelData?.percent ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
const pct = parent.parent.usedPct;
|
||||
if (pct > 0.95)
|
||||
return Theme.error;
|
||||
if (pct > 0.85)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 300
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: DgopService.diskMounts.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: 32
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No mount points found", "empty state in disk mounts list")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
function formatNetworkSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024) {
|
||||
return bytesPerSec.toFixed(0) + " B/s";
|
||||
} else if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
|
||||
}
|
||||
}
|
||||
|
||||
function formatDiskSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk"]);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "CPU"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.cpuUsage.toFixed(1)}%`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 280
|
||||
height: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.cpuCores} cores`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
clip: true
|
||||
width: parent.width
|
||||
height: parent.height - 40
|
||||
contentHeight: coreUsageColumn.implicitHeight
|
||||
|
||||
Column {
|
||||
id: coreUsageColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: DgopService.perCoreCpuUsage
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 20
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: `C${index}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 80
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.min(1, modelData / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
const usage = modelData;
|
||||
if (usage > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (usage > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.primary;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData ? `${modelData.toFixed(0)}%` : "0%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 80
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Memory")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.formatSystemMemory(DgopService.usedMemoryKB)} / ${DgopService.formatSystemMemory(DgopService.totalMemoryKB)}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
width: 120
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: DgopService.totalMemoryKB > 0 ? parent.width * (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
const usage = DgopService.totalMemoryKB > 0 ? (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0;
|
||||
if (usage > 0.9) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (usage > 0.7) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.secondary;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalMemoryKB > 0 ? `${((DgopService.usedMemoryKB / DgopService.totalMemoryKB) * 100).toFixed(1)}% used` : "No data"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Swap")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalSwapKB > 0 ? `${DgopService.formatSystemMemory(DgopService.usedSwapKB)} / ${DgopService.formatSystemMemory(DgopService.totalSwapKB)}` : "No swap"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
width: 120
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: DgopService.totalSwapKB > 0 ? parent.width * (DgopService.usedSwapKB / DgopService.totalSwapKB) : 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (!DgopService.totalSwapKB) {
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
|
||||
}
|
||||
const usage = DgopService.usedSwapKB / DgopService.totalSwapKB;
|
||||
if (usage > 0.9) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (usage > 0.7) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.info;
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalSwapKB > 0 ? `${((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(1)}% used` : "N/A"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 80
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Network")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "↓"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.info
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "↑"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disk")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "R"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: formatDiskSpeed(DgopService.diskReadRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "W"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.warning
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: formatDiskSpeed(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
304
quickshell/Modules/ProcessList/PerformanceView.qml
Normal file
304
quickshell/Modules/ProcessList/PerformanceView.qml
Normal file
@@ -0,0 +1,304 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int historySize: 60
|
||||
|
||||
property var cpuHistory: []
|
||||
property var memoryHistory: []
|
||||
property var networkRxHistory: []
|
||||
property var networkTxHistory: []
|
||||
property var diskReadHistory: []
|
||||
property var diskWriteHistory: []
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + " B/s";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(1) + " KB/s";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
function addToHistory(arr, val) {
|
||||
const newArr = arr.slice();
|
||||
newArr.push(val);
|
||||
if (newArr.length > historySize)
|
||||
newArr.shift();
|
||||
return newArr;
|
||||
}
|
||||
|
||||
function sampleData() {
|
||||
cpuHistory = addToHistory(cpuHistory, DgopService.cpuUsage);
|
||||
memoryHistory = addToHistory(memoryHistory, DgopService.memoryUsage);
|
||||
networkRxHistory = addToHistory(networkRxHistory, DgopService.networkRxRate);
|
||||
networkTxHistory = addToHistory(networkTxHistory, DgopService.networkTxRate);
|
||||
diskReadHistory = addToHistory(diskReadHistory, DgopService.diskReadRate);
|
||||
diskWriteHistory = addToHistory(diskWriteHistory, DgopService.diskWriteRate);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: sampleClock
|
||||
precision: SystemClock.Seconds
|
||||
onDateChanged: {
|
||||
if (date.getSeconds() % 1 === 0)
|
||||
root.sampleData();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: "CPU"
|
||||
icon: "memory"
|
||||
value: DgopService.cpuUsage.toFixed(1) + "%"
|
||||
subtitle: DgopService.cpuModel || (DgopService.cpuCores + " cores")
|
||||
accentColor: Theme.primary
|
||||
history: root.cpuHistory
|
||||
maxValue: 100
|
||||
showSecondary: false
|
||||
extraInfo: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°C") : ""
|
||||
extraInfoColor: DgopService.cpuTemperature > 80 ? Theme.error : (DgopService.cpuTemperature > 60 ? Theme.warning : Theme.surfaceVariantText)
|
||||
}
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: I18n.tr("Memory")
|
||||
icon: "sd_card"
|
||||
value: DgopService.memoryUsage.toFixed(1) + "%"
|
||||
subtitle: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
|
||||
accentColor: Theme.secondary
|
||||
history: root.memoryHistory
|
||||
maxValue: 100
|
||||
showSecondary: false
|
||||
extraInfo: DgopService.totalSwapKB > 0 ? ("Swap: " + DgopService.formatSystemMemory(DgopService.usedSwapKB)) : ""
|
||||
extraInfoColor: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: I18n.tr("Network")
|
||||
icon: "swap_horiz"
|
||||
value: "↓ " + root.formatBytes(DgopService.networkRxRate)
|
||||
subtitle: "↑ " + root.formatBytes(DgopService.networkTxRate)
|
||||
accentColor: Theme.info
|
||||
history: root.networkRxHistory
|
||||
history2: root.networkTxHistory
|
||||
maxValue: 0
|
||||
showSecondary: true
|
||||
extraInfo: ""
|
||||
extraInfoColor: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
PerformanceCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: I18n.tr("Disk")
|
||||
icon: "storage"
|
||||
value: "R: " + root.formatBytes(DgopService.diskReadRate)
|
||||
subtitle: "W: " + root.formatBytes(DgopService.diskWriteRate)
|
||||
accentColor: Theme.warning
|
||||
history: root.diskReadHistory
|
||||
history2: root.diskWriteHistory
|
||||
maxValue: 0
|
||||
showSecondary: true
|
||||
extraInfo: {
|
||||
const rootMount = DgopService.diskMounts.find(m => m.mountpoint === "/");
|
||||
if (rootMount) {
|
||||
const usedPct = ((rootMount.used || 0) / Math.max(1, rootMount.total || 1) * 100).toFixed(0);
|
||||
return "/ " + usedPct + "% used";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
extraInfoColor: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PerformanceCard: Rectangle {
|
||||
id: card
|
||||
|
||||
property string title: ""
|
||||
property string icon: ""
|
||||
property string value: ""
|
||||
property string subtitle: ""
|
||||
property color accentColor: Theme.primary
|
||||
property var history: []
|
||||
property var history2: null
|
||||
property real maxValue: 100
|
||||
property bool showSecondary: false
|
||||
property string extraInfo: ""
|
||||
property color extraInfoColor: Theme.surfaceVariantText
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Canvas {
|
||||
id: graphCanvas
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
property var hist: card.history
|
||||
property var hist2: card.history2
|
||||
|
||||
onHistChanged: requestPaint()
|
||||
onHist2Changed: requestPaint()
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
if (!hist || hist.length < 2)
|
||||
return;
|
||||
|
||||
let max = card.maxValue;
|
||||
if (max <= 0) {
|
||||
max = 1;
|
||||
for (let k = 0; k < hist.length; k++)
|
||||
max = Math.max(max, hist[k]);
|
||||
if (hist2) {
|
||||
for (let l = 0; l < hist2.length; l++)
|
||||
max = Math.max(max, hist2[l]);
|
||||
}
|
||||
max *= 1.1;
|
||||
}
|
||||
|
||||
const c = card.accentColor;
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, height);
|
||||
grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.25));
|
||||
grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0.02));
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, height);
|
||||
for (let i = 0; i < hist.length; i++) {
|
||||
const x = (width / (root.historySize - 1)) * i;
|
||||
const y = height - (hist[i] / max) * height * 0.8;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo((width / (root.historySize - 1)) * (hist.length - 1), height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.8);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
for (let j = 0; j < hist.length; j++) {
|
||||
const px = (width / (root.historySize - 1)) * j;
|
||||
const py = height - (hist[j] / max) * height * 0.8;
|
||||
j === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
if (hist2 && hist2.length >= 2 && card.showSecondary) {
|
||||
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.4);
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
for (let m = 0; m < hist2.length; m++) {
|
||||
const sx = (width / (root.historySize - 1)) * m;
|
||||
const sy = height - (hist2[m] / max) * height * 0.8;
|
||||
m === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: card.icon
|
||||
size: Theme.iconSize
|
||||
color: card.accentColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.extraInfo
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: card.extraInfoColor
|
||||
visible: card.extraInfo.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.value
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: card.subtitle
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,61 @@ Popup {
|
||||
id: processContextMenu
|
||||
|
||||
property var processData: null
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigation: false
|
||||
property var parentFocusItem: null
|
||||
|
||||
function show(x, y) {
|
||||
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) {
|
||||
let finalX = x;
|
||||
let finalY = y;
|
||||
|
||||
@@ -19,39 +72,111 @@ 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();
|
||||
}
|
||||
|
||||
width: 180
|
||||
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
|
||||
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 {
|
||||
@@ -59,163 +184,147 @@ 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
|
||||
|
||||
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"
|
||||
Repeater {
|
||||
model: menuItems
|
||||
|
||||
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
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
height: modelData.type === "separator" ? 5 : 32
|
||||
visible: modelData.type !== "separator" || index > 0
|
||||
|
||||
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()]);
|
||||
property int itemVisibleIndex: {
|
||||
let count = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (menuItems[i].type !== "separator")
|
||||
count++;
|
||||
}
|
||||
|
||||
processContextMenu.close();
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
MouseArea {
|
||||
id: forceKillArea
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
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()]);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
processContextMenu.close();
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: processItem
|
||||
|
||||
property var process: null
|
||||
property var contextMenu: null
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.primary, 0)
|
||||
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: processMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (process && process.pid > 0 && contextMenu) {
|
||||
contextMenu.processData = process;
|
||||
const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
|
||||
const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
|
||||
contextMenu.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
onPressAndHold: {
|
||||
if (process && process.pid > 0 && contextMenu) {
|
||||
contextMenu.processData = process;
|
||||
const globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2);
|
||||
contextMenu.show(globalPos.x, globalPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
DankIcon {
|
||||
id: processIcon
|
||||
|
||||
name: DgopService.getProcessIcon(process ? process.command : "")
|
||||
size: Theme.iconSize - 4
|
||||
color: {
|
||||
if (process && process.cpu > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
opacity: 0.8
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: process ? process.displayName : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: 250
|
||||
elide: Text.ElideRight
|
||||
anchors.left: processIcon.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cpuBadge
|
||||
|
||||
width: 80
|
||||
height: 20
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (process && process.cpu > 80) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 194
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatCpuUsage(process ? process.cpu : 0)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (process && process.cpu > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: memoryBadge
|
||||
|
||||
width: 80
|
||||
height: 20
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (process && process.memoryKB > 1024 * 1024) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 102
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatMemoryUsage(process ? process.memoryKB : 0)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (process && process.memoryKB > 1024 * 1024) {
|
||||
return Theme.error;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: process ? process.pid.toString() : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
width: 50
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 40
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuButton
|
||||
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "more_vert"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: menuButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (process && process.pid > 0 && contextMenu) {
|
||||
contextMenu.processData = process;
|
||||
const globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height);
|
||||
const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
|
||||
contextMenu.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,32 +12,41 @@ DankPopout {
|
||||
|
||||
property var parentWidget: null
|
||||
property var triggerScreen: null
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
if (processContextMenu.visible) {
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
open();
|
||||
}
|
||||
|
||||
popupWidth: 600
|
||||
popupHeight: 600
|
||||
popupWidth: 650
|
||||
popupHeight: 550
|
||||
triggerWidth: 55
|
||||
positioning: ""
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
|
||||
onBackgroundClicked: {
|
||||
if (processContextMenu.visible) {
|
||||
if (processContextMenu.visible)
|
||||
processContextMenu.close();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
searchText = "";
|
||||
expandedPid = "";
|
||||
if (processesView)
|
||||
processesView.reset();
|
||||
}
|
||||
}
|
||||
|
||||
Ref {
|
||||
service: DgopService
|
||||
}
|
||||
@@ -55,55 +64,259 @@ 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 (event.key === Qt.Key_Escape) {
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (processListPopout.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
processListContent.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
target: processListPopout
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (processListPopout.shouldBeVisible)
|
||||
Qt.callLater(() => processListContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
height: systemOverview.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SystemOverview {
|
||||
id: systemOverview
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
DankIcon {
|
||||
name: "analytics"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
Layout.preferredWidth: Theme.fontSizeMedium * 14
|
||||
Layout.preferredHeight: Theme.fontSizeMedium * 2.5
|
||||
placeholderText: I18n.tr("Search...")
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
text: processListPopout.searchText
|
||||
onTextChanged: processListPopout.searchText = text
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,16 +325,177 @@ DankPopout {
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||
border.width: 0
|
||||
clip: true
|
||||
|
||||
ProcessListView {
|
||||
ProcessesView {
|
||||
id: processesView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
searchText: processListPopout.searchText
|
||||
expandedPid: processListPopout.expandedPid
|
||||
contextMenu: processContextMenu
|
||||
onExpandedPidChanged: processListPopout.expandedPid = expandedPid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component CircleGauge: Item {
|
||||
id: gaugeRoot
|
||||
|
||||
property real value: 0
|
||||
property string label: ""
|
||||
property string sublabel: ""
|
||||
property string detail: ""
|
||||
property color accentColor: Theme.primary
|
||||
property color detailColor: Theme.surfaceVariantText
|
||||
|
||||
readonly property real thickness: Math.max(4, Math.min(width, height) / 15)
|
||||
readonly property real glowExtra: thickness * 1.4
|
||||
readonly property real arcPadding: thickness / 1.3
|
||||
|
||||
readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2
|
||||
readonly property real maxTextWidth: innerDiameter * 0.9
|
||||
readonly property real baseLabelSize: Math.round(width * 0.18)
|
||||
readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65)))
|
||||
readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7)))
|
||||
readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65)))
|
||||
|
||||
property real animValue: 0
|
||||
|
||||
onValueChanged: animValue = Math.min(1, Math.max(0, value))
|
||||
|
||||
Behavior on animValue {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: animValue = Math.min(1, Math.max(0, value))
|
||||
|
||||
Canvas {
|
||||
id: glowCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
|
||||
const startAngle = -Math.PI * 0.5;
|
||||
const endAngle = Math.PI * 1.5;
|
||||
|
||||
ctx.lineCap = "round";
|
||||
|
||||
if (gaugeRoot.animValue > 0) {
|
||||
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, startAngle, prog);
|
||||
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2);
|
||||
ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: gaugeRoot
|
||||
function onAnimValueChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
function onAccentColorChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
function onWidthChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
glowCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: arcCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
|
||||
const startAngle = -Math.PI * 0.5;
|
||||
const endAngle = Math.PI * 1.5;
|
||||
|
||||
ctx.lineCap = "round";
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, startAngle, endAngle);
|
||||
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1);
|
||||
ctx.lineWidth = gaugeRoot.thickness;
|
||||
ctx.stroke();
|
||||
|
||||
if (gaugeRoot.animValue > 0) {
|
||||
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, startAngle, prog);
|
||||
ctx.strokeStyle = gaugeRoot.accentColor;
|
||||
ctx.lineWidth = gaugeRoot.thickness;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: gaugeRoot
|
||||
function onAnimValueChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
function onAccentColorChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
function onWidthChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
arcCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 1
|
||||
|
||||
StyledText {
|
||||
text: gaugeRoot.label
|
||||
font.pixelSize: gaugeRoot.labelSize
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: gaugeRoot.sublabel
|
||||
font.pixelSize: gaugeRoot.sublabelSize
|
||||
font.weight: Font.Medium
|
||||
color: gaugeRoot.accentColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: gaugeRoot.detail
|
||||
font.pixelSize: gaugeRoot.detailSize
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: gaugeRoot.detailColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: gaugeRoot.detail.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property var contextMenu: null
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["processes"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["processes"]);
|
||||
}
|
||||
|
||||
Item {
|
||||
id: columnHeaders
|
||||
|
||||
width: parent.width
|
||||
anchors.leftMargin: 8
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
width: 60
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "name") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Process")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "name" ? 1 : 0.7
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: processHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("name");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "cpu") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 200
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "CPU"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "cpu" ? 1 : 0.7
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cpuHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("cpu");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "memory") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 112
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "RAM"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "memory" ? 1 : 0.7
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: memoryHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("memory");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 50
|
||||
height: 20
|
||||
color: {
|
||||
if (DgopService.currentSort === "pid") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
}
|
||||
return pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0);
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 53
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: "PID"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium
|
||||
color: Theme.surfaceText
|
||||
opacity: DgopService.currentSort === "pid" ? 1 : 0.7
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pidHeaderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("pid");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: DgopService.sortDescending ? "↓" : "↑"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// TODO: Re-implement sort order toggle
|
||||
|
||||
id: sortOrderArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: processListView
|
||||
|
||||
property string keyRoleName: "pid"
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - columnHeaders.height
|
||||
clip: true
|
||||
spacing: 4
|
||||
model: ScriptModel {
|
||||
values: DgopService.processes
|
||||
objectProp: "pid"
|
||||
}
|
||||
|
||||
delegate: ProcessListItem {
|
||||
process: modelData
|
||||
contextMenu: root.contextMenu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: processesTab
|
||||
|
||||
property var contextMenu: null
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SystemOverview {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ProcessListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contextMenu: processesTab.contextMenu
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: localContextMenu
|
||||
}
|
||||
}
|
||||
757
quickshell/Modules/ProcessList/ProcessesView.qml
Normal file
757
quickshell/Modules/ProcessList/ProcessesView.qml
Normal file
@@ -0,0 +1,757 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["cpu", "memory", "system"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["cpu", "memory", "system"]);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (DgopService.sortBy === "cpu") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
|
||||
} else if (cpuCardMouseArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
border.width: DgopService.sortBy === "cpu" ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: cpuCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("cpu");
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("CPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary
|
||||
opacity: DgopService.sortBy === "cpu" ? 1 : 0.8
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null) {
|
||||
return "--%";
|
||||
}
|
||||
return DgopService.cpuUsage.toFixed(1) + "%";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 20
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature <= 0) {
|
||||
return "--°";
|
||||
}
|
||||
return Math.round(DgopService.cpuTemperature) + "°";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: {
|
||||
if (DgopService.cpuTemperature > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (DgopService.cpuTemperature > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.cpuCores} cores`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (DgopService.sortBy === "memory") {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
|
||||
} else if (memoryCardMouseArea.containsMouse) {
|
||||
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12);
|
||||
} else {
|
||||
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: DgopService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2)
|
||||
border.width: DgopService.sortBy === "memory" ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: memoryCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
DgopService.setSortBy("memory");
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Memory")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary
|
||||
opacity: DgopService.sortBy === "memory" ? 1 : 0.8
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB)
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 20
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: DgopService.totalSwapKB > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(DgopService.usedSwapKB) : ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: DgopService.totalSwapKB > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (DgopService.totalSwapKB > 0) {
|
||||
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + " + swap";
|
||||
}
|
||||
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.vendor.toLowerCase();
|
||||
if (vendor.includes("nvidia")) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
|
||||
} else {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12);
|
||||
}
|
||||
} else if (vendor.includes("amd")) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
|
||||
} else {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
}
|
||||
} else if (vendor.includes("intel")) {
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
|
||||
} else {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.12);
|
||||
}
|
||||
}
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.vendor.toLowerCase();
|
||||
if (vendor.includes("nvidia")) {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
|
||||
} else if (vendor.includes("amd")) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
|
||||
} else if (vendor.includes("intel")) {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3);
|
||||
}
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: gpuCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (DgopService.availableGpus.length > 1) {
|
||||
const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length;
|
||||
SessionData.setSelectedGpuIndex(nextIndex);
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
gpuContextMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("GPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.secondary
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "No GPU";
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
// Check if temperature monitoring is enabled for this GPU
|
||||
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
|
||||
const temp = gpu.temperature;
|
||||
const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0;
|
||||
if (hasTemp) {
|
||||
return Math.round(temp) + "°";
|
||||
} else {
|
||||
return gpu.vendor;
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
|
||||
const temp = gpu.temperature || 0;
|
||||
if (tempEnabled && temp > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
if (tempEnabled && temp > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "No GPUs detected";
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1;
|
||||
const temp = gpu.temperature;
|
||||
const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0;
|
||||
if (hasTemp) {
|
||||
return gpu.vendor + " " + gpu.displayName;
|
||||
} else {
|
||||
return gpu.displayName;
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
width: parent.parent.width - Theme.spacingM * 2
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: gpuContextMenu
|
||||
|
||||
MenuItem {
|
||||
text: I18n.tr("Enable GPU Temperature")
|
||||
checkable: true
|
||||
checked: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
if (!gpu.pciId) {
|
||||
return false;
|
||||
}
|
||||
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 : false;
|
||||
}
|
||||
onTriggered: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return;
|
||||
}
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
if (!gpu.pciId) {
|
||||
return;
|
||||
}
|
||||
const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
|
||||
const index = enabledIds.indexOf(gpu.pciId);
|
||||
if (checked && index === -1) {
|
||||
enabledIds.push(gpu.pciId);
|
||||
DgopService.addGpuPciId(gpu.pciId);
|
||||
} else if (!checked && index !== -1) {
|
||||
enabledIds.splice(index, 1);
|
||||
DgopService.removeGpuPciId(gpu.pciId);
|
||||
}
|
||||
SessionData.setEnabledGpuPciIds(enabledIds);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,591 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: systemColumn.implicitHeight
|
||||
clip: true
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["system", "diskmounts"]);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["system", "diskmounts"]);
|
||||
}
|
||||
|
||||
Column {
|
||||
id: systemColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: systemInfoColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
SystemLogo {
|
||||
width: 80
|
||||
height: 80
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 80 - Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: DgopService.hostname
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Light
|
||||
color: Theme.surfaceText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.distribution} • ${DgopService.architecture} • ${DgopService.kernelVersion}`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.uptime} • Boot: ${DgopService.bootTime}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `Load: ${DgopService.loadAverage} • ${DgopService.processCount} processes, ${DgopService.threadCount} threads`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXL) / 2
|
||||
height: hardwareColumn.implicitHeight + Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
|
||||
Column {
|
||||
id: hardwareColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.cpuModel
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.motherboard
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `BIOS ${DgopService.biosVersion}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${DgopService.formatSystemMemory(DgopService.totalMemoryKB)} RAM`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXL) / 2
|
||||
height: gpuColumn.implicitHeight + Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const baseColor = Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
|
||||
const hoverColor = Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.popupTransparency * 1.5);
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor;
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.fullName.split(' ')[0].toLowerCase();
|
||||
let tintColor;
|
||||
if (vendor.includes("nvidia")) {
|
||||
tintColor = Theme.success;
|
||||
} else if (vendor.includes("amd")) {
|
||||
tintColor = Theme.error;
|
||||
} else if (vendor.includes("intel")) {
|
||||
tintColor = Theme.info;
|
||||
} else {
|
||||
return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor;
|
||||
}
|
||||
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) {
|
||||
return Qt.rgba((hoverColor.r + tintColor.r * 0.1) / 1.1, (hoverColor.g + tintColor.g * 0.1) / 1.1, (hoverColor.b + tintColor.b * 0.1) / 1.1, 0.6);
|
||||
} else {
|
||||
return Qt.rgba((baseColor.r + tintColor.r * 0.08) / 1.08, (baseColor.g + tintColor.g * 0.08) / 1.08, (baseColor.b + tintColor.b * 0.08) / 1.08, 0.4);
|
||||
}
|
||||
}
|
||||
border.width: 1
|
||||
border.color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1);
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const vendor = gpu.fullName.split(' ')[0].toLowerCase();
|
||||
if (vendor.includes("nvidia")) {
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
|
||||
} else if (vendor.includes("amd")) {
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
|
||||
} else if (vendor.includes("intel")) {
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3);
|
||||
}
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gpuCardMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (DgopService.availableGpus.length > 1) {
|
||||
const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length;
|
||||
SessionData.setSelectedGpuIndex(nextIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: gpuColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "auto_awesome_mosaic"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "GPU"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "No GPUs detected";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
return gpu.fullName;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "Device: N/A";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
return `Device: ${gpu.pciId}`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "Driver: N/A";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
return `Driver: ${gpu.driver}`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return "Temp: --°";
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const temp = gpu.temperature;
|
||||
return `Temp: ${(temp === undefined || temp === null || temp === 0) ? '--°' : `${Math.round(temp)}°C`}`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: {
|
||||
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
|
||||
const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)];
|
||||
const temp = gpu.temperature || 0;
|
||||
if (temp > 80) {
|
||||
return Theme.error;
|
||||
}
|
||||
|
||||
if (temp > 60) {
|
||||
return Theme.warning;
|
||||
}
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: storageColumn.implicitHeight + 2 * Theme.spacingL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: storageColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Storage & Disks")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 24
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Device")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.25
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Mount")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.2
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Size")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Used")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Available")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Use%")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.1
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: diskMountRepeater
|
||||
|
||||
model: DgopService.diskMounts
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: diskMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData.device
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.25
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.mount
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.2
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.size
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.used
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.avail
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.percent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: {
|
||||
const percent = parseInt(modelData.percent);
|
||||
if (percent > 90) {
|
||||
return Theme.error;
|
||||
}
|
||||
|
||||
if (percent > 75) {
|
||||
return Theme.warning;
|
||||
}
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
width: parent.width * 0.1
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
386
quickshell/Modules/ProcessList/SystemView.qml
Normal file
386
quickshell/Modules/ProcessList/SystemView.qml
Normal file
@@ -0,0 +1,386 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["system", "cpu"]);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
DgopService.removeRef(["system", "cpu"]);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
ColumnLayout {
|
||||
id: systemInfoColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "computer"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Information", "system info header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
rowSpacing: Theme.spacingS
|
||||
columnSpacing: Theme.spacingXL
|
||||
|
||||
InfoRow {
|
||||
label: I18n.tr("Hostname", "system info label")
|
||||
value: DgopService.hostname || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Distribution", "system info label")
|
||||
value: DgopService.distribution || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Kernel", "system info label")
|
||||
value: DgopService.kernelVersion || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Architecture", "system info label")
|
||||
value: DgopService.architecture || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("CPU")
|
||||
value: DgopService.cpuModel || ("" + DgopService.cpuCores + " cores")
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Uptime")
|
||||
value: DgopService.uptime || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Load Average", "system info label")
|
||||
value: DgopService.loadAverage || "--"
|
||||
}
|
||||
InfoRow {
|
||||
label: I18n.tr("Processes")
|
||||
value: DgopService.processCount > 0 ? DgopService.processCount.toString() : "--"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board"
|
||||
size: Theme.iconSize
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("GPU Monitoring", "gpu section header in system monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.outlineLight
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: gpuListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 8
|
||||
|
||||
model: DgopService.availableGpus
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: gpuListView.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const vendor = (modelData?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.08);
|
||||
if (vendor.includes("amd"))
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||
if (vendor.includes("intel"))
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.08);
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
border.color: {
|
||||
const vendor = (modelData?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
|
||||
if (vendor.includes("amd"))
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
|
||||
if (vendor.includes("intel"))
|
||||
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
readonly property bool tempEnabled: {
|
||||
const pciId = modelData?.pciId ?? "";
|
||||
if (!pciId)
|
||||
return false;
|
||||
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(pciId) !== -1 : false;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board"
|
||||
size: Theme.iconSize + 4
|
||||
color: {
|
||||
const vendor = (modelData?.vendor ?? "").toLowerCase();
|
||||
if (vendor.includes("nvidia"))
|
||||
return Theme.success;
|
||||
if (vendor.includes("amd"))
|
||||
return Theme.error;
|
||||
if (vendor.includes("intel"))
|
||||
return Theme.info;
|
||||
return Theme.surfaceVariantText;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.displayName ?? I18n.tr("Unknown GPU", "fallback gpu name")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData?.vendor ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: (modelData?.driver ?? "").length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.driver ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: (modelData?.driver ?? "").length > 0
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.pciId ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceVariantText
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 70
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: parent.parent.tempEnabled ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
|
||||
border.color: tempMouseArea.containsMouse ? Theme.outline : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: tempRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "thermostat"
|
||||
size: 16
|
||||
color: {
|
||||
if (!parent.parent.parent.parent.tempEnabled)
|
||||
return Theme.surfaceVariantText;
|
||||
const temp = modelData?.temperature ?? 0;
|
||||
if (temp > 85)
|
||||
return Theme.error;
|
||||
if (temp > 70)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
opacity: parent.parent.parent.parent.tempEnabled ? 1 : 0.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!parent.parent.parent.parent.tempEnabled)
|
||||
return I18n.tr("Off");
|
||||
const temp = modelData?.temperature ?? 0;
|
||||
return temp > 0 ? (temp.toFixed(0) + "°C") : "--";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: parent.parent.parent.parent.tempEnabled ? SettingsData.monoFontFamily : ""
|
||||
font.weight: parent.parent.parent.parent.tempEnabled ? Font.Bold : Font.Normal
|
||||
color: {
|
||||
if (!parent.parent.parent.parent.tempEnabled)
|
||||
return Theme.surfaceVariantText;
|
||||
const temp = modelData?.temperature ?? 0;
|
||||
if (temp > 85)
|
||||
return Theme.error;
|
||||
if (temp > 70)
|
||||
return Theme.warning;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tempMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const pciId = modelData?.pciId;
|
||||
if (!pciId)
|
||||
return;
|
||||
|
||||
const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
|
||||
const idx = enabledIds.indexOf(pciId);
|
||||
const wasEnabled = idx !== -1;
|
||||
|
||||
if (!wasEnabled) {
|
||||
enabledIds.push(pciId);
|
||||
DgopService.addGpuPciId(pciId);
|
||||
} else {
|
||||
enabledIds.splice(idx, 1);
|
||||
DgopService.removeGpuPciId(pciId);
|
||||
}
|
||||
|
||||
SessionData.setEnabledGpuPciIds(enabledIds);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 300
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
visible: DgopService.availableGpus.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board_off"
|
||||
size: 32
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No GPUs detected", "empty state in gpu list")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoRow: RowLayout {
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: label + ":"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
Layout.preferredWidth: 100
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: value
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,33 @@ Item {
|
||||
}
|
||||
]
|
||||
|
||||
readonly property var maxPinnedOptions: [
|
||||
{
|
||||
text: "5",
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
text: "10",
|
||||
value: 10
|
||||
},
|
||||
{
|
||||
text: "15",
|
||||
value: 15
|
||||
},
|
||||
{
|
||||
text: "25",
|
||||
value: 25
|
||||
},
|
||||
{
|
||||
text: "50",
|
||||
value: 50
|
||||
},
|
||||
{
|
||||
text: "100",
|
||||
value: 100
|
||||
}
|
||||
]
|
||||
|
||||
function getMaxHistoryText(value) {
|
||||
if (value <= 0)
|
||||
return "∞";
|
||||
@@ -152,6 +179,14 @@ Item {
|
||||
return value + " " + I18n.tr("days");
|
||||
}
|
||||
|
||||
function getMaxPinnedText(value) {
|
||||
for (let opt of maxPinnedOptions) {
|
||||
if (opt.value === value)
|
||||
return opt.text;
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
configLoaded = false;
|
||||
configError = false;
|
||||
@@ -295,6 +330,24 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "pinned", "max", "limit"]
|
||||
settingKey: "maxPinned"
|
||||
text: I18n.tr("Maximum Pinned Entries")
|
||||
description: I18n.tr("Maximum number of entries that can be saved")
|
||||
currentValue: root.getMaxPinnedText(root.config.maxPinned ?? 25)
|
||||
options: root.maxPinnedOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let opt of root.maxPinnedOptions) {
|
||||
if (opt.text === value) {
|
||||
root.saveConfig("maxPinned", opt.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -373,7 +373,9 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
switch (barCard.modelData.position) {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
switch (cfg?.position ?? SettingsData.Position.Top) {
|
||||
case SettingsData.Position.Top:
|
||||
return I18n.tr("Top");
|
||||
case SettingsData.Position.Bottom:
|
||||
@@ -398,7 +400,9 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const prefs = barCard.modelData.screenPreferences || ["all"];
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
const prefs = cfg?.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);
|
||||
@@ -415,9 +419,11 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const left = barCard.modelData.leftWidgets?.length || 0;
|
||||
const center = barCard.modelData.centerWidgets?.length || 0;
|
||||
const right = barCard.modelData.rightWidgets?.length || 0;
|
||||
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;
|
||||
return I18n.tr("%1 widgets").replace("%1", left + center + right);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -428,14 +434,22 @@ Item {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: !barCard.modelData.enabled && barCard.modelData.id !== "default"
|
||||
visible: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
return !cfg?.enabled && barCard.modelData.id !== "default";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disabled")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
visible: !barCard.modelData.enabled && barCard.modelData.id !== "default"
|
||||
visible: {
|
||||
SettingsData.barConfigs;
|
||||
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
|
||||
return !cfg?.enabled && barCard.modelData.id !== "default";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,6 +759,21 @@ 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
|
||||
@@ -1057,6 +1086,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: shadowCard
|
||||
iconName: "layers"
|
||||
title: I18n.tr("Shadow", "bar shadow settings card")
|
||||
visible: selectedBarConfig?.enabled
|
||||
@@ -1076,7 +1106,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
visible: parent.shadowActive
|
||||
visible: shadowCard.shadowActive
|
||||
text: I18n.tr("Opacity")
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
@@ -1088,7 +1118,7 @@ Item {
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: parent.shadowActive
|
||||
visible: shadowCard.shadowActive
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
|
||||
@@ -86,10 +86,30 @@ Item {
|
||||
settingKey: "dockAutoHide"
|
||||
tags: ["dock", "autohide", "hide", "hover"]
|
||||
text: I18n.tr("Auto-hide Dock")
|
||||
description: I18n.tr("Hide the dock when not in use and reveal it when hovering near the dock area")
|
||||
description: I18n.tr("Always hide the dock and reveal it when hovering near the dock area")
|
||||
checked: SettingsData.dockAutoHide
|
||||
visible: SettingsData.showDock
|
||||
onToggled: checked => SettingsData.set("dockAutoHide", checked)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
|
||||
@@ -462,6 +462,250 @@ 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,6 +131,15 @@ 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,6 +199,48 @@ 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,6 +10,8 @@ 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
|
||||
@@ -40,6 +42,51 @@ 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")
|
||||
@@ -226,7 +273,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshApplications()
|
||||
Component.onCompleted: {
|
||||
_rebuildHiddenSet();
|
||||
refreshApplications();
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 0);
|
||||
@@ -345,17 +395,17 @@ Singleton {
|
||||
}
|
||||
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0) {
|
||||
return applications;
|
||||
}
|
||||
if (!query || query.length === 0)
|
||||
return getVisibleApplications();
|
||||
if (applications.length === 0)
|
||||
return [];
|
||||
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
const scoredApps = [];
|
||||
const results = [];
|
||||
const visibleApps = getVisibleApplications();
|
||||
|
||||
for (const app of applications) {
|
||||
for (const app of visibleApps) {
|
||||
const name = (app.name || "").toLowerCase();
|
||||
const genericName = (app.genericName || "").toLowerCase();
|
||||
const comment = (app.comment || "").toLowerCase();
|
||||
@@ -440,10 +490,58 @@ 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 [];
|
||||
@@ -525,17 +623,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
if (category === I18n.tr("All")) {
|
||||
return applications;
|
||||
}
|
||||
const visibleApps = getVisibleApplications();
|
||||
if (category === I18n.tr("All"))
|
||||
return visibleApps;
|
||||
|
||||
// Check if it's a plugin category
|
||||
const pluginItems = getPluginItems(category, "");
|
||||
if (pluginItems.length > 0) {
|
||||
if (pluginItems.length > 0)
|
||||
return pluginItems;
|
||||
}
|
||||
|
||||
return applications.filter(app => {
|
||||
return visibleApps.filter(app => {
|
||||
const appCategories = getCategoriesForApp(app);
|
||||
return appCategories.includes(category);
|
||||
});
|
||||
|
||||
@@ -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"]
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
@@ -191,17 +191,19 @@ Singleton {
|
||||
if (!line || line.length === 0)
|
||||
return;
|
||||
|
||||
let response;
|
||||
try {
|
||||
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);
|
||||
response = JSON.parse(line);
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse request response");
|
||||
console.warn("DMSService: Failed to parse request response:", line.substring(0, 100));
|
||||
return;
|
||||
}
|
||||
const isClipboard = clipboardRequestIds[response.id];
|
||||
if (isClipboard)
|
||||
delete clipboardRequestIds[response.id];
|
||||
else
|
||||
console.log("DMSService: Request socket <<", line);
|
||||
handleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,14 +225,16 @@ Singleton {
|
||||
if (!line || line.length === 0)
|
||||
return;
|
||||
|
||||
let response;
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
if (!line.includes("clipboard"))
|
||||
console.log("DMSService: Subscribe socket <<", line);
|
||||
handleSubscriptionEvent(response);
|
||||
response = JSON.parse(line);
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse subscription event");
|
||||
console.warn("DMSService: Failed to parse subscription event:", line.substring(0, 100));
|
||||
return;
|
||||
}
|
||||
if (!line.includes("clipboard"))
|
||||
console.log("DMSService: Subscribe socket <<", line);
|
||||
handleSubscriptionEvent(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +304,7 @@ Singleton {
|
||||
excludeServices = [excludeServices];
|
||||
}
|
||||
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser"];
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
|
||||
const filtered = allServices.filter(s => !excludeServices.includes(s));
|
||||
subscribe(filtered);
|
||||
}
|
||||
@@ -383,6 +387,8 @@ Singleton {
|
||||
screensaverInhibited = data.inhibited || false;
|
||||
screensaverInhibitors = data.inhibitors || [];
|
||||
screensaverStateUpdate(data);
|
||||
} else if (service === "dbus") {
|
||||
dbusSignalReceived(data.subscriptionId || "", data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,4 +652,89 @@ 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,6 +21,7 @@ Singleton {
|
||||
property int processLimit: 20
|
||||
property string processSort: "cpu"
|
||||
property bool noCpu: false
|
||||
property int dgopProcessPid: 0
|
||||
|
||||
// Cursor data for accurate CPU calculations
|
||||
property string cpuCursor: ""
|
||||
@@ -59,6 +60,7 @@ Singleton {
|
||||
property var processes: []
|
||||
property var allProcesses: []
|
||||
property string currentSort: "cpu"
|
||||
property bool sortAscending: false
|
||||
property var availableGpus: []
|
||||
|
||||
property string kernelVersion: ""
|
||||
@@ -93,10 +95,8 @@ Singleton {
|
||||
if (modules) {
|
||||
const modulesToAdd = Array.isArray(modules) ? modules : [modules];
|
||||
for (const module of modulesToAdd) {
|
||||
// Increment reference count for this module
|
||||
const currentCount = moduleRefCounts[module] || 0;
|
||||
moduleRefCounts[module] = currentCount + 1;
|
||||
console.log("Adding ref for module:", module, "count:", moduleRefCounts[module]);
|
||||
|
||||
// Add to enabled modules if not already there
|
||||
if (enabledModules.indexOf(module) === -1) {
|
||||
@@ -126,17 +126,13 @@ Singleton {
|
||||
for (const module of modulesToRemove) {
|
||||
const currentCount = moduleRefCounts[module] || 0;
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
moduleRefCounts[module] = currentCount - 1;
|
||||
console.log("Removing ref for module:", module, "count:", moduleRefCounts[module]);
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete moduleRefCounts[module];
|
||||
const index = enabledModules.indexOf(module);
|
||||
if (index > -1) {
|
||||
enabledModules.splice(index, 1);
|
||||
modulesChanged = true;
|
||||
console.log("Disabling module:", module, "(no more refs)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,17 +167,13 @@ Singleton {
|
||||
gpuPciIds = gpuPciIds.concat([pciId]);
|
||||
}
|
||||
|
||||
console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]);
|
||||
// Force property change notification
|
||||
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts);
|
||||
}
|
||||
|
||||
function removeGpuPciId(pciId) {
|
||||
const currentCount = gpuPciIdRefCounts[pciId] || 0;
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
gpuPciIdRefCounts[pciId] = currentCount - 1;
|
||||
console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]);
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete gpuPciIdRefCounts[pciId];
|
||||
@@ -203,8 +195,6 @@ Singleton {
|
||||
}
|
||||
availableGpus = updatedGpus;
|
||||
}
|
||||
|
||||
console.log("Removing GPU PCI ID completely:", pciId);
|
||||
}
|
||||
|
||||
// Force property change notification
|
||||
@@ -389,8 +379,12 @@ Singleton {
|
||||
if (data.processes && Array.isArray(data.processes)) {
|
||||
const newProcesses = [];
|
||||
processSampleCount++;
|
||||
const ourPid = dgopProcessPid;
|
||||
|
||||
for (const proc of data.processes) {
|
||||
if (ourPid > 0 && proc.pid === ourPid)
|
||||
continue;
|
||||
|
||||
const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0;
|
||||
|
||||
newProcesses.push({
|
||||
@@ -577,39 +571,55 @@ Singleton {
|
||||
function setSortBy(newSortBy) {
|
||||
if (newSortBy !== currentSort) {
|
||||
currentSort = newSortBy;
|
||||
sortAscending = false;
|
||||
applySorting();
|
||||
}
|
||||
}
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0) {
|
||||
return;
|
||||
function toggleSort(column) {
|
||||
if (column === currentSort) {
|
||||
sortAscending = !sortAscending;
|
||||
} else {
|
||||
currentSort = column;
|
||||
sortAscending = false;
|
||||
}
|
||||
applySorting();
|
||||
}
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0)
|
||||
return;
|
||||
|
||||
const asc = sortAscending;
|
||||
const sorted = allProcesses.slice();
|
||||
sorted.sort((a, b) => {
|
||||
let valueA, valueB;
|
||||
let valueA, valueB, result;
|
||||
|
||||
switch (currentSort) {
|
||||
case "cpu":
|
||||
valueA = a.cpu || 0;
|
||||
valueB = b.cpu || 0;
|
||||
return valueB - valueA;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
case "memory":
|
||||
valueA = a.memoryKB || 0;
|
||||
valueB = b.memoryKB || 0;
|
||||
return valueB - valueA;
|
||||
result = valueB - valueA;
|
||||
break;
|
||||
case "name":
|
||||
valueA = (a.command || "").toLowerCase();
|
||||
valueB = (b.command || "").toLowerCase();
|
||||
return valueA.localeCompare(valueB);
|
||||
result = valueA.localeCompare(valueB);
|
||||
break;
|
||||
case "pid":
|
||||
valueA = a.pid || 0;
|
||||
valueB = b.pid || 0;
|
||||
return valueA - valueB;
|
||||
result = valueA - valueB;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return asc ? -result : result;
|
||||
});
|
||||
|
||||
processes = sorted.slice(0, processLimit);
|
||||
@@ -628,10 +638,7 @@ Singleton {
|
||||
id: dgopProcess
|
||||
command: root.buildDgopCommand()
|
||||
running: false
|
||||
onCommandChanged:
|
||||
|
||||
//console.log("DgopService command:", JSON.stringify(command))
|
||||
{}
|
||||
onStarted: dgopProcessPid = processId ?? 0
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:", exitCode);
|
||||
|
||||
@@ -556,6 +556,33 @@ Singleton {
|
||||
return loadPlugin(pluginId, true);
|
||||
}
|
||||
|
||||
function togglePlugin(pluginId) {
|
||||
let instance = pluginInstances[pluginId];
|
||||
|
||||
// Lazy instantiate daemon plugins on first toggle
|
||||
// This respects the daemon lifecycle (not instantiated on load)
|
||||
// while supporting toggle functionality for slideout-capable daemons
|
||||
if (!instance && pluginDaemonComponents[pluginId]) {
|
||||
const comp = pluginDaemonComponents[pluginId];
|
||||
const newInstance = comp.createObject(root, {
|
||||
"pluginId": pluginId,
|
||||
"pluginService": root
|
||||
});
|
||||
if (newInstance) {
|
||||
const newInstances = Object.assign({}, pluginInstances);
|
||||
newInstances[pluginId] = newInstance;
|
||||
pluginInstances = newInstances;
|
||||
instance = newInstance;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance && typeof instance.toggle === "function") {
|
||||
instance.toggle();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function savePluginData(pluginId, key, value) {
|
||||
SettingsData.setPluginSetting(pluginId, key, value);
|
||||
pluginDataChanged(pluginId);
|
||||
|
||||
@@ -182,17 +182,44 @@ 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(" ");
|
||||
@@ -200,7 +227,7 @@ Singleton {
|
||||
Quickshell.execDetached({
|
||||
command: [terminal, "-e", "sh", "-c", shellCmd],
|
||||
workingDirectory: workDir,
|
||||
environment: cursorEnv
|
||||
environment: finalEnv
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -210,7 +237,7 @@ Singleton {
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", `${prefix} ${escapedCmd}`],
|
||||
workingDirectory: workDir,
|
||||
environment: cursorEnv
|
||||
environment: finalEnv
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -221,7 +248,7 @@ Singleton {
|
||||
Quickshell.execDetached({
|
||||
command: cmd,
|
||||
workingDirectory: workDir,
|
||||
environment: cursorEnv
|
||||
environment: finalEnv
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ StyledRect {
|
||||
property bool enabled: true
|
||||
property int buttonSize: 32
|
||||
property var tooltipText: null
|
||||
property string tooltipSide: "bottom"
|
||||
|
||||
signal clicked
|
||||
signal entered
|
||||
@@ -38,5 +39,6 @@ StyledRect {
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
tooltipText: root.tooltipText
|
||||
tooltipSide: root.tooltipSide
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ Flow {
|
||||
animationTimer.restart();
|
||||
} else {
|
||||
const oldIndex = currentIndex;
|
||||
currentIndex = index;
|
||||
selectionChanged(index, true);
|
||||
if (oldIndex !== index && oldIndex >= 0) {
|
||||
selectionChanged(oldIndex, false);
|
||||
|
||||
@@ -29,6 +29,7 @@ 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
|
||||
@@ -461,8 +462,12 @@ Item {
|
||||
id: focusHelper
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
visible: !root.contentHandlesKeys
|
||||
enabled: !root.contentHandlesKeys
|
||||
focus: !root.contentHandlesKeys
|
||||
Keys.onPressed: event => {
|
||||
if (root.contentHandlesKeys)
|
||||
return;
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close();
|
||||
event.accepted = true;
|
||||
|
||||
@@ -53,6 +53,7 @@ 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
|
||||
@@ -145,9 +146,16 @@ 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]) {
|
||||
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])
|
||||
root.keyForwardTargets[i].Keys.pressed(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
|
||||
@@ -26,7 +27,7 @@ MouseArea {
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
tooltip.show(root.tooltipText, root, 0, 0, "bottom");
|
||||
tooltip.show(root.tooltipText, root, 0, 0, root.tooltipSide);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
{
|
||||
"scope": ["keyword"],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color5.dark.hex}}"
|
||||
"foreground": "{{dank16.color6.dark.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -320,7 +320,7 @@
|
||||
"foreground": "{{dank16.color12.dark.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color5.dark.hex}}"
|
||||
"foreground": "{{dank16.color6.dark.hex}}"
|
||||
},
|
||||
"comment": {
|
||||
"foreground": "{{dank16.color8.dark.hex}}"
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
{
|
||||
"scope": ["keyword"],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color5.default.hex}}"
|
||||
"foreground": "{{dank16.color6.default.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -272,7 +272,7 @@
|
||||
"foreground": "{{dank16.color12.default.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color5.default.hex}}"
|
||||
"foreground": "{{dank16.color6.default.hex}}"
|
||||
},
|
||||
"comment": {
|
||||
"foreground": "{{colors.outline.default.hex}}"
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"storage.type"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "{{dank16.color5.light.hex}}"
|
||||
"foreground": "{{dank16.color6.light.hex}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -444,7 +444,7 @@
|
||||
"foreground": "{{colors.outline.light.hex}}"
|
||||
},
|
||||
"keyword": {
|
||||
"foreground": "{{dank16.color5.light.hex}}"
|
||||
"foreground": "{{dank16.color6.light.hex}}"
|
||||
},
|
||||
"operator": {
|
||||
"foreground": "{{colors.on_surface.light.hex}}"
|
||||
|
||||
@@ -24,7 +24,8 @@ LANGUAGES = {
|
||||
"es": "es.json",
|
||||
"he": "he.json",
|
||||
"hu": "hu.json",
|
||||
"fa": "fa.json"
|
||||
"fa": "fa.json",
|
||||
"fr": "fr.json"
|
||||
}
|
||||
|
||||
def error(msg):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -251,6 +251,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -707,6 +710,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -917,6 +923,12 @@
|
||||
"Copy": {
|
||||
"Copy": ""
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "Copiar ID del Proceso"
|
||||
},
|
||||
@@ -1124,6 +1136,9 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "Eliminar impresora"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "Eliminar VPN"
|
||||
},
|
||||
@@ -1208,6 +1223,9 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "Uso de disco"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "Descartar"
|
||||
},
|
||||
@@ -1442,6 +1460,12 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "Empresa"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "Error"
|
||||
},
|
||||
@@ -1484,6 +1508,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1559,6 +1586,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1604,6 +1634,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1709,6 +1742,9 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "Forzar HDR"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "Forzar terminar el proceso"
|
||||
},
|
||||
@@ -1763,6 +1799,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1880,6 +1919,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1994,6 +2036,9 @@
|
||||
"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:"
|
||||
},
|
||||
@@ -2021,6 +2066,166 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2255,9 +2460,18 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2549,6 +2763,12 @@
|
||||
"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."
|
||||
},
|
||||
@@ -2624,6 +2844,9 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "Posición OSD"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "Apagado"
|
||||
},
|
||||
@@ -2771,6 +2994,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -2870,6 +3096,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2939,6 +3168,9 @@
|
||||
"Process Count": {
|
||||
"Process Count": "Conteo de procesos"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "Procesando"
|
||||
},
|
||||
@@ -3137,6 +3369,9 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "Configuraciones guardadas"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "Escala"
|
||||
},
|
||||
@@ -3158,6 +3393,9 @@
|
||||
"Science": {
|
||||
"Science": "Ciencia"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "Compartir pantalla"
|
||||
},
|
||||
@@ -3437,6 +3675,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -3752,9 +3993,15 @@
|
||||
"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"
|
||||
},
|
||||
@@ -3854,6 +4101,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -3920,6 +4170,9 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "Actualizar complemento"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": ""
|
||||
},
|
||||
@@ -4303,6 +4556,18 @@
|
||||
"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."
|
||||
},
|
||||
@@ -4336,18 +4601,33 @@
|
||||
"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": ""
|
||||
},
|
||||
@@ -4540,6 +4820,9 @@
|
||||
"minutes": {
|
||||
"minutes": "minutos"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "ms"
|
||||
},
|
||||
@@ -4614,6 +4897,15 @@
|
||||
"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"
|
||||
},
|
||||
@@ -4639,12 +4931,25 @@
|
||||
"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"
|
||||
},
|
||||
@@ -4675,6 +4980,9 @@
|
||||
"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,6 +251,9 @@
|
||||
"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": "آیکونها همیشه فعال"
|
||||
},
|
||||
@@ -707,6 +710,9 @@
|
||||
"Clear All Jobs": {
|
||||
"Clear All Jobs": "پاککردن همه کارهای چاپ"
|
||||
},
|
||||
"Clear History?": {
|
||||
"Clear History?": ""
|
||||
},
|
||||
"Clear all history when server starts": {
|
||||
"Clear all history when server starts": "هنگام راهاندازی سرور تمام تاریخچه را پاک کن"
|
||||
},
|
||||
@@ -917,6 +923,12 @@
|
||||
"Copy": {
|
||||
"Copy": ""
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "کپی PID"
|
||||
},
|
||||
@@ -1124,6 +1136,9 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "حذف چاپگر"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "حذف VPN"
|
||||
},
|
||||
@@ -1208,6 +1223,9 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "میزان مصرف دیسک"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "رد کردن"
|
||||
},
|
||||
@@ -1442,6 +1460,12 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "شرکت"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "خطا"
|
||||
},
|
||||
@@ -1484,6 +1508,9 @@
|
||||
"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 ناموفق بود"
|
||||
},
|
||||
@@ -1559,6 +1586,9 @@
|
||||
"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": "چاپ صفحه تست ناموفق بود"
|
||||
},
|
||||
@@ -1604,6 +1634,9 @@
|
||||
"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 ناموفق بود"
|
||||
},
|
||||
@@ -1709,6 +1742,9 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "اجبار HDR"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "بستن اجباری فرایند"
|
||||
},
|
||||
@@ -1763,6 +1799,9 @@
|
||||
"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": "گیتهاب"
|
||||
},
|
||||
@@ -1880,6 +1919,9 @@
|
||||
"History Settings": {
|
||||
"History Settings": "تنظیمات تاریخچه"
|
||||
},
|
||||
"History cleared. %1 pinned entries kept.": {
|
||||
"History cleared. %1 pinned entries kept.": ""
|
||||
},
|
||||
"Hold Duration": {
|
||||
"Hold Duration": "مدت زمان نگهداشتن"
|
||||
},
|
||||
@@ -1994,6 +2036,9 @@
|
||||
"Install plugins from the DMS plugin registry": {
|
||||
"Install plugins from the DMS plugin registry": "نصب افزونهها از مخزن افزونه DMS"
|
||||
},
|
||||
"Intelligent Auto-hide": {
|
||||
"Intelligent Auto-hide": ""
|
||||
},
|
||||
"Interface:": {
|
||||
"Interface:": "اینترفیس:"
|
||||
},
|
||||
@@ -2021,6 +2066,166 @@
|
||||
"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": "بیدار نگه دار"
|
||||
},
|
||||
@@ -2255,9 +2460,18 @@
|
||||
"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": "بیشینه اندازه برای هر ورودی کلیپبورد"
|
||||
},
|
||||
@@ -2549,6 +2763,12 @@
|
||||
"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.": "هیچ گونهی دیگری ایجاد نشد. برای ایجاد یک ابزارک مانیتور جدید، روی افزودن کلیک کنید."
|
||||
},
|
||||
@@ -2624,6 +2844,9 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "موقعیت OSD"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "خاموش"
|
||||
},
|
||||
@@ -2771,6 +2994,9 @@
|
||||
"Percentage": {
|
||||
"Percentage": "درصد"
|
||||
},
|
||||
"Performance": {
|
||||
"Performance": ""
|
||||
},
|
||||
"Permission denied to set profile image.": {
|
||||
"Permission denied to set profile image.": "اجازه تنظیم تصویر نمایه داده نشد."
|
||||
},
|
||||
@@ -2870,6 +3096,9 @@
|
||||
"Power Profile Degradation": {
|
||||
"Power Profile Degradation": "تنزلدادن پروفایل پاور"
|
||||
},
|
||||
"Power off monitors on lock": {
|
||||
"Power off monitors on lock": ""
|
||||
},
|
||||
"Power profile management available": {
|
||||
"Power profile management available": "مدیریت پروفایل پاور در دسترس"
|
||||
},
|
||||
@@ -2939,6 +3168,9 @@
|
||||
"Process Count": {
|
||||
"Process Count": "تعداد فرایندها"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "درحال پردازش"
|
||||
},
|
||||
@@ -3137,6 +3369,9 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "پیکربندیهای ذخیره شده"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "بزرگنمایی"
|
||||
},
|
||||
@@ -3158,6 +3393,9 @@
|
||||
"Science": {
|
||||
"Science": "علمی"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "اشتراکگذاری صفحه"
|
||||
},
|
||||
@@ -3437,6 +3675,9 @@
|
||||
"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 لانچر را هنگام تایپ در نمای کلی نیری نمایش بده. برای استفاده از لانچر دیگری غیرفعال کنید."
|
||||
},
|
||||
@@ -3752,9 +3993,15 @@
|
||||
"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": "تایلینگ"
|
||||
},
|
||||
@@ -3854,6 +4101,9 @@
|
||||
"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": "خاموشکردن مانیتور پس از"
|
||||
},
|
||||
@@ -3920,6 +4170,9 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "بروزرسانی افزونه"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": ""
|
||||
},
|
||||
@@ -4303,6 +4556,18 @@
|
||||
"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 گنجانده نشده است. کلیدهای ترکیبی سفارشی تا زمانی که این مشکل برطرف نشود، کار نخواهند کرد."
|
||||
},
|
||||
@@ -4336,18 +4601,33 @@
|
||||
"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": "بازگشت"
|
||||
},
|
||||
@@ -4540,6 +4820,9 @@
|
||||
"minutes": {
|
||||
"minutes": "دقیقه"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "ms"
|
||||
},
|
||||
@@ -4614,6 +4897,15 @@
|
||||
"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": "انتخاب تصویر نمایه"
|
||||
},
|
||||
@@ -4639,12 +4931,25 @@
|
||||
"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"
|
||||
},
|
||||
@@ -4675,6 +4980,9 @@
|
||||
"update dms for NM integration.": {
|
||||
"update dms for NM integration.": "DMS را برای یکپارچهسازی NM بروز کنید."
|
||||
},
|
||||
"uptime label in footer": {
|
||||
"Uptime:": ""
|
||||
},
|
||||
"version requirement": {
|
||||
"Requires %1": ""
|
||||
},
|
||||
|
||||
5055
quickshell/translations/poexports/fr.json
Normal file
5055
quickshell/translations/poexports/fr.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -251,6 +251,9 @@
|
||||
"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": "סמלים דולקים תמיד"
|
||||
},
|
||||
@@ -489,7 +492,7 @@
|
||||
"Behavior": "התנהגות"
|
||||
},
|
||||
"Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen": {
|
||||
"Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen": "קשירת מסך הנעילה לאותות D-Bus מloginctl. יש להשבית אם משתמשים במסך נעילה חיצוני."
|
||||
"Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen": "מקשר את מסך הנעילה לאותות D-Bus מloginctl. יש להשבית אם משתמשים במסך נעילה חיצוני."
|
||||
},
|
||||
"Binds Include Missing": {
|
||||
"Binds Include Missing": "קובץ includes של קיצורי מקלדת חסר"
|
||||
@@ -707,6 +710,9 @@
|
||||
"Clear All Jobs": {
|
||||
"Clear All Jobs": "נקה/י את כל העבודות"
|
||||
},
|
||||
"Clear History?": {
|
||||
"Clear History?": ""
|
||||
},
|
||||
"Clear all history when server starts": {
|
||||
"Clear all history when server starts": "נקה/י את כל ההיסטוריה כאשר השרת מופעל"
|
||||
},
|
||||
@@ -753,7 +759,7 @@
|
||||
"Clipboard service not available": "שירות לוח ההעתקה אינו זמין"
|
||||
},
|
||||
"Clipboard works but nothing saved to disk": {
|
||||
"Clipboard works but nothing saved to disk": "לוח ההעתקה עובד אך שום דבר לא נשמר לדיסק"
|
||||
"Clipboard works but nothing saved to disk": "לוח ההעתקה יעבוד אך שום דבר לא יישמר לדיסק"
|
||||
},
|
||||
"Clock": {
|
||||
"Clock": "שעון"
|
||||
@@ -917,6 +923,12 @@
|
||||
"Copy": {
|
||||
"Copy": "העתק/י"
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "העתק/י PID"
|
||||
},
|
||||
@@ -1124,6 +1136,9 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "מחק/י מדפסת"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "מחק/י VPN"
|
||||
},
|
||||
@@ -1208,6 +1223,9 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "שימוש בדיסק"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "סגירה"
|
||||
},
|
||||
@@ -1221,10 +1239,10 @@
|
||||
"Display Settings": "הגדרות תצוגה"
|
||||
},
|
||||
"Display a dock with pinned and running applications": {
|
||||
"Display a dock with pinned and running applications": "הצג/י Dock עם יישומים מוצמדים ופעילים"
|
||||
"Display a dock with pinned and running applications": "הצג/י את הDock עם אפליקציות מוצמדות ופעילות"
|
||||
},
|
||||
"Display all priorities over fullscreen apps": {
|
||||
"Display all priorities over fullscreen apps": "הצג/י את כל רמות העדיפות מעל אפליקציות במסך מלא"
|
||||
"Display all priorities over fullscreen apps": "הצג/י את כל ההתראות מעל אפליקציות במסך מלא"
|
||||
},
|
||||
"Display and switch DWL layouts": {
|
||||
"Display and switch DWL layouts": "הצג/י והחלף/י פריסות DWL"
|
||||
@@ -1371,7 +1389,7 @@
|
||||
"Enable fingerprint authentication": "הפעלת אימות באמצעות טביעת אצבע"
|
||||
},
|
||||
"Enable loginctl lock integration": {
|
||||
"Enable loginctl lock integration": "הפעל/י שילוב נעילה עם loginctl"
|
||||
"Enable loginctl lock integration": "הפעלת האינטגרציה עם loginctl"
|
||||
},
|
||||
"Enable password field display on the lock screen window": {
|
||||
"Show Password Field": "הצג/י שדה סיסמה"
|
||||
@@ -1442,6 +1460,12 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "ארגוני"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "שגיאה"
|
||||
},
|
||||
@@ -1484,6 +1508,9 @@
|
||||
"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 נכשל"
|
||||
},
|
||||
@@ -1559,6 +1586,9 @@
|
||||
"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": "הדפסת דף בדיקה נכשלה"
|
||||
},
|
||||
@@ -1604,6 +1634,9 @@
|
||||
"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 נכשל"
|
||||
},
|
||||
@@ -1689,7 +1722,7 @@
|
||||
"Focused Window": "חלון ממוקד"
|
||||
},
|
||||
"Follow Monitor Focus": {
|
||||
"Follow Monitor Focus": "עקוב/י אחר מיקוד צג"
|
||||
"Follow Monitor Focus": "מעקב אחרי המסך הממוקד"
|
||||
},
|
||||
"Follow focus": {
|
||||
"Follow focus": "עקוב/י אחר מיקוד"
|
||||
@@ -1709,6 +1742,9 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "אלץ/י HDR"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "אילוץ סיום תהליך"
|
||||
},
|
||||
@@ -1763,6 +1799,9 @@
|
||||
"Gamma control not available. Requires DMS API v6+.": {
|
||||
"Gamma control not available. Requires DMS API v6+.": "בקרת גאמה אינה זמינה. נדרש API של DMS גרסה 6 ומעלה."
|
||||
},
|
||||
"Generic device name | Generic device name fallback": {
|
||||
"device": ""
|
||||
},
|
||||
"GitHub": {
|
||||
"GitHub": "GitHub"
|
||||
},
|
||||
@@ -1782,7 +1821,7 @@
|
||||
"Gradually fade the screen before locking with a configurable grace period": "הפעל/י דהייה הדרגתית של המסך לפני הנעילה עם תקופת חסד הניתנת להגדרה"
|
||||
},
|
||||
"Gradually fade the screen before turning off monitors with a configurable grace period": {
|
||||
"Gradually fade the screen before turning off monitors with a configurable grace period": "דהייה הדרגתית של התצוגה לפני כיבוי מסכים עם תקופת חסד הניתנת להגדרה"
|
||||
"Gradually fade the screen before turning off monitors with a configurable grace period": "הפעל/י דהייה הדרגתית של התצוגה לפני כיבוי המסכים עם תקופת חסד הניתנת להגדרה"
|
||||
},
|
||||
"Graph Time Range": {
|
||||
"Graph Time Range": "טווח זמן לגרף"
|
||||
@@ -1800,7 +1839,7 @@
|
||||
"Group": "קבוצה"
|
||||
},
|
||||
"Group Workspace Apps": {
|
||||
"Group Workspace Apps": "קיבוץ יישומי שולחן עבודה"
|
||||
"Group Workspace Apps": "קיבוץ האפליקציות של סביבת העבודה"
|
||||
},
|
||||
"Group by App": {
|
||||
"Group by App": "קבץ/י לפי אפליקציה"
|
||||
@@ -1880,6 +1919,9 @@
|
||||
"History Settings": {
|
||||
"History Settings": "הגדרות היסטוריה"
|
||||
},
|
||||
"History cleared. %1 pinned entries kept.": {
|
||||
"History cleared. %1 pinned entries kept.": ""
|
||||
},
|
||||
"Hold Duration": {
|
||||
"Hold Duration": "משך הלחיצה"
|
||||
},
|
||||
@@ -1994,6 +2036,9 @@
|
||||
"Install plugins from the DMS plugin registry": {
|
||||
"Install plugins from the DMS plugin registry": "התקן/י תוספים ממאגר התוספים של DMS"
|
||||
},
|
||||
"Intelligent Auto-hide": {
|
||||
"Intelligent Auto-hide": ""
|
||||
},
|
||||
"Interface:": {
|
||||
"Interface:": "ממשק:"
|
||||
},
|
||||
@@ -2021,6 +2066,166 @@
|
||||
"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": "השאר/י ער"
|
||||
},
|
||||
@@ -2255,9 +2460,18 @@
|
||||
"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": "גודל מקסימלי לרשומת לוח ההעתקה"
|
||||
},
|
||||
@@ -2549,6 +2763,12 @@
|
||||
"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.": "לא נוצרו גרסאות. לחץ/י על הוספה כדי ליצור ווידג׳ט תצוגה חדש."
|
||||
},
|
||||
@@ -2624,6 +2844,9 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "מיקום OSD"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "כבוי"
|
||||
},
|
||||
@@ -2771,6 +2994,9 @@
|
||||
"Percentage": {
|
||||
"Percentage": "אחוזים"
|
||||
},
|
||||
"Performance": {
|
||||
"Performance": ""
|
||||
},
|
||||
"Permission denied to set profile image.": {
|
||||
"Permission denied to set profile image.": "ההרשאה להגדרת תמונת הפרופיל נדחתה."
|
||||
},
|
||||
@@ -2870,6 +3096,9 @@
|
||||
"Power Profile Degradation": {
|
||||
"Power Profile Degradation": "ירידת ביצועים בפרופיל צריכת החשמל"
|
||||
},
|
||||
"Power off monitors on lock": {
|
||||
"Power off monitors on lock": ""
|
||||
},
|
||||
"Power profile management available": {
|
||||
"Power profile management available": "ניהול פרופיל חשמל זמין"
|
||||
},
|
||||
@@ -2939,6 +3168,9 @@
|
||||
"Process Count": {
|
||||
"Process Count": "מספר תהליכים"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "מעבד"
|
||||
},
|
||||
@@ -3137,6 +3369,9 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "תצורות שמורות"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "קנה מידה"
|
||||
},
|
||||
@@ -3158,11 +3393,14 @@
|
||||
"Science": {
|
||||
"Science": "מדע"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "שיתוף מסך"
|
||||
},
|
||||
"Scroll Wheel": {
|
||||
"Scroll Wheel": "גלגל גלילה"
|
||||
"Scroll Wheel": "גלגל הגלילה"
|
||||
},
|
||||
"Scroll on widget changes media volume": {
|
||||
"Scroll on widget changes media volume": "גלילה על הווידג׳ט משנה עוצמת מדיה"
|
||||
@@ -3174,7 +3412,7 @@
|
||||
"Scroll title if it doesn't fit in widget": "גלילה של הכותרת אם היא לא נכנסת במלואה בווידג׳ט"
|
||||
},
|
||||
"Scroll wheel behavior on media widget": {
|
||||
"Scroll wheel behavior on media widget": "התנהגות גלגל גלילה על ווידג׳ט מדיה"
|
||||
"Scroll wheel behavior on media widget": "התנהגות גלגל הגלילה על ווידג׳ט המדיה"
|
||||
},
|
||||
"Scrolling": {
|
||||
"Scrolling": "גלילה"
|
||||
@@ -3333,7 +3571,7 @@
|
||||
"Show Disk": "הצג/י דיסק"
|
||||
},
|
||||
"Show Dock": {
|
||||
"Show Dock": "הצג/י Dock"
|
||||
"Show Dock": "הצג/י את הDock"
|
||||
},
|
||||
"Show Feels Like Temperature": {
|
||||
"Show Feels Like Temperature": "הצג/י טמפרטורה מורגשת"
|
||||
@@ -3384,7 +3622,7 @@
|
||||
"Show Network Graph": "הצג/י גרף רשת"
|
||||
},
|
||||
"Show Occupied Workspaces Only": {
|
||||
"Show Occupied Workspaces Only": "הצג/י רק סביבות עבודה שתפוסות"
|
||||
"Show Occupied Workspaces Only": "הצג/י רק סביבות עבודה שאינן ריקות"
|
||||
},
|
||||
"Show Power Off": {
|
||||
"Show Power Off": "הצג/י כיבוי"
|
||||
@@ -3437,6 +3675,9 @@
|
||||
"Show darkened overlay behind modal dialogs": {
|
||||
"Show darkened overlay behind modal dialogs": "הצג/י שכבה כהה מאחורי חלוניות שיח"
|
||||
},
|
||||
"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.": "הצג/י שכבת משגר בעת הקלדה בסקירה של Niri. השבת/י כדי להשתמש במשגר אחר."
|
||||
},
|
||||
@@ -3501,7 +3742,7 @@
|
||||
"Show workspace name on horizontal bars, and first letter on vertical bars": "הצג/י את השם של סביבת העבודה על סרגלים אופקיים, ואות ראשונה על סרגלים אנכיים"
|
||||
},
|
||||
"Show workspaces of the currently focused monitor": {
|
||||
"Show workspaces of the currently focused monitor": "הצג/י סביבות עבודה שפעילות על המסך הממוקד שכרגע ממוקד"
|
||||
"Show workspaces of the currently focused monitor": "הצג/י סביבות עבודה שפעילות על המסך שכרגע ממוקד"
|
||||
},
|
||||
"Shows all running applications with focus indication": {
|
||||
"Shows all running applications with focus indication": "מציג את כל האפליקציות הפעילות עם סימון למיקוד"
|
||||
@@ -3657,7 +3898,7 @@
|
||||
"System": "מערכת"
|
||||
},
|
||||
"System App Theming": {
|
||||
"System App Theming": "ערכת נושא ליישומי מערכת"
|
||||
"System App Theming": "ערכת נושא לאפליקציות מערכת"
|
||||
},
|
||||
"System Monitor Unavailable": {
|
||||
"System Monitor Unavailable": "מנטר המערכת אינו זמין"
|
||||
@@ -3752,9 +3993,15 @@
|
||||
"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": "ריצוף"
|
||||
},
|
||||
@@ -3854,6 +4101,9 @@
|
||||
"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": "כבה/י את כל המסכים לאחר"
|
||||
},
|
||||
@@ -3920,6 +4170,9 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "עדכן/י תוסף"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": "צבע דחוף"
|
||||
},
|
||||
@@ -4303,6 +4556,18 @@
|
||||
"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. קיצורי מקלדת מותאמים אישית לא יעבדו עד שהבעיה תתוקן."
|
||||
},
|
||||
@@ -4319,7 +4584,7 @@
|
||||
"Dynamic colors from wallpaper": "צבעים דינמיים מתמונת הרקע"
|
||||
},
|
||||
"dynamic theme description": {
|
||||
"Material colors generated from wallpaper": "צבעי Material שנוצרו מטפט"
|
||||
"Material colors generated from wallpaper": "צבעי Material שנוצרו מהרקע"
|
||||
},
|
||||
"dynamic theme name": {
|
||||
"Dynamic": "דינמי"
|
||||
@@ -4336,18 +4601,33 @@
|
||||
"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": "ערכות צבעים בהשראת Material Design"
|
||||
},
|
||||
"gpu section header in system monitor": {
|
||||
"GPU Monitoring": ""
|
||||
},
|
||||
"greeter back button": {
|
||||
"Back": "חזרה"
|
||||
},
|
||||
@@ -4404,7 +4684,7 @@
|
||||
},
|
||||
"greeter feature card description": {
|
||||
"Background app icons": "סמלי אפליקציות ברקע",
|
||||
"Colors from wallpaper": "צבעים מטפט",
|
||||
"Colors from wallpaper": "צבעים מתמונת רקע",
|
||||
"Community themes": "ערכות נושא קהילתיות",
|
||||
"Extensible architecture": "ארכיטקטורה ניתנת להרחבה",
|
||||
"GTK, Qt, IDEs, more": "GTK, Qt, סביבות פיתוח, ועוד",
|
||||
@@ -4427,7 +4707,7 @@
|
||||
"Plugins": "תוספים"
|
||||
},
|
||||
"greeter feature card title | greeter settings link": {
|
||||
"DankBar": "DankBar"
|
||||
"DankBar": "Dank Bar"
|
||||
},
|
||||
"greeter feature card title | lock screen notifications settings card": {
|
||||
"Lock Screen": "מסך הנעילה"
|
||||
@@ -4526,7 +4806,7 @@
|
||||
"Lock Screen": "מסך הנעילה"
|
||||
},
|
||||
"loginctl not available - lock integration requires DMS socket connection": {
|
||||
"loginctl not available - lock integration requires DMS socket connection": "loginctl אינו זמין, שילוב הנעילה דורש חיבור socket לDMS"
|
||||
"loginctl not available - lock integration requires DMS socket connection": "loginctl אינו זמין, אינטגרציה של הנעילה דורשת חיבור socket לDMS"
|
||||
},
|
||||
"matugen error": {
|
||||
"matugen not found - install matugen package for dynamic theming": "matugen לא נמצא - התקן/י את החבילה של matugen כדי לאפשר עיצוב דינמי"
|
||||
@@ -4540,6 +4820,9 @@
|
||||
"minutes": {
|
||||
"minutes": "דקות"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "מילישניות"
|
||||
},
|
||||
@@ -4614,6 +4897,15 @@
|
||||
"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": "בחר/י תמונת פרופיל"
|
||||
},
|
||||
@@ -4639,12 +4931,25 @@
|
||||
"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"
|
||||
},
|
||||
@@ -4673,7 +4978,10 @@
|
||||
"Unknown": "לא ידוע"
|
||||
},
|
||||
"update dms for NM integration.": {
|
||||
"update dms for NM integration.": "עדכן/י את dms עבור שילוב עם NM."
|
||||
"update dms for NM integration.": "עדכן/י את DMS כדי לאפשר אינטגרציה עם NM."
|
||||
},
|
||||
"uptime label in footer": {
|
||||
"Uptime:": ""
|
||||
},
|
||||
"version requirement": {
|
||||
"Requires %1": "גרסה %1 נדרשת"
|
||||
|
||||
@@ -251,6 +251,9 @@
|
||||
"Always Show Percentage": {
|
||||
"Always Show Percentage": "Mindig mutassa a százalékot"
|
||||
},
|
||||
"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": "Mindig látható ikonok"
|
||||
},
|
||||
@@ -707,6 +710,9 @@
|
||||
"Clear All Jobs": {
|
||||
"Clear All Jobs": "Összes feladat törlése"
|
||||
},
|
||||
"Clear History?": {
|
||||
"Clear History?": ""
|
||||
},
|
||||
"Clear all history when server starts": {
|
||||
"Clear all history when server starts": "Minden előzmény törlése a szerver indításakor"
|
||||
},
|
||||
@@ -917,6 +923,12 @@
|
||||
"Copy": {
|
||||
"Copy": "Másolás"
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "PID másolása"
|
||||
},
|
||||
@@ -1124,6 +1136,9 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "Nyomtató törlése"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "VPN törlése"
|
||||
},
|
||||
@@ -1208,6 +1223,9 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "Lemezhasználat"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "Bezárás"
|
||||
},
|
||||
@@ -1442,6 +1460,12 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "Vállalati"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "Hiba"
|
||||
},
|
||||
@@ -1484,6 +1508,9 @@
|
||||
"Failed to cancel selected job": {
|
||||
"Failed to cancel selected job": "Nem sikerült a kiválasztott feladatot törölni"
|
||||
},
|
||||
"Failed to check pin limit": {
|
||||
"Failed to check pin limit": ""
|
||||
},
|
||||
"Failed to connect VPN": {
|
||||
"Failed to connect VPN": "Nem sikerült csatlakozni a VPN-hez"
|
||||
},
|
||||
@@ -1559,6 +1586,9 @@
|
||||
"Failed to pause printer": {
|
||||
"Failed to pause printer": "Nem sikerült a nyomtatót szüneteltetetni"
|
||||
},
|
||||
"Failed to pin entry": {
|
||||
"Failed to pin entry": ""
|
||||
},
|
||||
"Failed to print test page": {
|
||||
"Failed to print test page": "Nem sikerült kinyomtatni a tesztoldalt"
|
||||
},
|
||||
@@ -1604,6 +1634,9 @@
|
||||
"Failed to start connection to %1": {
|
||||
"Failed to start connection to %1": "Nem sikerült elindítani a csatlakozást ehhez: %1"
|
||||
},
|
||||
"Failed to unpin entry": {
|
||||
"Failed to unpin entry": ""
|
||||
},
|
||||
"Failed to update VPN": {
|
||||
"Failed to update VPN": "Nem sikerült frissíteni a VPN-t"
|
||||
},
|
||||
@@ -1659,7 +1692,7 @@
|
||||
"Fix Now": "Javítás most"
|
||||
},
|
||||
"Fixing...": {
|
||||
"Fixing...": "Javítás..."
|
||||
"Fixing...": "Javítás…"
|
||||
},
|
||||
"Flags": {
|
||||
"Flags": "Jelzők"
|
||||
@@ -1709,6 +1742,9 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "HDR kényszerítése"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "Folyamat kényszerített leállítása"
|
||||
},
|
||||
@@ -1763,6 +1799,9 @@
|
||||
"Gamma control not available. Requires DMS API v6+.": {
|
||||
"Gamma control not available. Requires DMS API v6+.": "Gamma vezérlés nem érhető el. DMS API v6+ szükséges."
|
||||
},
|
||||
"Generic device name | Generic device name fallback": {
|
||||
"device": ""
|
||||
},
|
||||
"GitHub": {
|
||||
"GitHub": "GitHub"
|
||||
},
|
||||
@@ -1880,6 +1919,9 @@
|
||||
"History Settings": {
|
||||
"History Settings": "Előzménybeállítások"
|
||||
},
|
||||
"History cleared. %1 pinned entries kept.": {
|
||||
"History cleared. %1 pinned entries kept.": ""
|
||||
},
|
||||
"Hold Duration": {
|
||||
"Hold Duration": "Nyomva tartás időtartama"
|
||||
},
|
||||
@@ -1994,6 +2036,9 @@
|
||||
"Install plugins from the DMS plugin registry": {
|
||||
"Install plugins from the DMS plugin registry": "Bővítmények telepítése a DMS bővítményregisztrációból"
|
||||
},
|
||||
"Intelligent Auto-hide": {
|
||||
"Intelligent Auto-hide": ""
|
||||
},
|
||||
"Interface:": {
|
||||
"Interface:": "Interfész:"
|
||||
},
|
||||
@@ -2021,6 +2066,166 @@
|
||||
"Jobs: ": {
|
||||
"Jobs: ": "Munkák: "
|
||||
},
|
||||
"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": "Ébren tartás"
|
||||
},
|
||||
@@ -2255,9 +2460,18 @@
|
||||
"Maximum History": {
|
||||
"Maximum History": "Maximális előzmény"
|
||||
},
|
||||
"Maximum Pinned Entries": {
|
||||
"Maximum Pinned Entries": ""
|
||||
},
|
||||
"Maximum number of clipboard entries to keep": {
|
||||
"Maximum number of clipboard entries to keep": "Megtartandó vágólap-bejegyzések maximális száma"
|
||||
},
|
||||
"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": "Maximális méret vágólap-bejegyzésenként"
|
||||
},
|
||||
@@ -2439,7 +2653,7 @@
|
||||
"New York, NY": "New York, NY"
|
||||
},
|
||||
"New group name...": {
|
||||
"New group name...": "Új csoportnév..."
|
||||
"New group name...": "Új csoportnév…"
|
||||
},
|
||||
"Next Transition": {
|
||||
"Next Transition": "Következő átmenet"
|
||||
@@ -2549,6 +2763,12 @@
|
||||
"No printers found": {
|
||||
"No printers found": "Nem található nyomtató"
|
||||
},
|
||||
"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.": "Nincsenek létrehozott variánsok. Kattints a Hozzáadás gombra új monitor widget létrehozásához."
|
||||
},
|
||||
@@ -2624,6 +2844,9 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "OSD pozíció"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "Kikapcsolva"
|
||||
},
|
||||
@@ -2771,6 +2994,9 @@
|
||||
"Percentage": {
|
||||
"Percentage": "Százalék"
|
||||
},
|
||||
"Performance": {
|
||||
"Performance": ""
|
||||
},
|
||||
"Permission denied to set profile image.": {
|
||||
"Permission denied to set profile image.": "Engedély megtagadva a profilkép beállításához."
|
||||
},
|
||||
@@ -2870,6 +3096,9 @@
|
||||
"Power Profile Degradation": {
|
||||
"Power Profile Degradation": "Energiaprofil romlása"
|
||||
},
|
||||
"Power off monitors on lock": {
|
||||
"Power off monitors on lock": ""
|
||||
},
|
||||
"Power profile management available": {
|
||||
"Power profile management available": "Energia profil kezelés elérhető"
|
||||
},
|
||||
@@ -2939,6 +3168,9 @@
|
||||
"Process Count": {
|
||||
"Process Count": "Folyamatszám"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "Feldolgozás"
|
||||
},
|
||||
@@ -3137,6 +3369,9 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "Mentett konfigurációk"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "Skála"
|
||||
},
|
||||
@@ -3158,6 +3393,9 @@
|
||||
"Science": {
|
||||
"Science": "Tudomány"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "Képernyő megosztás"
|
||||
},
|
||||
@@ -3437,6 +3675,9 @@
|
||||
"Show darkened overlay behind modal dialogs": {
|
||||
"Show darkened overlay behind modal dialogs": "Sötétített átfedés megjelenítése a modális párbeszédablakok mögött"
|
||||
},
|
||||
"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.": "Indító átfedés megjelenítése, amikor gépelsz a Niri-áttekintésben. Kapcsold ki, ha másik indítót használsz."
|
||||
},
|
||||
@@ -3752,9 +3993,15 @@
|
||||
"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.": "Ez a widget megakadályozza a GPU kikapcsolt állapotát, ami jelentősen befolyásolhatja a laptopok akkumulátor élettartamát. Nem ajánlott hibrid grafikus kártyával rendelkező laptopokon használni."
|
||||
},
|
||||
"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.": "Ez véglegesen törölni fogja a vágólapelőzményeket."
|
||||
},
|
||||
"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": "Csempézés"
|
||||
},
|
||||
@@ -3854,6 +4101,9 @@
|
||||
"Trigger Prefix": {
|
||||
"Trigger Prefix": "Indító előtag"
|
||||
},
|
||||
"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": "Monitorok kikapcsolása ennyi idő után:"
|
||||
},
|
||||
@@ -3920,6 +4170,9 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "Bővítmény frissítése"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": "Sürgős szín"
|
||||
},
|
||||
@@ -4303,6 +4556,18 @@
|
||||
"dgop not available": {
|
||||
"dgop not available": "dgop nem elérhető"
|
||||
},
|
||||
"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.": "A dms/binds.kdl létezik, de nincs benne a config.kdl-ben. Az egyéni gyorsbillentyűk nem fognak működni, amíg ez nincs javítva."
|
||||
},
|
||||
@@ -4336,18 +4601,33 @@
|
||||
"empty plugin list": {
|
||||
"No plugins found": "Nem található bővítmény"
|
||||
},
|
||||
"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": "Nem található téma"
|
||||
},
|
||||
"events": {
|
||||
"events": "események"
|
||||
},
|
||||
"fallback gpu name": {
|
||||
"Unknown GPU": ""
|
||||
},
|
||||
"files": {
|
||||
"files": "fájlok"
|
||||
},
|
||||
"generic theme description": {
|
||||
"Material Design inspired color themes": "Material Design ihlette színtémák"
|
||||
},
|
||||
"gpu section header in system monitor": {
|
||||
"GPU Monitoring": ""
|
||||
},
|
||||
"greeter back button": {
|
||||
"Back": "Vissza"
|
||||
},
|
||||
@@ -4382,7 +4662,7 @@
|
||||
"%1 issue(s) found": "%1 hiba található"
|
||||
},
|
||||
"greeter doctor page loading text": {
|
||||
"Analyzing configuration...": "Konfiguráció elemzése..."
|
||||
"Analyzing configuration...": "Konfiguráció elemzése…"
|
||||
},
|
||||
"greeter doctor page status card": {
|
||||
"Errors": "Hibák",
|
||||
@@ -4540,6 +4820,9 @@
|
||||
"minutes": {
|
||||
"minutes": "percek"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "ms"
|
||||
},
|
||||
@@ -4614,6 +4897,15 @@
|
||||
"plugin search placeholder": {
|
||||
"Search plugins...": "Bővítmények keresése…"
|
||||
},
|
||||
"process count label in footer": {
|
||||
"Processes:": ""
|
||||
},
|
||||
"process detail label": {
|
||||
"Full Command:": ""
|
||||
},
|
||||
"process search placeholder": {
|
||||
"Search processes...": ""
|
||||
},
|
||||
"profile image file browser title": {
|
||||
"Select Profile Image": "Profilkép kiválasztása"
|
||||
},
|
||||
@@ -4639,12 +4931,25 @@
|
||||
"shadow intensity slider": {
|
||||
"Intensity": "Intenzitás"
|
||||
},
|
||||
"short for processes": {
|
||||
"procs": ""
|
||||
},
|
||||
"source code link": {
|
||||
"source": "forrás"
|
||||
},
|
||||
"sysmon window title": {
|
||||
"System Monitor": "Rendszerfigyelő"
|
||||
},
|
||||
"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": "Színtémák telepítése a DMS téma-regiszterből"
|
||||
},
|
||||
@@ -4675,6 +4980,9 @@
|
||||
"update dms for NM integration.": {
|
||||
"update dms for NM integration.": "dms frissítése a NM integrációhoz."
|
||||
},
|
||||
"uptime label in footer": {
|
||||
"Uptime:": ""
|
||||
},
|
||||
"version requirement": {
|
||||
"Requires %1": "%1 szükséges"
|
||||
},
|
||||
|
||||
@@ -251,6 +251,9 @@
|
||||
"Always Show Percentage": {
|
||||
"Always Show Percentage": "Mostra sempre percentuale"
|
||||
},
|
||||
"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": "Icone sempre attive"
|
||||
},
|
||||
@@ -707,6 +710,9 @@
|
||||
"Clear All Jobs": {
|
||||
"Clear All Jobs": "Elimina Tutte le Stampe"
|
||||
},
|
||||
"Clear History?": {
|
||||
"Clear History?": ""
|
||||
},
|
||||
"Clear all history when server starts": {
|
||||
"Clear all history when server starts": "Cancella tutta la cronologia all'avvio del server"
|
||||
},
|
||||
@@ -917,6 +923,12 @@
|
||||
"Copy": {
|
||||
"Copy": "Copia"
|
||||
},
|
||||
"Copy Full Command": {
|
||||
"Copy Full Command": ""
|
||||
},
|
||||
"Copy Name": {
|
||||
"Copy Name": ""
|
||||
},
|
||||
"Copy PID": {
|
||||
"Copy PID": "Copia PID"
|
||||
},
|
||||
@@ -1124,6 +1136,9 @@
|
||||
"Delete Printer": {
|
||||
"Delete Printer": "Elimina Stampante"
|
||||
},
|
||||
"Delete Saved Item?": {
|
||||
"Delete Saved Item?": ""
|
||||
},
|
||||
"Delete VPN": {
|
||||
"Delete VPN": "Elimina VPN"
|
||||
},
|
||||
@@ -1208,6 +1223,9 @@
|
||||
"Disk Usage": {
|
||||
"Disk Usage": "Uso Disco"
|
||||
},
|
||||
"Disks": {
|
||||
"Disks": ""
|
||||
},
|
||||
"Dismiss": {
|
||||
"Dismiss": "Ignora"
|
||||
},
|
||||
@@ -1442,6 +1460,12 @@
|
||||
"Enterprise": {
|
||||
"Enterprise": "Enterprise"
|
||||
},
|
||||
"Entry pinned": {
|
||||
"Entry pinned": ""
|
||||
},
|
||||
"Entry unpinned": {
|
||||
"Entry unpinned": ""
|
||||
},
|
||||
"Error": {
|
||||
"Error": "Errore"
|
||||
},
|
||||
@@ -1484,6 +1508,9 @@
|
||||
"Failed to cancel selected job": {
|
||||
"Failed to cancel selected job": "Impossibile cancellare la stampa selezionata"
|
||||
},
|
||||
"Failed to check pin limit": {
|
||||
"Failed to check pin limit": ""
|
||||
},
|
||||
"Failed to connect VPN": {
|
||||
"Failed to connect VPN": "Impossibile connettersi alla VPN"
|
||||
},
|
||||
@@ -1559,6 +1586,9 @@
|
||||
"Failed to pause printer": {
|
||||
"Failed to pause printer": "Impossibile mettere in pausa la stampante"
|
||||
},
|
||||
"Failed to pin entry": {
|
||||
"Failed to pin entry": ""
|
||||
},
|
||||
"Failed to print test page": {
|
||||
"Failed to print test page": "Impossibile stampare la pagina di prova"
|
||||
},
|
||||
@@ -1604,6 +1634,9 @@
|
||||
"Failed to start connection to %1": {
|
||||
"Failed to start connection to %1": "Impossibile avviare la connessione a %1"
|
||||
},
|
||||
"Failed to unpin entry": {
|
||||
"Failed to unpin entry": ""
|
||||
},
|
||||
"Failed to update VPN": {
|
||||
"Failed to update VPN": "Impossibile aggiornare la VPN"
|
||||
},
|
||||
@@ -1709,6 +1742,9 @@
|
||||
"Force HDR": {
|
||||
"Force HDR": "Forza HDR"
|
||||
},
|
||||
"Force Kill (SIGKILL)": {
|
||||
"Force Kill (SIGKILL)": ""
|
||||
},
|
||||
"Force Kill Process": {
|
||||
"Force Kill Process": "Forza Arresto Processo"
|
||||
},
|
||||
@@ -1763,6 +1799,9 @@
|
||||
"Gamma control not available. Requires DMS API v6+.": {
|
||||
"Gamma control not available. Requires DMS API v6+.": "Controllo gamma non disponibile. Richiede API DMS v6+."
|
||||
},
|
||||
"Generic device name | Generic device name fallback": {
|
||||
"device": ""
|
||||
},
|
||||
"GitHub": {
|
||||
"GitHub": "GitHub"
|
||||
},
|
||||
@@ -1880,6 +1919,9 @@
|
||||
"History Settings": {
|
||||
"History Settings": "Impostazioni Cronologia"
|
||||
},
|
||||
"History cleared. %1 pinned entries kept.": {
|
||||
"History cleared. %1 pinned entries kept.": ""
|
||||
},
|
||||
"Hold Duration": {
|
||||
"Hold Duration": "Durata della pressione"
|
||||
},
|
||||
@@ -1994,6 +2036,9 @@
|
||||
"Install plugins from the DMS plugin registry": {
|
||||
"Install plugins from the DMS plugin registry": "Installa plugin dal registro dei plugin DMS"
|
||||
},
|
||||
"Intelligent Auto-hide": {
|
||||
"Intelligent Auto-hide": ""
|
||||
},
|
||||
"Interface:": {
|
||||
"Interface:": "Interfaccia"
|
||||
},
|
||||
@@ -2021,6 +2066,166 @@
|
||||
"Jobs: ": {
|
||||
"Jobs: ": "Stampe: "
|
||||
},
|
||||
"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": "Mantieni Attivo"
|
||||
},
|
||||
@@ -2255,9 +2460,18 @@
|
||||
"Maximum History": {
|
||||
"Maximum History": "Cronologia Massima"
|
||||
},
|
||||
"Maximum Pinned Entries": {
|
||||
"Maximum Pinned Entries": ""
|
||||
},
|
||||
"Maximum number of clipboard entries to keep": {
|
||||
"Maximum number of clipboard entries to keep": "Numero massimo di voci degli appunti da conservare"
|
||||
},
|
||||
"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": "Dimensione massima per ogni voce degli appunti"
|
||||
},
|
||||
@@ -2549,6 +2763,12 @@
|
||||
"No printers found": {
|
||||
"No printers found": "Nessuna stampante trovata"
|
||||
},
|
||||
"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.": "Nessuna variante creata. Fai clic su Aggiungi per creare un nuovo widget monitor."
|
||||
},
|
||||
@@ -2624,6 +2844,9 @@
|
||||
"OSD Position": {
|
||||
"OSD Position": "Posizione OSD"
|
||||
},
|
||||
"Occupied Color": {
|
||||
"Occupied Color": ""
|
||||
},
|
||||
"Off": {
|
||||
"Off": "Disattivato"
|
||||
},
|
||||
@@ -2771,6 +2994,9 @@
|
||||
"Percentage": {
|
||||
"Percentage": "Percentuale"
|
||||
},
|
||||
"Performance": {
|
||||
"Performance": ""
|
||||
},
|
||||
"Permission denied to set profile image.": {
|
||||
"Permission denied to set profile image.": "Impossibile impostare immagine profilo: Permesso negato."
|
||||
},
|
||||
@@ -2870,6 +3096,9 @@
|
||||
"Power Profile Degradation": {
|
||||
"Power Profile Degradation": "Riduzione Prestazioni Energetiche"
|
||||
},
|
||||
"Power off monitors on lock": {
|
||||
"Power off monitors on lock": ""
|
||||
},
|
||||
"Power profile management available": {
|
||||
"Power profile management available": "Gestione dei profili energetici disponibile"
|
||||
},
|
||||
@@ -2939,6 +3168,9 @@
|
||||
"Process Count": {
|
||||
"Process Count": "Numero di Processi"
|
||||
},
|
||||
"Processes": {
|
||||
"Processes": ""
|
||||
},
|
||||
"Processing": {
|
||||
"Processing": "In Elaborazione"
|
||||
},
|
||||
@@ -3137,6 +3369,9 @@
|
||||
"Saved Configurations": {
|
||||
"Saved Configurations": "Configurazioni Salvate"
|
||||
},
|
||||
"Saved item deleted": {
|
||||
"Saved item deleted": ""
|
||||
},
|
||||
"Scale": {
|
||||
"Scale": "Scala"
|
||||
},
|
||||
@@ -3158,6 +3393,9 @@
|
||||
"Science": {
|
||||
"Science": "Scienza"
|
||||
},
|
||||
"Screen Sharing": {
|
||||
"Screen Sharing": ""
|
||||
},
|
||||
"Screen sharing": {
|
||||
"Screen sharing": "Condivisione schermo"
|
||||
},
|
||||
@@ -3437,6 +3675,9 @@
|
||||
"Show darkened overlay behind modal dialogs": {
|
||||
"Show darkened overlay behind modal dialogs": "Mostra la sovrapposizione oscurata dietro le finestre di dialogo modali"
|
||||
},
|
||||
"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.": "Mostra la sovrapposizione del launcher durante la digitazione nella panoramica di Niri. Disabilita per usare un altro launcher."
|
||||
},
|
||||
@@ -3752,9 +3993,15 @@
|
||||
"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.": "Questo widget impedisce gli stati di spegnimento della GPU, che possono influire in modo significativo sulla durata della batteria sui laptop. Non è consigliabile utilizzarlo su laptop con grafica ibrida."
|
||||
},
|
||||
"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.": "La cronologia degli appunti verrà cancellata definitivamente."
|
||||
},
|
||||
"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": "Tiling"
|
||||
},
|
||||
@@ -3854,6 +4101,9 @@
|
||||
"Trigger Prefix": {
|
||||
"Trigger Prefix": "Prefisso Attivatore"
|
||||
},
|
||||
"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": "Spegni monitor dopo"
|
||||
},
|
||||
@@ -3920,6 +4170,9 @@
|
||||
"Update Plugin": {
|
||||
"Update Plugin": "Aggiorna Plugin"
|
||||
},
|
||||
"Uptime": {
|
||||
"Uptime": ""
|
||||
},
|
||||
"Urgent Color": {
|
||||
"Urgent Color": "Colore Urgente"
|
||||
},
|
||||
@@ -4303,6 +4556,18 @@
|
||||
"dgop not available": {
|
||||
"dgop not available": "dgop non disponibile"
|
||||
},
|
||||
"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 esiste ma non è incluso in config.kdl. Le scorciatoie personalizzate non funzioneranno finché questo non sarà risolto."
|
||||
},
|
||||
@@ -4336,18 +4601,33 @@
|
||||
"empty plugin list": {
|
||||
"No plugins found": "Nessun plugin trovato"
|
||||
},
|
||||
"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": "Nessun tema trovato"
|
||||
},
|
||||
"events": {
|
||||
"events": "eventi"
|
||||
},
|
||||
"fallback gpu name": {
|
||||
"Unknown GPU": ""
|
||||
},
|
||||
"files": {
|
||||
"files": "file"
|
||||
},
|
||||
"generic theme description": {
|
||||
"Material Design inspired color themes": "Temi colore ispirati al Material Design"
|
||||
},
|
||||
"gpu section header in system monitor": {
|
||||
"GPU Monitoring": ""
|
||||
},
|
||||
"greeter back button": {
|
||||
"Back": "Indietro"
|
||||
},
|
||||
@@ -4540,6 +4820,9 @@
|
||||
"minutes": {
|
||||
"minutes": "minuti"
|
||||
},
|
||||
"mount points header in system monitor": {
|
||||
"Mount Points": ""
|
||||
},
|
||||
"ms": {
|
||||
"ms": "ms"
|
||||
},
|
||||
@@ -4614,6 +4897,15 @@
|
||||
"plugin search placeholder": {
|
||||
"Search plugins...": "Cerca plugin..."
|
||||
},
|
||||
"process count label in footer": {
|
||||
"Processes:": ""
|
||||
},
|
||||
"process detail label": {
|
||||
"Full Command:": ""
|
||||
},
|
||||
"process search placeholder": {
|
||||
"Search processes...": ""
|
||||
},
|
||||
"profile image file browser title": {
|
||||
"Select Profile Image": "Seleziona Immagine Profilo"
|
||||
},
|
||||
@@ -4639,12 +4931,25 @@
|
||||
"shadow intensity slider": {
|
||||
"Intensity": "Intensità"
|
||||
},
|
||||
"short for processes": {
|
||||
"procs": ""
|
||||
},
|
||||
"source code link": {
|
||||
"source": "sorgente"
|
||||
},
|
||||
"sysmon window title": {
|
||||
"System Monitor": "Monitor 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": "Installa temi colore dal registro temi DMS"
|
||||
},
|
||||
@@ -4675,6 +4980,9 @@
|
||||
"update dms for NM integration.": {
|
||||
"update dms for NM integration.": "aggiorna dms per l'integrazione NM."
|
||||
},
|
||||
"uptime label in footer": {
|
||||
"Uptime:": ""
|
||||
},
|
||||
"version requirement": {
|
||||
"Requires %1": "Richiede %1"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user