mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
87 Commits
v1.2.0
...
6bf1438ef1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bf1438ef1 | ||
|
|
b819306ab6 | ||
|
|
b140afca8e | ||
|
|
6735989455 | ||
|
|
db37ac24c7 | ||
|
|
0231270f9e | ||
|
|
b5194aa9e1 | ||
|
|
ea0ffaacb0 | ||
|
|
3b1f084a13 | ||
|
|
39a9e3a89f | ||
|
|
7a7af775c2 | ||
|
|
6ac2a305f7 | ||
|
|
3507c6cec3 | ||
|
|
3ff00768ac | ||
|
|
556d253ea8 | ||
|
|
3922070488 | ||
|
|
eebb4827c4 | ||
|
|
fd2c6a0784 | ||
|
|
417bf37515 | ||
|
|
132e799265 | ||
|
|
bdc864781b | ||
|
|
a343bc7562 | ||
|
|
1f2e231386 | ||
|
|
0e7f628c4a | ||
|
|
553f5257b3 | ||
|
|
80ce6aa19c | ||
|
|
2b2977de4a | ||
|
|
1d5d876e16 | ||
|
|
3c39162016 | ||
|
|
d38767fb5a | ||
|
|
f2be6cfeb1 | ||
|
|
65486ed3cf | ||
|
|
cc30e2a9e4 | ||
|
|
ac68451cdf | ||
|
|
0f6ae11c3d | ||
|
|
7cb39f00ad | ||
|
|
f313d03348 | ||
|
|
1adbf3937b | ||
|
|
a685d9da52 | ||
|
|
13dededcc9 | ||
|
|
3bed2d9feb | ||
|
|
7241877995 | ||
|
|
340d79000c | ||
|
|
162ec909da | ||
|
|
53f5240d41 | ||
|
|
27f0df07af | ||
|
|
ad940b5884 | ||
|
|
ec8ab47462 | ||
|
|
35cbfeb008 | ||
|
|
7036362b9b | ||
|
|
2bcb33e85c | ||
|
|
76ac036f85 | ||
|
|
581073394a | ||
|
|
d7b7086b21 | ||
|
|
59be179821 | ||
|
|
1cf2f6b946 | ||
|
|
a57a9c2121 | ||
|
|
67568c3746 | ||
|
|
afce792b80 | ||
|
|
f5c7493dbb | ||
|
|
f9b9d98638 | ||
|
|
2a97e03fa6 | ||
|
|
d6dacc2975 | ||
|
|
aab4b6765d | ||
|
|
3539aca1f7 | ||
|
|
81fbe9eaba | ||
|
|
f9dc6de485 | ||
|
|
012022d370 | ||
|
|
993216e157 | ||
|
|
c992f2b582 | ||
|
|
3243adebca | ||
|
|
baccef57d4 | ||
|
|
a823095372 | ||
|
|
172a743de4 | ||
|
|
623eec3689 | ||
|
|
53a033fe35 | ||
|
|
c490ee24f4 | ||
|
|
cc1e49294e | ||
|
|
e6fa46ae26 | ||
|
|
35fe774a1b | ||
|
|
1e6a0f9423 | ||
|
|
cc1877aadb | ||
|
|
f1eb1fa9ba | ||
|
|
bdd01e335d | ||
|
|
4b7baf82cd | ||
|
|
15c88ce1d2 | ||
|
|
8891c388d0 |
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,12 +42,12 @@ body:
|
|||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: textarea
|
||||||
id: dms_version
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms version
|
label: dms doctor -v
|
||||||
description: Output of dms version command
|
description: Output of `dms doctor -v` command
|
||||||
placeholder: e.g., 1.2.3
|
placeholder: Paste the output of `dms doctor -v` here
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/support_request.yml
vendored
10
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -27,12 +27,12 @@ body:
|
|||||||
placeholder: Your Linux distribution
|
placeholder: Your Linux distribution
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: textarea
|
||||||
id: dms_version
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms version
|
label: dms doctor -v
|
||||||
description: Output of dms version command
|
description: Output of `dms doctor -v` command
|
||||||
placeholder: e.g., 1.2.3
|
placeholder: Paste the output of `dms doctor -v` here
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,13 +4,14 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to update (dms, dms-git, or all)"
|
description: "Package to update"
|
||||||
required: false
|
required: true
|
||||||
default: "all"
|
type: choice
|
||||||
tag_version:
|
options:
|
||||||
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
- dms
|
||||||
required: false
|
- dms-git
|
||||||
default: ""
|
- all
|
||||||
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||||
required: false
|
required: false
|
||||||
@@ -56,8 +57,9 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to check dms stable tag
|
# Helper function to check dms stable tag
|
||||||
|
# Sets LATEST_TAG variable in parent scope if update needed
|
||||||
check_dms_stable() {
|
check_dms_stable() {
|
||||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||||
|
|
||||||
@@ -73,8 +75,8 @@ jobs:
|
|||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
# Tag push - always update stable package
|
# Tag selected or pushed - always update stable package
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
@@ -104,7 +106,12 @@ jobs:
|
|||||||
# Check each package and build list of those needing updates
|
# Check each package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
if check_dms_stable; then
|
||||||
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -129,6 +136,9 @@ jobs:
|
|||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
@@ -161,12 +171,19 @@ jobs:
|
|||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||||
# Tag push event - use the pushed tag
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by tag: $VERSION"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
|
# Check if check-updates already determined a version (from auto-detection)
|
||||||
|
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
|
# Use version from check-updates job
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
# Scheduled run - dms-git only
|
# Scheduled run - dms-git only
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
@@ -176,22 +193,28 @@ jobs:
|
|||||||
|
|
||||||
# Determine version for dms stable
|
# Determine version for dms stable
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||||
# For explicit dms selection, require tag_version
|
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
VERSION="${{ github.event.inputs.tag_version }}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using specified tag: $VERSION"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
else
|
else
|
||||||
echo "ERROR: tag_version is required when package=dms"
|
# Auto-detect latest release for dms
|
||||||
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
exit 1
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Auto-detected latest release: $LATEST_TAG"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not auto-detect latest release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
# For "all", auto-detect if tag_version not specified
|
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
VERSION="${{ github.event.inputs.tag_version }}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using specified tag: $VERSION"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
else
|
else
|
||||||
# Auto-detect latest release for "all"
|
# Auto-detect latest release for "all"
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
@@ -206,7 +229,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||||
else
|
else
|
||||||
@@ -215,6 +238,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
- name: Update dms-git spec version
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
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
|
# 1.2.0
|
||||||
|
|
||||||
- Added clipboard and clipboard history integration
|
- Added clipboard and clipboard history integration
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -43,7 +43,6 @@ install-shell:
|
|||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
@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"
|
@echo "Shell files installed"
|
||||||
|
|
||||||
install-completions:
|
install-completions:
|
||||||
|
|||||||
300
core/cmd/dms/commands_chroma.go
Normal file
300
core/cmd/dms/commands_chroma.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"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
|
||||||
|
chromaLineNumbers bool
|
||||||
|
|
||||||
|
// Caching layer for performance
|
||||||
|
lexerCache = make(map[string]chroma.Lexer)
|
||||||
|
styleCache = make(map[string]*chroma.Style)
|
||||||
|
formatterCache = make(map[string]*html.Formatter)
|
||||||
|
cacheMutex sync.RWMutex
|
||||||
|
maxFileSize = int64(5 * 1024 * 1024) // 5MB default
|
||||||
|
)
|
||||||
|
|
||||||
|
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().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output")
|
||||||
|
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
|
||||||
|
chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)")
|
||||||
|
|
||||||
|
chromaCmd.AddCommand(chromaListLanguagesCmd)
|
||||||
|
chromaCmd.AddCommand(chromaListStylesCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer {
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if lexer, ok := lexerCache[key]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
lexer := fallbackFunc()
|
||||||
|
if lexer != nil {
|
||||||
|
cacheMutex.Lock()
|
||||||
|
lexerCache[key] = lexer
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
}
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedStyle(name string) *chroma.Style {
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if style, ok := styleCache[name]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
style := styles.Get(name)
|
||||||
|
if style == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
|
||||||
|
style = styles.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheMutex.Lock()
|
||||||
|
styleCache[name] = style
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter {
|
||||||
|
key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers)
|
||||||
|
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if formatter, ok := formatterCache[key]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
var opts []html.Option
|
||||||
|
if inline {
|
||||||
|
opts = append(opts, html.WithClasses(false))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, html.WithClasses(true))
|
||||||
|
}
|
||||||
|
opts = append(opts, html.TabWidth(4))
|
||||||
|
|
||||||
|
if lineNumbers {
|
||||||
|
opts = append(opts, html.WithLineNumbers(true))
|
||||||
|
opts = append(opts, html.LineNumbersInTable(false))
|
||||||
|
opts = append(opts, html.WithLinkableLineNumbers(false, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := html.New(opts...)
|
||||||
|
|
||||||
|
cacheMutex.Lock()
|
||||||
|
formatterCache[key] = formatter
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
// Check file size before reading
|
||||||
|
fileInfo, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.Size() > maxFileSize {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n",
|
||||||
|
fileInfo.Size(), maxFileSize)
|
||||||
|
fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
stat, _ := os.Stdin.Stat()
|
||||||
|
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||||
|
_ = cmd.Help()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getCachedLexer(chromaLanguage, func() chroma.Lexer {
|
||||||
|
l := lexers.Get(chromaLanguage)
|
||||||
|
if l == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
})
|
||||||
|
} else if filename != "" {
|
||||||
|
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
|
||||||
|
return lexers.Match(filename)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try content analysis if no lexer found (limit to first 1KB for performance)
|
||||||
|
if lexer == nil {
|
||||||
|
analyzeContent := source
|
||||||
|
if len(source) > 1024 {
|
||||||
|
analyzeContent = source[:1024]
|
||||||
|
}
|
||||||
|
lexer = lexers.Analyse(analyzeContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to plaintext
|
||||||
|
if lexer == nil {
|
||||||
|
lexer = lexers.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
lexer = chroma.Coalesce(lexer)
|
||||||
|
|
||||||
|
// Get cached style
|
||||||
|
style := getCachedStyle(chromaStyle)
|
||||||
|
|
||||||
|
// Get cached formatter
|
||||||
|
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
|
||||||
|
|
||||||
|
// 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 chromaLineNumbers {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := formatter.Format(&buf, style, iterator); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Add spacing between line numbers
|
||||||
|
output := buf.String()
|
||||||
|
output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>")
|
||||||
|
fmt.Print(output)
|
||||||
|
} else {
|
||||||
|
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -511,8 +511,11 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
colorCmd,
|
colorCmd,
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
|
notifyCmd,
|
||||||
|
genericNotifyActionCmd,
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
clipboardCmd,
|
||||||
|
chromaCmd,
|
||||||
doctorCmd,
|
doctorCmd,
|
||||||
configCmd,
|
configCmd,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ var (
|
|||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
|
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
|
||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -448,11 +450,13 @@ func checkWindowManagers() []checkResult {
|
|||||||
versionRegex *regexp.Regexp
|
versionRegex *regexp.Regexp
|
||||||
commands []string
|
commands []string
|
||||||
}{
|
}{
|
||||||
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
{"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
||||||
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
||||||
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
||||||
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -477,7 +481,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, c.name, statusOK,
|
catCompositor, c.name, statusOK,
|
||||||
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
|
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
|
||||||
doctorDocsURL + "#compositor",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +490,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, or Wayfire",
|
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||||
doctorDocsURL + "#compositor",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,8 +502,8 @@ func checkWindowManagers() []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).Output()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil && len(output) == 0 {
|
||||||
return "installed"
|
return "installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,19 +638,14 @@ func checkI2CAvailability() checkResult {
|
|||||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectNetworkBackend() string {
|
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||||
result, err := network.DetectNetworkStack()
|
switch stackResult.Backend {
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result.Backend {
|
|
||||||
case network.BackendNetworkManager:
|
case network.BackendNetworkManager:
|
||||||
return "NetworkManager"
|
return "NetworkManager"
|
||||||
case network.BackendIwd:
|
case network.BackendIwd:
|
||||||
return "iwd"
|
return "iwd"
|
||||||
case network.BackendNetworkd:
|
case network.BackendNetworkd:
|
||||||
if result.HasIwd {
|
if stackResult.HasIwd {
|
||||||
return "iwd + systemd-networkd"
|
return "iwd + systemd-networkd"
|
||||||
}
|
}
|
||||||
return "systemd-networkd"
|
return "systemd-networkd"
|
||||||
@@ -657,75 +656,73 @@ func detectNetworkBackend() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOptionalDBusStatus(busName string) (status, string) {
|
||||||
|
if utils.IsDBusServiceAvailable(busName) {
|
||||||
|
return statusOK, "Available"
|
||||||
|
} else {
|
||||||
|
return statusWarn, "Not available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkOptionalDependencies() []checkResult {
|
func checkOptionalDependencies() []checkResult {
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
|
|
||||||
if utils.IsServiceActive("accounts-daemon", false) {
|
optionalFeaturesURL := doctorDocsURL + "#optional-features"
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.IsServiceActive("power-profiles-daemon", false) {
|
accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts")
|
||||||
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL})
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"})
|
ppdStatus, ppdMsg := getOptionalDBusStatus("org.freedesktop.UPower.PowerProfiles")
|
||||||
}
|
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", ppdStatus, ppdMsg, "Power profile management", optionalFeaturesURL})
|
||||||
|
|
||||||
|
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
|
||||||
|
|
||||||
results = append(results, checkI2CAvailability())
|
results = append(results, checkI2CAvailability())
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
networkStatus, networkMessage, networkDetails := statusOK, "Not available", "Network management"
|
||||||
|
|
||||||
|
if err == nil && networkResult.Backend != network.BackendNone {
|
||||||
|
networkMessage = detectNetworkBackend(networkResult)
|
||||||
|
if doctorVerbose {
|
||||||
|
networkDetails = networkResult.ChosenReason
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
networkStatus = statusInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL})
|
||||||
|
|
||||||
deps := []struct {
|
deps := []struct {
|
||||||
name, cmd, altCmd, desc string
|
name, cmd, desc string
|
||||||
important bool
|
important bool
|
||||||
}{
|
}{
|
||||||
{"matugen", "matugen", "", "Dynamic theming", true},
|
{"matugen", "matugen", "Dynamic theming", true},
|
||||||
{"dgop", "dgop", "", "System monitoring", true},
|
{"dgop", "dgop", "System monitoring", true},
|
||||||
{"cava", "cava", "", "Audio visualizer", true},
|
{"cava", "cava", "Audio visualizer", true},
|
||||||
{"khal", "khal", "", "Calendar events", false},
|
{"khal", "khal", "Calendar events", false},
|
||||||
{"Network", "nmcli", "iwctl", "Network management", false},
|
{"danksearch", "dsearch", "File search", false},
|
||||||
{"danksearch", "dsearch", "", "File search", false},
|
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
||||||
{"loginctl", "loginctl", "", "Session management", false},
|
|
||||||
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range deps {
|
for _, d := range deps {
|
||||||
found, foundCmd := utils.CommandExists(d.cmd), d.cmd
|
found := utils.CommandExists(d.cmd)
|
||||||
if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
|
|
||||||
found, foundCmd = true, d.altCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case found:
|
case found:
|
||||||
message := "Installed"
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL})
|
||||||
details := d.desc
|
|
||||||
if d.name == "Network" {
|
|
||||||
result, err := network.DetectNetworkStack()
|
|
||||||
if err == nil && result.Backend != network.BackendNone {
|
|
||||||
message = detectNetworkBackend() + " (active)"
|
|
||||||
if doctorVerbose {
|
|
||||||
details = result.ChosenReason
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch foundCmd {
|
|
||||||
case "nmcli":
|
|
||||||
message = "NetworkManager (installed)"
|
|
||||||
case "iwctl":
|
|
||||||
message = "iwd (installed)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
|
|
||||||
case d.important:
|
case d.important:
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL})
|
||||||
default:
|
default:
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,6 +890,10 @@ func printResultLine(r checkResult, styles tui.Styles) {
|
|||||||
if doctorVerbose && r.details != "" {
|
if doctorVerbose && r.details != "" {
|
||||||
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
|
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r.status == statusError || r.status == statusWarn) && r.url != "" {
|
||||||
|
fmt.Printf(" %s\n", styles.Subtle.Render("→ "+r.url))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSummary(results []checkResult, qsMissingFeatures bool) {
|
func printSummary(results []checkResult, qsMissingFeatures bool) {
|
||||||
|
|||||||
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 (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
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/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
@@ -15,22 +16,25 @@ require (
|
|||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
|
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/yuin/goldmark v1.7.16
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||||
golang.org/x/image v0.34.0
|
golang.org/x/image v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // 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/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.2 // indirect
|
github.com/cloudflare/circl v1.6.2 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // 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/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // 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/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||||
@@ -38,8 +42,8 @@ require (
|
|||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
golang.org/x/crypto v0.46.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -47,12 +51,12 @@ require (
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // 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/cellbuf v0.0.14 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -66,7 +70,7 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.39.0
|
golang.org/x/sys v0.40.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.33.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
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/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 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
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 h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
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=
|
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/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 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
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 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
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/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 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
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.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||||
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/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
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/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 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
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.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
github.com/clipperhouse/displaywidth v0.7.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/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
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/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 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
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 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
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=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
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/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 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
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-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
||||||
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-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||||
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-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 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-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
||||||
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-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
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/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.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 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
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=
|
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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/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 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
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=
|
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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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/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 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
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 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
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 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
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 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
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 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
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/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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -215,8 +215,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
// Skip if file already exists to preserve user modifications
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
if _, err := os.Stat(path); err == nil {
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -567,7 +567,8 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
if _, err := os.Stat(path); err == nil {
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,31 +199,6 @@ func labToHex(L, a, b float64) string {
|
|||||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
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 {
|
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||||
Lf := getLstar(hexFg)
|
Lf := getLstar(hexFg)
|
||||||
Lb := getLstar(hexBg)
|
Lb := getLstar(hexBg)
|
||||||
@@ -356,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
return hexColor
|
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 {
|
type PaletteOptions struct {
|
||||||
IsLight bool
|
IsLight bool
|
||||||
Background string
|
Background string
|
||||||
@@ -369,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
|
|||||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
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 {
|
func DeriveContainer(primary string, isLight bool) string {
|
||||||
rgb := HexToRGB(primary)
|
rgb := HexToRGB(primary)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
@@ -389,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
rgb := HexToRGB(baseColor)
|
rgb := HexToRGB(baseColor)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
pr := HexToRGB(primaryColor)
|
||||||
|
ph := RGBToHSV(pr)
|
||||||
|
|
||||||
var palette Palette
|
var palette Palette
|
||||||
|
|
||||||
var normalTextTarget, secondaryTarget float64
|
var normalTextTarget, secondaryTarget float64
|
||||||
@@ -410,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
}
|
}
|
||||||
palette.Color0 = NewColorInfo(bgColor)
|
palette.Color0 = NewColorInfo(bgColor)
|
||||||
|
|
||||||
hueShift := (hsv.H - 0.6) * 0.12
|
baseSat := math.Max(ph.S, 0.5)
|
||||||
satBoost := 1.15
|
baseVal := math.Max(ph.V, 0.5)
|
||||||
|
|
||||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
redH := blendHue(0.0, ph.H, 0.12)
|
||||||
var redColor string
|
greenH := blendHue(0.33, ph.H, 0.10)
|
||||||
if opts.IsLight {
|
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
accentTarget := secondaryTarget * 0.7
|
||||||
var greenColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
|
||||||
var yellowColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
var blueColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
|
||||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
|
||||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
magH := hsv.H - 0.03
|
|
||||||
if magH < 0 {
|
|
||||||
magH += 1.0
|
|
||||||
}
|
|
||||||
var magColor string
|
|
||||||
hr := HexToRGB(primaryColor)
|
|
||||||
hh := RGBToHSV(hr)
|
|
||||||
if opts.IsLight {
|
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
|
||||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
|
||||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
cyanH := hsv.H + 0.08
|
|
||||||
if cyanH > 1.0 {
|
|
||||||
cyanH -= 1.0
|
|
||||||
}
|
|
||||||
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
|
||||||
|
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
redS := math.Min(baseSat*1.2, 1.0)
|
||||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
redV := baseVal * 0.95
|
||||||
} else {
|
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||||
palette.Color7 = NewColorInfo("#abb2bf")
|
|
||||||
palette.Color8 = NewColorInfo("#5c6370")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.IsLight {
|
greenS := math.Min(baseSat*1.3, 1.0)
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
greenV := baseVal * 0.75
|
||||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.IsLight {
|
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
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 {
|
} 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
|
return palette
|
||||||
|
|||||||
@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
|
|||||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
// Color15 is now derived from primary, so just verify it's a valid color
|
||||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
||||||
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
color15Lum := Luminance(result.Color15.Hex)
|
||||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", 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
|
bgColor := result.Color0.Hex
|
||||||
for i := 1; i < 8; i++ {
|
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)
|
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||||
minLc := 30.0
|
minLc := 30.0
|
||||||
if lc < minLc && lc > 0 {
|
if lc < minLc && lc > 0 {
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
// Standard zypper packages
|
// Standard zypper packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
@@ -117,6 +116,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,17 +111,15 @@ func (m *Manager) updateAdapterState() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
powered, _ := poweredVar.Value().(bool)
|
|
||||||
|
|
||||||
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
discovering, _ := discoveringVar.Value().(bool)
|
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Powered = powered
|
m.state.Powered = dbusutil.AsOr(poweredVar, false)
|
||||||
m.state.Discovering = discovering
|
m.state.Discovering = dbusutil.AsOr(discoveringVar, false)
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -169,65 +168,20 @@ func (m *Manager) updateDevices() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
||||||
dev := Device{Path: path}
|
return Device{
|
||||||
|
Path: path,
|
||||||
if v, ok := props["Address"]; ok {
|
Address: dbusutil.GetOr(props, "Address", ""),
|
||||||
if addr, ok := v.Value().(string); ok {
|
Name: dbusutil.GetOr(props, "Name", ""),
|
||||||
dev.Address = addr
|
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 {
|
func (m *Manager) startAgent() error {
|
||||||
@@ -328,17 +282,13 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
dirty := false
|
dirty := false
|
||||||
|
|
||||||
if v, ok := changed["Powered"]; ok {
|
if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok {
|
||||||
if powered, ok := v.Value().(bool); ok {
|
m.state.Powered = powered
|
||||||
m.state.Powered = powered
|
dirty = true
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if v, ok := changed["Discovering"]; ok {
|
if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok {
|
||||||
if discovering, ok := v.Value().(bool); ok {
|
m.state.Discovering = discovering
|
||||||
m.state.Discovering = discovering
|
dirty = true
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stateMutex.Unlock()
|
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) {
|
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"]
|
_, hasConnected := changed["Connected"]
|
||||||
_, hasTrusted := changed["Trusted"]
|
_, hasTrusted := changed["Trusted"]
|
||||||
|
|
||||||
if hasPaired {
|
if hasPaired {
|
||||||
devicePath := string(path)
|
devicePath := string(path)
|
||||||
if paired, ok := pairedVar.Value().(bool); ok {
|
if paired {
|
||||||
if paired {
|
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
if wasPending {
|
||||||
|
select {
|
||||||
if wasPending {
|
case m.eventQueue <- func() {
|
||||||
select {
|
time.Sleep(300 * time.Millisecond)
|
||||||
case m.eventQueue <- func() {
|
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||||
time.Sleep(300 * time.Millisecond)
|
if err := m.ConnectDevice(devicePath); err != nil {
|
||||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||||
if err := m.ConnectDevice(devicePath); err != nil {
|
|
||||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
|
||||||
}
|
|
||||||
}:
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
}:
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
m.pendingPairings.Delete(devicePath)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
m.pendingPairings.Delete(devicePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
handleSetConfig(conn, req, m)
|
handleSetConfig(conn, req, m)
|
||||||
case "clipboard.store":
|
case "clipboard.store":
|
||||||
handleStore(conn, req, m)
|
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:
|
default:
|
||||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
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 {
|
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||||
cfg.Disabled = v
|
cfg.Disabled = v
|
||||||
}
|
}
|
||||||
|
if v, ok := models.Get[float64](req, "maxPinned"); ok {
|
||||||
|
cfg.MaxPinned = int(v)
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.SetConfig(cfg); err != nil {
|
if err := m.SetConfig(cfg); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
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"})
|
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()
|
c := b.Cursor()
|
||||||
var count int
|
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 {
|
if count < m.config.MaxHistory {
|
||||||
count++
|
count++
|
||||||
continue
|
continue
|
||||||
@@ -419,6 +423,11 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
buf.WriteByte(0)
|
buf.WriteByte(0)
|
||||||
}
|
}
|
||||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||||
|
if e.Pinned {
|
||||||
|
buf.WriteByte(1)
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(0)
|
||||||
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
@@ -462,6 +471,12 @@ func decodeEntry(data []byte) (Entry, error) {
|
|||||||
binary.Read(buf, binary.BigEndian, &e.Hash)
|
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
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,19 +750,54 @@ func (m *Manager) ClearHistory() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete only non-pinned entries
|
||||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
return err
|
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 {
|
}); err != nil {
|
||||||
log.Errorf("Failed to clear clipboard history: %v", err)
|
log.Errorf("Failed to clear clipboard history: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.compactDB(); err != nil {
|
pinnedCount := 0
|
||||||
log.Errorf("Failed to compact database: %v", err)
|
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()
|
m.updateState()
|
||||||
@@ -960,6 +1010,10 @@ func (m *Manager) clearOldEntries(days int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Skip pinned entries
|
||||||
|
if entry.Pinned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if entry.Timestamp.Before(cutoff) {
|
if entry.Timestamp.Before(cutoff) {
|
||||||
toDelete = append(toDelete, k)
|
toDelete = append(toDelete, k)
|
||||||
}
|
}
|
||||||
@@ -1250,3 +1304,153 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
|||||||
|
|
||||||
return nil
|
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"`
|
AutoClearDays int `json:"autoClearDays"`
|
||||||
ClearAtStartup bool `json:"clearAtStartup"`
|
ClearAtStartup bool `json:"clearAtStartup"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
MaxPinned int `json:"maxPinned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
@@ -27,6 +28,7 @@ func DefaultConfig() Config {
|
|||||||
MaxEntrySize: 5 * 1024 * 1024,
|
MaxEntrySize: 5 * 1024 * 1024,
|
||||||
AutoClearDays: 0,
|
AutoClearDays: 0,
|
||||||
ClearAtStartup: false,
|
ClearAtStartup: false,
|
||||||
|
MaxPinned: 25,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ type Entry struct {
|
|||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
IsImage bool `json:"isImage"`
|
IsImage bool `json:"isImage"`
|
||||||
Hash uint64 `json:"hash,omitempty"`
|
Hash uint64 `json:"hash,omitempty"`
|
||||||
|
Pinned bool `json:"pinned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
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"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,61 +111,17 @@ func (m *Manager) updateAccountsState() error {
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
defer m.stateMutex.Unlock()
|
defer m.stateMutex.Unlock()
|
||||||
|
|
||||||
if v, ok := props["IconFile"]; ok {
|
m.state.Accounts.IconFile = dbusutil.GetOr(props, "IconFile", "")
|
||||||
if val, ok := v.Value().(string); ok {
|
m.state.Accounts.RealName = dbusutil.GetOr(props, "RealName", "")
|
||||||
m.state.Accounts.IconFile = val
|
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", "")
|
||||||
if v, ok := props["RealName"]; ok {
|
m.state.Accounts.Shell = dbusutil.GetOr(props, "Shell", "")
|
||||||
if val, ok := v.Value().(string); ok {
|
m.state.Accounts.Email = dbusutil.GetOr(props, "Email", "")
|
||||||
m.state.Accounts.RealName = val
|
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)
|
||||||
if v, ok := props["UserName"]; ok {
|
m.state.Accounts.PasswordMode = dbusutil.GetOr(props, "PasswordMode", int32(0))
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.UserName = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["AccountType"]; ok {
|
|
||||||
if val, ok := v.Value().(int32); ok {
|
|
||||||
m.state.Accounts.AccountType = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["HomeDirectory"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.HomeDirectory = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Shell"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Shell = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Email"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Email = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Language"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Language = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Location"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Location = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Locked"]; ok {
|
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.Accounts.Locked = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["PasswordMode"]; ok {
|
|
||||||
if val, ok := v.Value().(int32); ok {
|
|
||||||
m.state.Accounts.PasswordMode = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -180,7 +137,7 @@ func (m *Manager) updateSettingsState() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if colorScheme, ok := variant.Value().(uint32); ok {
|
if colorScheme, ok := dbusutil.As[uint32](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Settings.ColorScheme = colorScheme
|
m.state.Settings.ColorScheme = colorScheme
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,37 +133,15 @@ func (m *Manager) updateSessionState() error {
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
defer m.stateMutex.Unlock()
|
defer m.stateMutex.Unlock()
|
||||||
|
|
||||||
if v, ok := props["Active"]; ok {
|
m.state.Active = dbusutil.GetOr(props, "Active", m.state.Active)
|
||||||
if val, ok := v.Value().(bool); ok {
|
m.state.IdleHint = dbusutil.GetOr(props, "IdleHint", m.state.IdleHint)
|
||||||
m.state.Active = val
|
m.state.IdleSinceHint = dbusutil.GetOr(props, "IdleSinceHint", m.state.IdleSinceHint)
|
||||||
}
|
if lockedHint, ok := dbusutil.Get[bool](props, "LockedHint"); ok {
|
||||||
}
|
m.state.LockedHint = lockedHint
|
||||||
if v, ok := props["IdleHint"]; ok {
|
m.state.Locked = lockedHint
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.IdleHint = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["IdleSinceHint"]; ok {
|
|
||||||
if val, ok := v.Value().(uint64); ok {
|
|
||||||
m.state.IdleSinceHint = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["LockedHint"]; ok {
|
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.LockedHint = val
|
|
||||||
m.state.Locked = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Type"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.SessionType = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Class"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.SessionClass = val
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
m.state.SessionType = dbusutil.GetOr(props, "Type", m.state.SessionType)
|
||||||
|
m.state.SessionClass = dbusutil.GetOr(props, "Class", m.state.SessionClass)
|
||||||
if v, ok := props["User"]; ok {
|
if v, ok := props["User"]; ok {
|
||||||
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
||||||
if uid, ok := userArr[0].(uint32); ok {
|
if uid, ok := userArr[0].(uint32); ok {
|
||||||
@@ -170,36 +149,12 @@ func (m *Manager) updateSessionState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, ok := props["Name"]; ok {
|
m.state.UserName = dbusutil.GetOr(props, "Name", m.state.UserName)
|
||||||
if val, ok := v.Value().(string); ok {
|
m.state.RemoteHost = dbusutil.GetOr(props, "RemoteHost", m.state.RemoteHost)
|
||||||
m.state.UserName = val
|
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)
|
||||||
if v, ok := props["RemoteHost"]; ok {
|
m.state.Remote = dbusutil.GetOr(props, "Remote", m.state.Remote)
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.RemoteHost = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Service"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Service = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["TTY"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.TTY = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Display"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Display = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Remote"]; ok {
|
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.Remote = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Seat"]; ok {
|
if v, ok := props["Seat"]; ok {
|
||||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||||
if seatID, ok := seatArr[0].(string); ok {
|
if seatID, ok := seatArr[0].(string); ok {
|
||||||
@@ -207,11 +162,7 @@ func (m *Manager) updateSessionState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, ok := props["VTNr"]; ok {
|
m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr)
|
||||||
if val, ok := v.Value().(uint32); ok {
|
|
||||||
m.state.VTNr = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package loginctl
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,31 +118,28 @@ func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) {
|
|||||||
for key, variant := range changes {
|
for key, variant := range changes {
|
||||||
switch key {
|
switch key {
|
||||||
case "Active":
|
case "Active":
|
||||||
if val, ok := variant.Value().(bool); ok {
|
if val, ok := dbusutil.As[bool](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Active = val
|
m.state.Active = val
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "IdleHint":
|
case "IdleHint":
|
||||||
if val, ok := variant.Value().(bool); ok {
|
if val, ok := dbusutil.As[bool](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.IdleHint = val
|
m.state.IdleHint = val
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "IdleSinceHint":
|
case "IdleSinceHint":
|
||||||
if val, ok := variant.Value().(uint64); ok {
|
if val, ok := dbusutil.As[uint64](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.IdleSinceHint = val
|
m.state.IdleSinceHint = val
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "LockedHint":
|
case "LockedHint":
|
||||||
if val, ok := variant.Value().(bool); ok {
|
if val, ok := dbusutil.As[bool](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.LockedHint = val
|
m.state.LockedHint = val
|
||||||
m.state.Locked = 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,
|
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||||
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority)).Run(); err != nil {
|
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority),
|
||||||
log.Warnf("Failed to set autoconnect-priority for %v: %v", connName, err)
|
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric),
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
|
||||||
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
|
||||||
log.Warnf("Failed to set ipv4.route-metric for %v: %v", connName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
|
||||||
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
"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)
|
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/brightness"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
"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/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
@@ -154,6 +155,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
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.") {
|
if strings.HasPrefix(req.Method, "clipboard.") {
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "clipboard.getConfig":
|
case "clipboard.getConfig":
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
"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/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
@@ -65,8 +66,11 @@ var brightnessManager *brightness.Manager
|
|||||||
var wlrOutputManager *wlroutput.Manager
|
var wlrOutputManager *wlroutput.Manager
|
||||||
var evdevManager *evdev.Manager
|
var evdevManager *evdev.Manager
|
||||||
var clipboardManager *clipboard.Manager
|
var clipboardManager *clipboard.Manager
|
||||||
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
|
|
||||||
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||||
var cupsSubscribers syncmap.Map[string, bool]
|
var cupsSubscribers syncmap.Map[string, bool]
|
||||||
var cupsSubscriberCount atomic.Int32
|
var cupsSubscriberCount atomic.Int32
|
||||||
@@ -363,6 +367,19 @@ func InitializeClipboardManager() error {
|
|||||||
return nil
|
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) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -440,6 +457,10 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "clipboard")
|
caps = append(caps, "clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbusManager != nil {
|
||||||
|
caps = append(caps, "dbus")
|
||||||
|
}
|
||||||
|
|
||||||
return Capabilities{Capabilities: caps}
|
return Capabilities{Capabilities: caps}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +519,10 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "clipboard")
|
caps = append(caps, "clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbusManager != nil {
|
||||||
|
caps = append(caps, "dbus")
|
||||||
|
}
|
||||||
|
|
||||||
return ServerInfo{
|
return ServerInfo{
|
||||||
APIVersion: APIVersion,
|
APIVersion: APIVersion,
|
||||||
CLIVersion: CLIVersion,
|
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() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(eventChan)
|
close(eventChan)
|
||||||
@@ -1198,6 +1248,9 @@ func cleanupManagers() {
|
|||||||
if clipboardManager != nil {
|
if clipboardManager != nil {
|
||||||
clipboardManager.Close()
|
clipboardManager.Close()
|
||||||
}
|
}
|
||||||
|
if dbusManager != nil {
|
||||||
|
dbusManager.Close()
|
||||||
|
}
|
||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
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.Info("")
|
||||||
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||||
|
|
||||||
|
|||||||
20
core/internal/utils/dbus.go
Normal file
20
core/internal/utils/dbus.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsDBusServiceAvailable(busName string) bool {
|
||||||
|
conn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
||||||
|
var owned bool
|
||||||
|
if err := obj.Call("org.freedesktop.DBus.NameHasOwner", 0, busName).Store(&owned); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return owned
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppChecker interface {
|
type AppChecker interface {
|
||||||
@@ -43,16 +42,3 @@ func AnyCommandExists(cmds ...string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsServiceActive(name string, userService bool) bool {
|
|
||||||
if !CommandExists("systemctl") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"is-active", name}
|
|
||||||
if userService {
|
|
||||||
args = []string{"--user", "is-active", name}
|
|
||||||
}
|
|
||||||
output, _ := exec.Command("systemctl", args...).Output()
|
|
||||||
return strings.EqualFold(strings.TrimSpace(string(output)), "active")
|
|
||||||
}
|
|
||||||
|
|||||||
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,
|
quickshell-git | quickshell,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Provides: dms
|
Provides: dms
|
||||||
Conflicts: dms
|
Conflicts: dms
|
||||||
Replaces: dms
|
Replaces: dms
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Conflicts: dms-git
|
Conflicts: dms-git
|
||||||
Replaces: dms-git
|
Replaces: dms-git
|
||||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ Recommends: cava
|
|||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: quickshell-git
|
Recommends: quickshell-git
|
||||||
Recommends: wl-clipboard
|
|
||||||
|
|
||||||
# Recommended system packages
|
# Recommended system packages
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
|
|||||||
@@ -24,10 +24,8 @@ Requires: dms-cli = %{version}-%{release}
|
|||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ let
|
|||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
|
compositorPackage =
|
||||||
|
let
|
||||||
|
configured = lib.attrByPath [ "programs" cfg.compositor.name "package" ] null config;
|
||||||
|
in
|
||||||
|
if configured != null then configured else builtins.getAttr cfg.compositor.name pkgs;
|
||||||
|
|
||||||
cacheDir = "/var/lib/dms-greeter";
|
cacheDir = "/var/lib/dms-greeter";
|
||||||
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
|
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
|
||||||
export PATH=$PATH:${
|
export PATH=$PATH:${
|
||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
config.programs.${cfg.compositor.name}.package
|
compositorPackage
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -64,6 +70,7 @@ in
|
|||||||
"niri"
|
"niri"
|
||||||
"hyprland"
|
"hyprland"
|
||||||
"sway"
|
"sway"
|
||||||
|
"labwc"
|
||||||
];
|
];
|
||||||
description = "Compositor to run greeter in";
|
description = "Compositor to run greeter in";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ in
|
|||||||
default = hasPluginSettings;
|
default = hasPluginSettings;
|
||||||
description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.'';
|
description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.target = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = config.wayland.systemd.target;
|
||||||
|
defaultText = lib.literalExpression "config.wayland.systemd.target";
|
||||||
|
description = "Systemd target to bind to.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -84,8 +91,8 @@ in
|
|||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
Unit = {
|
Unit = {
|
||||||
Description = "DankMaterialShell";
|
Description = "DankMaterialShell";
|
||||||
PartOf = [ config.wayland.systemd.target ];
|
PartOf = [ cfg.systemd.target ];
|
||||||
After = [ config.wayland.systemd.target ];
|
After = [ cfg.systemd.target ];
|
||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
@@ -93,7 +100,7 @@ in
|
|||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
Install.WantedBy = [ config.wayland.systemd.target ];
|
Install.WantedBy = [ cfg.systemd.target ];
|
||||||
};
|
};
|
||||||
|
|
||||||
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
|
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ in
|
|||||||
imports = [
|
imports = [
|
||||||
(import ./options.nix args)
|
(import ./options.nix args)
|
||||||
];
|
];
|
||||||
|
options.programs.dank-material-shell.systemd.target = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Systemd target to bind to.";
|
||||||
|
default = "graphical-session.target";
|
||||||
|
};
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
description = "DankMaterialShell";
|
description = "DankMaterialShell";
|
||||||
path = lib.mkForce [ ];
|
path = lib.mkForce [ ];
|
||||||
|
|
||||||
partOf = [ "graphical-session.target" ];
|
partOf = [ cfg.systemd.target ];
|
||||||
after = [ "graphical-session.target" ];
|
after = [ cfg.systemd.target ];
|
||||||
wantedBy = [ "graphical-session.target" ];
|
wantedBy = [ cfg.systemd.target ];
|
||||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
|||||||
@@ -20,12 +20,9 @@ Requires: accountsservice
|
|||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: quickshell-git
|
Recommends: quickshell-git
|
||||||
Recommends: wl-clipboard
|
|
||||||
|
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ Requires: dgop
|
|||||||
|
|
||||||
# Core utilities (Highly recommended for DMS functionality)
|
# Core utilities (Highly recommended for DMS functionality)
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
Recommends: wl-clipboard
|
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell-git | quickshell,
|
quickshell-git | quickshell,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Provides: dms
|
Provides: dms
|
||||||
Conflicts: dms
|
Conflicts: dms
|
||||||
Replaces: dms
|
Replaces: dms
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Conflicts: dms-git
|
Conflicts: dms-git
|
||||||
Replaces: dms-git
|
Replaces: dms-git
|
||||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||||
|
|||||||
16
flake.nix
16
flake.nix
@@ -61,11 +61,13 @@
|
|||||||
(builtins.substring 6 2 longDate)
|
(builtins.substring 6 2 longDate)
|
||||||
];
|
];
|
||||||
version =
|
version =
|
||||||
pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION))
|
let
|
||||||
+ "+date="
|
rawVersion = pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION));
|
||||||
+ mkDate (self.lastModifiedDate or "19700101")
|
cleanVersion = builtins.replaceStrings [ " " ] [ "" ] rawVersion;
|
||||||
+ "_"
|
dateSuffix = "+date=" + mkDate (self.lastModifiedDate or "19700101");
|
||||||
+ (self.shortRev or "dirty");
|
revSuffix = "_" + (self.shortRev or "dirty");
|
||||||
|
in
|
||||||
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.buildGoModule (
|
dms-shell = pkgs.buildGoModule (
|
||||||
@@ -76,14 +78,14 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
pname = "dms-shell";
|
pname = "dms-shell";
|
||||||
src = ./core;
|
src = ./core;
|
||||||
vendorHash = "sha256-9CnZFtjXXWYELRiBX2UbZvWopnl9Y1ILuK+xP6YQZ9U=";
|
vendorHash = "sha256-kWHB/FN6Z2Ydh+VvNrDnbg18RuJSDAle4DHDAP4NpNk=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X main.Version=${version}"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Spicy Miso
|
Saffron Bloom
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ Singleton {
|
|||||||
Quickshell.execDetached(["cp", strip(from), strip(to)]);
|
Quickshell.execDetached(["cp", strip(from), strip(to)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSteamApp(appId: string): bool {
|
||||||
|
return appId && /^steam_app_\d+$/.test(appId);
|
||||||
|
}
|
||||||
|
|
||||||
function moddedAppId(appId: string): string {
|
function moddedAppId(appId: string): string {
|
||||||
const subs = SettingsData.appIdSubstitutions || [];
|
const subs = SettingsData.appIdSubstitutions || [];
|
||||||
for (let i = 0; i < subs.length; i++) {
|
for (let i = 0; i < subs.length; i++) {
|
||||||
@@ -60,6 +64,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const steamMatch = appId.match(/^steam_app_(\d+)$/);
|
||||||
|
if (steamMatch)
|
||||||
|
return `steam_icon_${steamMatch[1]}`;
|
||||||
return appId;
|
return appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,15 +82,19 @@ Singleton {
|
|||||||
popoutOpening();
|
popoutOpening();
|
||||||
}
|
}
|
||||||
|
|
||||||
let justClosedSamePopout = false;
|
let movedFromOtherScreen = false;
|
||||||
for (const otherScreenName in currentPopoutsByScreen) {
|
for (const otherScreenName in currentPopoutsByScreen) {
|
||||||
if (otherScreenName === screenName)
|
if (otherScreenName === screenName)
|
||||||
continue;
|
continue;
|
||||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||||
if (!otherPopout)
|
if (!otherPopout)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (otherPopout === popout) {
|
if (otherPopout === popout) {
|
||||||
justClosedSamePopout = true;
|
movedFromOtherScreen = true;
|
||||||
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
|
currentPopoutTriggers[otherScreenName] = null;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherPopout.dashVisible !== undefined) {
|
if (otherPopout.dashVisible !== undefined) {
|
||||||
@@ -112,7 +116,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout === popout && popout.shouldBeVisible) {
|
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
||||||
if (popout.dashVisible !== undefined) {
|
if (popout.dashVisible !== undefined) {
|
||||||
popout.dashVisible = false;
|
popout.dashVisible = false;
|
||||||
@@ -139,6 +143,7 @@ Singleton {
|
|||||||
popout.currentTabIndex = tabIndex;
|
popout.currentTabIndex = tabIndex;
|
||||||
}
|
}
|
||||||
currentPopoutTriggers[screenName] = triggerId;
|
currentPopoutTriggers[screenName] = triggerId;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPopoutTriggers[screenName] = triggerId;
|
currentPopoutTriggers[screenName] = triggerId;
|
||||||
@@ -153,16 +158,8 @@ Singleton {
|
|||||||
ModalManager.closeAllModalsExcept(null);
|
ModalManager.closeAllModalsExcept(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (justClosedSamePopout) {
|
if (movedFromOtherScreen) {
|
||||||
Qt.callLater(() => {
|
popout.open();
|
||||||
if (popout.dashVisible !== undefined) {
|
|
||||||
popout.dashVisible = true;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = true;
|
|
||||||
} else {
|
|
||||||
popout.open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
if (popout.dashVisible !== undefined) {
|
if (popout.dashVisible !== undefined) {
|
||||||
popout.dashVisible = true;
|
popout.dashVisible = true;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Singleton {
|
|||||||
property bool _isReadOnly: false
|
property bool _isReadOnly: false
|
||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
property var _loadedSessionSnapshot: null
|
property var _loadedSessionSnapshot: null
|
||||||
|
readonly property var _hooks: ({})
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -102,6 +103,10 @@ Singleton {
|
|||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
property string weatherCoordinates: "40.7128,-74.0060"
|
property string weatherCoordinates: "40.7128,-74.0060"
|
||||||
|
|
||||||
|
property var hiddenApps: []
|
||||||
|
property var appOverrides: ({})
|
||||||
|
property bool searchAppActions: true
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -261,6 +266,10 @@ Singleton {
|
|||||||
_checkSessionWritable();
|
_checkSessionWritable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function set(key, value) {
|
||||||
|
Spec.set(root, key, value, saveSettings, _hooks);
|
||||||
|
}
|
||||||
|
|
||||||
function migrateFromUndefinedToV1(settings) {
|
function migrateFromUndefinedToV1(settings) {
|
||||||
console.info("SessionData: Migrating configuration from undefined to version 1");
|
console.info("SessionData: Migrating configuration from undefined to version 1");
|
||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
@@ -906,6 +915,61 @@ Singleton {
|
|||||||
saveSettings();
|
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() {
|
function syncWallpaperForCurrentMode() {
|
||||||
if (!perModeWallpaper)
|
if (!perModeWallpaper)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -79,6 +79,45 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var launcherPluginVisibility: ({})
|
||||||
|
|
||||||
|
function getPluginAllowWithoutTrigger(pluginId) {
|
||||||
|
if (!launcherPluginVisibility[pluginId])
|
||||||
|
return true;
|
||||||
|
return launcherPluginVisibility[pluginId].allowWithoutTrigger !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPluginAllowWithoutTrigger(pluginId, allow) {
|
||||||
|
const updated = JSON.parse(JSON.stringify(launcherPluginVisibility));
|
||||||
|
if (!updated[pluginId])
|
||||||
|
updated[pluginId] = {};
|
||||||
|
updated[pluginId].allowWithoutTrigger = allow;
|
||||||
|
launcherPluginVisibility = updated;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
property var launcherPluginOrder: []
|
||||||
|
onLauncherPluginOrderChanged: saveSettings()
|
||||||
|
|
||||||
|
function setLauncherPluginOrder(order) {
|
||||||
|
launcherPluginOrder = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderedLauncherPlugins(allPlugins) {
|
||||||
|
if (!launcherPluginOrder || launcherPluginOrder.length === 0)
|
||||||
|
return allPlugins;
|
||||||
|
const orderMap = {};
|
||||||
|
for (let i = 0; i < launcherPluginOrder.length; i++)
|
||||||
|
orderMap[launcherPluginOrder[i]] = i;
|
||||||
|
return allPlugins.slice().sort((a, b) => {
|
||||||
|
const aOrder = orderMap[a.id] ?? 9999;
|
||||||
|
const bOrder = orderMap[b.id] ?? 9999;
|
||||||
|
if (aOrder !== bOrder)
|
||||||
|
return aOrder - bOrder;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
property alias dankBarLeftWidgetsModel: leftWidgetsModel
|
property alias dankBarLeftWidgetsModel: leftWidgetsModel
|
||||||
property alias dankBarCenterWidgetsModel: centerWidgetsModel
|
property alias dankBarCenterWidgetsModel: centerWidgetsModel
|
||||||
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
||||||
@@ -145,6 +184,7 @@ Singleton {
|
|||||||
property bool controlCenterShowMicPercent: true
|
property bool controlCenterShowMicPercent: true
|
||||||
property bool controlCenterShowBatteryIcon: false
|
property bool controlCenterShowBatteryIcon: false
|
||||||
property bool controlCenterShowPrinterIcon: false
|
property bool controlCenterShowPrinterIcon: false
|
||||||
|
property bool controlCenterShowScreenSharingIcon: true
|
||||||
property bool showPrivacyButton: true
|
property bool showPrivacyButton: true
|
||||||
property bool privacyShowMicIcon: false
|
property bool privacyShowMicIcon: false
|
||||||
property bool privacyShowCameraIcon: false
|
property bool privacyShowCameraIcon: false
|
||||||
@@ -205,6 +245,7 @@ Singleton {
|
|||||||
property bool reverseScrolling: false
|
property bool reverseScrolling: false
|
||||||
property bool dwlShowAllTags: false
|
property bool dwlShowAllTags: false
|
||||||
property string workspaceColorMode: "default"
|
property string workspaceColorMode: "default"
|
||||||
|
property string workspaceOccupiedColorMode: "none"
|
||||||
property string workspaceUnfocusedColorMode: "default"
|
property string workspaceUnfocusedColorMode: "default"
|
||||||
property string workspaceUrgentColorMode: "default"
|
property string workspaceUrgentColorMode: "default"
|
||||||
property bool workspaceFocusedBorderEnabled: false
|
property bool workspaceFocusedBorderEnabled: false
|
||||||
@@ -234,7 +275,16 @@ Singleton {
|
|||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
|
property var spotlightSectionViewModes: ({})
|
||||||
|
onSpotlightSectionViewModesChanged: saveSettings()
|
||||||
|
property var appDrawerSectionViewModes: ({})
|
||||||
|
onAppDrawerSectionViewModesChanged: saveSettings()
|
||||||
property bool niriOverviewOverlayEnabled: true
|
property bool niriOverviewOverlayEnabled: true
|
||||||
|
property string dankLauncherV2Size: "compact"
|
||||||
|
property bool dankLauncherV2BorderEnabled: false
|
||||||
|
property int dankLauncherV2BorderThickness: 2
|
||||||
|
property string dankLauncherV2BorderColor: "primary"
|
||||||
|
property bool dankLauncherV2ShowFooter: true
|
||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
@@ -365,6 +415,7 @@ Singleton {
|
|||||||
|
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
|
property bool dockSmartAutoHide: false
|
||||||
property bool dockGroupByApp: false
|
property bool dockGroupByApp: false
|
||||||
property bool dockOpenOnOverview: false
|
property bool dockOpenOnOverview: false
|
||||||
property int dockPosition: SettingsData.Position.Bottom
|
property int dockPosition: SettingsData.Position.Bottom
|
||||||
@@ -392,6 +443,7 @@ Singleton {
|
|||||||
property bool lockScreenShowDate: true
|
property bool lockScreenShowDate: true
|
||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
property bool lockScreenShowPasswordField: true
|
property bool lockScreenShowPasswordField: true
|
||||||
|
property bool lockScreenPowerOffMonitorsOnLock: false
|
||||||
|
|
||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
@@ -491,7 +543,8 @@ Singleton {
|
|||||||
"shadowIntensity": 0,
|
"shadowIntensity": 0,
|
||||||
"shadowOpacity": 60,
|
"shadowOpacity": 60,
|
||||||
"shadowColorMode": "text",
|
"shadowColorMode": "text",
|
||||||
"shadowCustomColor": "#000000"
|
"shadowCustomColor": "#000000",
|
||||||
|
"clickThrough": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -752,9 +752,11 @@ Singleton {
|
|||||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
function barIconSize(barThickness, offset) {
|
function barIconSize(barThickness, offset, noBackground) {
|
||||||
const defaultOffset = offset !== undefined ? offset : -6;
|
const defaultOffset = offset !== undefined ? offset : -6;
|
||||||
return Math.round((barThickness / 48) * (iconSize + defaultOffset));
|
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
|
||||||
|
|
||||||
|
return Math.round((barThickness / 48) * (size + defaultOffset));
|
||||||
}
|
}
|
||||||
|
|
||||||
function barTextSize(barThickness, fontScale) {
|
function barTextSize(barThickness, fontScale) {
|
||||||
@@ -904,7 +906,7 @@ Singleton {
|
|||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
const skipTemplates = [];
|
const skipTemplates = [];
|
||||||
if (!SettingsData.runDmsMatugenTemplates) {
|
if (!SettingsData.runDmsMatugenTemplates) {
|
||||||
skipTemplates.push("gtk", "neovim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
|
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
|
||||||
} else {
|
} else {
|
||||||
if (!SettingsData.matugenTemplateGtk)
|
if (!SettingsData.matugenTemplateGtk)
|
||||||
skipTemplates.push("gtk");
|
skipTemplates.push("gtk");
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ Singleton {
|
|||||||
showMicIcon: false,
|
showMicIcon: false,
|
||||||
showMicPercent: true,
|
showMicPercent: true,
|
||||||
showBatteryIcon: false,
|
showBatteryIcon: false,
|
||||||
showPrinterIcon: false
|
showPrinterIcon: false,
|
||||||
|
showScreenSharingIcon: true
|
||||||
};
|
};
|
||||||
leftModel.append(dummy);
|
leftModel.append(dummy);
|
||||||
centerModel.append(dummy);
|
centerModel.append(dummy);
|
||||||
@@ -84,6 +85,8 @@ Singleton {
|
|||||||
item.showBatteryIcon = order[i].showBatteryIcon;
|
item.showBatteryIcon = order[i].showBatteryIcon;
|
||||||
if (isObj && order[i].showPrinterIcon !== undefined)
|
if (isObj && order[i].showPrinterIcon !== undefined)
|
||||||
item.showPrinterIcon = order[i].showPrinterIcon;
|
item.showPrinterIcon = order[i].showPrinterIcon;
|
||||||
|
if (isObj && order[i].showScreenSharingIcon !== undefined)
|
||||||
|
item.showScreenSharingIcon = order[i].showScreenSharingIcon;
|
||||||
|
|
||||||
model.append(item);
|
model.append(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,23 @@ var SPEC = {
|
|||||||
enabledGpuPciIds: { def: [] },
|
enabledGpuPciIds: { def: [] },
|
||||||
|
|
||||||
wifiDeviceOverride: { def: "" },
|
wifiDeviceOverride: { def: "" },
|
||||||
weatherHourlyDetailed: { def: true }
|
weatherHourlyDetailed: { def: true },
|
||||||
|
|
||||||
|
hiddenApps: { def: [] },
|
||||||
|
appOverrides: { def: {} },
|
||||||
|
searchAppActions: { def: true }
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
return Object.keys(SPEC).concat(["configVersion"]);
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ var SPEC = {
|
|||||||
controlCenterShowMicPercent: { def: false },
|
controlCenterShowMicPercent: { def: false },
|
||||||
controlCenterShowBatteryIcon: { def: false },
|
controlCenterShowBatteryIcon: { def: false },
|
||||||
controlCenterShowPrinterIcon: { def: false },
|
controlCenterShowPrinterIcon: { def: false },
|
||||||
|
controlCenterShowScreenSharingIcon: { def: true },
|
||||||
|
|
||||||
showPrivacyButton: { def: true },
|
showPrivacyButton: { def: true },
|
||||||
privacyShowMicIcon: { def: false },
|
privacyShowMicIcon: { def: false },
|
||||||
@@ -99,6 +100,7 @@ var SPEC = {
|
|||||||
reverseScrolling: { def: false },
|
reverseScrolling: { def: false },
|
||||||
dwlShowAllTags: { def: false },
|
dwlShowAllTags: { def: false },
|
||||||
workspaceColorMode: { def: "default" },
|
workspaceColorMode: { def: "default" },
|
||||||
|
workspaceOccupiedColorMode: { def: "none" },
|
||||||
workspaceUnfocusedColorMode: { def: "default" },
|
workspaceUnfocusedColorMode: { def: "default" },
|
||||||
workspaceUrgentColorMode: { def: "default" },
|
workspaceUrgentColorMode: { def: "default" },
|
||||||
workspaceFocusedBorderEnabled: { def: false },
|
workspaceFocusedBorderEnabled: { def: false },
|
||||||
@@ -132,7 +134,14 @@ var SPEC = {
|
|||||||
sortAppsAlphabetically: { def: false },
|
sortAppsAlphabetically: { def: false },
|
||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
|
spotlightSectionViewModes: { def: {} },
|
||||||
|
appDrawerSectionViewModes: { def: {} },
|
||||||
niriOverviewOverlayEnabled: { def: true },
|
niriOverviewOverlayEnabled: { def: true },
|
||||||
|
dankLauncherV2Size: { def: "compact" },
|
||||||
|
dankLauncherV2BorderEnabled: { def: false },
|
||||||
|
dankLauncherV2BorderThickness: { def: 2 },
|
||||||
|
dankLauncherV2BorderColor: { def: "primary" },
|
||||||
|
dankLauncherV2ShowFooter: { def: true },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
@@ -230,6 +239,7 @@ var SPEC = {
|
|||||||
|
|
||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
|
dockSmartAutoHide: { def: false },
|
||||||
dockGroupByApp: { def: false },
|
dockGroupByApp: { def: false },
|
||||||
dockOpenOnOverview: { def: false },
|
dockOpenOnOverview: { def: false },
|
||||||
dockPosition: { def: 1 },
|
dockPosition: { def: 1 },
|
||||||
@@ -257,6 +267,7 @@ var SPEC = {
|
|||||||
lockScreenShowDate: { def: true },
|
lockScreenShowDate: { def: true },
|
||||||
lockScreenShowProfileImage: { def: true },
|
lockScreenShowProfileImage: { def: true },
|
||||||
lockScreenShowPasswordField: { def: true },
|
lockScreenShowPasswordField: { def: true },
|
||||||
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
@@ -354,7 +365,8 @@ var SPEC = {
|
|||||||
shadowIntensity: 0,
|
shadowIntensity: 0,
|
||||||
shadowOpacity: 60,
|
shadowOpacity: 60,
|
||||||
shadowColorMode: "text",
|
shadowColorMode: "text",
|
||||||
shadowCustomColor: "#000000"
|
shadowCustomColor: "#000000",
|
||||||
|
clickThrough: false
|
||||||
}], onChange: "updateBarConfigs" },
|
}], onChange: "updateBarConfigs" },
|
||||||
|
|
||||||
desktopClockEnabled: { def: false },
|
desktopClockEnabled: { def: false },
|
||||||
@@ -404,7 +416,9 @@ var SPEC = {
|
|||||||
|
|
||||||
desktopWidgetGroups: { def: [] },
|
desktopWidgetGroups: { def: [] },
|
||||||
|
|
||||||
builtInPluginSettings: { def: {} }
|
builtInPluginSettings: { def: {} },
|
||||||
|
launcherPluginVisibility: { def: {} },
|
||||||
|
launcherPluginOrder: { def: [] }
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Modals.Changelog
|
|||||||
import qs.Modals.Clipboard
|
import qs.Modals.Clipboard
|
||||||
import qs.Modals.Greeter
|
import qs.Modals.Greeter
|
||||||
import qs.Modals.Settings
|
import qs.Modals.Settings
|
||||||
import qs.Modals.Spotlight
|
import qs.Modals.DankLauncherV2
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Modules.AppDrawer
|
import qs.Modules.AppDrawer
|
||||||
import qs.Modules.DankDash
|
import qs.Modules.DankDash
|
||||||
@@ -203,6 +203,8 @@ Item {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
|
// Force PolkitService singleton to initialize
|
||||||
|
PolkitService.polkitAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -315,19 +317,44 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WifiPasswordModal {
|
LazyLoader {
|
||||||
id: wifiPasswordModal
|
id: wifiPasswordModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
PopoutService.wifiPasswordModal = wifiPasswordModal;
|
PopoutService.wifiPasswordModalLoader = wifiPasswordModalLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
WifiPasswordModal {
|
||||||
|
id: wifiPasswordModalItem
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PopoutService.wifiPasswordModal = wifiPasswordModalItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PolkitAuthModal {
|
LazyLoader {
|
||||||
id: polkitAuthModal
|
id: polkitAuthModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
PolkitAuthModal {
|
||||||
PopoutService.polkitAuthModal = polkitAuthModal;
|
id: polkitAuthModal
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PopoutService.polkitAuthModal = polkitAuthModal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: PolkitService.agent
|
||||||
|
enabled: PolkitService.polkitAvailable
|
||||||
|
|
||||||
|
function onAuthenticationRequestStarted() {
|
||||||
|
polkitAuthModalLoader.active = true;
|
||||||
|
if (polkitAuthModalLoader.item)
|
||||||
|
polkitAuthModalLoader.item.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,17 +376,21 @@ Item {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timeSinceLastPrompt = now - lastCredentialsTime;
|
const timeSinceLastPrompt = now - lastCredentialsTime;
|
||||||
|
|
||||||
if (wifiPasswordModal.visible && timeSinceLastPrompt < 1000) {
|
wifiPasswordModalLoader.active = true;
|
||||||
|
if (!wifiPasswordModalLoader.item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (wifiPasswordModalLoader.item.visible && timeSinceLastPrompt < 1000) {
|
||||||
NetworkService.cancelCredentials(lastCredentialsToken);
|
NetworkService.cancelCredentials(lastCredentialsToken);
|
||||||
lastCredentialsToken = token;
|
lastCredentialsToken = token;
|
||||||
lastCredentialsTime = now;
|
lastCredentialsTime = now;
|
||||||
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCredentialsToken = token;
|
lastCredentialsToken = token;
|
||||||
lastCredentialsTime = now;
|
lastCredentialsTime = now;
|
||||||
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,11 +508,22 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpotlightModal {
|
LazyLoader {
|
||||||
id: spotlightModal
|
id: dankLauncherV2ModalLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
PopoutService.spotlightModal = spotlightModal;
|
PopoutService.dankLauncherV2ModalLoader = dankLauncherV2ModalLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
DankLauncherV2Modal {
|
||||||
|
id: dankLauncherV2Modal
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PopoutService.dankLauncherV2Modal = dankLauncherV2Modal;
|
||||||
|
PopoutService._onDankLauncherV2ModalLoaded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,8 +132,11 @@ Item {
|
|||||||
case "media":
|
case "media":
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
||||||
break;
|
break;
|
||||||
|
case "wallpaper":
|
||||||
|
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
||||||
|
break;
|
||||||
case "weather":
|
case "weather":
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0;
|
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
||||||
@@ -797,11 +800,9 @@ Item {
|
|||||||
const modal = PopoutService.settingsModal;
|
const modal = PopoutService.settingsModal;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
if (type === "wallpaper") {
|
if (type === "wallpaper") {
|
||||||
modal.wallpaperBrowser.allowStacking = false;
|
modal.openWallpaperBrowser(false);
|
||||||
modal.wallpaperBrowser.open();
|
|
||||||
} else if (type === "profile") {
|
} else if (type === "profile") {
|
||||||
modal.profileBrowser.allowStacking = false;
|
modal.openProfileBrowser(false);
|
||||||
modal.profileBrowser.open();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PopoutService.openSettings();
|
PopoutService.openSettings();
|
||||||
@@ -965,6 +966,17 @@ Item {
|
|||||||
return success ? `PLUGIN_DISABLE_SUCCESS: ${pluginId}` : `PLUGIN_DISABLE_FAILED: ${pluginId}`;
|
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 {
|
function list(): string {
|
||||||
const plugins = PluginService.getAvailablePlugins();
|
const plugins = PluginService.getAvailablePlugins();
|
||||||
if (plugins.length === 0)
|
if (plugins.length === 0)
|
||||||
@@ -1013,6 +1025,94 @@ Item {
|
|||||||
target: "clipboard"
|
target: "clipboard"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! spotlight and launcher should be synonymous for backwards compat
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
PopoutService.openDankLauncherV2();
|
||||||
|
return "LAUNCHER_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
PopoutService.closeDankLauncherV2();
|
||||||
|
return "LAUNCHER_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
PopoutService.toggleDankLauncherV2();
|
||||||
|
return "LAUNCHER_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWith(mode: string): string {
|
||||||
|
if (!mode)
|
||||||
|
return "LAUNCHER_OPEN_FAILED: No mode specified";
|
||||||
|
PopoutService.openDankLauncherV2WithMode(mode);
|
||||||
|
return `LAUNCHER_OPEN_SUCCESS: ${mode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWith(mode: string): string {
|
||||||
|
if (!mode)
|
||||||
|
return "LAUNCHER_TOGGLE_FAILED: No mode specified";
|
||||||
|
PopoutService.toggleDankLauncherV2WithMode(mode);
|
||||||
|
return `LAUNCHER_TOGGLE_SUCCESS: ${mode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQuery(query: string): string {
|
||||||
|
PopoutService.openDankLauncherV2WithQuery(query);
|
||||||
|
return "LAUNCHER_OPEN_QUERY_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleQuery(query: string): string {
|
||||||
|
PopoutService.toggleDankLauncherV2();
|
||||||
|
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "launcher"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ! spotlight and launcher should be synonymous for backwards compat
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
PopoutService.openDankLauncherV2();
|
||||||
|
return "SPOTLIGHT_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
PopoutService.closeDankLauncherV2();
|
||||||
|
return "SPOTLIGHT_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
PopoutService.toggleDankLauncherV2();
|
||||||
|
return "SPOTLIGHT_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWith(mode: string): string {
|
||||||
|
if (!mode)
|
||||||
|
return "SPOTLIGHT_OPEN_FAILED: No mode specified";
|
||||||
|
PopoutService.openDankLauncherV2WithMode(mode);
|
||||||
|
return `SPOTLIGHT_OPEN_SUCCESS: ${mode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWith(mode: string): string {
|
||||||
|
if (!mode)
|
||||||
|
return "SPOTLIGHT_TOGGLE_FAILED: No mode specified";
|
||||||
|
PopoutService.toggleDankLauncherV2WithMode(mode);
|
||||||
|
return `SPOTLIGHT_TOGGLE_SUCCESS: ${mode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQuery(query: string): string {
|
||||||
|
PopoutService.openDankLauncherV2WithQuery(query);
|
||||||
|
return "SPOTLIGHT_OPEN_QUERY_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleQuery(query: string): string {
|
||||||
|
PopoutService.toggleDankLauncherV2();
|
||||||
|
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "spotlight"
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
FirstLaunchService.showWelcome();
|
FirstLaunchService.showWelcome();
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ FloatingWindow {
|
|||||||
iconName: "open_in_new"
|
iconName: "open_in_new"
|
||||||
backgroundColor: Theme.surfaceContainerHighest
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1.2-release")
|
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-2-release")
|
||||||
}
|
}
|
||||||
|
|
||||||
DankButton {
|
DankButton {
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ Singleton {
|
|||||||
readonly property int viewportBuffer: 100
|
readonly property int viewportBuffer: 100
|
||||||
readonly property int extendedBuffer: 200
|
readonly property int extendedBuffer: 200
|
||||||
readonly property int keyboardHintsHeight: 80
|
readonly property int keyboardHintsHeight: 80
|
||||||
readonly property int headerHeight: 40
|
readonly property int headerHeight: 32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ Item {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingL
|
anchors.margins: Theme.spacingM
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingM
|
||||||
focus: false
|
focus: false
|
||||||
|
|
||||||
ClipboardHeader {
|
ClipboardHeader {
|
||||||
@@ -25,7 +25,10 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
totalCount: modal.totalCount
|
totalCount: modal.totalCount
|
||||||
showKeyboardHints: modal.showKeyboardHints
|
showKeyboardHints: modal.showKeyboardHints
|
||||||
|
activeTab: modal.activeTab
|
||||||
|
pinnedCount: modal.pinnedCount
|
||||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
|
onTabChanged: tabName => modal.activeTab = tabName
|
||||||
onClearAllClicked: {
|
onClearAllClicked: {
|
||||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
||||||
modal.clearAll();
|
modal.clearAll();
|
||||||
@@ -70,18 +73,20 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - ClipboardConstants.headerHeight - 70
|
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
// Recents Tab
|
||||||
DankListView {
|
DankListView {
|
||||||
id: clipboardListView
|
id: clipboardListView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: clipboardContent.modal.clipboardEntries
|
values: clipboardContent.modal.unpinnedEntries
|
||||||
objectProp: "id"
|
objectProp: "id"
|
||||||
}
|
}
|
||||||
|
visible: modal.activeTab === "recents"
|
||||||
|
|
||||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
@@ -114,11 +119,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("No clipboard entries found")
|
text: I18n.tr("No recent clipboard entries found")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: clipboardContent.modal.clipboardEntries.length === 0
|
visible: clipboardContent.modal.unpinnedEntries.length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: ClipboardEntry {
|
delegate: ClipboardEntry {
|
||||||
@@ -135,13 +140,62 @@ Item {
|
|||||||
listView: clipboardListView
|
listView: clipboardListView
|
||||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||||
onDeleteRequested: clipboardContent.modal.deleteEntry(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 {
|
Item {
|
||||||
|
id: keyboardHintsContainer
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingM : 0
|
||||||
|
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -156,7 +210,7 @@ Item {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: Theme.spacingL
|
anchors.margins: Theme.spacingM
|
||||||
visible: modal.showKeyboardHints
|
visible: modal.showKeyboardHints
|
||||||
wtypeAvailable: modal.wtypeAvailable
|
wtypeAvailable: modal.wtypeAvailable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Rectangle {
|
|||||||
|
|
||||||
signal copyRequested
|
signal copyRequested
|
||||||
signal deleteRequested
|
signal deleteRequested
|
||||||
|
signal pinRequested
|
||||||
|
signal unpinRequested
|
||||||
|
|
||||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||||
@@ -50,7 +52,7 @@ Rectangle {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width - 68
|
width: parent.width - 110
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
ClipboardThumbnail {
|
ClipboardThumbnail {
|
||||||
@@ -100,20 +102,32 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
Row {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "close"
|
spacing: Theme.spacingXS
|
||||||
iconSize: Theme.iconSize - 6
|
|
||||||
iconColor: Theme.surfaceText
|
DankActionButton {
|
||||||
onClicked: deleteRequested()
|
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 {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.rightMargin: 40
|
anchors.rightMargin: 80
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: copyRequested()
|
onClicked: copyRequested()
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ Item {
|
|||||||
|
|
||||||
property int totalCount: 0
|
property int totalCount: 0
|
||||||
property bool showKeyboardHints: false
|
property bool showKeyboardHints: false
|
||||||
|
property string activeTab: "recents"
|
||||||
|
property int pinnedCount: 0
|
||||||
|
|
||||||
signal keyboardHintsToggled
|
signal keyboardHintsToggled
|
||||||
signal clearAllClicked
|
signal clearAllClicked
|
||||||
signal closeClicked
|
signal closeClicked
|
||||||
|
signal tabChanged(string tabName)
|
||||||
|
|
||||||
height: ClipboardConstants.headerHeight
|
height: ClipboardConstants.headerHeight
|
||||||
|
|
||||||
@@ -41,10 +44,28 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "push_pin"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
|
||||||
|
visible: header.pinnedCount > 0
|
||||||
|
tooltipText: I18n.tr("Saved")
|
||||||
|
onClicked: tabChanged("saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "history"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
|
||||||
|
tooltipText: I18n.tr("History")
|
||||||
|
onClicked: tabChanged("recents")
|
||||||
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "info"
|
iconName: "info"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
||||||
|
tooltipText: I18n.tr("Keyboard Shortcuts")
|
||||||
onClicked: keyboardHintsToggled()
|
onClicked: keyboardHintsToggled()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +73,7 @@ Item {
|
|||||||
iconName: "delete_sweep"
|
iconName: "delete_sweep"
|
||||||
iconSize: Theme.iconSize
|
iconSize: Theme.iconSize
|
||||||
iconColor: Theme.surfaceText
|
iconColor: Theme.surfaceText
|
||||||
|
tooltipText: I18n.tr("Clear All")
|
||||||
onClicked: clearAllClicked()
|
onClicked: clearAllClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ DankModal {
|
|||||||
|
|
||||||
property int totalCount: 0
|
property int totalCount: 0
|
||||||
property var clipboardEntries: []
|
property var clipboardEntries: []
|
||||||
|
property var pinnedEntries: []
|
||||||
|
property int pinnedCount: 0
|
||||||
property string searchText: ""
|
property string searchText: ""
|
||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
property bool keyboardNavigationActive: false
|
property bool keyboardNavigationActive: false
|
||||||
@@ -74,22 +76,36 @@ DankModal {
|
|||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
const query = searchText.trim();
|
const query = searchText.trim();
|
||||||
|
let filtered = [];
|
||||||
|
|
||||||
if (query.length === 0) {
|
if (query.length === 0) {
|
||||||
clipboardEntries = internalEntries;
|
filtered = internalEntries;
|
||||||
} else {
|
} else {
|
||||||
const lowerQuery = query.toLowerCase();
|
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;
|
totalCount = clipboardEntries.length;
|
||||||
if (clipboardEntries.length === 0) {
|
if (unpinnedEntries.length === 0) {
|
||||||
keyboardNavigationActive = false;
|
keyboardNavigationActive = false;
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
} else if (selectedIndex >= clipboardEntries.length) {
|
} else if (selectedIndex >= unpinnedEntries.length) {
|
||||||
selectedIndex = clipboardEntries.length - 1;
|
selectedIndex = unpinnedEntries.length - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var internalEntries: []
|
property var internalEntries: []
|
||||||
|
property var unpinnedEntries: []
|
||||||
|
property string activeTab: "recents"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
@@ -135,6 +151,10 @@ DankModal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
internalEntries = response.result || [];
|
internalEntries = response.result || [];
|
||||||
|
|
||||||
|
pinnedEntries = internalEntries.filter(e => e.pinned);
|
||||||
|
pinnedCount = pinnedEntries.length;
|
||||||
|
|
||||||
updateFilteredModel();
|
updateFilteredModel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,18 +191,79 @@ DankModal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll() {
|
function deletePinnedEntry(entry) {
|
||||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
clearConfirmDialog.show(I18n.tr("Delete Saved Item?"), I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."), function () {
|
||||||
if (response.error) {
|
DMSService.sendRequest("clipboard.deleteEntry", {
|
||||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
"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;
|
return;
|
||||||
}
|
}
|
||||||
internalEntries = [];
|
|
||||||
clipboardEntries = [];
|
const maxPinned = 25; // TODO: Get from config
|
||||||
totalCount = 0;
|
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) {
|
function getEntryPreview(entry) {
|
||||||
return entry.preview || "";
|
return entry.preview || "";
|
||||||
}
|
}
|
||||||
|
|||||||
231
quickshell/Modals/DankLauncherV2/ActionPanel.qml
Normal file
231
quickshell/Modals/DankLauncherV2/ActionPanel.qml
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var selectedItem: null
|
||||||
|
property var controller: null
|
||||||
|
property bool expanded: false
|
||||||
|
property int selectedActionIndex: 0
|
||||||
|
|
||||||
|
function getPluginContextMenuActions() {
|
||||||
|
if (selectedItem?.type !== "plugin" || !selectedItem?.pluginId)
|
||||||
|
return [];
|
||||||
|
var instance = PluginService.pluginInstances[selectedItem.pluginId];
|
||||||
|
if (!instance)
|
||||||
|
return [];
|
||||||
|
if (typeof instance.getContextMenuActions !== "function")
|
||||||
|
return [];
|
||||||
|
var actions = instance.getContextMenuActions(selectedItem.data);
|
||||||
|
if (!Array.isArray(actions))
|
||||||
|
return [];
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var actions: {
|
||||||
|
var result = [];
|
||||||
|
if (selectedItem?.primaryAction) {
|
||||||
|
result.push(selectedItem.primaryAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedItem?.type === "plugin") {
|
||||||
|
var pluginActions = getPluginContextMenuActions();
|
||||||
|
for (var i = 0; i < pluginActions.length; i++) {
|
||||||
|
var act = pluginActions[i];
|
||||||
|
result.push({
|
||||||
|
name: act.text || act.name || "",
|
||||||
|
icon: act.icon || "play_arrow",
|
||||||
|
action: "plugin_action",
|
||||||
|
pluginAction: act.action
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (selectedItem?.type === "app" && !selectedItem?.isCore) {
|
||||||
|
if (selectedItem?.actions) {
|
||||||
|
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||||
|
result.push(selectedItem.actions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool hasActions: {
|
||||||
|
if (selectedItem?.type === "app" && !selectedItem?.isCore)
|
||||||
|
return true;
|
||||||
|
if (selectedItem?.type === "plugin") {
|
||||||
|
var pluginActions = getPluginContextMenuActions();
|
||||||
|
return pluginActions.length > 0;
|
||||||
|
}
|
||||||
|
return actions.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent?.width ?? 200
|
||||||
|
height: expanded && hasActions ? 52 : 0
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: actionsFlickable
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: tabHint.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
height: parent.height
|
||||||
|
contentWidth: actionsRow.width
|
||||||
|
contentHeight: height
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
flickableDirection: Flickable.HorizontalFlick
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actionsRow
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.actions
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: actionButton
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: actionContent.implicitWidth + Theme.spacingM * 2
|
||||||
|
height: actionsRow.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: index === root.selectedActionIndex ? Theme.primaryHover : actionArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actionContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
name: actionButton.modelData?.icon ?? "play_arrow"
|
||||||
|
size: 16
|
||||||
|
color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: actionButton.modelData?.name ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: actionArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller && root.selectedItem) {
|
||||||
|
root.controller.executeAction(root.selectedItem, actionButton.modelData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onEntered: root.selectedActionIndex = actionButton.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: tabHint
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: root.hasActions
|
||||||
|
text: "Tab"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Theme.outlineButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
expanded = !expanded;
|
||||||
|
selectedActionIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
expanded = true;
|
||||||
|
selectedActionIndex = actions.length > 1 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
expanded = false;
|
||||||
|
selectedActionIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleAction() {
|
||||||
|
if (actions.length > 0) {
|
||||||
|
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||||
|
ensureSelectedVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSelectedVisible() {
|
||||||
|
if (selectedActionIndex < 0 || !actionsRow.children || selectedActionIndex >= actionsRow.children.length)
|
||||||
|
return;
|
||||||
|
var buttonX = 0;
|
||||||
|
for (var i = 0; i < selectedActionIndex; i++) {
|
||||||
|
var child = actionsRow.children[i];
|
||||||
|
if (child)
|
||||||
|
buttonX += child.width + actionsRow.spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var button = actionsRow.children[selectedActionIndex];
|
||||||
|
if (!button)
|
||||||
|
return;
|
||||||
|
var buttonRight = buttonX + button.width;
|
||||||
|
var viewLeft = actionsFlickable.contentX;
|
||||||
|
var viewRight = viewLeft + actionsFlickable.width;
|
||||||
|
|
||||||
|
if (buttonX < viewLeft) {
|
||||||
|
actionsFlickable.contentX = Math.max(0, buttonX - Theme.spacingS);
|
||||||
|
} else if (buttonRight > viewRight) {
|
||||||
|
actionsFlickable.contentX = Math.min(actionsFlickable.contentWidth - actionsFlickable.width, buttonRight - actionsFlickable.width + Theme.spacingS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeSelectedAction() {
|
||||||
|
if (!controller || !selectedItem || selectedActionIndex >= actions.length)
|
||||||
|
return;
|
||||||
|
var action = actions[selectedActionIndex];
|
||||||
|
if (action.action === "plugin_action" && typeof action.pluginAction === "function") {
|
||||||
|
action.pluginAction();
|
||||||
|
controller.performSearch();
|
||||||
|
controller.itemExecuted();
|
||||||
|
} else {
|
||||||
|
controller.executeAction(selectedItem, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1769
quickshell/Modals/DankLauncherV2/Controller.qml
Normal file
1769
quickshell/Modals/DankLauncherV2/Controller.qml
Normal file
File diff suppressed because it is too large
Load Diff
361
quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
Normal file
361
quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
property bool spotlightOpen: false
|
||||||
|
property bool keyboardActive: false
|
||||||
|
property bool contentVisible: false
|
||||||
|
property alias spotlightContent: launcherContent
|
||||||
|
property bool openedFromOverview: false
|
||||||
|
property bool isClosing: false
|
||||||
|
property bool _windowEnabled: true
|
||||||
|
|
||||||
|
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
|
readonly property var effectiveScreen: launcherWindow.screen
|
||||||
|
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||||
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
|
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||||
|
|
||||||
|
readonly property int baseWidth: SettingsData.dankLauncherV2Size === "medium" ? 720 : SettingsData.dankLauncherV2Size === "large" ? 860 : 620
|
||||||
|
readonly property int baseHeight: SettingsData.dankLauncherV2Size === "medium" ? 720 : SettingsData.dankLauncherV2Size === "large" ? 860 : 600
|
||||||
|
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
|
||||||
|
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
|
||||||
|
readonly property real modalX: (screenWidth - modalWidth) / 2
|
||||||
|
readonly property real modalY: (screenHeight - modalHeight) / 2
|
||||||
|
|
||||||
|
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
readonly property real cornerRadius: Theme.cornerRadius
|
||||||
|
readonly property color borderColor: {
|
||||||
|
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||||
|
return Theme.outlineMedium;
|
||||||
|
switch (SettingsData.dankLauncherV2BorderColor) {
|
||||||
|
case "primary":
|
||||||
|
return Theme.primary;
|
||||||
|
case "secondary":
|
||||||
|
return Theme.secondary;
|
||||||
|
case "outline":
|
||||||
|
return Theme.outline;
|
||||||
|
case "surfaceText":
|
||||||
|
return Theme.surfaceText;
|
||||||
|
default:
|
||||||
|
return Theme.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 1
|
||||||
|
|
||||||
|
signal dialogClosed
|
||||||
|
|
||||||
|
function _initializeAndShow(query, mode) {
|
||||||
|
contentVisible = true;
|
||||||
|
spotlightContent.searchField.forceActiveFocus();
|
||||||
|
|
||||||
|
if (spotlightContent.searchField) {
|
||||||
|
spotlightContent.searchField.text = query;
|
||||||
|
}
|
||||||
|
if (spotlightContent.controller) {
|
||||||
|
var targetMode = mode || "all";
|
||||||
|
spotlightContent.controller.searchMode = targetMode;
|
||||||
|
spotlightContent.controller.activePluginId = "";
|
||||||
|
spotlightContent.controller.activePluginName = "";
|
||||||
|
spotlightContent.controller.pluginFilter = "";
|
||||||
|
spotlightContent.controller.collapsedSections = {};
|
||||||
|
if (query) {
|
||||||
|
spotlightContent.controller.setSearchQuery(query);
|
||||||
|
} else {
|
||||||
|
spotlightContent.controller.searchQuery = "";
|
||||||
|
spotlightContent.controller.performSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spotlightContent.resetScroll) {
|
||||||
|
spotlightContent.resetScroll();
|
||||||
|
}
|
||||||
|
if (spotlightContent.actionPanel) {
|
||||||
|
spotlightContent.actionPanel.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
closeCleanupTimer.stop();
|
||||||
|
isClosing = false;
|
||||||
|
openedFromOverview = false;
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen)
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
|
||||||
|
spotlightOpen = true;
|
||||||
|
keyboardActive = true;
|
||||||
|
ModalManager.openModal(root);
|
||||||
|
if (useHyprlandFocusGrab)
|
||||||
|
focusGrab.active = true;
|
||||||
|
|
||||||
|
_initializeAndShow("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWithQuery(query) {
|
||||||
|
closeCleanupTimer.stop();
|
||||||
|
isClosing = false;
|
||||||
|
openedFromOverview = false;
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen)
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
|
||||||
|
spotlightOpen = true;
|
||||||
|
keyboardActive = true;
|
||||||
|
ModalManager.openModal(root);
|
||||||
|
if (useHyprlandFocusGrab)
|
||||||
|
focusGrab.active = true;
|
||||||
|
|
||||||
|
_initializeAndShow(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (!spotlightOpen)
|
||||||
|
return;
|
||||||
|
openedFromOverview = false;
|
||||||
|
isClosing = true;
|
||||||
|
contentVisible = false;
|
||||||
|
|
||||||
|
keyboardActive = false;
|
||||||
|
spotlightOpen = false;
|
||||||
|
focusGrab.active = false;
|
||||||
|
ModalManager.closeModal(root);
|
||||||
|
|
||||||
|
closeCleanupTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
spotlightOpen ? hide() : show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWithMode(mode) {
|
||||||
|
closeCleanupTimer.stop();
|
||||||
|
isClosing = false;
|
||||||
|
openedFromOverview = false;
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen)
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
|
||||||
|
spotlightOpen = true;
|
||||||
|
keyboardActive = true;
|
||||||
|
ModalManager.openModal(root);
|
||||||
|
if (useHyprlandFocusGrab)
|
||||||
|
focusGrab.active = true;
|
||||||
|
|
||||||
|
_initializeAndShow("", mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWithMode(mode) {
|
||||||
|
if (spotlightOpen) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
showWithMode(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: closeCleanupTimer
|
||||||
|
interval: Theme.expressiveDurations.expressiveFastSpatial + 50
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
isClosing = false;
|
||||||
|
dialogClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: focusGrab
|
||||||
|
windows: [launcherWindow]
|
||||||
|
active: false
|
||||||
|
|
||||||
|
onCleared: {
|
||||||
|
if (spotlightOpen) {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ModalManager
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== root && spotlightOpen) {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Quickshell
|
||||||
|
function onScreensChanged() {
|
||||||
|
if (Quickshell.screens.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const screen = launcherWindow.screen;
|
||||||
|
const screenName = screen?.name;
|
||||||
|
|
||||||
|
let needsReset = !screen || !screenName;
|
||||||
|
if (!needsReset) {
|
||||||
|
needsReset = true;
|
||||||
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||||
|
if (Quickshell.screens[i].name === screenName) {
|
||||||
|
needsReset = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsReset)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||||
|
if (!newScreen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
root._windowEnabled = false;
|
||||||
|
launcherWindow.screen = newScreen;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root._windowEnabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: launcherWindow
|
||||||
|
visible: root._windowEnabled
|
||||||
|
color: "transparent"
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:launcher"
|
||||||
|
WlrLayershell.layer: {
|
||||||
|
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||||
|
case "bottom":
|
||||||
|
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||||
|
return WlrLayershell.Top;
|
||||||
|
case "background":
|
||||||
|
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||||
|
return WlrLayershell.Top;
|
||||||
|
case "overlay":
|
||||||
|
return WlrLayershell.Overlay;
|
||||||
|
default:
|
||||||
|
return WlrLayershell.Top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: spotlightOpen ? fullScreenMask : null
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: fullScreenMask
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: backgroundDarken
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||||
|
visible: contentVisible || opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.expressiveDurations.expressiveFastSpatial
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: spotlightOpen
|
||||||
|
onClicked: mouse => {
|
||||||
|
var contentX = modalContainer.x;
|
||||||
|
var contentY = modalContainer.y;
|
||||||
|
var contentW = modalContainer.width;
|
||||||
|
var contentH = modalContainer.height;
|
||||||
|
|
||||||
|
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: modalContainer
|
||||||
|
x: root.modalX
|
||||||
|
y: root.modalY
|
||||||
|
width: root.modalWidth
|
||||||
|
height: root.modalHeight
|
||||||
|
visible: contentVisible || opacity > 0
|
||||||
|
|
||||||
|
opacity: contentVisible ? 1 : 0
|
||||||
|
scale: contentVisible ? 1 : 0.96
|
||||||
|
transformOrigin: Item.Center
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.expressiveDurations.fast
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.expressiveDurations.fast
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: root.backgroundColor
|
||||||
|
borderColor: root.borderColor
|
||||||
|
borderWidth: root.borderWidth
|
||||||
|
radius: root.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onPressed: mouse => mouse.accepted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: keyboardActive
|
||||||
|
|
||||||
|
LauncherContent {
|
||||||
|
id: launcherContent
|
||||||
|
anchors.fill: parent
|
||||||
|
parentModal: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
root.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
quickshell/Modals/DankLauncherV2/GridItem.qml
Normal file
93
quickshell/Modals/DankLauncherV2/GridItem.qml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var item: null
|
||||||
|
property bool isSelected: false
|
||||||
|
property bool isHovered: itemArea.containsMouse
|
||||||
|
property var controller: null
|
||||||
|
property int flatIndex: -1
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
signal rightClicked(real mouseX, real mouseY)
|
||||||
|
|
||||||
|
readonly property string iconValue: {
|
||||||
|
if (!item)
|
||||||
|
return "";
|
||||||
|
switch (item.iconType) {
|
||||||
|
case "material":
|
||||||
|
case "nerd":
|
||||||
|
return "material:" + (item.icon || "apps");
|
||||||
|
case "unicode":
|
||||||
|
return "unicode:" + (item.icon || "");
|
||||||
|
case "composite":
|
||||||
|
return item.iconFull || "";
|
||||||
|
case "image":
|
||||||
|
default:
|
||||||
|
return item.icon || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
width: parent.width - Theme.spacingM
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
width: root.computedIconSize
|
||||||
|
height: root.computedIconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
iconValue: root.iconValue
|
||||||
|
iconSize: root.computedIconSize
|
||||||
|
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
|
||||||
|
iconColor: root.isSelected ? Theme.primary : Theme.surfaceText
|
||||||
|
materialIconSizeAdjustment: root.computedIconSize * 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.item?.name ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: root.isSelected ? Theme.primary : Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||||
|
root.rightClicked(scenePos.x, scenePos.y);
|
||||||
|
} else {
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
if (root.controller) {
|
||||||
|
root.controller.keyboardNavigationActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
810
quickshell/Modals/DankLauncherV2/LauncherContent.qml
Normal file
810
quickshell/Modals/DankLauncherV2/LauncherContent.qml
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
|
property var parentModal: null
|
||||||
|
property string viewModeContext: "spotlight"
|
||||||
|
property alias searchField: searchField
|
||||||
|
property alias controller: controller
|
||||||
|
property alias resultsList: resultsList
|
||||||
|
property alias actionPanel: actionPanel
|
||||||
|
|
||||||
|
property bool editMode: false
|
||||||
|
property var editingApp: null
|
||||||
|
property string editAppId: ""
|
||||||
|
|
||||||
|
function resetScroll() {
|
||||||
|
resultsList.resetScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSearchField() {
|
||||||
|
searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditMode(app) {
|
||||||
|
if (!app)
|
||||||
|
return;
|
||||||
|
editingApp = app;
|
||||||
|
editAppId = app.id || app.execString || app.exec || "";
|
||||||
|
var 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() {
|
||||||
|
var 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContextMenu(item, x, y, fromKeyboard) {
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
if (item.isCore)
|
||||||
|
return;
|
||||||
|
if (!contextMenu.hasContextMenuActions(item))
|
||||||
|
return;
|
||||||
|
contextMenu.show(x, y, item, fromKeyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Controller {
|
||||||
|
id: controller
|
||||||
|
viewModeContext: root.viewModeContext
|
||||||
|
|
||||||
|
onItemExecuted: {
|
||||||
|
if (root.parentModal) {
|
||||||
|
root.parentModal.hide();
|
||||||
|
}
|
||||||
|
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
|
||||||
|
NiriService.toggleOverview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LauncherContextMenu {
|
||||||
|
id: contextMenu
|
||||||
|
parent: root
|
||||||
|
controller: root.controller
|
||||||
|
searchField: root.searchField
|
||||||
|
parentHandler: root
|
||||||
|
|
||||||
|
onEditAppRequested: app => {
|
||||||
|
root.openEditMode(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (editMode) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
closeEditMode();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasCtrl = event.modifiers & Qt.ControlModifier;
|
||||||
|
event.accepted = true;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
if (actionPanel.expanded) {
|
||||||
|
actionPanel.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (controller.clearPluginFilter())
|
||||||
|
return;
|
||||||
|
if (root.parentModal)
|
||||||
|
root.parentModal.hide();
|
||||||
|
return;
|
||||||
|
case Qt.Key_Backspace:
|
||||||
|
if (searchField.text.length === 0) {
|
||||||
|
if (controller.clearPluginFilter())
|
||||||
|
return;
|
||||||
|
if (controller.autoSwitchedToFiles) {
|
||||||
|
controller.restorePreviousMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
controller.selectNext();
|
||||||
|
return;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
controller.selectPrevious();
|
||||||
|
return;
|
||||||
|
case Qt.Key_PageDown:
|
||||||
|
controller.selectPageDown(8);
|
||||||
|
return;
|
||||||
|
case Qt.Key_PageUp:
|
||||||
|
controller.selectPageUp(8);
|
||||||
|
return;
|
||||||
|
case Qt.Key_Right:
|
||||||
|
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||||
|
controller.selectRight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Left:
|
||||||
|
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||||
|
controller.selectLeft();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_J:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.selectNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_K:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.selectPrevious();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_N:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.selectNextSection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_P:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.selectPreviousSection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (actionPanel.hasActions) {
|
||||||
|
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
if (actionPanel.expanded)
|
||||||
|
actionPanel.hide();
|
||||||
|
return;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
|
||||||
|
actionPanel.executeSelectedAction();
|
||||||
|
} else {
|
||||||
|
controller.executeSelected();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case Qt.Key_Menu:
|
||||||
|
case Qt.Key_F10:
|
||||||
|
if (contextMenu.hasContextMenuActions(controller.selectedItem)) {
|
||||||
|
var scenePos = resultsList.getSelectedItemPosition();
|
||||||
|
var localPos = root.mapFromItem(null, scenePos.x, scenePos.y);
|
||||||
|
showContextMenu(controller.selectedItem, localPos.x, localPos.y, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case Qt.Key_1:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.setMode("all");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_2:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.setMode("apps");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_3:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.setMode("files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_4:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.setMode("plugins");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Slash:
|
||||||
|
if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
|
||||||
|
controller.setMode("files", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !editMode
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: footerBar
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: SettingsData.dankLauncherV2ShowFooter ? 32 : 0
|
||||||
|
visible: SettingsData.dankLauncherV2ShowFooter
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: modeButtonsRow
|
||||||
|
x: I18n.isRtl ? parent.width - width - Theme.spacingS : Theme.spacingS
|
||||||
|
y: (parent.height - height) / 2
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{
|
||||||
|
id: "all",
|
||||||
|
label: I18n.tr("All"),
|
||||||
|
icon: "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "apps",
|
||||||
|
label: I18n.tr("Apps"),
|
||||||
|
icon: "apps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "files",
|
||||||
|
label: I18n.tr("Files"),
|
||||||
|
icon: "folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plugins",
|
||||||
|
label: I18n.tr("Plugins"),
|
||||||
|
icon: "extension"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: modeButtonMetrics.width + 14 + Theme.spacingXS + Theme.spacingM * 2 + Theme.spacingS
|
||||||
|
height: footerBar.height - 4
|
||||||
|
radius: Theme.cornerRadius - 2
|
||||||
|
color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: modeButtonMetrics
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
text: modelData.label
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: modelData.icon
|
||||||
|
size: 14
|
||||||
|
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.label
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: modeArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: controller.setMode(modelData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: hintsRow
|
||||||
|
x: I18n.isRtl ? Theme.spacingS : parent.width - width - Theme.spacingS
|
||||||
|
y: (parent.height - height) / 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "↑↓ " + I18n.tr("nav")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "↵ " + I18n.tr("open")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Tab " + I18n.tr("actions")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: actionPanel.hasActions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: footerBar.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: Theme.spacingXS
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
clip: false
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pluginBadge
|
||||||
|
visible: controller.activePluginName.length > 0
|
||||||
|
width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0
|
||||||
|
height: searchField.height
|
||||||
|
radius: 16
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: pluginBadgeContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
name: "extension"
|
||||||
|
size: 14
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: controller.activePluginName
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||||
|
placeholderText: ""
|
||||||
|
ignoreUpDownKeys: true
|
||||||
|
ignoreTabKeys: true
|
||||||
|
keyForwardTargets: [root]
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
controller.setSearchQuery(text);
|
||||||
|
if (text.length === 0) {
|
||||||
|
controller.restorePreviousMode();
|
||||||
|
}
|
||||||
|
if (actionPanel.expanded) {
|
||||||
|
actionPanel.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (root.parentModal) {
|
||||||
|
root.parentModal.hide();
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) {
|
||||||
|
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
|
||||||
|
actionPanel.executeSelectedAction();
|
||||||
|
} else {
|
||||||
|
controller.executeSelected();
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - searchField.height - actionPanel.height - Theme.spacingXS * 2
|
||||||
|
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||||
|
|
||||||
|
ResultsList {
|
||||||
|
id: resultsList
|
||||||
|
anchors.fill: parent
|
||||||
|
controller: root.controller
|
||||||
|
|
||||||
|
onItemRightClicked: (index, item, sceneX, sceneY) => {
|
||||||
|
if (item && contextMenu.hasContextMenuActions(item)) {
|
||||||
|
var localPos = root.mapFromItem(null, sceneX, sceneY);
|
||||||
|
root.showContextMenu(item, localPos.x, localPos.y, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionPanel {
|
||||||
|
id: actionPanel
|
||||||
|
width: parent.width
|
||||||
|
selectedItem: controller.selectedItem
|
||||||
|
controller: controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: controller
|
||||||
|
function onSelectedItemChanged() {
|
||||||
|
if (actionPanel.expanded && !actionPanel.hasActions) {
|
||||||
|
actionPanel.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onSearchQueryRequested(query) {
|
||||||
|
searchField.text = query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: editView
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
visible: editMode
|
||||||
|
focus: editMode
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
closeEditMode();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
saveAppOverride();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
|
||||||
|
saveAppOverride();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
484
quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
Normal file
484
quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var item: null
|
||||||
|
property var controller: null
|
||||||
|
property var searchField: null
|
||||||
|
property var parentHandler: null
|
||||||
|
|
||||||
|
signal hideRequested
|
||||||
|
signal editAppRequested(var app)
|
||||||
|
|
||||||
|
function hasContextMenuActions(spotlightItem) {
|
||||||
|
if (!spotlightItem)
|
||||||
|
return false;
|
||||||
|
if (spotlightItem.type === "app" && !spotlightItem.isCore)
|
||||||
|
return true;
|
||||||
|
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
||||||
|
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
||||||
|
if (!instance)
|
||||||
|
return false;
|
||||||
|
if (typeof instance.getContextMenuActions !== "function")
|
||||||
|
return false;
|
||||||
|
var actions = instance.getContextMenuActions(spotlightItem.data);
|
||||||
|
return Array.isArray(actions) && actions.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var desktopEntry: item?.data ?? null
|
||||||
|
readonly property string appId: desktopEntry?.id || desktopEntry?.execString || ""
|
||||||
|
readonly property bool isPinned: SessionData.isPinnedApp(appId)
|
||||||
|
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
|
||||||
|
readonly property bool isPluginItem: item?.type === "plugin"
|
||||||
|
|
||||||
|
function getPluginContextMenuActions() {
|
||||||
|
if (!isPluginItem || !item?.pluginId)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var instance = PluginService.pluginInstances[item.pluginId];
|
||||||
|
if (!instance)
|
||||||
|
return [];
|
||||||
|
if (typeof instance.getContextMenuActions !== "function")
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var actions = instance.getContextMenuActions(item.data);
|
||||||
|
if (!Array.isArray(actions))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executePluginAction(actionFunc) {
|
||||||
|
if (typeof actionFunc === "function") {
|
||||||
|
actionFunc();
|
||||||
|
}
|
||||||
|
controller?.performSearch();
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var menuItems: {
|
||||||
|
var items = [];
|
||||||
|
|
||||||
|
if (isPluginItem) {
|
||||||
|
var pluginActions = getPluginContextMenuActions();
|
||||||
|
for (var i = 0; i < pluginActions.length; i++) {
|
||||||
|
var act = pluginActions[i];
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: act.icon || "play_arrow",
|
||||||
|
text: act.text || act.name || "",
|
||||||
|
pluginAction: act.action
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!desktopEntry)
|
||||||
|
return items;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: isPinned ? "keep_off" : "push_pin",
|
||||||
|
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
||||||
|
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 (item?.actions && item.actions.length > 0) {
|
||||||
|
items.push({
|
||||||
|
type: "separator"
|
||||||
|
});
|
||||||
|
for (var i = 0; i < item.actions.length; i++) {
|
||||||
|
var act = item.actions[i];
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: act.icon || "play_arrow",
|
||||||
|
text: act.name || "",
|
||||||
|
actionData: act
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: "separator"
|
||||||
|
});
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: "launch",
|
||||||
|
text: I18n.tr("Launch"),
|
||||||
|
action: launchApp
|
||||||
|
});
|
||||||
|
|
||||||
|
if (SessionService.nvidiaCommand) {
|
||||||
|
items.push({
|
||||||
|
type: "separator"
|
||||||
|
});
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: "memory",
|
||||||
|
text: I18n.tr("Launch on dGPU"),
|
||||||
|
action: launchWithNvidia
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(x, y, spotlightItem, fromKeyboard) {
|
||||||
|
if (!spotlightItem?.data)
|
||||||
|
return;
|
||||||
|
item = spotlightItem;
|
||||||
|
selectedMenuIndex = fromKeyboard ? 0 : -1;
|
||||||
|
keyboardNavigation = fromKeyboard;
|
||||||
|
|
||||||
|
if (parentHandler)
|
||||||
|
parentHandler.enabled = false;
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
var parentW = parent?.width ?? 500;
|
||||||
|
var parentH = parent?.height ?? 600;
|
||||||
|
var menuW = width > 0 ? width : 200;
|
||||||
|
var menuH = height > 0 ? height : 200;
|
||||||
|
var margin = 8;
|
||||||
|
|
||||||
|
var posX = x + 4;
|
||||||
|
var posY = y + 4;
|
||||||
|
|
||||||
|
if (posX + menuW > parentW - margin) {
|
||||||
|
posX = Math.max(margin, parentW - menuW - margin);
|
||||||
|
}
|
||||||
|
if (posY + menuH > parentH - margin) {
|
||||||
|
posY = Math.max(margin, parentH - menuH - margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.x = posX;
|
||||||
|
root.y = posY;
|
||||||
|
open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (parentHandler)
|
||||||
|
parentHandler.enabled = true;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePin() {
|
||||||
|
if (!appId)
|
||||||
|
return;
|
||||||
|
if (isPinned)
|
||||||
|
SessionData.removePinnedApp(appId);
|
||||||
|
else
|
||||||
|
SessionData.addPinnedApp(appId);
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideCurrentApp() {
|
||||||
|
if (!appId)
|
||||||
|
return;
|
||||||
|
SessionData.hideApp(appId);
|
||||||
|
controller?.performSearch();
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editCurrentApp() {
|
||||||
|
if (!desktopEntry)
|
||||||
|
return;
|
||||||
|
editAppRequested(desktopEntry);
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchApp() {
|
||||||
|
if (!desktopEntry)
|
||||||
|
return;
|
||||||
|
SessionService.launchDesktopEntry(desktopEntry);
|
||||||
|
AppUsageHistoryData.addAppUsage(desktopEntry);
|
||||||
|
controller?.itemExecuted();
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchWithNvidia() {
|
||||||
|
if (!desktopEntry)
|
||||||
|
return;
|
||||||
|
SessionService.launchDesktopEntry(desktopEntry, true);
|
||||||
|
AppUsageHistoryData.addAppUsage(desktopEntry);
|
||||||
|
controller?.itemExecuted();
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeDesktopAction(actionData) {
|
||||||
|
if (!desktopEntry || !actionData)
|
||||||
|
return;
|
||||||
|
SessionService.launchDesktopAction(desktopEntry, actionData.actionData || actionData);
|
||||||
|
AppUsageHistoryData.addAppUsage(desktopEntry);
|
||||||
|
controller?.itemExecuted();
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
property int selectedMenuIndex: 0
|
||||||
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
|
readonly property int visibleItemCount: {
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i < menuItems.length; i++) {
|
||||||
|
if (menuItems[i].type === "item")
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (visibleItemCount > 0)
|
||||||
|
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
if (visibleItemCount > 0)
|
||||||
|
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSelected() {
|
||||||
|
var itemIndex = 0;
|
||||||
|
for (var i = 0; i < menuItems.length; i++) {
|
||||||
|
if (menuItems[i].type !== "item")
|
||||||
|
continue;
|
||||||
|
if (itemIndex === selectedMenuIndex) {
|
||||||
|
var menuItem = menuItems[i];
|
||||||
|
if (menuItem.action)
|
||||||
|
menuItem.action();
|
||||||
|
else if (menuItem.pluginAction)
|
||||||
|
executePluginAction(menuItem.pluginAction);
|
||||||
|
else if (menuItem.actionData)
|
||||||
|
executeDesktopAction(menuItem.actionData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: menuContainer.implicitWidth
|
||||||
|
height: menuContainer.implicitHeight
|
||||||
|
padding: 0
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
modal: true
|
||||||
|
dim: false
|
||||||
|
background: Item {}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
Qt.callLater(() => keyboardHandler.forceActiveFocus());
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
if (parentHandler)
|
||||||
|
parentHandler.enabled = true;
|
||||||
|
if (searchField?.visible) {
|
||||||
|
Qt.callLater(() => searchField.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
id: keyboardHandler
|
||||||
|
focus: true
|
||||||
|
implicitWidth: menuContainer.implicitWidth
|
||||||
|
implicitHeight: menuContainer.implicitHeight
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Down:
|
||||||
|
root.selectNext();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
root.selectPrevious();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
root.activateSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
case Qt.Key_Left:
|
||||||
|
root.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: menuContainer
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||||
|
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.menuItems
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: menuItemDelegate
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: menuColumn.width
|
||||||
|
height: modelData.type === "separator" ? 5 : 32
|
||||||
|
|
||||||
|
readonly property int itemIndex: {
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i < index; i++) {
|
||||||
|
if (root.menuItems[i].type === "item")
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: menuItemDelegate.modelData.type === "separator"
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: parent.height
|
||||||
|
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: menuItemDelegate.modelData.type === "item"
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
|
||||||
|
}
|
||||||
|
return itemMouseArea.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.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Theme.iconSize - 2
|
||||||
|
height: Theme.iconSize - 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
|
||||||
|
name: menuItemDelegate.modelData?.icon ?? ""
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: menuItemDelegate.modelData.text || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: {
|
||||||
|
root.keyboardNavigation = false;
|
||||||
|
root.selectedMenuIndex = menuItemDelegate.itemIndex;
|
||||||
|
}
|
||||||
|
onClicked: {
|
||||||
|
var menuItem = menuItemDelegate.modelData;
|
||||||
|
if (menuItem.action)
|
||||||
|
menuItem.action();
|
||||||
|
else if (menuItem.pluginAction)
|
||||||
|
root.executePluginAction(menuItem.pluginAction);
|
||||||
|
else if (menuItem.actionData)
|
||||||
|
root.executeDesktopAction(menuItem.actionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
quickshell/Modals/DankLauncherV2/ResultItem.qml
Normal file
142
quickshell/Modals/DankLauncherV2/ResultItem.qml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var item: null
|
||||||
|
property bool isSelected: false
|
||||||
|
property bool isHovered: itemArea.containsMouse
|
||||||
|
property var controller: null
|
||||||
|
property int flatIndex: -1
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
signal rightClicked(real mouseX, real mouseY)
|
||||||
|
|
||||||
|
readonly property string iconValue: {
|
||||||
|
if (!item)
|
||||||
|
return "";
|
||||||
|
switch (item.iconType) {
|
||||||
|
case "material":
|
||||||
|
case "nerd":
|
||||||
|
return "material:" + (item.icon || "apps");
|
||||||
|
case "unicode":
|
||||||
|
return "unicode:" + (item.icon || "");
|
||||||
|
case "composite":
|
||||||
|
return item.iconFull || "";
|
||||||
|
case "image":
|
||||||
|
default:
|
||||||
|
return item.icon || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent?.width ?? 200
|
||||||
|
height: 52
|
||||||
|
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconValue: root.iconValue
|
||||||
|
iconSize: 36
|
||||||
|
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
|
||||||
|
materialIconSizeAdjustment: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.item?.name ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.item?.subtitle ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text.length > 0
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: rightContent
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.item?.type && root.item.type !== "app"
|
||||||
|
width: typeBadge.implicitWidth + Theme.spacingS * 2
|
||||||
|
height: 20
|
||||||
|
radius: 10
|
||||||
|
color: Theme.surfaceVariantAlpha
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: typeBadge
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
if (!root.item)
|
||||||
|
return "";
|
||||||
|
switch (root.item.type) {
|
||||||
|
case "calculator":
|
||||||
|
return I18n.tr("Calc");
|
||||||
|
case "plugin":
|
||||||
|
return I18n.tr("Plugin");
|
||||||
|
case "file":
|
||||||
|
return I18n.tr("File");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||||
|
root.rightClicked(scenePos.x, scenePos.y);
|
||||||
|
} else {
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
if (root.controller) {
|
||||||
|
root.controller.keyboardNavigationActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
484
quickshell/Modals/DankLauncherV2/ResultsList.qml
Normal file
484
quickshell/Modals/DankLauncherV2/ResultsList.qml
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var controller: null
|
||||||
|
property int gridColumns: controller?.gridColumns ?? 4
|
||||||
|
|
||||||
|
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function resetScroll() {
|
||||||
|
mainFlickable.contentY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
|
||||||
|
return;
|
||||||
|
var entry = controller.flatModel[index];
|
||||||
|
if (!entry || entry.isHeader)
|
||||||
|
return;
|
||||||
|
scrollItemIntoView(index, entry.sectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollItemIntoView(flatIndex, sectionId) {
|
||||||
|
var sections = controller?.sections ?? [];
|
||||||
|
var sectionIndex = -1;
|
||||||
|
for (var i = 0; i < sections.length; i++) {
|
||||||
|
if (sections[i].id === sectionId) {
|
||||||
|
sectionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sectionIndex < 0)
|
||||||
|
return;
|
||||||
|
var itemInSection = 0;
|
||||||
|
var foundSection = false;
|
||||||
|
for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) {
|
||||||
|
var e = controller.flatModel[i];
|
||||||
|
if (e.isHeader && e.section?.id === sectionId)
|
||||||
|
foundSection = true;
|
||||||
|
else if (foundSection && !e.isHeader && e.sectionId === sectionId)
|
||||||
|
itemInSection++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode = controller.getSectionViewMode(sectionId);
|
||||||
|
var sectionY = 0;
|
||||||
|
for (var i = 0; i < sectionIndex; i++) {
|
||||||
|
sectionY += getSectionHeight(sections[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemY, itemHeight;
|
||||||
|
if (mode === "list") {
|
||||||
|
itemY = itemInSection * 52;
|
||||||
|
itemHeight = 52;
|
||||||
|
} else {
|
||||||
|
var cols = controller.getGridColumns(sectionId);
|
||||||
|
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor(mainFlickable.width / root.gridColumns);
|
||||||
|
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
||||||
|
var row = Math.floor(itemInSection / cols);
|
||||||
|
itemY = row * cellHeight;
|
||||||
|
itemHeight = cellHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetY = sectionY + 32 + itemY;
|
||||||
|
var targetBottom = targetY + itemHeight;
|
||||||
|
var stickyHeight = mainFlickable.contentY > 0 ? 32 : 0;
|
||||||
|
|
||||||
|
var shadowPadding = 24;
|
||||||
|
if (targetY < mainFlickable.contentY + stickyHeight) {
|
||||||
|
mainFlickable.contentY = Math.max(0, targetY - 32);
|
||||||
|
} else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) {
|
||||||
|
mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSectionHeight(section) {
|
||||||
|
var mode = controller?.getSectionViewMode(section.id) ?? "list";
|
||||||
|
if (section.collapsed)
|
||||||
|
return 32;
|
||||||
|
|
||||||
|
if (mode === "list") {
|
||||||
|
return 32 + (section.items?.length ?? 0) * 52;
|
||||||
|
} else {
|
||||||
|
var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
|
||||||
|
var rows = Math.ceil((section.items?.length ?? 0) / cols);
|
||||||
|
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / cols);
|
||||||
|
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
||||||
|
return 32 + rows * cellHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedItemPosition() {
|
||||||
|
var fallback = mapToItem(null, width / 2, height / 2);
|
||||||
|
if (!controller?.flatModel || controller.selectedFlatIndex < 0)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
var entry = controller.flatModel[controller.selectedFlatIndex];
|
||||||
|
if (!entry || entry.isHeader)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
var sections = controller.sections;
|
||||||
|
var sectionIndex = -1;
|
||||||
|
for (var i = 0; i < sections.length; i++) {
|
||||||
|
if (sections[i].id === entry.sectionId) {
|
||||||
|
sectionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sectionIndex < 0)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
var sectionY = 0;
|
||||||
|
for (var i = 0; i < sectionIndex; i++) {
|
||||||
|
sectionY += getSectionHeight(sections[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode = controller.getSectionViewMode(entry.sectionId);
|
||||||
|
var itemInSection = entry.indexInSection || 0;
|
||||||
|
|
||||||
|
var itemY, itemX, itemH;
|
||||||
|
if (mode === "list") {
|
||||||
|
itemY = sectionY + 32 + itemInSection * 52;
|
||||||
|
itemX = width / 2;
|
||||||
|
itemH = 52;
|
||||||
|
} else {
|
||||||
|
var cols = controller.getGridColumns(entry.sectionId);
|
||||||
|
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor(width / cols);
|
||||||
|
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
||||||
|
var row = Math.floor(itemInSection / cols);
|
||||||
|
var col = itemInSection % cols;
|
||||||
|
itemY = sectionY + 32 + row * cellHeight;
|
||||||
|
itemX = col * cellWidth + cellWidth / 2;
|
||||||
|
itemH = cellHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var visualY = itemY - mainFlickable.contentY + itemH / 2;
|
||||||
|
var clampedY = Math.max(40, Math.min(height - 40, visualY));
|
||||||
|
return mapToItem(null, itemX, clampedY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.controller
|
||||||
|
function onSelectedFlatIndexChanged() {
|
||||||
|
if (root.controller?.keyboardNavigationActive) {
|
||||||
|
Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: mainFlickable
|
||||||
|
anchors.fill: parent
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: sectionsColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: sectionsColumn
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.controller?.sections ?? []
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: sectionDelegate
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
||||||
|
readonly property string sectionId: modelData?.id ?? ""
|
||||||
|
readonly property string currentViewMode: {
|
||||||
|
void (versionTrigger);
|
||||||
|
return root.controller?.getSectionViewMode(sectionId) ?? "list";
|
||||||
|
}
|
||||||
|
readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile"
|
||||||
|
readonly property bool isCollapsed: modelData?.collapsed ?? false
|
||||||
|
|
||||||
|
width: sectionsColumn.width
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
section: sectionDelegate.modelData
|
||||||
|
controller: root.controller
|
||||||
|
viewMode: sectionDelegate.currentViewMode
|
||||||
|
canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false
|
||||||
|
canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: listContent
|
||||||
|
width: parent.width
|
||||||
|
visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? [])
|
||||||
|
|
||||||
|
ResultItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: listContent.width
|
||||||
|
height: 52
|
||||||
|
item: modelData
|
||||||
|
isSelected: getFlatIndex() === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: getFlatIndex()
|
||||||
|
|
||||||
|
function getFlatIndex() {
|
||||||
|
if (!sectionDelegate?.sectionId)
|
||||||
|
return -1;
|
||||||
|
var flatIdx = 0;
|
||||||
|
var sections = root.controller?.sections ?? [];
|
||||||
|
for (var i = 0; i < sections.length; i++) {
|
||||||
|
flatIdx++;
|
||||||
|
if (sections[i].id === sectionDelegate.sectionId)
|
||||||
|
return flatIdx + index;
|
||||||
|
if (!sections[i].collapsed)
|
||||||
|
flatIdx += sections[i].items?.length ?? 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller) {
|
||||||
|
root.controller.executeItem(modelData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(getFlatIndex(), modelData, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: gridContent
|
||||||
|
width: parent.width
|
||||||
|
visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
|
||||||
|
columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
|
||||||
|
|
||||||
|
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / root.gridColumns)
|
||||||
|
readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: gridDelegateItem
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: gridContent.cellWidth
|
||||||
|
height: gridContent.cellHeight
|
||||||
|
|
||||||
|
function getFlatIndex() {
|
||||||
|
if (!sectionDelegate?.sectionId)
|
||||||
|
return -1;
|
||||||
|
var flatIdx = 0;
|
||||||
|
var sections = root.controller?.sections ?? [];
|
||||||
|
for (var i = 0; i < sections.length; i++) {
|
||||||
|
flatIdx++;
|
||||||
|
if (sections[i].id === sectionDelegate.sectionId)
|
||||||
|
return flatIdx + index;
|
||||||
|
if (!sections[i].collapsed)
|
||||||
|
flatIdx += sections[i].items?.length ?? 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int cachedFlatIndex: getFlatIndex()
|
||||||
|
|
||||||
|
GridItem {
|
||||||
|
width: parent.width - 4
|
||||||
|
height: parent.height - 4
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: sectionDelegate.currentViewMode === "grid"
|
||||||
|
item: gridDelegateItem.modelData
|
||||||
|
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: gridDelegateItem.cachedFlatIndex
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller) {
|
||||||
|
root.controller.executeItem(gridDelegateItem.modelData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TileItem {
|
||||||
|
width: parent.width - 4
|
||||||
|
height: parent.height - 4
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: sectionDelegate.currentViewMode === "tile"
|
||||||
|
item: gridDelegateItem.modelData
|
||||||
|
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: gridDelegateItem.cachedFlatIndex
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller) {
|
||||||
|
root.controller.executeItem(gridDelegateItem.modelData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bottomShadow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 24
|
||||||
|
z: 100
|
||||||
|
visible: {
|
||||||
|
if (mainFlickable.contentHeight <= mainFlickable.height)
|
||||||
|
return false;
|
||||||
|
var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5;
|
||||||
|
if (atBottom)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var flatModel = root.controller?.flatModel;
|
||||||
|
if (!flatModel || flatModel.length === 0)
|
||||||
|
return false;
|
||||||
|
var lastItemIdx = -1;
|
||||||
|
for (var i = flatModel.length - 1; i >= 0; i--) {
|
||||||
|
if (!flatModel[i].isHeader) {
|
||||||
|
lastItemIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
gradient: Gradient {
|
||||||
|
GradientStop {
|
||||||
|
position: 0.0
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
position: 1.0
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: stickyHeader
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: 32
|
||||||
|
z: 101
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
visible: stickyHeaderSection !== null
|
||||||
|
|
||||||
|
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
||||||
|
|
||||||
|
readonly property var stickyHeaderSection: {
|
||||||
|
if (!root.controller?.sections || root.controller.sections.length === 0)
|
||||||
|
return null;
|
||||||
|
var sections = root.controller.sections;
|
||||||
|
if (sections.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var scrollY = mainFlickable.contentY;
|
||||||
|
if (scrollY <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var y = 0;
|
||||||
|
for (var i = 0; i < sections.length; i++) {
|
||||||
|
var section = sections[i];
|
||||||
|
var sectionHeight = root.getSectionHeight(section);
|
||||||
|
if (scrollY < y + sectionHeight)
|
||||||
|
return section;
|
||||||
|
y += sectionHeight;
|
||||||
|
}
|
||||||
|
return sections[sections.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
width: parent.width
|
||||||
|
section: stickyHeader.stickyHeaderSection
|
||||||
|
controller: root.controller
|
||||||
|
viewMode: {
|
||||||
|
void (stickyHeader.versionTrigger);
|
||||||
|
return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
|
||||||
|
}
|
||||||
|
canChangeViewMode: {
|
||||||
|
void (stickyHeader.versionTrigger);
|
||||||
|
return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
|
||||||
|
}
|
||||||
|
canCollapse: {
|
||||||
|
void (stickyHeader.versionTrigger);
|
||||||
|
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
|
||||||
|
}
|
||||||
|
isSticky: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching
|
||||||
|
width: emptyColumn.implicitWidth
|
||||||
|
height: emptyColumn.implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: emptyColumn
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: getEmptyIcon()
|
||||||
|
size: 48
|
||||||
|
color: Theme.outlineButton
|
||||||
|
|
||||||
|
function getEmptyIcon() {
|
||||||
|
var mode = root.controller?.searchMode ?? "all";
|
||||||
|
switch (mode) {
|
||||||
|
case "files":
|
||||||
|
return "folder_open";
|
||||||
|
case "plugins":
|
||||||
|
return "extension";
|
||||||
|
case "apps":
|
||||||
|
return "apps";
|
||||||
|
default:
|
||||||
|
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: getEmptyText()
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
|
function getEmptyText() {
|
||||||
|
var mode = root.controller?.searchMode ?? "all";
|
||||||
|
var hasQuery = root.controller?.searchQuery?.length > 0;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case "files":
|
||||||
|
if (!DSearchService.dsearchAvailable)
|
||||||
|
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
|
||||||
|
if (!hasQuery)
|
||||||
|
return I18n.tr("Type to search files");
|
||||||
|
if (root.controller.searchQuery.length < 2)
|
||||||
|
return I18n.tr("Type at least 2 characters");
|
||||||
|
return I18n.tr("No files found");
|
||||||
|
case "plugins":
|
||||||
|
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
||||||
|
case "apps":
|
||||||
|
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
||||||
|
default:
|
||||||
|
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
quickshell/Modals/DankLauncherV2/Scorer.js
Normal file
245
quickshell/Modals/DankLauncherV2/Scorer.js
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
const Weights = {
|
||||||
|
exactMatch: 10000,
|
||||||
|
prefixMatch: 5000,
|
||||||
|
wordBoundary: 1000,
|
||||||
|
substring: 500,
|
||||||
|
fuzzy: 100,
|
||||||
|
frecency: 2000,
|
||||||
|
typeBonus: {
|
||||||
|
app: 1000,
|
||||||
|
plugin: 900,
|
||||||
|
file: 800,
|
||||||
|
action: 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize(text) {
|
||||||
|
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function(w) { return w.length > 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasWordBoundaryMatch(text, query) {
|
||||||
|
var textWords = tokenize(text)
|
||||||
|
var queryWords = tokenize(query)
|
||||||
|
|
||||||
|
if (queryWords.length === 0) return false
|
||||||
|
if (queryWords.length > textWords.length) return false
|
||||||
|
|
||||||
|
for (var i = 0; i <= textWords.length - queryWords.length; i++) {
|
||||||
|
var allMatch = true
|
||||||
|
for (var j = 0; j < queryWords.length; j++) {
|
||||||
|
if (!textWords[i + j].startsWith(queryWords[j])) {
|
||||||
|
allMatch = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allMatch) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function levenshteinDistance(s1, s2) {
|
||||||
|
var len1 = s1.length
|
||||||
|
var len2 = s2.length
|
||||||
|
var matrix = []
|
||||||
|
|
||||||
|
for (var i = 0; i <= len1; i++) {
|
||||||
|
matrix[i] = [i]
|
||||||
|
}
|
||||||
|
for (var j = 0; j <= len2; j++) {
|
||||||
|
matrix[0][j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i <= len1; i++) {
|
||||||
|
for (var j = 1; j <= len2; j++) {
|
||||||
|
var cost = s1[i - 1] === s2[j - 1] ? 0 : 1
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j] + 1,
|
||||||
|
matrix[i][j - 1] + 1,
|
||||||
|
matrix[i - 1][j - 1] + cost
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matrix[len1][len2]
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuzzyScore(text, query) {
|
||||||
|
var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
|
||||||
|
var bestScore = 0
|
||||||
|
|
||||||
|
if (Math.abs(text.length - query.length) <= maxDistance) {
|
||||||
|
var distance = levenshteinDistance(text, query)
|
||||||
|
if (distance <= maxDistance) {
|
||||||
|
var maxLen = Math.max(text.length, query.length)
|
||||||
|
bestScore = 1 - (distance / maxLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var words = tokenize(text)
|
||||||
|
for (var i = 0; i < words.length && bestScore < 0.8; i++) {
|
||||||
|
if (Math.abs(words[i].length - query.length) > maxDistance) continue
|
||||||
|
var wordDistance = levenshteinDistance(words[i], query)
|
||||||
|
if (wordDistance <= maxDistance) {
|
||||||
|
var wordMaxLen = Math.max(words[i].length, query.length)
|
||||||
|
var score = 1 - (wordDistance / wordMaxLen)
|
||||||
|
bestScore = Math.max(bestScore, score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestScore
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeBucketWeight(daysSinceUsed) {
|
||||||
|
for (var i = 0; i < TimeBuckets.length; i++) {
|
||||||
|
if (daysSinceUsed <= TimeBuckets[i].maxDays) {
|
||||||
|
return TimeBuckets[i].weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTextScore(name, query) {
|
||||||
|
if (name === query) return Weights.exactMatch
|
||||||
|
if (name.startsWith(query)) return Weights.prefixMatch
|
||||||
|
if (name.includes(query)) return Weights.substring
|
||||||
|
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
|
||||||
|
|
||||||
|
if (query.length >= 3) {
|
||||||
|
var fs = fuzzyScore(name, query)
|
||||||
|
if (fs > 0) return fs * Weights.fuzzy
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function score(item, query, frecencyData) {
|
||||||
|
var typeBonus = Weights.typeBonus[item.type] || 0
|
||||||
|
|
||||||
|
if (!query || query.length === 0) {
|
||||||
|
var usageCount = frecencyData ? frecencyData.usageCount : 0
|
||||||
|
return typeBonus + (usageCount * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = (item.name || "").toLowerCase()
|
||||||
|
var q = query.toLowerCase()
|
||||||
|
|
||||||
|
var textScore = calculateTextScore(name, q)
|
||||||
|
|
||||||
|
if (textScore === 0 && item.subtitle) {
|
||||||
|
var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q)
|
||||||
|
textScore = subtitleScore * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textScore === 0 && item.keywords) {
|
||||||
|
for (var i = 0; i < item.keywords.length; i++) {
|
||||||
|
var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q)
|
||||||
|
if (keywordScore > 0) {
|
||||||
|
textScore = keywordScore * 0.3
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textScore === 0) return 0
|
||||||
|
|
||||||
|
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 10, Weights.frecency) : 0
|
||||||
|
|
||||||
|
return textScore + usageBonus + typeBonus
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreItems(items, query, getFrecencyFn) {
|
||||||
|
var scored = []
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i]
|
||||||
|
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
||||||
|
var itemScore = score(item, query, frecencyData)
|
||||||
|
|
||||||
|
if (itemScore > 0 || !query || query.length === 0) {
|
||||||
|
scored.push({
|
||||||
|
item: item,
|
||||||
|
score: itemScore
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scored.sort(function(a, b) {
|
||||||
|
return b.score - a.score
|
||||||
|
})
|
||||||
|
|
||||||
|
return scored
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) {
|
||||||
|
var sections = {}
|
||||||
|
var result = []
|
||||||
|
var limit = maxPerSection || 50
|
||||||
|
|
||||||
|
for (var i = 0; i < sectionOrder.length; i++) {
|
||||||
|
var sectionId = sectionOrder[i].id
|
||||||
|
sections[sectionId] = {
|
||||||
|
id: sectionId,
|
||||||
|
title: sectionOrder[i].title,
|
||||||
|
icon: sectionOrder[i].icon,
|
||||||
|
priority: sectionOrder[i].priority,
|
||||||
|
items: [],
|
||||||
|
collapsed: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < scoredItems.length; i++) {
|
||||||
|
var scoredItem = scoredItems[i]
|
||||||
|
var item = scoredItem.item
|
||||||
|
var sectionId = item.section || "apps"
|
||||||
|
|
||||||
|
if (sections[sectionId] && sections[sectionId].items.length < limit) {
|
||||||
|
sections[sectionId].items.push(item)
|
||||||
|
} else if (sections["apps"] && sections["apps"].items.length < limit) {
|
||||||
|
sections["apps"].items.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < sectionOrder.length; i++) {
|
||||||
|
var section = sections[sectionOrder[i].id]
|
||||||
|
if (section && section.items.length > 0) {
|
||||||
|
if (sortAlphabetically && section.id === "apps") {
|
||||||
|
section.items.sort(function(a, b) {
|
||||||
|
return (a.name || "").localeCompare(b.name || "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result.push(section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenSections(sections) {
|
||||||
|
var flat = []
|
||||||
|
|
||||||
|
for (var i = 0; i < sections.length; i++) {
|
||||||
|
var section = sections[i]
|
||||||
|
|
||||||
|
flat.push({
|
||||||
|
isHeader: true,
|
||||||
|
section: section,
|
||||||
|
sectionId: section.id,
|
||||||
|
sectionIndex: i
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!section.collapsed) {
|
||||||
|
for (var j = 0; j < section.items.length; j++) {
|
||||||
|
flat.push({
|
||||||
|
isHeader: false,
|
||||||
|
item: section.items[j],
|
||||||
|
sectionId: section.id,
|
||||||
|
sectionIndex: i,
|
||||||
|
indexInSection: j
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flat
|
||||||
|
}
|
||||||
114
quickshell/Modals/DankLauncherV2/Section.qml
Normal file
114
quickshell/Modals/DankLauncherV2/Section.qml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var section: null
|
||||||
|
property var controller: null
|
||||||
|
property string viewMode: "list"
|
||||||
|
property int gridColumns: 4
|
||||||
|
property int startIndex: 0
|
||||||
|
|
||||||
|
signal itemClicked(int flatIndex)
|
||||||
|
signal itemRightClicked(int flatIndex, var item, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
height: headerItem.height + (section?.collapsed ? 0 : contentLoader.height + Theme.spacingXS)
|
||||||
|
width: parent?.width ?? 200
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
id: headerItem
|
||||||
|
width: parent.width
|
||||||
|
section: root.section
|
||||||
|
controller: root.controller
|
||||||
|
viewMode: root.viewMode
|
||||||
|
canChangeViewMode: root.controller?.canChangeSectionViewMode(root.section?.id) ?? true
|
||||||
|
|
||||||
|
onViewModeToggled: {
|
||||||
|
if (root.controller && root.section) {
|
||||||
|
var newMode = root.viewMode === "list" ? "grid" : "list";
|
||||||
|
root.controller.setSectionViewMode(root.section.id, newMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
anchors.top: headerItem.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
active: !root.section?.collapsed
|
||||||
|
visible: active
|
||||||
|
|
||||||
|
sourceComponent: root.viewMode === "grid" ? gridComponent : listComponent
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: listComponent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
width: contentLoader.width
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.section?.items ?? []
|
||||||
|
objectProp: "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent?.width ?? 200
|
||||||
|
item: modelData
|
||||||
|
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: root.startIndex + index
|
||||||
|
|
||||||
|
onClicked: root.itemClicked(root.startIndex + index)
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: gridComponent
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: contentLoader.width
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.section?.items ?? []
|
||||||
|
objectProp: "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
GridItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: Math.floor(contentLoader.width / root.gridColumns)
|
||||||
|
height: width + 24
|
||||||
|
item: modelData
|
||||||
|
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: root.startIndex + index
|
||||||
|
|
||||||
|
onClicked: root.itemClicked(root.startIndex + index)
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
quickshell/Modals/DankLauncherV2/SectionHeader.qml
Normal file
169
quickshell/Modals/DankLauncherV2/SectionHeader.qml
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var section: null
|
||||||
|
property var controller: null
|
||||||
|
property string viewMode: "list"
|
||||||
|
property bool canChangeViewMode: true
|
||||||
|
property bool canCollapse: true
|
||||||
|
property bool isSticky: false
|
||||||
|
|
||||||
|
signal viewModeToggled
|
||||||
|
|
||||||
|
width: parent?.width ?? 200
|
||||||
|
height: 32
|
||||||
|
color: isSticky ? "transparent" : (hoverArea.containsMouse ? Theme.surfaceHover : "transparent")
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: leftContent
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
name: root.section?.icon ?? "folder"
|
||||||
|
size: 16
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.section?.title ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.section?.items?.length ?? 0
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.outlineButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: rightContent
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: viewModeRow
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
visible: root.canChangeViewMode && !root.section?.collapsed
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{
|
||||||
|
mode: "list",
|
||||||
|
icon: "view_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "grid",
|
||||||
|
icon: "grid_view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "tile",
|
||||||
|
icon: "view_module"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
radius: 4
|
||||||
|
color: root.viewMode === modelData.mode ? Theme.primaryHover : modeArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: parent.modelData.icon
|
||||||
|
size: 14
|
||||||
|
color: root.viewMode === parent.modelData.mode ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: modeArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (root.viewMode !== parent.modelData.mode && root.controller && root.section) {
|
||||||
|
root.controller.setSectionViewMode(root.section.id, parent.modelData.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: collapseButton
|
||||||
|
width: root.canCollapse ? 24 : 0
|
||||||
|
height: 24
|
||||||
|
visible: root.canCollapse
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: root.section?.collapsed ? "expand_more" : "expand_less"
|
||||||
|
size: 16
|
||||||
|
color: collapseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: collapseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller && root.section) {
|
||||||
|
root.controller.toggleSection(root.section.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: rightContent.width + Theme.spacingS
|
||||||
|
cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: root.canCollapse
|
||||||
|
onClicked: {
|
||||||
|
if (root.canCollapse && root.controller && root.section) {
|
||||||
|
root.controller.toggleSection(root.section.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
visible: root.isSticky
|
||||||
|
}
|
||||||
|
}
|
||||||
136
quickshell/Modals/DankLauncherV2/TileItem.qml
Normal file
136
quickshell/Modals/DankLauncherV2/TileItem.qml
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var item: null
|
||||||
|
property bool isSelected: false
|
||||||
|
property bool isHovered: itemArea.containsMouse
|
||||||
|
property var controller: null
|
||||||
|
property int flatIndex: -1
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
signal rightClicked(real mouseX, real mouseY)
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
||||||
|
border.width: isSelected ? 2 : 0
|
||||||
|
border.color: Theme.primary
|
||||||
|
|
||||||
|
readonly property string iconValue: {
|
||||||
|
if (!item)
|
||||||
|
return "";
|
||||||
|
var data = item.data;
|
||||||
|
if (data?.imageUrl)
|
||||||
|
return "image:" + data.imageUrl;
|
||||||
|
if (data?.imagePath)
|
||||||
|
return "image:" + data.imagePath;
|
||||||
|
if (data?.path && isImageFile(data.path))
|
||||||
|
return "image:" + data.path;
|
||||||
|
switch (item.iconType) {
|
||||||
|
case "material":
|
||||||
|
case "nerd":
|
||||||
|
return "material:" + (item.icon || "image");
|
||||||
|
case "unicode":
|
||||||
|
return "unicode:" + (item.icon || "");
|
||||||
|
case "composite":
|
||||||
|
return item.iconFull || "";
|
||||||
|
case "image":
|
||||||
|
default:
|
||||||
|
return item.icon || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageFile(path) {
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
var ext = path.split('.').pop().toLowerCase();
|
||||||
|
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].indexOf(ext) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: imageContainer
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius - 2
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
anchors.fill: parent
|
||||||
|
iconValue: root.iconValue
|
||||||
|
iconSize: Math.min(parent.width, parent.height)
|
||||||
|
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
|
||||||
|
materialIconSizeAdjustment: iconSize * 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: labelText.implicitHeight + Theme.spacingS * 2
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, 0.85)
|
||||||
|
visible: root.item?.name?.length > 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: labelText
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingXS
|
||||||
|
text: root.item?.name ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingXS
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
radius: 10
|
||||||
|
color: Theme.primary
|
||||||
|
visible: root.isSelected
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "check"
|
||||||
|
size: 14
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||||
|
root.rightClicked(scenePos.x, scenePos.y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
if (root.controller)
|
||||||
|
root.controller.keyboardNavigationActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ FloatingWindow {
|
|||||||
id: processListModal
|
id: processListModal
|
||||||
|
|
||||||
property int currentTab: 0
|
property int currentTab: 0
|
||||||
property var tabNames: ["Processes", "Performance", "System"]
|
property string searchText: ""
|
||||||
|
property string expandedPid: ""
|
||||||
property bool shouldHaveFocus: visible
|
property bool shouldHaveFocus: visible
|
||||||
property alias shouldBeVisible: processListModal.visible
|
property alias shouldBeVisible: processListModal.visible
|
||||||
|
|
||||||
@@ -27,9 +28,8 @@ FloatingWindow {
|
|||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
visible = false;
|
visible = false;
|
||||||
if (processContextMenu.visible) {
|
if (processContextMenu.visible)
|
||||||
processContextMenu.close();
|
processContextMenu.close();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
@@ -61,48 +61,63 @@ FloatingWindow {
|
|||||||
show();
|
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"
|
objectName: "processListModal"
|
||||||
title: I18n.tr("System Monitor", "sysmon window title")
|
title: I18n.tr("System Monitor", "sysmon window title")
|
||||||
minimumSize: Qt.size(650, 400)
|
minimumSize: Qt.size(750, 550)
|
||||||
implicitWidth: 900
|
implicitWidth: 1000
|
||||||
implicitHeight: 680
|
implicitHeight: 720
|
||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
|
onCurrentTabChanged: {
|
||||||
|
if (visible && currentTab === 0 && searchField.visible)
|
||||||
|
searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
closingModal();
|
closingModal();
|
||||||
|
searchText = "";
|
||||||
|
expandedPid = "";
|
||||||
|
if (processesTabLoader.item)
|
||||||
|
processesTabLoader.item.reset();
|
||||||
|
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
|
||||||
} else {
|
} else {
|
||||||
|
DgopService.addRef(["cpu", "memory", "network", "disk", "system"]);
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (contentFocusScope) {
|
if (currentTab === 0 && searchField.visible)
|
||||||
|
searchField.forceActiveFocus();
|
||||||
|
else if (contentFocusScope)
|
||||||
contentFocusScope.forceActiveFocus();
|
contentFocusScope.forceActiveFocus();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
|
||||||
id: processesTabComponent
|
|
||||||
|
|
||||||
ProcessesTab {
|
|
||||||
contextMenu: processContextMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: performanceTabComponent
|
|
||||||
|
|
||||||
PerformanceTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: systemTabComponent
|
|
||||||
|
|
||||||
SystemTab {}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessContextMenu {
|
ProcessContextMenu {
|
||||||
id: processContextMenu
|
id: processContextMenu
|
||||||
|
parentFocusItem: contentFocusScope
|
||||||
|
onProcessKilled: {
|
||||||
|
if (processesTabLoader.item)
|
||||||
|
processesTabLoader.item.forceRefresh(3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
@@ -115,6 +130,9 @@ FloatingWindow {
|
|||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
|
if (processContextMenu.visible)
|
||||||
|
return;
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_1:
|
case Qt.Key_1:
|
||||||
currentTab = 0;
|
currentTab = 0;
|
||||||
@@ -128,7 +146,43 @@ FloatingWindow {
|
|||||||
currentTab = 2;
|
currentTab = 2;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
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 {
|
Rectangle {
|
||||||
@@ -161,7 +215,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
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
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -171,14 +225,14 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
visible: DgopService.dgopAvailable
|
visible: DgopService.dgopAvailable
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: 48
|
Layout.preferredHeight: 48
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -233,166 +287,278 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
RowLayout {
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: parent.height - 48
|
Layout.preferredHeight: 52
|
||||||
|
Layout.leftMargin: Theme.spacingL
|
||||||
|
Layout.rightMargin: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
ColumnLayout {
|
Row {
|
||||||
anchors.fill: parent
|
spacing: 2
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.bottomMargin: Theme.spacingL
|
|
||||||
anchors.topMargin: 0
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
Repeater {
|
||||||
Layout.fillWidth: true
|
model: [
|
||||||
height: 52
|
{
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
text: I18n.tr("Processes"),
|
||||||
radius: Theme.cornerRadius
|
icon: "list_alt"
|
||||||
border.color: Theme.outlineLight
|
},
|
||||||
border.width: 1
|
{
|
||||||
|
text: I18n.tr("Performance"),
|
||||||
|
icon: "analytics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: I18n.tr("Disks"),
|
||||||
|
icon: "storage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: I18n.tr("System"),
|
||||||
|
icon: "computer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
Row {
|
Rectangle {
|
||||||
anchors.fill: parent
|
width: 120
|
||||||
anchors.margins: 4
|
height: 44
|
||||||
spacing: 2
|
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 {
|
Row {
|
||||||
model: tabNames
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
Rectangle {
|
DankIcon {
|
||||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
name: modelData.icon
|
||||||
height: 44
|
size: Theme.iconSize - 2
|
||||||
radius: Theme.cornerRadius
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
opacity: currentTab === index ? 1 : 0.7
|
||||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
border.width: currentTab === index ? 1 : 0
|
}
|
||||||
|
|
||||||
Row {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
text: modelData.text
|
||||||
spacing: Theme.spacingXS
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
MouseArea {
|
||||||
name: {
|
id: tabMouseArea
|
||||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
anchors.fill: parent
|
||||||
return tabIcons[index] || "tab";
|
hoverEnabled: true
|
||||||
}
|
cursorShape: Qt.PointingHandCursor
|
||||||
size: Theme.iconSize - 2
|
onClicked: currentTab = index
|
||||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
}
|
||||||
opacity: currentTab === index ? 1 : 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: Theme.shortDuration
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
}
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Loader {
|
DankTextField {
|
||||||
id: processesTab
|
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
|
Rectangle {
|
||||||
anchors.margins: Theme.spacingS
|
Layout.fillWidth: true
|
||||||
active: processListModal.visible && currentTab === 0
|
Layout.fillHeight: true
|
||||||
visible: currentTab === 0
|
Layout.margins: Theme.spacingL
|
||||||
opacity: currentTab === 0 ? 1 : 0
|
Layout.topMargin: Theme.spacingM
|
||||||
sourceComponent: processesTabComponent
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
Behavior on opacity {
|
Loader {
|
||||||
NumberAnimation {
|
id: processesTabLoader
|
||||||
duration: Theme.mediumDuration
|
anchors.fill: parent
|
||||||
easing.type: Theme.emphasizedEasing
|
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 {
|
StyledText {
|
||||||
id: performanceTab
|
text: DgopService.processCount.toString()
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
Row {
|
||||||
anchors.margins: Theme.spacingS
|
spacing: Theme.spacingXS
|
||||||
active: processListModal.visible && currentTab === 1
|
|
||||||
visible: currentTab === 1
|
|
||||||
opacity: currentTab === 1 ? 1 : 0
|
|
||||||
sourceComponent: performanceTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
StyledText {
|
||||||
NumberAnimation {
|
text: I18n.tr("Uptime:", "uptime label in footer")
|
||||||
duration: Theme.mediumDuration
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
easing.type: Theme.emphasizedEasing
|
color: Theme.surfaceVariantText
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
StyledText {
|
||||||
id: systemTab
|
text: DgopService.shortUptime || "--"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
Row {
|
||||||
anchors.margins: Theme.spacingS
|
anchors.right: parent.right
|
||||||
active: processListModal.visible && currentTab === 2
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: currentTab === 2
|
spacing: Theme.spacingL
|
||||||
opacity: currentTab === 2 ? 1 : 0
|
|
||||||
sourceComponent: systemTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
Row {
|
||||||
NumberAnimation {
|
spacing: Theme.spacingXS
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,9 +74,7 @@ Rectangle {
|
|||||||
if (root.parentModal) {
|
if (root.parentModal) {
|
||||||
root.parentModal.allowFocusOverride = true;
|
root.parentModal.allowFocusOverride = true;
|
||||||
root.parentModal.shouldHaveFocus = false;
|
root.parentModal.shouldHaveFocus = false;
|
||||||
if (root.parentModal.profileBrowser) {
|
root.parentModal.openProfileBrowser();
|
||||||
root.parentModal.profileBrowser.open();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,26 @@ import qs.Widgets
|
|||||||
FloatingWindow {
|
FloatingWindow {
|
||||||
id: settingsModal
|
id: settingsModal
|
||||||
|
|
||||||
property alias profileBrowser: profileBrowser
|
property var profileBrowser: profileBrowserLoader.item
|
||||||
property alias wallpaperBrowser: wallpaperBrowser
|
property var wallpaperBrowser: wallpaperBrowserLoader.item
|
||||||
|
|
||||||
|
function openProfileBrowser(allowStacking) {
|
||||||
|
profileBrowserLoader.active = true;
|
||||||
|
if (!profileBrowserLoader.item)
|
||||||
|
return;
|
||||||
|
if (allowStacking !== undefined)
|
||||||
|
profileBrowserLoader.item.allowStacking = allowStacking;
|
||||||
|
profileBrowserLoader.item.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWallpaperBrowser(allowStacking) {
|
||||||
|
wallpaperBrowserLoader.active = true;
|
||||||
|
if (!wallpaperBrowserLoader.item)
|
||||||
|
return;
|
||||||
|
if (allowStacking !== undefined)
|
||||||
|
wallpaperBrowserLoader.item.allowStacking = allowStacking;
|
||||||
|
wallpaperBrowserLoader.item.open();
|
||||||
|
}
|
||||||
property alias sidebar: sidebar
|
property alias sidebar: sidebar
|
||||||
property int currentTabIndex: 0
|
property int currentTabIndex: 0
|
||||||
property bool shouldHaveFocus: visible
|
property bool shouldHaveFocus: visible
|
||||||
@@ -96,41 +114,51 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileBrowserModal {
|
LazyLoader {
|
||||||
id: profileBrowser
|
id: profileBrowserLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
allowStacking: true
|
FileBrowserModal {
|
||||||
parentModal: settingsModal
|
id: profileBrowserItem
|
||||||
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
|
|
||||||
browserIcon: "person"
|
allowStacking: true
|
||||||
browserType: "profile"
|
parentModal: settingsModal
|
||||||
showHiddenFiles: true
|
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
browserIcon: "person"
|
||||||
onFileSelected: path => {
|
browserType: "profile"
|
||||||
PortalService.setProfileImage(path);
|
showHiddenFiles: true
|
||||||
close();
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
}
|
onFileSelected: path => {
|
||||||
onDialogClosed: () => {
|
PortalService.setProfileImage(path);
|
||||||
allowStacking = true;
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileBrowserModal {
|
LazyLoader {
|
||||||
id: wallpaperBrowser
|
id: wallpaperBrowserLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
allowStacking: true
|
FileBrowserModal {
|
||||||
parentModal: settingsModal
|
id: wallpaperBrowserItem
|
||||||
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
|
|
||||||
browserIcon: "wallpaper"
|
allowStacking: true
|
||||||
browserType: "wallpaper"
|
parentModal: settingsModal
|
||||||
showHiddenFiles: true
|
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
browserIcon: "wallpaper"
|
||||||
onFileSelected: path => {
|
browserType: "wallpaper"
|
||||||
SessionData.setWallpaper(path);
|
showHiddenFiles: true
|
||||||
close();
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
}
|
onFileSelected: path => {
|
||||||
onDialogClosed: () => {
|
SessionData.setWallpaper(path);
|
||||||
allowStacking = true;
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ Rectangle {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||||
height: Theme.spacingS
|
height: Theme.spacingXS
|
||||||
}
|
}
|
||||||
|
|
||||||
DankTextField {
|
DankTextField {
|
||||||
@@ -717,7 +717,7 @@ Rectangle {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||||
height: Theme.spacingS
|
height: Theme.spacingXS
|
||||||
visible: !root.searchActive
|
visible: !root.searchActive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: controller
|
|
||||||
|
|
||||||
property string searchQuery: ""
|
|
||||||
property alias model: fileModel
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool isSearching: false
|
|
||||||
property int totalResults: 0
|
|
||||||
property string searchField: "filename"
|
|
||||||
|
|
||||||
signal searchCompleted
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: fileModel
|
|
||||||
}
|
|
||||||
|
|
||||||
function performSearch() {
|
|
||||||
if (!DSearchService.dsearchAvailable) {
|
|
||||||
model.clear()
|
|
||||||
totalResults = 0
|
|
||||||
isSearching = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery.length === 0) {
|
|
||||||
model.clear()
|
|
||||||
totalResults = 0
|
|
||||||
isSearching = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isSearching = true
|
|
||||||
const params = {
|
|
||||||
"limit": 50,
|
|
||||||
"fuzzy": true,
|
|
||||||
"sort": "score",
|
|
||||||
"desc": true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchField && searchField !== "all") {
|
|
||||||
params.field = searchField
|
|
||||||
}
|
|
||||||
|
|
||||||
DSearchService.search(searchQuery, params, response => {
|
|
||||||
if (response.error) {
|
|
||||||
model.clear()
|
|
||||||
totalResults = 0
|
|
||||||
isSearching = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.result) {
|
|
||||||
updateModel(response.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
isSearching = false
|
|
||||||
searchCompleted()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateModel(result) {
|
|
||||||
model.clear()
|
|
||||||
totalResults = result.total_hits || 0
|
|
||||||
selectedIndex = 0
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
|
|
||||||
if (!result.hits || result.hits.length === 0) {
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < result.hits.length; i++) {
|
|
||||||
const hit = result.hits[i]
|
|
||||||
const filePath = hit.id || ""
|
|
||||||
const fileName = getFileName(filePath)
|
|
||||||
const fileExt = getFileExtension(fileName)
|
|
||||||
const fileType = determineFileType(fileName, filePath)
|
|
||||||
const dirPath = getDirPath(filePath)
|
|
||||||
|
|
||||||
model.append({
|
|
||||||
"filePath": filePath,
|
|
||||||
"fileName": fileName,
|
|
||||||
"fileExtension": fileExt,
|
|
||||||
"fileType": fileType,
|
|
||||||
"dirPath": dirPath,
|
|
||||||
"score": hit.score || 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileName(path) {
|
|
||||||
const parts = path.split('/')
|
|
||||||
return parts[parts.length - 1] || path
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileExtension(fileName) {
|
|
||||||
const parts = fileName.split('.')
|
|
||||||
if (parts.length > 1) {
|
|
||||||
return parts[parts.length - 1].toLowerCase()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDirPath(path) {
|
|
||||||
const lastSlash = path.lastIndexOf('/')
|
|
||||||
if (lastSlash > 0) {
|
|
||||||
return path.substring(0, lastSlash)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function determineFileType(fileName, filePath) {
|
|
||||||
const ext = getFileExtension(fileName)
|
|
||||||
|
|
||||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
|
|
||||||
if (imageExts.includes(ext)) {
|
|
||||||
return "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
|
|
||||||
if (videoExts.includes(ext)) {
|
|
||||||
return "video"
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
|
|
||||||
if (audioExts.includes(ext)) {
|
|
||||||
return "audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
|
|
||||||
if (codeExts.includes(ext)) {
|
|
||||||
return "code"
|
|
||||||
}
|
|
||||||
|
|
||||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
|
|
||||||
if (docExts.includes(ext)) {
|
|
||||||
return "document"
|
|
||||||
}
|
|
||||||
|
|
||||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
|
|
||||||
if (archiveExts.includes(ext)) {
|
|
||||||
return "archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ext || fileName.indexOf('.') === -1) {
|
|
||||||
return "binary"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (model.count === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1, model.count - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (model.count === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
signal fileOpened
|
|
||||||
|
|
||||||
function openFile(filePath) {
|
|
||||||
if (!filePath || filePath.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = filePath
|
|
||||||
if (!url.startsWith("file://")) {
|
|
||||||
url = "file://" + filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt.openUrlExternally(url)
|
|
||||||
fileOpened()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFolder(filePath) {
|
|
||||||
if (!filePath || filePath.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSlash = filePath.lastIndexOf('/')
|
|
||||||
if (lastSlash <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirPath = filePath.substring(0, lastSlash)
|
|
||||||
let url = dirPath
|
|
||||||
if (!url.startsWith("file://")) {
|
|
||||||
url = "file://" + dirPath
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt.openUrlExternally(url)
|
|
||||||
fileOpened()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSelected() {
|
|
||||||
if (model.count === 0 || selectedIndex < 0 || selectedIndex >= model.count) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = model.get(selectedIndex)
|
|
||||||
if (item && item.filePath) {
|
|
||||||
openFile(item.filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
searchQuery = ""
|
|
||||||
model.clear()
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
isSearching = false
|
|
||||||
totalResults = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChanged: {
|
|
||||||
performSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchFieldChanged: {
|
|
||||||
performSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: entry
|
|
||||||
|
|
||||||
required property string filePath
|
|
||||||
required property string fileName
|
|
||||||
required property string fileExtension
|
|
||||||
required property string fileType
|
|
||||||
required property string dirPath
|
|
||||||
required property bool isSelected
|
|
||||||
required property int itemIndex
|
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
readonly property int iconSize: 40
|
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isSelected ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: imagePreview
|
|
||||||
anchors.fill: parent
|
|
||||||
source: fileType === "image" ? `file://${filePath}` : ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
smooth: true
|
|
||||||
cache: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: fileType === "image" && status === Image.Ready
|
|
||||||
sourceSize.width: 128
|
|
||||||
sourceSize.height: 128
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: imagePreview
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: imageMask
|
|
||||||
visible: fileType === "image" && imagePreview.status === Image.Ready
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: imageMask
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: getFileTypeColor()
|
|
||||||
visible: fileType !== "image" || imagePreview.status !== Image.Ready
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: getFileIconText()
|
|
||||||
font.pixelSize: fileExtension.length > 0 ? (fileExtension.length > 3 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall) : Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - iconSize - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: fileName
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: dirPath
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: entry.clicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileTypeColor() {
|
|
||||||
switch (fileType) {
|
|
||||||
case "code":
|
|
||||||
return Theme.codeFileColor || Theme.primarySelected
|
|
||||||
case "document":
|
|
||||||
return Theme.docFileColor || Theme.secondarySelected
|
|
||||||
case "video":
|
|
||||||
return Theme.videoFileColor || Theme.tertiarySelected
|
|
||||||
case "audio":
|
|
||||||
return Theme.audioFileColor || Theme.errorSelected
|
|
||||||
case "archive":
|
|
||||||
return Theme.archiveFileColor || Theme.warningSelected
|
|
||||||
case "binary":
|
|
||||||
return Theme.binaryFileColor || Theme.surfaceDim
|
|
||||||
default:
|
|
||||||
return Theme.surfaceLight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileIconText() {
|
|
||||||
if (fileType === "binary") {
|
|
||||||
return "bin"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileExtension.length > 0) {
|
|
||||||
return fileExtension
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: resultsContainer
|
|
||||||
|
|
||||||
property var fileSearchController: null
|
|
||||||
|
|
||||||
function resetScroll() {
|
|
||||||
filesList.contentY = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
height: 32
|
|
||||||
z: 100
|
|
||||||
visible: filesList.contentHeight > filesList.height && (filesList.currentIndex < filesList.count - 1 || filesList.contentY < filesList.contentHeight - filesList.height - 1)
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.0
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 1.0
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: filesList
|
|
||||||
|
|
||||||
property int itemHeight: 60
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: fileSearchController ? fileSearchController.keyboardNavigationActive : false
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index)
|
|
||||||
signal itemRightClicked(int index)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return;
|
|
||||||
const itemY = index * (itemHeight + itemSpacing);
|
|
||||||
const itemBottom = itemY + itemHeight;
|
|
||||||
const fadeHeight = 32;
|
|
||||||
const isLastItem = index === count - 1;
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY;
|
|
||||||
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
|
|
||||||
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
model: fileSearchController ? fileSearchController.model : null
|
|
||||||
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemClicked: function (index) {
|
|
||||||
if (fileSearchController) {
|
|
||||||
const item = fileSearchController.model.get(index);
|
|
||||||
fileSearchController.openFile(item.filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemRightClicked: function (index) {
|
|
||||||
if (fileSearchController) {
|
|
||||||
const item = fileSearchController.model.get(index);
|
|
||||||
fileSearchController.openFolder(item.filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
if (fileSearchController)
|
|
||||||
fileSearchController.keyboardNavigationActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property int index
|
|
||||||
required property string filePath
|
|
||||||
required property string fileName
|
|
||||||
required property string fileExtension
|
|
||||||
required property string fileType
|
|
||||||
required property string dirPath
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
height: filesList.itemHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: ListView.isCurrentItem ? Theme.widgetBaseHoverColor : fileMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: iconBackground
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: width / 2
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
visible: fileType !== "image"
|
|
||||||
|
|
||||||
DankNFIcon {
|
|
||||||
id: nerdIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
const lowerName = fileName.toLowerCase();
|
|
||||||
if (lowerName.startsWith("dockerfile"))
|
|
||||||
return "docker";
|
|
||||||
if (lowerName.startsWith("makefile"))
|
|
||||||
return "makefile";
|
|
||||||
if (lowerName.startsWith("license"))
|
|
||||||
return "license";
|
|
||||||
if (lowerName.startsWith("readme"))
|
|
||||||
return "readme";
|
|
||||||
return fileExtension.toLowerCase();
|
|
||||||
}
|
|
||||||
size: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: fileExtension ? (fileExtension.length > 4 ? fileExtension.substring(0, 4) : fileExtension) : "?"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Bold
|
|
||||||
visible: !nerdIcon.visible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
active: fileType === "image"
|
|
||||||
sourceComponent: Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: "file://" + filePath
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
asynchronous: true
|
|
||||||
cache: false
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1.0
|
|
||||||
maskSource: ShaderEffectSource {
|
|
||||||
sourceItem: Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: parent.width - 40 - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: fileName || ""
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: dirPath || ""
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: fileMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive)
|
|
||||||
filesList.currentIndex = index;
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
filesList.keyboardNavigationReset();
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
filesList.itemClicked(index);
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
filesList.itemRightClicked(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !fileSearchController || !fileSearchController.model || fileSearchController.model.count === 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
property string displayText: {
|
|
||||||
if (!fileSearchController) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (!DSearchService.dsearchAvailable) {
|
|
||||||
return I18n.tr("DankSearch not available");
|
|
||||||
}
|
|
||||||
if (fileSearchController.isSearching) {
|
|
||||||
return I18n.tr("Searching...");
|
|
||||||
}
|
|
||||||
if (fileSearchController.searchQuery.length === 0) {
|
|
||||||
return I18n.tr("Enter a search query");
|
|
||||||
}
|
|
||||||
if (!fileSearchController.model || fileSearchController.model.count === 0) {
|
|
||||||
return I18n.tr("No files found");
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
text: displayText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
visible: displayText.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,516 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Spotlight
|
|
||||||
import qs.Modules.AppDrawer
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: spotlightKeyHandler
|
|
||||||
|
|
||||||
LayoutMirroring.enabled: I18n.isRtl
|
|
||||||
LayoutMirroring.childrenInherit: true
|
|
||||||
|
|
||||||
property alias appLauncher: appLauncher
|
|
||||||
property alias searchField: searchField
|
|
||||||
property alias fileSearchController: fileSearchController
|
|
||||||
property alias resultsView: resultsView
|
|
||||||
property var parentModal: null
|
|
||||||
property string searchMode: "apps"
|
|
||||||
property bool usePopupContextMenu: false
|
|
||||||
|
|
||||||
function resetScroll() {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
resultsView.resetScroll();
|
|
||||||
} else {
|
|
||||||
fileSearchResults.resetScroll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSearchMode() {
|
|
||||||
if (searchField.text.startsWith("/")) {
|
|
||||||
if (searchMode !== "files") {
|
|
||||||
searchMode = "files";
|
|
||||||
}
|
|
||||||
const query = searchField.text.substring(1);
|
|
||||||
fileSearchController.searchQuery = query;
|
|
||||||
} else {
|
|
||||||
if (searchMode !== "apps") {
|
|
||||||
searchMode = "apps";
|
|
||||||
fileSearchController.reset();
|
|
||||||
appLauncher.searchQuery = searchField.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchModeChanged: {
|
|
||||||
if (searchMode === "files") {
|
|
||||||
appLauncher.keyboardNavigationActive = false;
|
|
||||||
} else {
|
|
||||||
fileSearchController.keyboardNavigationActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
clip: false
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide();
|
|
||||||
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
appLauncher.selectNext();
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectNext();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
appLauncher.selectPrevious();
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectPrevious();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
I18n.isRtl ? appLauncher.selectNextInRow() : appLauncher.selectPreviousInRow();
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
appLauncher.selectNext();
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectNext();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
appLauncher.selectPrevious();
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectPrevious();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
I18n.isRtl ? appLauncher.selectNextInRow() : appLauncher.selectPreviousInRow();
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow();
|
|
||||||
} else {
|
|
||||||
appLauncher.selectNext();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectNext();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow();
|
|
||||||
} else {
|
|
||||||
appLauncher.selectPrevious();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectPrevious();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectNextInRow();
|
|
||||||
} else {
|
|
||||||
appLauncher.selectNext();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectNext();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
if (appLauncher.viewMode === "grid") {
|
|
||||||
appLauncher.selectPreviousInRow();
|
|
||||||
} else {
|
|
||||||
appLauncher.selectPrevious();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileSearchController.selectPrevious();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
appLauncher.launchSelected();
|
|
||||||
} else if (searchMode === "files") {
|
|
||||||
fileSearchController.openSelected();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Menu || event.key == Qt.Key_F10) {
|
|
||||||
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);
|
|
||||||
menu.show(contentPos.x, contentPos.y, selectedApp, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLauncher {
|
|
||||||
id: appLauncher
|
|
||||||
|
|
||||||
viewMode: SettingsData.spotlightModalViewMode
|
|
||||||
gridColumns: SettingsData.appLauncherGridColumns
|
|
||||||
onAppLaunched: () => {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide();
|
|
||||||
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
|
|
||||||
NiriService.toggleOverview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onViewModeSelected: mode => {
|
|
||||||
SettingsData.set("spotlightModalViewMode", mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSearchController {
|
|
||||||
id: fileSearchController
|
|
||||||
|
|
||||||
onFileOpened: () => {
|
|
||||||
if (parentModal)
|
|
||||||
parentModal.hide();
|
|
||||||
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
|
|
||||||
NiriService.toggleOverview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightContextMenuPopup {
|
|
||||||
id: popupContextMenu
|
|
||||||
|
|
||||||
parent: spotlightKeyHandler
|
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
|
||||||
parentHandler: spotlightKeyHandler
|
|
||||||
searchField: spotlightKeyHandler.searchField
|
|
||||||
visible: false
|
|
||||||
z: 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: usePopupContextMenu && popupContextMenu.visible
|
|
||||||
hoverEnabled: true
|
|
||||||
z: 999
|
|
||||||
onClicked: popupContextMenu.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: layerContextMenuLoader
|
|
||||||
active: !spotlightKeyHandler.usePopupContextMenu
|
|
||||||
asynchronous: false
|
|
||||||
sourceComponent: Component {
|
|
||||||
SpotlightContextMenu {
|
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
|
||||||
parentHandler: spotlightKeyHandler
|
|
||||||
parentModal: spotlightKeyHandler.parentModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: parentModal
|
|
||||||
function onSpotlightOpenChanged() {
|
|
||||||
if (parentModal && !parentModal.spotlightOpen) {
|
|
||||||
if (layerContextMenuLoader.item) {
|
|
||||||
layerContextMenuLoader.item.hide();
|
|
||||||
}
|
|
||||||
popupContextMenu.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enabled: parentModal !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: searchRow
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 56
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: buttonsContainer.left
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
height: 56
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
normalBorderColor: Theme.outlineMedium
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
leftIconName: searchMode === "files" ? "folder" : "search"
|
|
||||||
leftIconSize: Theme.iconSize
|
|
||||||
leftIconColor: Theme.surfaceVariantText
|
|
||||||
leftIconFocusedColor: Theme.primary
|
|
||||||
showClearButton: true
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
|
||||||
placeholderText: ""
|
|
||||||
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
|
|
||||||
ignoreTabKeys: true
|
|
||||||
keyForwardTargets: [spotlightKeyHandler]
|
|
||||||
onTextChanged: {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
appLauncher.searchQuery = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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") {
|
|
||||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchSelected();
|
|
||||||
else if (appLauncher.model.count > 0)
|
|
||||||
appLauncher.launchApp(appLauncher.model.get(0));
|
|
||||||
} else if (searchMode === "files") {
|
|
||||||
if (fileSearchController.model.count > 0)
|
|
||||||
fileSearchController.openSelected();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
|
||||||
event.accepted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: buttonsContainer
|
|
||||||
width: viewModeButtons.visible ? viewModeButtons.width : (fileSearchButtons.visible ? fileSearchButtons.width : 0)
|
|
||||||
height: 36
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: viewModeButtons
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: searchMode === "apps"
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "view_list"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
appLauncher.setViewMode("list");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "grid_view"
|
|
||||||
size: 18
|
|
||||||
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: gridViewArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
appLauncher.setViewMode("grid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: fileSearchButtons
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: searchMode === "files"
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: filenameFilterButton
|
|
||||||
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "title"
|
|
||||||
size: 18
|
|
||||||
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: filenameFilterArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
fileSearchController.searchField = "filename";
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
filenameTooltipLoader.active = true;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (filenameTooltipLoader.item) {
|
|
||||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS);
|
|
||||||
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (filenameTooltipLoader.item)
|
|
||||||
filenameTooltipLoader.item.hide();
|
|
||||||
|
|
||||||
filenameTooltipLoader.active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contentFilterButton
|
|
||||||
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "description"
|
|
||||||
size: 18
|
|
||||||
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: contentFilterArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
fileSearchController.searchField = "body";
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
contentTooltipLoader.active = true;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentTooltipLoader.item) {
|
|
||||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS);
|
|
||||||
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (contentTooltipLoader.item)
|
|
||||||
contentTooltipLoader.item.hide();
|
|
||||||
|
|
||||||
contentTooltipLoader.active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
opacity: parentModal?.isClosing ? 0 : 1
|
|
||||||
|
|
||||||
SpotlightResults {
|
|
||||||
id: resultsView
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
menu.show(mouseX, mouseY, modelData, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSearchResults {
|
|
||||||
id: fileSearchResults
|
|
||||||
anchors.fill: parent
|
|
||||||
fileSearchController: spotlightKeyHandler.fileSearchController
|
|
||||||
visible: searchMode === "files"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: filenameTooltipLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
sourceComponent: DankTooltip {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentTooltipLoader
|
|
||||||
|
|
||||||
active: false
|
|
||||||
sourceComponent: DankTooltip {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Spotlight
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight-context-menu"
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
||||||
|
|
||||||
property var appLauncher: null
|
|
||||||
property var parentHandler: null
|
|
||||||
property var parentModal: null
|
|
||||||
property real menuPositionX: 0
|
|
||||||
property real menuPositionY: 0
|
|
||||||
|
|
||||||
readonly property real shadowBuffer: 5
|
|
||||||
|
|
||||||
screen: parentModal?.effectiveScreen
|
|
||||||
|
|
||||||
function show(x, y, app, fromKeyboard) {
|
|
||||||
fromKeyboard = fromKeyboard || false;
|
|
||||||
menuContent.currentApp = app;
|
|
||||||
|
|
||||||
let screenX = x;
|
|
||||||
let screenY = y;
|
|
||||||
|
|
||||||
if (parentModal) {
|
|
||||||
if (fromKeyboard) {
|
|
||||||
screenX = x + parentModal.alignedX;
|
|
||||||
screenY = y + parentModal.alignedY;
|
|
||||||
} else {
|
|
||||||
screenX = x + (parentModal.alignedX - shadowBuffer);
|
|
||||||
screenY = y + (parentModal.alignedY - shadowBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
menuPositionX = screenX;
|
|
||||||
menuPositionY = screenY;
|
|
||||||
|
|
||||||
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
|
|
||||||
menuContent.keyboardNavigation = true;
|
|
||||||
visible = true;
|
|
||||||
|
|
||||||
if (parentHandler) {
|
|
||||||
parentHandler.enabled = false;
|
|
||||||
}
|
|
||||||
Qt.callLater(() => {
|
|
||||||
menuContent.keyboardHandler.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
if (parentHandler) {
|
|
||||||
parentHandler.enabled = true;
|
|
||||||
}
|
|
||||||
visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
color: "transparent"
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible && parentHandler) {
|
|
||||||
parentHandler.enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightContextMenuContent {
|
|
||||||
id: menuContent
|
|
||||||
|
|
||||||
x: {
|
|
||||||
const left = 10;
|
|
||||||
const right = root.width - width - 10;
|
|
||||||
const want = menuPositionX;
|
|
||||||
return Math.max(left, Math.min(right, want));
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
const top = 10;
|
|
||||||
const bottom = root.height - height - 10;
|
|
||||||
const want = menuPositionY;
|
|
||||||
return Math.max(top, Math.min(bottom, want));
|
|
||||||
}
|
|
||||||
|
|
||||||
appLauncher: root.appLauncher
|
|
||||||
|
|
||||||
opacity: root.visible ? 1 : 0
|
|
||||||
visible: opacity > 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHideRequested: root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var currentApp: null
|
|
||||||
property var appLauncher: null
|
|
||||||
property int selectedMenuIndex: 0
|
|
||||||
property bool keyboardNavigation: false
|
|
||||||
|
|
||||||
signal hideRequested
|
|
||||||
|
|
||||||
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 var actualItem: (currentApp && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
|
||||||
|
|
||||||
function getPluginContextMenuActions() {
|
|
||||||
if (!currentApp || !currentApp.isPlugin || !actualItem)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const pluginId = appLauncher.getPluginIdForItem(actualItem);
|
|
||||||
if (!pluginId) {
|
|
||||||
console.log("[ContextMenu] No pluginId found for item:", JSON.stringify(actualItem.categories));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = PluginService.pluginInstances[pluginId];
|
|
||||||
if (!instance) {
|
|
||||||
console.log("[ContextMenu] No instance for pluginId:", pluginId);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (typeof instance.getContextMenuActions !== "function") {
|
|
||||||
console.log("[ContextMenu] Instance has no getContextMenuActions:", pluginId);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = instance.getContextMenuActions(actualItem);
|
|
||||||
if (!Array.isArray(actions))
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function executePluginAction(actionData) {
|
|
||||||
if (!currentApp || !actualItem)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const pluginId = appLauncher.getPluginIdForItem(actualItem);
|
|
||||||
if (!pluginId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const instance = PluginService.pluginInstances[pluginId];
|
|
||||||
if (!instance)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (typeof actionData === "function") {
|
|
||||||
actionData();
|
|
||||||
} else if (typeof instance.executeContextMenuAction === "function") {
|
|
||||||
instance.executeContextMenuAction(actualItem, actionData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.updateFilteredModel();
|
|
||||||
|
|
||||||
hideRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var menuItems: {
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
if (currentApp && currentApp.isPlugin) {
|
|
||||||
const pluginActions = getPluginContextMenuActions();
|
|
||||||
for (let i = 0; i < pluginActions.length; i++) {
|
|
||||||
const act = pluginActions[i];
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
icon: act.icon || "",
|
|
||||||
text: act.text || act.name || "",
|
|
||||||
action: () => executePluginAction(act.action)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (items.length === 0) {
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
icon: "content_copy",
|
|
||||||
text: I18n.tr("Copy"),
|
|
||||||
action: launchCurrentApp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appId = desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : "";
|
|
||||||
const isPinned = SessionData.isPinnedApp(appId);
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
icon: isPinned ? "keep_off" : "push_pin",
|
|
||||||
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
|
||||||
action: togglePin
|
|
||||||
});
|
|
||||||
|
|
||||||
if (desktopEntry && desktopEntry.actions) {
|
|
||||||
items.push({
|
|
||||||
type: "separator"
|
|
||||||
});
|
|
||||||
for (let i = 0; i < desktopEntry.actions.length; i++) {
|
|
||||||
const act = desktopEntry.actions[i];
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
text: act.name || "",
|
|
||||||
action: () => launchAction(act)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
type: "separator",
|
|
||||||
hidden: !desktopEntry || !desktopEntry.actions || desktopEntry.actions.length === 0
|
|
||||||
});
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
icon: "launch",
|
|
||||||
text: I18n.tr("Launch"),
|
|
||||||
action: launchCurrentApp
|
|
||||||
});
|
|
||||||
|
|
||||||
if (SessionService.nvidiaCommand) {
|
|
||||||
items.push({
|
|
||||||
type: "separator"
|
|
||||||
});
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
icon: "memory",
|
|
||||||
text: I18n.tr("Launch on dGPU"),
|
|
||||||
action: launchWithNvidia
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property int visibleItemCount: {
|
|
||||||
let count = 0;
|
|
||||||
for (let i = 0; i < menuItems.length; i++) {
|
|
||||||
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (visibleItemCount > 0) {
|
|
||||||
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (visibleItemCount > 0) {
|
|
||||||
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePin() {
|
|
||||||
if (!desktopEntry)
|
|
||||||
return;
|
|
||||||
const appId = desktopEntry.id || desktopEntry.execString || "";
|
|
||||||
if (SessionData.isPinnedApp(appId))
|
|
||||||
SessionData.removePinnedApp(appId);
|
|
||||||
else
|
|
||||||
SessionData.addPinnedApp(appId);
|
|
||||||
hideRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchCurrentApp() {
|
|
||||||
if (currentApp && appLauncher)
|
|
||||||
appLauncher.launchApp(currentApp);
|
|
||||||
hideRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchWithNvidia() {
|
|
||||||
if (desktopEntry) {
|
|
||||||
SessionService.launchDesktopEntry(desktopEntry, true);
|
|
||||||
if (appLauncher && currentApp) {
|
|
||||||
appLauncher.appLaunched(currentApp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hideRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchAction(action) {
|
|
||||||
if (desktopEntry) {
|
|
||||||
SessionService.launchDesktopAction(desktopEntry, action);
|
|
||||||
if (appLauncher && currentApp) {
|
|
||||||
appLauncher.appLaunched(currentApp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hideRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateSelected() {
|
|
||||||
let itemIndex = 0;
|
|
||||||
for (let i = 0; i < menuItems.length; i++) {
|
|
||||||
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
|
|
||||||
if (itemIndex === selectedMenuIndex) {
|
|
||||||
menuItems[i].action();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
itemIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias keyboardHandler: keyboardHandler
|
|
||||||
|
|
||||||
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
|
||||||
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
|
|
||||||
width: implicitWidth
|
|
||||||
height: implicitHeight
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: menuContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: keyboardHandler
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: keyboardNavigation
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Down:
|
|
||||||
selectNext();
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Up:
|
|
||||||
selectPrevious();
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
activateSelected();
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Escape:
|
|
||||||
case Qt.Key_Left:
|
|
||||||
hideRequested();
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: menuItems
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: modelData.type === "separator" ? 5 : 32
|
|
||||||
visible: !modelData.hidden
|
|
||||||
|
|
||||||
property int itemIndex: {
|
|
||||||
let count = 0;
|
|
||||||
for (let i = 0; i < index; i++) {
|
|
||||||
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: modelData.type === "separator"
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: parent.height
|
|
||||||
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: modelData.type === "item"
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (keyboardNavigation && selectedMenuIndex === itemIndex) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
|
|
||||||
}
|
|
||||||
return mouseArea.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.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: Theme.iconSize - 2
|
|
||||||
height: Theme.iconSize - 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
visible: modelData.icon !== undefined && modelData.icon !== ""
|
|
||||||
name: modelData.icon || ""
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onEntered: {
|
|
||||||
keyboardNavigation = false;
|
|
||||||
selectedMenuIndex = itemIndex;
|
|
||||||
}
|
|
||||||
onClicked: modelData.action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Spotlight
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var appLauncher: null
|
|
||||||
property var parentHandler: null
|
|
||||||
property var searchField: null
|
|
||||||
|
|
||||||
function show(x, y, app, fromKeyboard) {
|
|
||||||
fromKeyboard = fromKeyboard || false;
|
|
||||||
menuContent.currentApp = app;
|
|
||||||
|
|
||||||
root.x = x + 4;
|
|
||||||
root.y = y + 4;
|
|
||||||
|
|
||||||
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
|
|
||||||
menuContent.keyboardNavigation = true;
|
|
||||||
|
|
||||||
if (parentHandler) {
|
|
||||||
parentHandler.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
menuContent.keyboardHandler.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
if (parentHandler) {
|
|
||||||
parentHandler.enabled = true;
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
width: menuContent.implicitWidth
|
|
||||||
height: menuContent.implicitHeight
|
|
||||||
padding: 0
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
modal: true
|
|
||||||
dim: false
|
|
||||||
background: Item {}
|
|
||||||
|
|
||||||
onClosed: {
|
|
||||||
if (parentHandler) {
|
|
||||||
parentHandler.enabled = true;
|
|
||||||
}
|
|
||||||
if (searchField) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
searchField.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: SpotlightContextMenuContent {
|
|
||||||
id: menuContent
|
|
||||||
appLauncher: root.appLauncher
|
|
||||||
onHideRequested: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: spotlightModal
|
|
||||||
|
|
||||||
layerNamespace: "dms:spotlight"
|
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
|
||||||
windows: [spotlightModal.contentWindow]
|
|
||||||
active: spotlightModal.useHyprlandFocusGrab && spotlightModal.shouldHaveFocus
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool spotlightOpen: false
|
|
||||||
property alias spotlightContent: spotlightContentInstance
|
|
||||||
property bool openedFromOverview: false
|
|
||||||
property bool isClosing: false
|
|
||||||
|
|
||||||
function resetContent() {
|
|
||||||
if (!spotlightContent)
|
|
||||||
return;
|
|
||||||
if (spotlightContent.appLauncher)
|
|
||||||
spotlightContent.appLauncher.reset();
|
|
||||||
if (spotlightContent.fileSearchController)
|
|
||||||
spotlightContent.fileSearchController.reset();
|
|
||||||
if (spotlightContent.resetScroll)
|
|
||||||
spotlightContent.resetScroll();
|
|
||||||
if (spotlightContent.searchField)
|
|
||||||
spotlightContent.searchField.text = "";
|
|
||||||
spotlightContent.searchMode = "apps";
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
openedFromOverview = false;
|
|
||||||
isClosing = false;
|
|
||||||
resetContent();
|
|
||||||
spotlightOpen = true;
|
|
||||||
open();
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (spotlightContent?.appLauncher)
|
|
||||||
spotlightContent.appLauncher.ensureInitialized();
|
|
||||||
if (spotlightContent?.searchField)
|
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWithQuery(query) {
|
|
||||||
openedFromOverview = false;
|
|
||||||
isClosing = false;
|
|
||||||
resetContent();
|
|
||||||
spotlightOpen = true;
|
|
||||||
if (spotlightContent?.searchField)
|
|
||||||
spotlightContent.searchField.text = query;
|
|
||||||
open();
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (spotlightContent?.appLauncher) {
|
|
||||||
spotlightContent.appLauncher.ensureInitialized();
|
|
||||||
spotlightContent.appLauncher.searchQuery = query;
|
|
||||||
}
|
|
||||||
if (spotlightContent?.searchField)
|
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
openedFromOverview = false;
|
|
||||||
isClosing = true;
|
|
||||||
spotlightOpen = false;
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDialogClosed: {
|
|
||||||
isClosing = false;
|
|
||||||
resetContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (spotlightOpen) {
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: spotlightOpen
|
|
||||||
modalWidth: 500
|
|
||||||
modalHeight: 600
|
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
borderColor: Theme.outlineMedium
|
|
||||||
borderWidth: 1
|
|
||||||
enableShadow: true
|
|
||||||
keepContentLoaded: true
|
|
||||||
animationScaleCollapsed: 0.96
|
|
||||||
animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
||||||
animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
||||||
animationExitCurve: Theme.expressiveCurves.emphasized
|
|
||||||
onVisibleChanged: () => {
|
|
||||||
if (visible && !spotlightOpen) {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
if (visible && spotlightContent) {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (spotlightContent.searchField) {
|
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
|
||||||
spotlightOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: ModalManager
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
spotlightModal.show();
|
|
||||||
return "SPOTLIGHT_OPEN_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
spotlightModal.hide();
|
|
||||||
return "SPOTLIGHT_CLOSE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
spotlightModal.toggle();
|
|
||||||
return "SPOTLIGHT_TOGGLE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function openQuery(query: string): string {
|
|
||||||
spotlightModal.showWithQuery(query);
|
|
||||||
return "SPOTLIGHT_OPEN_QUERY_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleQuery(query: string): string {
|
|
||||||
if (spotlightModal.spotlightOpen) {
|
|
||||||
spotlightModal.hide();
|
|
||||||
} else {
|
|
||||||
spotlightModal.showWithQuery(query);
|
|
||||||
}
|
|
||||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "spotlight"
|
|
||||||
}
|
|
||||||
|
|
||||||
SpotlightContent {
|
|
||||||
id: spotlightContentInstance
|
|
||||||
|
|
||||||
parentModal: spotlightModal
|
|
||||||
}
|
|
||||||
|
|
||||||
directContent: spotlightContentInstance
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: resultsContainer
|
|
||||||
|
|
||||||
property var appLauncher: null
|
|
||||||
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function resetScroll() {
|
|
||||||
resultsList.contentY = 0;
|
|
||||||
if (gridLoader.item) {
|
|
||||||
gridLoader.item.contentY = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedItemPosition() {
|
|
||||||
if (!appLauncher)
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedIndex = appLauncher.selectedIndex;
|
|
||||||
if (appLauncher.viewMode === "list") {
|
|
||||||
const itemY = selectedIndex * (resultsList.itemHeight + resultsList.itemSpacing) - resultsList.contentY;
|
|
||||||
return {
|
|
||||||
x: resultsList.width / 2,
|
|
||||||
y: itemY + resultsList.itemHeight / 2
|
|
||||||
};
|
|
||||||
} else if (gridLoader.item) {
|
|
||||||
const grid = gridLoader.item;
|
|
||||||
const row = Math.floor(selectedIndex / grid.actualColumns);
|
|
||||||
const col = selectedIndex % grid.actualColumns;
|
|
||||||
const itemX = col * grid.cellWidth + grid.leftMargin + grid.cellWidth / 2;
|
|
||||||
const itemY = row * grid.cellHeight - grid.contentY + grid.cellHeight / 2;
|
|
||||||
return {
|
|
||||||
x: itemX,
|
|
||||||
y: itemY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
height: 32
|
|
||||||
z: 100
|
|
||||||
visible: {
|
|
||||||
if (!appLauncher)
|
|
||||||
return false;
|
|
||||||
const view = appLauncher.viewMode === "list" ? resultsList : (gridLoader.item || resultsList);
|
|
||||||
const isLastItem = appLauncher.viewMode === "list" ? view.currentIndex >= view.count - 1 : (gridLoader.item ? Math.floor(view.currentIndex / view.actualColumns) >= Math.floor((view.count - 1) / view.actualColumns) : false);
|
|
||||||
const hasOverflow = view.contentHeight > view.height;
|
|
||||||
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
|
|
||||||
return hasOverflow && (!isLastItem || !atBottom);
|
|
||||||
}
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.0
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 1.0
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: resultsList
|
|
||||||
|
|
||||||
property int itemHeight: 60
|
|
||||||
property int iconSize: 40
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return;
|
|
||||||
const itemY = index * (itemHeight + itemSpacing);
|
|
||||||
const itemBottom = itemY + itemHeight;
|
|
||||||
const fadeHeight = 32;
|
|
||||||
const isLastItem = index === count - 1;
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY;
|
|
||||||
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
|
|
||||||
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "list"
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData);
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
resultsContainer.itemRightClicked(index, modelData, mouseX, mouseY);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: AppLauncherListDelegate {
|
|
||||||
listView: resultsList
|
|
||||||
itemHeight: resultsList.itemHeight
|
|
||||||
iconSize: resultsList.iconSize
|
|
||||||
showDescription: resultsList.showDescription
|
|
||||||
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
|
|
||||||
keyboardNavigationActive: resultsList.keyboardNavigationActive
|
|
||||||
isCurrentItem: ListView.isCurrentItem
|
|
||||||
iconMaterialSizeAdjustment: 0
|
|
||||||
iconUnicodeScale: 0.8
|
|
||||||
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
|
|
||||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
|
||||||
resultsList.itemRightClicked(idx, modelData, mouseX, mouseY);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: gridLoader
|
|
||||||
|
|
||||||
property real _lastWidth: 0
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
|
||||||
active: appLauncher && appLauncher.viewMode === "grid"
|
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (item) {
|
|
||||||
item.appLauncher = Qt.binding(() => resultsContainer.appLauncher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onWidthChanged: {
|
|
||||||
if (visible && Math.abs(width - _lastWidth) > 1) {
|
|
||||||
_lastWidth = width;
|
|
||||||
active = false;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
active = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceComponent: Component {
|
|
||||||
DankGridView {
|
|
||||||
id: resultsGrid
|
|
||||||
|
|
||||||
property var appLauncher: null
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
property int columns: appLauncher ? appLauncher.gridColumns : 4
|
|
||||||
property bool adaptiveColumns: false
|
|
||||||
property int minCellWidth: 120
|
|
||||||
property int maxCellWidth: 160
|
|
||||||
property real iconSizeRatio: 0.55
|
|
||||||
property int maxIconSize: 48
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
property real baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : width / columns
|
|
||||||
property real baseCellHeight: baseCellWidth + 20
|
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return;
|
|
||||||
const itemY = Math.floor(index / actualColumns) * cellHeight;
|
|
||||||
const itemBottom = itemY + cellHeight;
|
|
||||||
const fadeHeight = 32;
|
|
||||||
const isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY;
|
|
||||||
else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
|
|
||||||
contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
clip: true
|
|
||||||
cellWidth: baseCellWidth
|
|
||||||
cellHeight: baseCellHeight
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData);
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
resultsContainer.itemRightClicked(index, modelData, mouseX, mouseY);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: AppLauncherGridDelegate {
|
|
||||||
gridView: resultsGrid
|
|
||||||
cellWidth: resultsGrid.cellWidth
|
|
||||||
cellHeight: resultsGrid.cellHeight
|
|
||||||
minIconSize: resultsGrid.minIconSize
|
|
||||||
maxIconSize: resultsGrid.maxIconSize
|
|
||||||
iconSizeRatio: resultsGrid.iconSizeRatio
|
|
||||||
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
|
|
||||||
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
|
|
||||||
currentIndex: resultsGrid.currentIndex
|
|
||||||
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
|
|
||||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
|
||||||
resultsGrid.itemRightClicked(idx, modelData, mouseX, mouseY);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Spotlight
|
import qs.Modals.DankLauncherV2
|
||||||
import qs.Modules.AppDrawer
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
DankPopout {
|
DankPopout {
|
||||||
@@ -13,100 +8,66 @@ DankPopout {
|
|||||||
|
|
||||||
layerNamespace: "dms:app-launcher"
|
layerNamespace: "dms:app-launcher"
|
||||||
|
|
||||||
property string searchMode: "apps"
|
|
||||||
property alias fileSearch: fileSearchController
|
|
||||||
|
|
||||||
function updateSearchMode(text) {
|
|
||||||
if (text.startsWith("/")) {
|
|
||||||
if (searchMode === "files") {
|
|
||||||
fileSearchController.searchQuery = text.substring(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
searchMode = "files";
|
|
||||||
fileSearchController.searchQuery = text.substring(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
searchMode = "apps";
|
|
||||||
fileSearchController.reset();
|
|
||||||
appLauncher.searchQuery = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
open();
|
open();
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: 520
|
popupWidth: 560
|
||||||
popupHeight: 600
|
popupHeight: 640
|
||||||
triggerWidth: 40
|
triggerWidth: 40
|
||||||
positioning: ""
|
positioning: ""
|
||||||
|
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||||
|
|
||||||
onBackgroundClicked: {
|
onBackgroundClicked: {
|
||||||
if (contextMenu.visible) {
|
if (contentLoader.item?.launcherContent?.editMode) {
|
||||||
contextMenu.close();
|
contentLoader.item.launcherContent.closeEditMode();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
searchMode = "apps";
|
var lc = contentLoader.item?.launcherContent;
|
||||||
appLauncher.ensureInitialized();
|
if (!lc)
|
||||||
appLauncher.searchQuery = "";
|
return;
|
||||||
appLauncher.selectedIndex = 0;
|
if (lc.searchField) {
|
||||||
appLauncher.setCategory(I18n.tr("All"));
|
lc.searchField.text = "";
|
||||||
fileSearchController.reset();
|
lc.searchField.forceActiveFocus();
|
||||||
if (contentLoader.item?.searchField) {
|
|
||||||
contentLoader.item.searchField.text = "";
|
|
||||||
contentLoader.item.searchField.forceActiveFocus();
|
|
||||||
}
|
}
|
||||||
contextMenu.parent = contentLoader.item;
|
if (lc.controller) {
|
||||||
}
|
lc.controller.searchMode = "apps";
|
||||||
|
lc.controller.pluginFilter = "";
|
||||||
AppLauncher {
|
lc.controller.searchQuery = "";
|
||||||
id: appLauncher
|
lc.controller.performSearch();
|
||||||
|
|
||||||
viewMode: SettingsData.appLauncherViewMode
|
|
||||||
gridColumns: SettingsData.appLauncherGridColumns
|
|
||||||
onAppLaunched: appDrawerPopout.close()
|
|
||||||
onViewModeSelected: function (mode) {
|
|
||||||
SettingsData.set("appLauncherViewMode", mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSearchController {
|
|
||||||
id: fileSearchController
|
|
||||||
|
|
||||||
onFileOpened: appDrawerPopout.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchModeChanged: {
|
|
||||||
switch (searchMode) {
|
|
||||||
case "files":
|
|
||||||
appLauncher.keyboardNavigationActive = false;
|
|
||||||
break;
|
|
||||||
case "apps":
|
|
||||||
fileSearchController.keyboardNavigationActive = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
lc.resetScroll?.();
|
||||||
|
lc.actionPanel?.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: launcherPanel
|
id: contentContainer
|
||||||
|
|
||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
property alias searchField: searchField
|
property alias launcherContent: launcherContent
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
smooth: true
|
smooth: true
|
||||||
|
|
||||||
// Multi-layer border effect
|
QtObject {
|
||||||
|
id: modalAdapter
|
||||||
|
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||||
|
property bool isClosing: false
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
appDrawerPopout.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: [
|
model: [
|
||||||
{
|
{
|
||||||
@@ -136,824 +97,25 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
FocusScope {
|
||||||
id: keyHandler
|
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
function selectNext() {
|
LauncherContent {
|
||||||
switch (appDrawerPopout.searchMode) {
|
id: launcherContent
|
||||||
case "files":
|
anchors.fill: parent
|
||||||
fileSearchController.selectNext();
|
parentModal: modalAdapter
|
||||||
return;
|
viewModeContext: "appDrawer"
|
||||||
default:
|
|
||||||
appLauncher.selectNext();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPrevious() {
|
Keys.onEscapePressed: event => {
|
||||||
switch (appDrawerPopout.searchMode) {
|
if (launcherContent.editMode) {
|
||||||
case "files":
|
launcherContent.closeEditMode();
|
||||||
fileSearchController.selectPrevious();
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
appLauncher.selectPrevious();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateSelected() {
|
|
||||||
switch (appDrawerPopout.searchMode) {
|
|
||||||
case "files":
|
|
||||||
fileSearchController.openSelected();
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
appLauncher.launchSelected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var keyMappings: {
|
|
||||||
const mappings = {};
|
|
||||||
mappings[Qt.Key_Escape] = () => appDrawerPopout.close();
|
|
||||||
mappings[Qt.Key_Down] = () => keyHandler.selectNext();
|
|
||||||
mappings[Qt.Key_Up] = () => keyHandler.selectPrevious();
|
|
||||||
mappings[Qt.Key_Return] = () => keyHandler.activateSelected();
|
|
||||||
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();
|
|
||||||
|
|
||||||
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
mappings[Qt.Key_Right] = () => I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
|
|
||||||
mappings[Qt.Key_Left] = () => I18n.isRtl ? appLauncher.selectNextInRow() : appLauncher.selectPreviousInRow();
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappings;
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (keyMappings[event.key]) {
|
|
||||||
keyMappings[event.key]();
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
appDrawerPopout.close();
|
||||||
const hasCtrl = event.modifiers & Qt.ControlModifier;
|
event.accepted = true;
|
||||||
if (!hasCtrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_N:
|
|
||||||
case Qt.Key_J:
|
|
||||||
keyHandler.selectNext();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
case Qt.Key_P:
|
|
||||||
case Qt.Key_K:
|
|
||||||
keyHandler.selectPrevious();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
case Qt.Key_L:
|
|
||||||
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case Qt.Key_H:
|
|
||||||
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
|
|
||||||
I18n.isRtl ? appLauncher.selectNextInRow() : appLauncher.selectPreviousInRow();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: parent.height - Theme.spacingS * 2
|
|
||||||
x: Theme.spacingS
|
|
||||||
y: Theme.spacingS
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: appDrawerPopout.searchMode === "files" ? I18n.tr("Files") : I18n.tr("Applications")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge + 4
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: {
|
|
||||||
switch (appDrawerPopout.searchMode) {
|
|
||||||
case "files":
|
|
||||||
return fileSearchController.model.count + " " + I18n.tr("files");
|
|
||||||
default:
|
|
||||||
return appLauncher.model.count + " " + I18n.tr("apps");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
height: 52
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
normalBorderColor: Theme.outlineMedium
|
|
||||||
focusedBorderColor: Theme.primary
|
|
||||||
leftIconName: appDrawerPopout.searchMode === "files" ? "folder" : "search"
|
|
||||||
leftIconSize: Theme.iconSize
|
|
||||||
leftIconColor: Theme.surfaceVariantText
|
|
||||||
leftIconFocusedColor: Theme.primary
|
|
||||||
showClearButton: true
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
enabled: appDrawerPopout.shouldBeVisible
|
|
||||||
ignoreLeftRightKeys: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode !== "list"
|
|
||||||
ignoreTabKeys: true
|
|
||||||
keyForwardTargets: [keyHandler]
|
|
||||||
onTextChanged: {
|
|
||||||
if (appDrawerPopout.searchMode === "apps") {
|
|
||||||
appLauncher.searchQuery = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onTextEdited: {
|
|
||||||
appDrawerPopout.updateSearchMode(text);
|
|
||||||
}
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
appDrawerPopout.close();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key);
|
|
||||||
const hasText = text.length > 0;
|
|
||||||
|
|
||||||
if (isEnterKey && hasText) {
|
|
||||||
switch (appDrawerPopout.searchMode) {
|
|
||||||
case "files":
|
|
||||||
if (fileSearchController.model.count > 0) {
|
|
||||||
fileSearchController.openSelected();
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
|
|
||||||
appLauncher.launchSelected();
|
|
||||||
} else if (appLauncher.model.count > 0) {
|
|
||||||
appLauncher.launchApp(appLauncher.model.get(0));
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab];
|
|
||||||
const isNavigationKey = navigationKeys.includes(event.key);
|
|
||||||
const isEmptyEnter = isEnterKey && !hasText;
|
|
||||||
|
|
||||||
event.accepted = !(isNavigationKey || isEmptyEnter);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (!appDrawerPopout.shouldBeVisible) {
|
|
||||||
searchField.focus = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: appDrawerPopout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 40
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: appDrawerPopout.searchMode === "apps"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 180
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
anchors.fill: parent
|
|
||||||
text: ""
|
|
||||||
dropdownWidth: 180
|
|
||||||
currentValue: appLauncher.selectedCategory
|
|
||||||
options: appLauncher.categories
|
|
||||||
optionIcons: appLauncher.categoryIcons
|
|
||||||
onValueChanged: function (value) {
|
|
||||||
appLauncher.setCategory(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 36
|
|
||||||
circular: false
|
|
||||||
iconName: "view_list"
|
|
||||||
iconSize: 20
|
|
||||||
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
|
||||||
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("list");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
buttonSize: 36
|
|
||||||
circular: false
|
|
||||||
iconName: "grid_view"
|
|
||||||
iconSize: 20
|
|
||||||
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
|
||||||
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
onClicked: {
|
|
||||||
appLauncher.setViewMode("grid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: searchField.width
|
|
||||||
x: searchField.x
|
|
||||||
height: {
|
|
||||||
let usedHeight = 40 + Theme.spacingS;
|
|
||||||
usedHeight += 52 + Theme.spacingS;
|
|
||||||
usedHeight += appDrawerPopout.searchMode === "apps" ? 40 : 0;
|
|
||||||
return parent.height - usedHeight;
|
|
||||||
}
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
height: 32
|
|
||||||
z: 100
|
|
||||||
visible: {
|
|
||||||
if (appDrawerPopout.searchMode !== "apps")
|
|
||||||
return false;
|
|
||||||
const view = appLauncher.viewMode === "list" ? appList : appGrid;
|
|
||||||
const isLastItem = view.currentIndex >= view.count - 1;
|
|
||||||
const hasOverflow = view.contentHeight > view.height;
|
|
||||||
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
|
|
||||||
return hasOverflow && (!isLastItem || !atBottom);
|
|
||||||
}
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.0
|
|
||||||
color: "transparent"
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 1.0
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: appList
|
|
||||||
|
|
||||||
property int itemHeight: 72
|
|
||||||
property int iconSize: 56
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return;
|
|
||||||
var itemY = index * (itemHeight + itemSpacing);
|
|
||||||
var itemBottom = itemY + itemHeight;
|
|
||||||
var fadeHeight = 32;
|
|
||||||
var isLastItem = index === count - 1;
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY;
|
|
||||||
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
|
|
||||||
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "list"
|
|
||||||
model: appLauncher.model
|
|
||||||
currentIndex: appLauncher.selectedIndex
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemClicked: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData);
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: AppLauncherListDelegate {
|
|
||||||
listView: appList
|
|
||||||
itemHeight: appList.itemHeight
|
|
||||||
iconSize: appList.iconSize
|
|
||||||
showDescription: appList.showDescription
|
|
||||||
hoverUpdatesSelection: appList.hoverUpdatesSelection
|
|
||||||
keyboardNavigationActive: appList.keyboardNavigationActive
|
|
||||||
isCurrentItem: ListView.isCurrentItem
|
|
||||||
mouseAreaLeftMargin: Theme.spacingS
|
|
||||||
mouseAreaRightMargin: Theme.spacingS
|
|
||||||
mouseAreaBottomMargin: Theme.spacingM
|
|
||||||
iconMargins: Theme.spacingXS
|
|
||||||
iconFallbackLeftMargin: Theme.spacingS
|
|
||||||
iconFallbackRightMargin: Theme.spacingS
|
|
||||||
iconFallbackBottomMargin: Theme.spacingM
|
|
||||||
onItemClicked: (idx, modelData) => appList.itemClicked(idx, modelData)
|
|
||||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
|
||||||
const panelPos = contextMenu.parent.mapFromItem(null, mouseX, mouseY);
|
|
||||||
appList.itemRightClicked(idx, modelData, panelPos.x, panelPos.y);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: appList.keyboardNavigationReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: appGrid
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher.selectedIndex
|
|
||||||
property int columns: appLauncher.gridColumns
|
|
||||||
property bool adaptiveColumns: false
|
|
||||||
property int minCellWidth: 120
|
|
||||||
property int maxCellWidth: 160
|
|
||||||
property real iconSizeRatio: 0.6
|
|
||||||
property int maxIconSize: 56
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
|
||||||
property real baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : width / columns
|
|
||||||
property real baseCellHeight: baseCellWidth + 20
|
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
|
||||||
|
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return;
|
|
||||||
var itemY = Math.floor(index / actualColumns) * cellHeight;
|
|
||||||
var itemBottom = itemY + cellHeight;
|
|
||||||
var fadeHeight = 32;
|
|
||||||
var isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY;
|
|
||||||
else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
|
|
||||||
contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.bottomMargin: 1
|
|
||||||
visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid"
|
|
||||||
model: appLauncher.model
|
|
||||||
clip: true
|
|
||||||
cellWidth: baseCellWidth
|
|
||||||
cellHeight: baseCellHeight
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemClicked: function (index, modelData) {
|
|
||||||
appLauncher.launchApp(modelData);
|
|
||||||
}
|
|
||||||
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: {
|
|
||||||
appLauncher.keyboardNavigationActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: AppLauncherGridDelegate {
|
|
||||||
gridView: appGrid
|
|
||||||
cellWidth: appGrid.cellWidth
|
|
||||||
cellHeight: appGrid.cellHeight
|
|
||||||
minIconSize: appGrid.minIconSize
|
|
||||||
maxIconSize: appGrid.maxIconSize
|
|
||||||
iconSizeRatio: appGrid.iconSizeRatio
|
|
||||||
hoverUpdatesSelection: appGrid.hoverUpdatesSelection
|
|
||||||
keyboardNavigationActive: appGrid.keyboardNavigationActive
|
|
||||||
currentIndex: appGrid.currentIndex
|
|
||||||
mouseAreaLeftMargin: Theme.spacingS
|
|
||||||
mouseAreaRightMargin: Theme.spacingS
|
|
||||||
mouseAreaBottomMargin: Theme.spacingS
|
|
||||||
iconFallbackLeftMargin: Theme.spacingS
|
|
||||||
iconFallbackRightMargin: Theme.spacingS
|
|
||||||
iconFallbackBottomMargin: Theme.spacingS
|
|
||||||
iconMaterialSizeAdjustment: Theme.spacingL
|
|
||||||
onItemClicked: (idx, modelData) => appGrid.itemClicked(idx, modelData)
|
|
||||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
|
||||||
const panelPos = contextMenu.parent.mapFromItem(null, mouseX, mouseY);
|
|
||||||
appGrid.itemRightClicked(idx, modelData, panelPos.x, panelPos.y);
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: appGrid.keyboardNavigationReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSearchResults {
|
|
||||||
anchors.fill: parent
|
|
||||||
fileSearchController: appDrawerPopout.fileSearch
|
|
||||||
visible: appDrawerPopout.searchMode === "files"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: contextMenu.visible
|
|
||||||
z: 998
|
|
||||||
onClicked: contextMenu.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
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)
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// DEVELOPER NOTE: This component manages the AppDrawer launcher (accessed via DankBar icon).
|
|
||||||
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
|
|
||||||
// likely require corresponding updates in Modals/Spotlight/SpotlightResults.qml and vice versa.
|
|
||||||
|
|
||||||
property string searchQuery: ""
|
|
||||||
property string selectedCategory: I18n.tr("All")
|
|
||||||
property string viewMode: "list" // "list" or "grid"
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int maxResults: 50
|
|
||||||
property int gridColumns: 4
|
|
||||||
property bool debounceSearch: true
|
|
||||||
property int debounceInterval: 50
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool suppressUpdatesWhileLaunching: false
|
|
||||||
property var categories: []
|
|
||||||
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
|
||||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
|
||||||
property alias model: filteredModel
|
|
||||||
property var _uniqueApps: []
|
|
||||||
property bool _initialized: false
|
|
||||||
property bool _isTriggered: false
|
|
||||||
property string _triggeredCategory: ""
|
|
||||||
property bool _updatingFromTrigger: false
|
|
||||||
|
|
||||||
signal appLaunched(var app)
|
|
||||||
signal categorySelected(string category)
|
|
||||||
signal viewModeSelected(string mode)
|
|
||||||
|
|
||||||
function ensureInitialized() {
|
|
||||||
if (_initialized)
|
|
||||||
return;
|
|
||||||
_initialized = true;
|
|
||||||
updateCategories();
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCategories() {
|
|
||||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science");
|
|
||||||
const result = [I18n.tr("All")];
|
|
||||||
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")));
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: PluginService
|
|
||||||
function onPluginLoaded() {
|
|
||||||
updateCategories();
|
|
||||||
}
|
|
||||||
function onPluginUnloaded() {
|
|
||||||
updateCategories();
|
|
||||||
}
|
|
||||||
function onPluginListUpdated() {
|
|
||||||
updateCategories();
|
|
||||||
}
|
|
||||||
function onRequestLauncherUpdate(pluginId) {
|
|
||||||
// Only update if we are actually looking at this plugin or in All category
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onSortAppsAlphabeticallyChanged() {
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFilteredModel() {
|
|
||||||
if (suppressUpdatesWhileLaunching) {
|
|
||||||
suppressUpdatesWhileLaunching = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
filteredModel.clear();
|
|
||||||
selectedIndex = 0;
|
|
||||||
keyboardNavigationActive = false;
|
|
||||||
|
|
||||||
const triggerResult = checkPluginTriggers(searchQuery);
|
|
||||||
if (triggerResult.triggered) {
|
|
||||||
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId);
|
|
||||||
}
|
|
||||||
|
|
||||||
let apps = [];
|
|
||||||
const allCategory = I18n.tr("All");
|
|
||||||
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : [];
|
|
||||||
|
|
||||||
if (triggerResult.triggered) {
|
|
||||||
_isTriggered = true;
|
|
||||||
_triggeredCategory = triggerResult.pluginCategory;
|
|
||||||
_updatingFromTrigger = true;
|
|
||||||
selectedCategory = triggerResult.pluginCategory;
|
|
||||||
_updatingFromTrigger = false;
|
|
||||||
if (triggerResult.isBuiltIn) {
|
|
||||||
apps = AppSearchService.getBuiltInLauncherItems(triggerResult.pluginId, triggerResult.query);
|
|
||||||
} else {
|
|
||||||
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (_isTriggered) {
|
|
||||||
_updatingFromTrigger = true;
|
|
||||||
selectedCategory = allCategory;
|
|
||||||
_updatingFromTrigger = false;
|
|
||||||
_isTriggered = false;
|
|
||||||
_triggeredCategory = "";
|
|
||||||
}
|
|
||||||
if (searchQuery.length === 0) {
|
|
||||||
if (selectedCategory === allCategory) {
|
|
||||||
let emptyTriggerItems = [];
|
|
||||||
emptyTriggerPlugins.forEach(pluginId => {
|
|
||||||
const plugin = PluginService.getLauncherPlugin(pluginId);
|
|
||||||
const pluginCategory = plugin.name || pluginId;
|
|
||||||
const items = AppSearchService.getPluginItems(pluginCategory, "");
|
|
||||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
|
||||||
});
|
|
||||||
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
|
|
||||||
builtInEmptyTrigger.forEach(pluginId => {
|
|
||||||
const items = AppSearchService.getBuiltInLauncherItems(pluginId, "");
|
|
||||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
|
||||||
});
|
|
||||||
const coreItems = AppSearchService.getCoreApps("");
|
|
||||||
apps = AppSearchService.applications.concat(emptyTriggerItems).concat(coreItems);
|
|
||||||
} else {
|
|
||||||
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
|
|
||||||
const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory));
|
|
||||||
apps = apps.concat(coreItems);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedCategory === allCategory) {
|
|
||||||
apps = AppSearchService.searchApplications(searchQuery);
|
|
||||||
|
|
||||||
let emptyTriggerItems = [];
|
|
||||||
emptyTriggerPlugins.forEach(pluginId => {
|
|
||||||
const plugin = PluginService.getLauncherPlugin(pluginId);
|
|
||||||
const pluginCategory = plugin.name || pluginId;
|
|
||||||
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery);
|
|
||||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
|
||||||
});
|
|
||||||
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
|
|
||||||
builtInEmptyTrigger.forEach(pluginId => {
|
|
||||||
const items = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
|
|
||||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
|
||||||
});
|
|
||||||
|
|
||||||
const coreItems = AppSearchService.getCoreApps(searchQuery);
|
|
||||||
apps = apps.concat(emptyTriggerItems).concat(coreItems);
|
|
||||||
} else {
|
|
||||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
|
|
||||||
if (categoryApps.length > 0) {
|
|
||||||
const allSearchResults = AppSearchService.searchApplications(searchQuery);
|
|
||||||
const categoryNames = new Set(categoryApps.map(app => app.name));
|
|
||||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults);
|
|
||||||
} else {
|
|
||||||
apps = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const coreItems = AppSearchService.getCoreApps(searchQuery).filter(app => app.categories.includes(selectedCategory));
|
|
||||||
apps = apps.concat(coreItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery.length === 0) {
|
|
||||||
if (SettingsData.sortAppsAlphabetically) {
|
|
||||||
apps = apps.sort((a, b) => {
|
|
||||||
return (a.name || "").localeCompare(b.name || "");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
apps = apps.sort((a, b) => {
|
|
||||||
const aId = a.id || a.execString || a.exec || "";
|
|
||||||
const bId = b.id || b.execString || b.exec || "";
|
|
||||||
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
|
|
||||||
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
|
|
||||||
if (aUsage !== bUsage) {
|
|
||||||
return bUsage - aUsage;
|
|
||||||
}
|
|
||||||
return (a.name || "").localeCompare(b.name || "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const seenNames = new Set();
|
|
||||||
const uniqueApps = [];
|
|
||||||
apps.forEach(app => {
|
|
||||||
if (app) {
|
|
||||||
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "");
|
|
||||||
if (seenNames.has(itemKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seenNames.add(itemKey);
|
|
||||||
uniqueApps.push(app);
|
|
||||||
|
|
||||||
const isPluginItem = app.isCore ? false : (app.action !== undefined);
|
|
||||||
filteredModel.append({
|
|
||||||
"name": app.name || "",
|
|
||||||
"exec": app.execString || app.exec || app.action || "",
|
|
||||||
"icon": app.icon !== undefined ? String(app.icon) : (isPluginItem ? "" : "application-x-executable"),
|
|
||||||
"comment": app.comment || "",
|
|
||||||
"categories": app.categories || [],
|
|
||||||
"isPlugin": isPluginItem,
|
|
||||||
"isCore": app.isCore === true,
|
|
||||||
"isBuiltInLauncher": app.isBuiltInLauncher === true,
|
|
||||||
"appIndex": uniqueApps.length - 1,
|
|
||||||
"pinned": app._pinned === true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
root._uniqueApps = uniqueApps;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNext() {
|
|
||||||
if (filteredModel.count === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true;
|
|
||||||
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPrevious() {
|
|
||||||
if (filteredModel.count === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true;
|
|
||||||
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNextInRow() {
|
|
||||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true;
|
|
||||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPreviousInRow() {
|
|
||||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keyboardNavigationActive = true;
|
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchSelected() {
|
|
||||||
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedApp = filteredModel.get(selectedIndex);
|
|
||||||
launchApp(selectedApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchApp(appData) {
|
|
||||||
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length)
|
|
||||||
return;
|
|
||||||
suppressUpdatesWhileLaunching = true;
|
|
||||||
|
|
||||||
const actualApp = _uniqueApps[appData.appIndex];
|
|
||||||
|
|
||||||
if (appData.isBuiltInLauncher) {
|
|
||||||
AppSearchService.executeBuiltInLauncherItem(actualApp);
|
|
||||||
appLaunched(appData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.isCore) {
|
|
||||||
AppSearchService.executeCoreApp(actualApp);
|
|
||||||
appLaunched(appData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appData.isPlugin) {
|
|
||||||
const pluginId = getPluginIdForItem(actualApp);
|
|
||||||
if (pluginId) {
|
|
||||||
AppSearchService.executePluginItem(actualApp, pluginId);
|
|
||||||
appLaunched(appData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionService.launchDesktopEntry(actualApp);
|
|
||||||
appLaunched(appData);
|
|
||||||
AppUsageHistoryData.addAppUsage(actualApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
suppressUpdatesWhileLaunching = false;
|
|
||||||
searchQuery = "";
|
|
||||||
selectedIndex = 0;
|
|
||||||
setCategory(I18n.tr("All"));
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCategory(category) {
|
|
||||||
selectedCategory = category;
|
|
||||||
categorySelected(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setViewMode(mode) {
|
|
||||||
viewMode = mode;
|
|
||||||
viewModeSelected(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChanged: {
|
|
||||||
if (!_initialized)
|
|
||||||
return;
|
|
||||||
if (debounceSearch) {
|
|
||||||
searchDebounceTimer.restart();
|
|
||||||
} else {
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectedCategoryChanged: {
|
|
||||||
if (_updatingFromTrigger || !_initialized)
|
|
||||||
return;
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
onAppUsageRankingChanged: {
|
|
||||||
if (_initialized)
|
|
||||||
updateFilteredModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DesktopEntries
|
|
||||||
function onApplicationsChanged() {
|
|
||||||
if (!root._initialized)
|
|
||||||
return;
|
|
||||||
root.updateCategories();
|
|
||||||
root.updateFilteredModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: filteredModel
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: searchDebounceTimer
|
|
||||||
|
|
||||||
interval: root.debounceInterval
|
|
||||||
repeat: false
|
|
||||||
onTriggered: updateFilteredModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPluginTriggers(query) {
|
|
||||||
if (!query)
|
|
||||||
return {
|
|
||||||
triggered: false,
|
|
||||||
pluginCategory: "",
|
|
||||||
query: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const builtInTriggers = AppSearchService.getBuiltInLauncherTriggers();
|
|
||||||
for (const trigger in builtInTriggers) {
|
|
||||||
if (!query.startsWith(trigger))
|
|
||||||
continue;
|
|
||||||
const pluginId = builtInTriggers[trigger];
|
|
||||||
const plugin = AppSearchService.builtInPlugins[pluginId];
|
|
||||||
if (!plugin)
|
|
||||||
continue;
|
|
||||||
return {
|
|
||||||
triggered: true,
|
|
||||||
pluginId: pluginId,
|
|
||||||
pluginCategory: plugin.name,
|
|
||||||
query: query.substring(trigger.length).trim(),
|
|
||||||
trigger: trigger,
|
|
||||||
isBuiltIn: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof PluginService === "undefined")
|
|
||||||
return {
|
|
||||||
triggered: false,
|
|
||||||
pluginCategory: "",
|
|
||||||
query: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggers = PluginService.getAllPluginTriggers();
|
|
||||||
for (const trigger in triggers) {
|
|
||||||
if (!query.startsWith(trigger))
|
|
||||||
continue;
|
|
||||||
const pluginId = triggers[trigger];
|
|
||||||
const plugin = PluginService.getLauncherPlugin(pluginId);
|
|
||||||
if (!plugin)
|
|
||||||
continue;
|
|
||||||
return {
|
|
||||||
triggered: true,
|
|
||||||
pluginId: pluginId,
|
|
||||||
pluginCategory: plugin.name || pluginId,
|
|
||||||
query: query.substring(trigger.length).trim(),
|
|
||||||
trigger: trigger,
|
|
||||||
isBuiltIn: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
triggered: false,
|
|
||||||
pluginCategory: "",
|
|
||||||
query: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginIdForItem(item) {
|
|
||||||
if (!item || !item.categories || typeof PluginService === "undefined") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const launchers = PluginService.getLauncherPlugins();
|
|
||||||
for (const pluginId in launchers) {
|
|
||||||
const plugin = launchers[pluginId];
|
|
||||||
const pluginCategory = plugin.name || pluginId;
|
|
||||||
|
|
||||||
let hasCategory = false;
|
|
||||||
if (Array.isArray(item.categories)) {
|
|
||||||
hasCategory = item.categories.includes(pluginCategory);
|
|
||||||
} else if (item.categories && typeof item.categories.count !== "undefined") {
|
|
||||||
for (let i = 0; i < item.categories.count; i++) {
|
|
||||||
if (item.categories.get(i) === pluginCategory) {
|
|
||||||
hasCategory = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCategory) {
|
|
||||||
return pluginId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var categories: []
|
|
||||||
property string selectedCategory: I18n.tr("All")
|
|
||||||
property bool compact: false
|
|
||||||
|
|
||||||
signal categorySelected(string category)
|
|
||||||
|
|
||||||
readonly property int maxCompactItems: 8
|
|
||||||
readonly property int itemHeight: 36
|
|
||||||
readonly property color selectedBorderColor: "transparent"
|
|
||||||
readonly property color unselectedBorderColor: "transparent"
|
|
||||||
|
|
||||||
function handleCategoryClick(category) {
|
|
||||||
categorySelected(category)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getButtonWidth(itemCount, containerWidth) {
|
|
||||||
return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
visible: compact
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
|
|
||||||
|
|
||||||
height: root.itemHeight
|
|
||||||
width: root.getButtonWidth(itemCount, parent.width)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.handleCategoryClick(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
visible: !compact
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
|
|
||||||
|
|
||||||
height: root.itemHeight
|
|
||||||
width: root.getButtonWidth(itemCount, parent.width)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.handleCategoryClick(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: categories && categories.length > 4
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: categories && categories.length > 4 ? categories.slice(4) : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
|
|
||||||
|
|
||||||
height: root.itemHeight
|
|
||||||
width: root.getButtonWidth(itemCount, parent.width)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.handleCategoryClick(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -122,6 +122,21 @@ Rectangle {
|
|||||||
contentHeight: audioColumn.height
|
contentHeight: audioColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
property int maxPinnedInputs: 3
|
||||||
|
|
||||||
|
function normalizePinList(value) {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.filter(v => v)
|
||||||
|
if (typeof value === "string" && value.length > 0)
|
||||||
|
return [value]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPinnedInputs() {
|
||||||
|
const pins = SettingsData.audioInputDevicePins || {}
|
||||||
|
return normalizePinList(pins["preferredInput"])
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: audioColumn
|
id: audioColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -133,16 +148,20 @@ Rectangle {
|
|||||||
const nodes = Pipewire.nodes.values.filter(node => {
|
const nodes = Pipewire.nodes.values.filter(node => {
|
||||||
return node.audio && !node.isSink && !node.isStream;
|
return node.audio && !node.isSink && !node.isStream;
|
||||||
});
|
});
|
||||||
const pins = SettingsData.audioInputDevicePins || {};
|
const pinnedList = audioContent.getPinnedInputs();
|
||||||
const pinnedName = pins["preferredInput"];
|
|
||||||
|
|
||||||
let sorted = [...nodes];
|
let sorted = [...nodes];
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
// Pinned device first
|
// Pinned device first
|
||||||
if (a.name === pinnedName && b.name !== pinnedName)
|
const aPinnedIndex = pinnedList.indexOf(a.name)
|
||||||
return -1;
|
const bPinnedIndex = pinnedList.indexOf(b.name)
|
||||||
if (b.name === pinnedName && a.name !== pinnedName)
|
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
|
||||||
return 1;
|
if (aPinnedIndex === -1)
|
||||||
|
return 1
|
||||||
|
if (bPinnedIndex === -1)
|
||||||
|
return -1
|
||||||
|
return aPinnedIndex - bPinnedIndex
|
||||||
|
}
|
||||||
// Then active device
|
// Then active device
|
||||||
if (a === AudioService.source && b !== AudioService.source)
|
if (a === AudioService.source && b !== AudioService.source)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -224,7 +243,7 @@ Rectangle {
|
|||||||
height: 28
|
height: 28
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +256,7 @@ Rectangle {
|
|||||||
name: "push_pin"
|
name: "push_pin"
|
||||||
size: 16
|
size: 16
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -245,12 +264,12 @@ Rectangle {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -261,16 +280,24 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
|
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}))
|
||||||
const isCurrentlyPinned = pins["preferredInput"] === modelData.name;
|
let pinnedList = audioContent.normalizePinList(pins["preferredInput"])
|
||||||
|
const pinIndex = pinnedList.indexOf(modelData.name)
|
||||||
|
|
||||||
if (isCurrentlyPinned) {
|
if (pinIndex !== -1) {
|
||||||
delete pins["preferredInput"];
|
pinnedList.splice(pinIndex, 1)
|
||||||
} else {
|
} else {
|
||||||
pins["preferredInput"] = modelData.name;
|
pinnedList.unshift(modelData.name)
|
||||||
|
if (pinnedList.length > audioContent.maxPinnedInputs)
|
||||||
|
pinnedList = pinnedList.slice(0, audioContent.maxPinnedInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsData.set("audioInputDevicePins", pins);
|
if (pinnedList.length > 0)
|
||||||
|
pins["preferredInput"] = pinnedList
|
||||||
|
else
|
||||||
|
delete pins["preferredInput"]
|
||||||
|
|
||||||
|
SettingsData.set("audioInputDevicePins", pins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,21 @@ Rectangle {
|
|||||||
contentHeight: audioColumn.height
|
contentHeight: audioColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
property int maxPinnedOutputs: 3
|
||||||
|
|
||||||
|
function normalizePinList(value) {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.filter(v => v)
|
||||||
|
if (typeof value === "string" && value.length > 0)
|
||||||
|
return [value]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPinnedOutputs() {
|
||||||
|
const pins = SettingsData.audioOutputDevicePins || {}
|
||||||
|
return normalizePinList(pins["preferredOutput"])
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: audioColumn
|
id: audioColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -143,16 +158,20 @@ Rectangle {
|
|||||||
const nodes = Pipewire.nodes.values.filter(node => {
|
const nodes = Pipewire.nodes.values.filter(node => {
|
||||||
return node.audio && node.isSink && !node.isStream;
|
return node.audio && node.isSink && !node.isStream;
|
||||||
});
|
});
|
||||||
const pins = SettingsData.audioOutputDevicePins || {};
|
const pinnedList = audioContent.getPinnedOutputs();
|
||||||
const pinnedName = pins["preferredOutput"];
|
|
||||||
|
|
||||||
let sorted = [...nodes];
|
let sorted = [...nodes];
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
// Pinned device first
|
// Pinned device first
|
||||||
if (a.name === pinnedName && b.name !== pinnedName)
|
const aPinnedIndex = pinnedList.indexOf(a.name)
|
||||||
return -1;
|
const bPinnedIndex = pinnedList.indexOf(b.name)
|
||||||
if (b.name === pinnedName && a.name !== pinnedName)
|
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
|
||||||
return 1;
|
if (aPinnedIndex === -1)
|
||||||
|
return 1
|
||||||
|
if (bPinnedIndex === -1)
|
||||||
|
return -1
|
||||||
|
return aPinnedIndex - bPinnedIndex
|
||||||
|
}
|
||||||
// Then active device
|
// Then active device
|
||||||
if (a === AudioService.sink && b !== AudioService.sink)
|
if (a === AudioService.sink && b !== AudioService.sink)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -236,7 +255,7 @@ Rectangle {
|
|||||||
height: 28
|
height: 28
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +268,7 @@ Rectangle {
|
|||||||
name: "push_pin"
|
name: "push_pin"
|
||||||
size: 16
|
size: 16
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -257,12 +276,12 @@ Rectangle {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name;
|
const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -273,16 +292,24 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
|
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}))
|
||||||
const isCurrentlyPinned = pins["preferredOutput"] === modelData.name;
|
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"])
|
||||||
|
const pinIndex = pinnedList.indexOf(modelData.name)
|
||||||
|
|
||||||
if (isCurrentlyPinned) {
|
if (pinIndex !== -1) {
|
||||||
delete pins["preferredOutput"];
|
pinnedList.splice(pinIndex, 1)
|
||||||
} else {
|
} else {
|
||||||
pins["preferredOutput"] = modelData.name;
|
pinnedList.unshift(modelData.name)
|
||||||
|
if (pinnedList.length > audioContent.maxPinnedOutputs)
|
||||||
|
pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsData.set("audioOutputDevicePins", pins);
|
if (pinnedList.length > 0)
|
||||||
|
pins["preferredOutput"] = pinnedList
|
||||||
|
else
|
||||||
|
delete pins["preferredOutput"]
|
||||||
|
|
||||||
|
SettingsData.set("audioOutputDevicePins", pins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,21 @@ Rectangle {
|
|||||||
contentHeight: bluetoothColumn.height
|
contentHeight: bluetoothColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
property int maxPinnedDevices: 3
|
||||||
|
|
||||||
|
function normalizePinList(value) {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.filter(v => v)
|
||||||
|
if (typeof value === "string" && value.length > 0)
|
||||||
|
return [value]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPinnedDevices() {
|
||||||
|
const pins = SettingsData.bluetoothDevicePins || {}
|
||||||
|
return normalizePinList(pins["preferredDevice"])
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: bluetoothColumn
|
id: bluetoothColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -162,14 +177,18 @@ Rectangle {
|
|||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
const pins = SettingsData.bluetoothDevicePins || {}
|
const pinnedList = bluetoothContent.getPinnedDevices()
|
||||||
const pinnedAddr = pins["preferredDevice"]
|
|
||||||
|
|
||||||
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
devices.sort((a, b) => {
|
devices.sort((a, b) => {
|
||||||
// Pinned device first
|
// Pinned device first
|
||||||
if (a.address === pinnedAddr && b.address !== pinnedAddr) return -1
|
const aPinnedIndex = pinnedList.indexOf(a.address)
|
||||||
if (b.address === pinnedAddr && a.address !== pinnedAddr) return 1
|
const bPinnedIndex = pinnedList.indexOf(b.address)
|
||||||
|
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
|
||||||
|
if (aPinnedIndex === -1) return 1
|
||||||
|
if (bPinnedIndex === -1) return -1
|
||||||
|
return aPinnedIndex - bPinnedIndex
|
||||||
|
}
|
||||||
// Then connected devices
|
// Then connected devices
|
||||||
if (a.connected && !b.connected) return -1
|
if (a.connected && !b.connected) return -1
|
||||||
if (!a.connected && b.connected) return 1
|
if (!a.connected && b.connected) return 1
|
||||||
@@ -302,7 +321,7 @@ Rectangle {
|
|||||||
height: 28
|
height: 28
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
|
||||||
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
|
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +334,7 @@ Rectangle {
|
|||||||
name: "push_pin"
|
name: "push_pin"
|
||||||
size: 16
|
size: 16
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
|
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -323,12 +342,12 @@ Rectangle {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
|
||||||
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
|
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: {
|
color: {
|
||||||
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address
|
const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
|
||||||
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
|
return isThisDevicePinned ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -340,14 +359,22 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}))
|
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}))
|
||||||
const isCurrentlyPinned = pins["preferredDevice"] === modelData.address
|
let pinnedList = bluetoothContent.normalizePinList(pins["preferredDevice"])
|
||||||
|
const pinIndex = pinnedList.indexOf(modelData.address)
|
||||||
|
|
||||||
if (isCurrentlyPinned) {
|
if (pinIndex !== -1) {
|
||||||
delete pins["preferredDevice"]
|
pinnedList.splice(pinIndex, 1)
|
||||||
} else {
|
} else {
|
||||||
pins["preferredDevice"] = modelData.address
|
pinnedList.unshift(modelData.address)
|
||||||
|
if (pinnedList.length > bluetoothContent.maxPinnedDevices)
|
||||||
|
pinnedList = pinnedList.slice(0, bluetoothContent.maxPinnedDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pinnedList.length > 0)
|
||||||
|
pins["preferredDevice"] = pinnedList
|
||||||
|
else
|
||||||
|
delete pins["preferredDevice"]
|
||||||
|
|
||||||
SettingsData.set("bluetoothDevicePins", pins)
|
SettingsData.set("bluetoothDevicePins", pins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,20 +463,39 @@ Rectangle {
|
|||||||
contentHeight: wifiColumn.height
|
contentHeight: wifiColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
property int maxPinnedNetworks: 3
|
||||||
|
|
||||||
|
function normalizePinList(value) {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.filter(v => v)
|
||||||
|
if (typeof value === "string" && value.length > 0)
|
||||||
|
return [value]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPinnedNetworks() {
|
||||||
|
const pins = SettingsData.wifiNetworkPins || {}
|
||||||
|
return normalizePinList(pins["preferredWifi"])
|
||||||
|
}
|
||||||
|
|
||||||
property var frozenNetworks: []
|
property var frozenNetworks: []
|
||||||
property bool menuOpen: false
|
property bool menuOpen: false
|
||||||
property var sortedNetworks: {
|
property var sortedNetworks: {
|
||||||
const ssid = NetworkService.currentWifiSSID;
|
const ssid = NetworkService.currentWifiSSID;
|
||||||
const networks = NetworkService.wifiNetworks;
|
const networks = NetworkService.wifiNetworks;
|
||||||
const pins = SettingsData.wifiNetworkPins || {};
|
const pinnedList = getPinnedNetworks()
|
||||||
const pinnedSSID = pins["preferredWifi"];
|
|
||||||
|
|
||||||
let sorted = [...networks];
|
let sorted = [...networks];
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID)
|
const aPinnedIndex = pinnedList.indexOf(a.ssid)
|
||||||
return -1;
|
const bPinnedIndex = pinnedList.indexOf(b.ssid)
|
||||||
if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID)
|
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
|
||||||
return 1;
|
if (aPinnedIndex === -1)
|
||||||
|
return 1
|
||||||
|
if (bPinnedIndex === -1)
|
||||||
|
return -1
|
||||||
|
return aPinnedIndex - bPinnedIndex
|
||||||
|
}
|
||||||
if (a.ssid === ssid)
|
if (a.ssid === ssid)
|
||||||
return -1;
|
return -1;
|
||||||
if (b.ssid === ssid)
|
if (b.ssid === ssid)
|
||||||
@@ -625,7 +644,7 @@ Rectangle {
|
|||||||
height: 28
|
height: 28
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: {
|
color: {
|
||||||
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
|
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
||||||
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,7 +657,7 @@ Rectangle {
|
|||||||
name: "push_pin"
|
name: "push_pin"
|
||||||
size: 16
|
size: 16
|
||||||
color: {
|
color: {
|
||||||
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
|
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
||||||
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
|
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -646,12 +665,12 @@ Rectangle {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
|
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
||||||
return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: {
|
color: {
|
||||||
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
|
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
||||||
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
|
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -662,16 +681,24 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}))
|
||||||
const isCurrentlyPinned = pins["preferredWifi"] === modelData.ssid;
|
let pinnedList = wifiContent.normalizePinList(pins["preferredWifi"])
|
||||||
|
const pinIndex = pinnedList.indexOf(modelData.ssid)
|
||||||
|
|
||||||
if (isCurrentlyPinned) {
|
if (pinIndex !== -1) {
|
||||||
delete pins["preferredWifi"];
|
pinnedList.splice(pinIndex, 1)
|
||||||
} else {
|
} else {
|
||||||
pins["preferredWifi"] = modelData.ssid;
|
pinnedList.unshift(modelData.ssid)
|
||||||
|
if (pinnedList.length > wifiContent.maxPinnedNetworks)
|
||||||
|
pinnedList = pinnedList.slice(0, wifiContent.maxPinnedNetworks)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsData.set("wifiNetworkPins", pins);
|
if (pinnedList.length > 0)
|
||||||
|
pins["preferredWifi"] = pinnedList
|
||||||
|
else
|
||||||
|
delete pins["preferredWifi"]
|
||||||
|
|
||||||
|
SettingsData.set("wifiNetworkPins", pins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,8 +714,8 @@ Rectangle {
|
|||||||
if (modelData.secured && !modelData.saved) {
|
if (modelData.secured && !modelData.saved) {
|
||||||
if (DMSService.apiVersion >= 7) {
|
if (DMSService.apiVersion >= 7) {
|
||||||
NetworkService.connectToWifi(modelData.ssid);
|
NetworkService.connectToWifi(modelData.ssid);
|
||||||
} else if (PopoutService.wifiPasswordModal) {
|
} else {
|
||||||
PopoutService.wifiPasswordModal.show(modelData.ssid);
|
PopoutService.showWifiPasswordModal(modelData.ssid);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NetworkService.connectToWifi(modelData.ssid);
|
NetworkService.connectToWifi(modelData.ssid);
|
||||||
@@ -749,8 +776,8 @@ Rectangle {
|
|||||||
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
||||||
if (DMSService.apiVersion >= 7) {
|
if (DMSService.apiVersion >= 7) {
|
||||||
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
||||||
} else if (PopoutService.wifiPasswordModal) {
|
} else {
|
||||||
PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID);
|
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
||||||
|
|||||||
@@ -296,6 +296,9 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
implicitWidth: isVertical ? widgetThickness : totalSize
|
||||||
|
implicitHeight: isVertical ? totalSize : widgetThickness
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: layoutTimer
|
id: layoutTimer
|
||||||
interval: 0
|
interval: 0
|
||||||
@@ -365,6 +368,7 @@ Item {
|
|||||||
onContentItemReady: contentItem => {
|
onContentItemReady: contentItem => {
|
||||||
contentItem.widthChanged.connect(() => layoutTimer.restart());
|
contentItem.widthChanged.connect(() => layoutTimer.restart());
|
||||||
contentItem.heightChanged.connect(() => layoutTimer.restart());
|
contentItem.heightChanged.connect(() => layoutTimer.restart());
|
||||||
|
layoutTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: layoutTimer.restart()
|
onActiveChanged: layoutTimer.restart()
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ Item {
|
|||||||
|
|
||||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
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.fill: parent
|
||||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||||
@@ -39,11 +46,24 @@ Item {
|
|||||||
|
|
||||||
function getRealWorkspaces() {
|
function getRealWorkspaces() {
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
|
const fallbackWorkspaces = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"idx": 0,
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"idx": 1,
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
];
|
||||||
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
|
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
|
||||||
return NiriService.getCurrentOutputWorkspaceNumbers();
|
const currentWorkspaces = NiriService.getCurrentOutputWorkspaces();
|
||||||
|
return currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces;
|
||||||
}
|
}
|
||||||
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1);
|
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName);
|
||||||
return workspaces.length > 0 ? workspaces : [1, 2];
|
return workspaces.length > 0 ? workspaces : fallbackWorkspaces;
|
||||||
} else if (CompositorService.isHyprland) {
|
} else if (CompositorService.isHyprland) {
|
||||||
const workspaces = Hyprland.workspaces?.values || [];
|
const workspaces = Hyprland.workspaces?.values || [];
|
||||||
|
|
||||||
@@ -111,7 +131,7 @@ Item {
|
|||||||
return NiriService.getCurrentWorkspaceNumber();
|
return NiriService.getCurrentWorkspaceNumber();
|
||||||
}
|
}
|
||||||
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active);
|
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active);
|
||||||
return activeWs ? activeWs.idx + 1 : 1;
|
return activeWs ? activeWs.idx : 1;
|
||||||
} else if (CompositorService.isHyprland) {
|
} else if (CompositorService.isHyprland) {
|
||||||
const monitors = Hyprland.monitors?.values || [];
|
const monitors = Hyprland.monitors?.values || [];
|
||||||
const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName);
|
const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName);
|
||||||
@@ -144,12 +164,16 @@ Item {
|
|||||||
|
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
const currentWs = getCurrentWorkspace();
|
const currentWs = getCurrentWorkspace();
|
||||||
const currentIndex = realWorkspaces.findIndex(ws => ws === currentWs);
|
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs);
|
||||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
||||||
|
|
||||||
if (nextIndex !== validIndex) {
|
if (nextIndex !== validIndex) {
|
||||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1);
|
const nextWorkspace = realWorkspaces[nextIndex];
|
||||||
|
if (!nextWorkspace || nextWorkspace.idx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NiriService.switchToWorkspace(nextWorkspace.idx);
|
||||||
}
|
}
|
||||||
} else if (CompositorService.isHyprland) {
|
} else if (CompositorService.isHyprland) {
|
||||||
const currentWs = getCurrentWorkspace();
|
const currentWs = getCurrentWorkspace();
|
||||||
|
|||||||
@@ -500,8 +500,78 @@ PanelWindow {
|
|||||||
height: axis.isVertical ? parent.height : maskThickness
|
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 {
|
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 {
|
Item {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: BatteryService.getBatteryIcon()
|
name: BatteryService.getBatteryIcon()
|
||||||
size: Theme.barIconSize(battery.barThickness)
|
size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.noBackground)
|
||||||
color: {
|
color: {
|
||||||
if (!BatteryService.batteryAvailable) {
|
if (!BatteryService.batteryAvailable) {
|
||||||
return Theme.widgetIconColor;
|
return Theme.widgetIconColor;
|
||||||
@@ -78,7 +78,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: BatteryService.getBatteryIcon()
|
name: BatteryService.getBatteryIcon()
|
||||||
size: Theme.barIconSize(battery.barThickness, -4)
|
size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.noBackground)
|
||||||
color: {
|
color: {
|
||||||
if (!BatteryService.batteryAvailable) {
|
if (!BatteryService.batteryAvailable) {
|
||||||
return Theme.widgetIconColor;
|
return Theme.widgetIconColor;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "shift_lock"
|
name: "shift_lock"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "content_paste"
|
name: "content_paste"
|
||||||
size: Theme.barIconSize(root.barThickness, -4)
|
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user