mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
33 Commits
auto-theme
...
a7cdb39b0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7cdb39b0b | ||
|
|
0ceba92a23 | ||
|
|
4daa7a4c88 | ||
|
|
cc4a6a5899 | ||
|
|
994947477c | ||
|
|
311817ee97 | ||
|
|
b80c73f9b9 | ||
|
|
a85101c099 | ||
|
|
3513d57e06 | ||
|
|
1234847abb | ||
|
|
0ed595b43d | ||
|
|
060cbefc79 | ||
|
|
e022c04519 | ||
|
|
f534384e5e | ||
|
|
a25cdb43d5 | ||
|
|
4e9b4ca400 | ||
|
|
5bab1c98b1 | ||
|
|
2284bb002f | ||
|
|
b0611d6104 | ||
|
|
27965862d6 | ||
|
|
e74a901e05 | ||
|
|
77794deb2c | ||
|
|
1c10746e50 | ||
|
|
8ecb7282b9 | ||
|
|
9b3fa804ab | ||
|
|
b2ad31a27e | ||
|
|
db17e4cb14 | ||
|
|
1b7dcf56a8 | ||
|
|
502bb88e92 | ||
|
|
b76d0ce97d | ||
|
|
fa66d330cf | ||
|
|
157eab2d07 | ||
|
|
f50ad2dc22 |
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.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
- type: input
|
||||
id: dms_version
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms version
|
||||
description: Output of dms version command
|
||||
placeholder: e.g., 1.2.3
|
||||
validations:
|
||||
required: true
|
||||
- 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
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
- type: input
|
||||
id: dms_version
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms version
|
||||
description: Output of dms version command
|
||||
placeholder: e.g., 1.2.3
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,14 +4,13 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
description: "Package to update (dms, dms-git, or all)"
|
||||
required: false
|
||||
default: "all"
|
||||
tag_version:
|
||||
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
||||
required: false
|
||||
default: ""
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
@@ -57,9 +56,8 @@ jobs:
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
# Sets LATEST_TAG variable in parent scope if update needed
|
||||
check_dms_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||
|
||||
@@ -75,8 +73,8 @@ jobs:
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - always update stable package
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push - always update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
@@ -106,12 +104,7 @@ jobs:
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
@@ -136,9 +129,6 @@ jobs:
|
||||
if check_dms_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
@@ -171,19 +161,12 @@ jobs:
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag push event - use the pushed tag
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
# Check if check-updates already determined a version (from auto-detection)
|
||||
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use version from check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||
echo "Triggered by tag: $VERSION"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - dms-git only
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
@@ -193,28 +176,22 @@ jobs:
|
||||
|
||||
# Determine version for dms stable
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
# For explicit dms selection, require tag_version
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
echo "Using specified tag: $VERSION"
|
||||
else
|
||||
# Auto-detect latest release for dms
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
echo "ERROR: tag_version is required when package=dms"
|
||||
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
# For "all", auto-detect if tag_version not specified
|
||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
VERSION="${{ github.event.inputs.tag_version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
echo "Using specified tag: $VERSION"
|
||||
else
|
||||
# Auto-detect latest release for "all"
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
@@ -229,7 +206,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
else
|
||||
@@ -238,9 +215,6 @@ jobs:
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Overhauled system monitor, graphs, styling
|
||||
- dbus API for plugins, KDEConnect
|
||||
- new dank16 algorithm
|
||||
- launcher actions, customize env, args, name, icon
|
||||
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||
- dock on bar
|
||||
|
||||
# 1.2.0
|
||||
|
||||
- Added clipboard and clipboard history integration
|
||||
|
||||
1
Makefile
1
Makefile
@@ -43,6 +43,7 @@ install-shell:
|
||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
||||
@echo "Shell files installed"
|
||||
|
||||
install-completions:
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,9 @@ var killCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var ipcCmd = &cobra.Command{
|
||||
Use: "ipc [target] [function] [args...]",
|
||||
Use: "ipc",
|
||||
Short: "Send IPC commands to running DMS shell",
|
||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
||||
PreRunE: findConfig,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
_ = findConfig(cmd, args)
|
||||
@@ -76,13 +77,6 @@ var ipcCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
_ = findConfig(cmd, args)
|
||||
printIPCHelp()
|
||||
})
|
||||
}
|
||||
|
||||
var debugSrvCmd = &cobra.Command{
|
||||
Use: "debug-srv",
|
||||
Short: "Start the debug server",
|
||||
@@ -517,11 +511,8 @@ func getCommonCommands() []*cobra.Command {
|
||||
colorCmd,
|
||||
screenshotCmd,
|
||||
notifyActionCmd,
|
||||
notifyCmd,
|
||||
genericNotifyActionCmd,
|
||||
matugenCmd,
|
||||
clipboardCmd,
|
||||
chromaCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
notifyAppName string
|
||||
notifyIcon string
|
||||
notifyFile string
|
||||
notifyTimeout int
|
||||
)
|
||||
|
||||
var notifyCmd = &cobra.Command{
|
||||
Use: "notify <summary> [body]",
|
||||
Short: "Send a desktop notification",
|
||||
Long: `Send a desktop notification with optional actions.
|
||||
|
||||
If --file is provided, the notification will have "Open" and "Open Folder" actions.
|
||||
|
||||
Examples:
|
||||
dms notify "Hello" "World"
|
||||
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone
|
||||
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: runNotify,
|
||||
}
|
||||
|
||||
var genericNotifyActionCmd = &cobra.Command{
|
||||
Use: "notify-action-generic",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
notify.RunActionListener(args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
notifyCmd.Flags().StringVar(¬ifyAppName, "app", "DMS", "Application name")
|
||||
notifyCmd.Flags().StringVar(¬ifyIcon, "icon", "", "Icon name or path")
|
||||
notifyCmd.Flags().StringVar(¬ifyFile, "file", "", "File path (enables Open/Open Folder actions)")
|
||||
notifyCmd.Flags().IntVar(¬ifyTimeout, "timeout", 5000, "Timeout in milliseconds")
|
||||
}
|
||||
|
||||
func runNotify(cmd *cobra.Command, args []string) {
|
||||
summary := args[0]
|
||||
body := ""
|
||||
if len(args) > 1 {
|
||||
body = args[1]
|
||||
}
|
||||
|
||||
n := notify.Notification{
|
||||
AppName: notifyAppName,
|
||||
Icon: notifyIcon,
|
||||
Summary: summary,
|
||||
Body: body,
|
||||
FilePath: notifyFile,
|
||||
Timeout: int32(notifyTimeout),
|
||||
}
|
||||
|
||||
if err := notify.Send(n); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -18,9 +20,11 @@ var rootCmd = &cobra.Command{
|
||||
Use: "dms",
|
||||
Short: "dms CLI",
|
||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||
Run: runInteractiveMode,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add the -c flag
|
||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||
}
|
||||
|
||||
@@ -34,7 +38,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
||||
if statErr == nil && !info.IsDir() {
|
||||
configPath = customConfigPath
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
return nil // <-- Guard statement
|
||||
}
|
||||
|
||||
if statErr != nil {
|
||||
@@ -72,3 +76,18 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
}
|
||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
||||
detector, _ := dms.NewDetector()
|
||||
|
||||
if !detector.IsDMSInstalled() {
|
||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
||||
log.Info("Please install DMS using dankinstall before using this management interface.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
model := dms.NewModel(Version)
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
log.Fatalf("Error running program: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,8 +618,9 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
||||
|
||||
func runShellIPCCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
printIPCHelp()
|
||||
return
|
||||
log.Error("IPC command requires arguments")
|
||||
log.Info("Usage: dms ipc <command> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if args[0] != "call" {
|
||||
@@ -641,45 +642,3 @@ func runShellIPCCommand(args []string) {
|
||||
log.Fatalf("Error running IPC command: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func printIPCHelp() {
|
||||
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||
fmt.Println()
|
||||
|
||||
cmdArgs := []string{"ipc"}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||
return
|
||||
}
|
||||
|
||||
targets := parseTargetsFromIPCShowOutput(string(output))
|
||||
if len(targets) == 0 {
|
||||
fmt.Println("No IPC targets available")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Targets:")
|
||||
|
||||
targetNames := make([]string, 0, len(targets))
|
||||
for name := range targets {
|
||||
targetNames = append(targetNames, name)
|
||||
}
|
||||
slices.Sort(targetNames)
|
||||
|
||||
for _, targetName := range targetNames {
|
||||
funcs := targets[targetName]
|
||||
funcNames := make([]string, 0, len(funcs))
|
||||
for fn := range funcs {
|
||||
funcNames = append(funcNames, fn)
|
||||
}
|
||||
slices.Sort(funcNames)
|
||||
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
24
core/go.mod
24
core/go.mod
@@ -4,7 +4,6 @@ go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.17.2
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
@@ -16,25 +15,22 @@ require (
|
||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
|
||||
github.com/spf13/cobra v1.10.2
|
||||
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
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||
golang.org/x/image v0.35.0
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||
golang.org/x/image v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.7.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.2 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
@@ -42,8 +38,8 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -51,12 +47,12 @@ require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -70,7 +66,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
97
core/go.sum
97
core/go.sum
@@ -4,14 +4,6 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
@@ -24,6 +16,8 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
@@ -32,18 +26,24 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
||||
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -52,10 +52,8 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
@@ -66,15 +64,22 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
||||
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-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
@@ -82,8 +87,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -124,12 +127,16 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -139,43 +146,45 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -215,8 +215,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
// Skip if file already exists to preserve user modifications
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
@@ -567,8 +567,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -91,9 +91,6 @@ bind = SUPER CTRL, up, movetoworkspace, e-1
|
||||
bind = SUPER CTRL, U, movetoworkspace, e+1
|
||||
bind = SUPER CTRL, I, movetoworkspace, e-1
|
||||
|
||||
# === Workspace Management ===
|
||||
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
||||
|
||||
# === Move Workspaces ===
|
||||
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
||||
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
||||
|
||||
@@ -133,11 +133,6 @@ binds {
|
||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||
|
||||
// === Workspace Management ===
|
||||
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
|
||||
spawn "dms" "ipc" "call" "workspace-rename" "open";
|
||||
}
|
||||
|
||||
// === Move Workspaces ===
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
|
||||
@@ -199,6 +199,31 @@ func labToHex(L, a, b float64) string {
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||
}
|
||||
|
||||
// Adjust brightness while keeping the same hue
|
||||
func retoneToL(hex string, Ltarget float64) string {
|
||||
rgb := HexToRGB(hex)
|
||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||
L, a, b := col.Lab()
|
||||
L100 := L * 100.0
|
||||
|
||||
scale := 1.0
|
||||
if L100 != 0 {
|
||||
scale = Ltarget / L100
|
||||
}
|
||||
|
||||
a2, b2 := a*scale, b*scale
|
||||
|
||||
// Don't let it get too saturated
|
||||
maxChroma := 0.4
|
||||
if math.Hypot(a2, b2) > maxChroma {
|
||||
k := maxChroma / math.Hypot(a2, b2)
|
||||
a2 *= k
|
||||
b2 *= k
|
||||
}
|
||||
|
||||
return labToHex(Ltarget, a2, b2)
|
||||
}
|
||||
|
||||
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||
Lf := getLstar(hexFg)
|
||||
Lb := getLstar(hexBg)
|
||||
@@ -331,59 +356,6 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
||||
return hexColor
|
||||
}
|
||||
|
||||
// Bidirectional contrast - tries both lighter and darker, picks closest to original
|
||||
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||
if current >= minLc {
|
||||
return hexColor
|
||||
}
|
||||
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
origL, af, bf := cf.Lab()
|
||||
|
||||
var darkerResult, lighterResult string
|
||||
darkerL, lighterL := origL, origL
|
||||
darkerFound, lighterFound := false, false
|
||||
|
||||
step := 0.5
|
||||
for i := range 120 {
|
||||
if !darkerFound {
|
||||
darkerL = math.Max(0, origL-float64(i)*step)
|
||||
cand := labToHex(darkerL, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
darkerResult = cand
|
||||
darkerFound = true
|
||||
}
|
||||
}
|
||||
if !lighterFound {
|
||||
lighterL = math.Min(100, origL+float64(i)*step)
|
||||
cand := labToHex(lighterL, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
lighterResult = cand
|
||||
lighterFound = true
|
||||
}
|
||||
}
|
||||
if darkerFound && lighterFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if darkerFound && lighterFound {
|
||||
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
|
||||
return darkerResult
|
||||
}
|
||||
return lighterResult
|
||||
}
|
||||
if darkerFound {
|
||||
return darkerResult
|
||||
}
|
||||
if lighterFound {
|
||||
return lighterResult
|
||||
}
|
||||
return hexColor
|
||||
}
|
||||
|
||||
type PaletteOptions struct {
|
||||
IsLight bool
|
||||
Background string
|
||||
@@ -397,29 +369,6 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||
if opts.UseDPS {
|
||||
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func blendHue(base, target, factor float64) float64 {
|
||||
diff := target - base
|
||||
if diff > 0.5 {
|
||||
diff -= 1.0
|
||||
} else if diff < -0.5 {
|
||||
diff += 1.0
|
||||
}
|
||||
result := base + diff*factor
|
||||
if result < 0 {
|
||||
result += 1.0
|
||||
} else if result >= 1.0 {
|
||||
result -= 1.0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func DeriveContainer(primary string, isLight bool) string {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -440,9 +389,6 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
rgb := HexToRGB(baseColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
pr := HexToRGB(primaryColor)
|
||||
ph := RGBToHSV(pr)
|
||||
|
||||
var palette Palette
|
||||
|
||||
var normalTextTarget, secondaryTarget float64
|
||||
@@ -464,136 +410,115 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
}
|
||||
palette.Color0 = NewColorInfo(bgColor)
|
||||
|
||||
baseSat := math.Max(ph.S, 0.5)
|
||||
baseVal := math.Max(ph.V, 0.5)
|
||||
hueShift := (hsv.H - 0.6) * 0.12
|
||||
satBoost := 1.15
|
||||
|
||||
redH := blendHue(0.0, ph.H, 0.12)
|
||||
greenH := blendHue(0.33, ph.H, 0.10)
|
||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
||||
var redColor string
|
||||
if opts.IsLight {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
accentTarget := secondaryTarget * 0.7
|
||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||
var greenColor string
|
||||
if opts.IsLight {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
||||
var yellowColor string
|
||||
if opts.IsLight {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
var blueColor string
|
||||
if opts.IsLight {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
magH := hsv.H - 0.03
|
||||
if magH < 0 {
|
||||
magH += 1.0
|
||||
}
|
||||
var magColor string
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
if opts.IsLight {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
cyanH := hsv.H + 0.08
|
||||
if cyanH > 1.0 {
|
||||
cyanH -= 1.0
|
||||
}
|
||||
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
||||
|
||||
if opts.IsLight {
|
||||
redS := math.Min(baseSat*1.2, 1.0)
|
||||
redV := baseVal * 0.95
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
greenS := math.Min(baseSat*1.3, 1.0)
|
||||
greenV := baseVal * 0.75
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
blueS := math.Min(ph.S*1.05, 1.0)
|
||||
blueV := math.Min(ph.V*1.05, 1.0)
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (light container in light mode)
|
||||
container5 := DeriveContainer(primaryColor, true)
|
||||
palette.Color5 = NewColorInfo(container5)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
gray7S := baseSat * 0.08
|
||||
gray7V := baseVal * 0.28
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.05
|
||||
gray8V := baseVal * 0.85
|
||||
dimTarget := secondaryTarget * 0.5
|
||||
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
|
||||
|
||||
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightGreenS := math.Min(baseSat*1.1, 1.0)
|
||||
brightGreenV := math.Min(baseVal*1.1, 1.0)
|
||||
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightYellowS := math.Min(baseSat*1.4, 1.0)
|
||||
brightYellowV := math.Min(baseVal*1.3, 1.0)
|
||||
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightBlueS := math.Min(ph.S*1.1, 1.0)
|
||||
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
lightContainer := DeriveContainer(primaryColor, true)
|
||||
palette.Color13 = NewColorInfo(lightContainer)
|
||||
|
||||
brightCyanS := ph.S * 0.5
|
||||
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
|
||||
|
||||
white15S := baseSat * 0.04
|
||||
white15V := math.Min(baseVal*1.5, 1.0)
|
||||
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
|
||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
||||
} else {
|
||||
redS := math.Min(baseSat*1.1, 1.0)
|
||||
redV := math.Min(baseVal*1.15, 1.0)
|
||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||
palette.Color7 = NewColorInfo("#abb2bf")
|
||||
palette.Color8 = NewColorInfo("#5c6370")
|
||||
}
|
||||
|
||||
greenS := math.Min(baseSat*1.0, 1.0)
|
||||
greenV := math.Min(baseVal*1.0, 1.0)
|
||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||
if opts.IsLight {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
||||
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
} else {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
brightBlue := retoneToL(primaryColor, 85.0)
|
||||
palette.Color12 = NewColorInfo(brightBlue)
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyanH := hsv.H + 0.02
|
||||
if brightCyanH > 1.0 {
|
||||
brightCyanH -= 1.0
|
||||
}
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
}
|
||||
|
||||
yellowS := math.Min(baseSat*1.1, 1.0)
|
||||
yellowV := math.Min(baseVal*1.25, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Slightly more saturated variant of primary
|
||||
blueS := math.Min(ph.S*1.2, 1.0)
|
||||
blueV := ph.V * 0.95
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||
darkContainer := DeriveContainer(primaryColor, false)
|
||||
palette.Color5 = NewColorInfo(darkContainer)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
gray7S := baseSat * 0.12
|
||||
gray7V := math.Min(baseVal*1.05, 1.0)
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.15
|
||||
gray8V := baseVal * 0.65
|
||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||
|
||||
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightGreenS := math.Min(baseSat*0.7, 1.0)
|
||||
brightGreenV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||
|
||||
brightYellowS := math.Min(baseSat*0.7, 1.0)
|
||||
brightYellowV := math.Min(baseVal*1.5, 1.0)
|
||||
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||
|
||||
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
|
||||
// Color12: Start of the lighter gradient - slightly desaturated
|
||||
brightBlueS := ph.S * 0.85
|
||||
brightBlueV := math.Min(ph.V*1.1, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
// Medium-high saturation pastel primary
|
||||
color13S := ph.S * 0.7
|
||||
color13V := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
|
||||
|
||||
// Lower saturation, lighter variant
|
||||
color14S := ph.S * 0.45
|
||||
color14V := math.Min(ph.V*1.4, 1.0)
|
||||
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
|
||||
|
||||
white15S := baseSat * 0.05
|
||||
white15V := math.Min(baseVal*1.45, 1.0)
|
||||
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
|
||||
if opts.IsLight {
|
||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
||||
} else {
|
||||
palette.Color15 = NewColorInfo("#ffffff")
|
||||
}
|
||||
|
||||
return palette
|
||||
|
||||
@@ -366,19 +366,10 @@ func TestGeneratePalette(t *testing.T) {
|
||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||
}
|
||||
|
||||
// Color15 is now derived from primary, so just verify it's a valid color
|
||||
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
||||
color15Lum := Luminance(result.Color15.Hex)
|
||||
if tt.opts.IsLight {
|
||||
// Light mode: Color15 should still be relatively light
|
||||
if color15Lum < 0.5 {
|
||||
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||
}
|
||||
} else {
|
||||
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
|
||||
if color15Lum < 0.5 {
|
||||
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||
}
|
||||
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
||||
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -588,10 +579,6 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||
|
||||
bgColor := result.Color0.Hex
|
||||
for i := 1; i < 8; i++ {
|
||||
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||
if i == 5 || i == 6 {
|
||||
continue
|
||||
}
|
||||
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||
minLc := 30.0
|
||||
if lc < minLc && lc > 0 {
|
||||
|
||||
@@ -41,9 +41,6 @@ func init() {
|
||||
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type ArchDistribution struct {
|
||||
|
||||
450
core/internal/dms/app.go
Normal file
450
core/internal/dms/app.go
Normal file
@@ -0,0 +1,450 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateUpdate
|
||||
StateUpdatePassword
|
||||
StateUpdateProgress
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateGreeterMenu
|
||||
StateGreeterCompositorSelect
|
||||
StateGreeterPassword
|
||||
StateGreeterInstalling
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
updateDeps []DependencyInfo
|
||||
selectedUpdateDep int
|
||||
updateToggles map[string]bool
|
||||
|
||||
updateProgressChan chan updateProgressMsg
|
||||
updateProgress updateProgressMsg
|
||||
updateLogs []string
|
||||
sudoPassword string
|
||||
passwordInput string
|
||||
passwordError string
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
selectedGreeterItem int
|
||||
greeterInstallChan chan greeterProgressMsg
|
||||
greeterProgress greeterProgressMsg
|
||||
greeterLogs []string
|
||||
greeterPasswordInput string
|
||||
greeterPasswordError string
|
||||
greeterSudoPassword string
|
||||
greeterCompositors []string
|
||||
greeterSelectedComp int
|
||||
greeterChosenCompositor string
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
var dependencies []DependencyInfo
|
||||
var hyprlandInstalled, niriInstalled bool
|
||||
var err error
|
||||
if detector != nil {
|
||||
dependencies = detector.GetInstalledComponents()
|
||||
|
||||
// Use the proper detection method for both window managers
|
||||
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
|
||||
if err != nil {
|
||||
// Fallback to false if detection fails
|
||||
hyprlandInstalled = false
|
||||
niriInstalled = false
|
||||
}
|
||||
}
|
||||
|
||||
updateToggles := make(map[string]bool)
|
||||
for _, dep := range dependencies {
|
||||
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
|
||||
updateToggles[dep.Name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
updateToggles: updateToggles,
|
||||
updateDeps: dependencies,
|
||||
updateProgressChan: make(chan updateProgressMsg, 100),
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
greeterInstallChan: make(chan greeterProgressMsg, 100),
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{
|
||||
{Label: "Update", Action: StateUpdate},
|
||||
}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
// Greeter management
|
||||
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
// Check for both -c and -p flag patterns since quickshell can be started either way
|
||||
// -c dms: config name mode
|
||||
// -p <path>/dms: path mode (used when installed via system packages)
|
||||
cmd := exec.Command("pgrep", "-f", "qs.*dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case shellStartedMsg:
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
return m, nil
|
||||
case updateProgressMsg:
|
||||
m.updateProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.updateLogs = append(m.updateLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForProgress()
|
||||
case updateCompleteMsg:
|
||||
m.updateProgress.complete = true
|
||||
m.updateProgress.err = msg.err
|
||||
m.dependencies = m.detector.GetInstalledComponents()
|
||||
m.updateDeps = m.dependencies
|
||||
m.menuItems = m.buildMenuItems()
|
||||
|
||||
// Restart shell if update was successful and shell is running
|
||||
if msg.err == nil && m.isShellRunning() {
|
||||
restartShell()
|
||||
}
|
||||
return m, nil
|
||||
case greeterProgressMsg:
|
||||
m.greeterProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForGreeterProgress()
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginUpdatedMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case greeterPasswordValidMsg:
|
||||
if msg.valid {
|
||||
m.greeterSudoPassword = msg.password
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
m.state = StateGreeterInstalling
|
||||
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
|
||||
m.greeterLogs = []string{}
|
||||
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
|
||||
} else {
|
||||
m.greeterPasswordError = "Incorrect password. Please try again."
|
||||
m.greeterPasswordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case passwordValidMsg:
|
||||
if msg.valid {
|
||||
m.sudoPassword = msg.password
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
m.state = StateUpdateProgress
|
||||
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
|
||||
m.updateLogs = []string{}
|
||||
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
|
||||
} else {
|
||||
m.passwordError = "Incorrect password. Please try again."
|
||||
m.passwordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateUpdate:
|
||||
return m.updateUpdateView(msg)
|
||||
case StateUpdatePassword:
|
||||
return m.updatePasswordView(msg)
|
||||
case StateUpdateProgress:
|
||||
return m.updateProgressView(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateGreeterMenu:
|
||||
return m.updateGreeterMenu(msg)
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.updateGreeterCompositorSelect(msg)
|
||||
case StateGreeterPassword:
|
||||
return m.updateGreeterPasswordView(msg)
|
||||
case StateGreeterInstalling:
|
||||
return m.updateGreeterInstalling(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type updateProgressMsg struct {
|
||||
progress float64
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type updateCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type passwordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
type greeterProgressMsg struct {
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type greeterPasswordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (m Model) waitForProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.updateProgressChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) waitForGreeterProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.greeterInstallChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateUpdate:
|
||||
return m.renderUpdateView()
|
||||
case StateUpdatePassword:
|
||||
return m.renderPasswordView()
|
||||
case StateUpdateProgress:
|
||||
return m.renderProgressView()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateGreeterMenu:
|
||||
return m.renderGreeterMenu()
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.renderGreeterCompositorSelect()
|
||||
case StateGreeterPassword:
|
||||
return m.renderGreeterPasswordView()
|
||||
case StateGreeterInstalling:
|
||||
return m.renderGreeterInstalling()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
267
core/internal/dms/app_distro.go
Normal file
267
core/internal/dms/app_distro.go
Normal file
@@ -0,0 +1,267 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
|
||||
var dependencies []DependencyInfo
|
||||
var hyprlandInstalled, niriInstalled bool
|
||||
|
||||
if detector != nil {
|
||||
dependencies = detector.GetInstalledComponents()
|
||||
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
cmd := exec.Command("pgrep", "-f", "qs -c dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginUpdatedMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
143
core/internal/dms/detector.go
Normal file
143
core/internal/dms/detector.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
)
|
||||
|
||||
type Detector struct {
|
||||
homeDir string
|
||||
distribution distros.Distribution
|
||||
}
|
||||
|
||||
func (d *Detector) GetDistribution() distros.Distribution {
|
||||
return d.distribution
|
||||
}
|
||||
|
||||
func NewDetector() (*Detector, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
go func() {
|
||||
for range logChan {
|
||||
}
|
||||
}()
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dist, err := distros.NewDistribution(osInfo.Distribution.ID, logChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Detector{
|
||||
homeDir: homeDir,
|
||||
distribution: dist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Detector) IsDMSInstalled() bool {
|
||||
_, err := config.LocateDMSConfig()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetDependencyStatus() ([]deps.Dependency, error) {
|
||||
hyprlandDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerHyprland)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
niriDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerNiri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Combine dependencies and deduplicate
|
||||
depMap := make(map[string]deps.Dependency)
|
||||
|
||||
for _, dep := range hyprlandDeps {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
|
||||
for _, dep := range niriDeps {
|
||||
// If dependency already exists, keep the one that's installed or needs update
|
||||
if existing, exists := depMap[dep.Name]; exists {
|
||||
if dep.Status > existing.Status {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
} else {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map back to slice
|
||||
var allDeps []deps.Dependency
|
||||
for _, dep := range depMap {
|
||||
allDeps = append(allDeps, dep)
|
||||
}
|
||||
|
||||
return allDeps, nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetWindowManagerStatus() (bool, bool, error) {
|
||||
// Reuse the existing command detection logic from BaseDistribution
|
||||
// Since all distros embed BaseDistribution, we can access it via interface
|
||||
type CommandChecker interface {
|
||||
CommandExists(string) bool
|
||||
}
|
||||
|
||||
checker, ok := d.distribution.(CommandChecker)
|
||||
if !ok {
|
||||
// Fallback to direct command check if interface not available
|
||||
hyprlandInstalled := d.commandExists("hyprland") || d.commandExists("Hyprland")
|
||||
niriInstalled := d.commandExists("niri")
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
hyprlandInstalled := checker.CommandExists("hyprland") || checker.CommandExists("Hyprland")
|
||||
niriInstalled := checker.CommandExists("niri")
|
||||
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
func (d *Detector) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
||||
dependencies, err := d.GetDependencyStatus()
|
||||
if err != nil {
|
||||
return []DependencyInfo{}
|
||||
}
|
||||
|
||||
var components []DependencyInfo
|
||||
for _, dep := range dependencies {
|
||||
components = append(components, DependencyInfo{
|
||||
Name: dep.Name,
|
||||
Status: dep.Status,
|
||||
Description: dep.Description,
|
||||
Required: dep.Required,
|
||||
})
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
type DependencyInfo struct {
|
||||
Name string
|
||||
Status deps.DependencyStatus
|
||||
Description string
|
||||
Required bool
|
||||
}
|
||||
54
core/internal/dms/handlers_common.go
Normal file
54
core/internal/dms/handlers_common.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateShellView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
default:
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateAboutView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
if msg.String() == "esc" {
|
||||
m.state = StateMainMenu
|
||||
} else {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func terminateShell() {
|
||||
patterns := []string{"dms run", "qs -c dms"}
|
||||
for _, pattern := range patterns {
|
||||
cmd := exec.Command("pkill", "-f", pattern)
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func startShellDaemon() {
|
||||
cmd := exec.Command("dms", "run", "-d")
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Errorf("Error starting daemon: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func restartShell() {
|
||||
terminateShell()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
startShellDaemon()
|
||||
}
|
||||
392
core/internal/dms/handlers_features.go
Normal file
392
core/internal/dms/handlers_features.go
Normal file
@@ -0,0 +1,392 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateUpdateView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
filteredDeps := m.getFilteredDeps()
|
||||
maxIndex := len(filteredDeps) - 1
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedUpdateDep > 0 {
|
||||
m.selectedUpdateDep--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedUpdateDep < maxIndex {
|
||||
m.selectedUpdateDep++
|
||||
}
|
||||
case " ":
|
||||
if dep := m.getDepAtVisualIndex(m.selectedUpdateDep); dep != nil {
|
||||
m.updateToggles[dep.Name] = !m.updateToggles[dep.Name]
|
||||
}
|
||||
case "enter":
|
||||
hasSelected := false
|
||||
for _, toggle := range m.updateToggles {
|
||||
if toggle {
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSelected {
|
||||
m.state = StateMainMenu
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.state = StateUpdatePassword
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateUpdate
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.passwordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validatePassword(m.passwordInput)
|
||||
case "backspace":
|
||||
if len(m.passwordInput) > 0 {
|
||||
m.passwordInput = m.passwordInput[:len(m.passwordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.passwordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateProgressView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.updateProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.updateProgress = updateProgressMsg{}
|
||||
m.updateLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) validatePassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) performUpdate() tea.Cmd {
|
||||
var depsToUpdate []deps.Dependency
|
||||
|
||||
for _, depInfo := range m.updateDeps {
|
||||
if m.updateToggles[depInfo.Name] {
|
||||
depsToUpdate = append(depsToUpdate, deps.Dependency{
|
||||
Name: depInfo.Name,
|
||||
Status: depInfo.Status,
|
||||
Description: depInfo.Description,
|
||||
Required: depInfo.Required,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(depsToUpdate) == 0 {
|
||||
return func() tea.Msg {
|
||||
return updateCompleteMsg{err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
wm := deps.WindowManagerHyprland
|
||||
if m.niriInstalled {
|
||||
wm = deps.WindowManagerNiri
|
||||
}
|
||||
|
||||
sudoPassword := m.sudoPassword
|
||||
reinstallFlags := make(map[string]bool)
|
||||
for name, toggled := range m.updateToggles {
|
||||
if toggled {
|
||||
reinstallFlags[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
distribution := m.detector.GetDistribution()
|
||||
progressChan := m.updateProgressChan
|
||||
|
||||
return func() tea.Msg {
|
||||
installerChan := make(chan distros.InstallProgressMsg, 100)
|
||||
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
disabledFlags := make(map[string]bool)
|
||||
err := distribution.InstallPackages(ctx, depsToUpdate, wm, sudoPassword, reinstallFlags, disabledFlags, false, installerChan)
|
||||
close(installerChan)
|
||||
|
||||
if err != nil {
|
||||
progressChan <- updateProgressMsg{complete: true, err: err}
|
||||
} else {
|
||||
progressChan <- updateProgressMsg{complete: true}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for msg := range installerChan {
|
||||
progressChan <- updateProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
complete: msg.IsComplete,
|
||||
err: msg.Error,
|
||||
logOutput: msg.LogOutput,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedGreeterItem > 0 {
|
||||
m.selectedGreeterItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedGreeterItem < len(greeterMenuItems)-1 {
|
||||
m.selectedGreeterItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedGreeterItem == 0 {
|
||||
compositors := greeter.DetectCompositors()
|
||||
if len(compositors) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.greeterCompositors = compositors
|
||||
|
||||
if len(compositors) > 1 {
|
||||
m.state = StateGreeterCompositorSelect
|
||||
m.greeterSelectedComp = 0
|
||||
return m, nil
|
||||
} else {
|
||||
m.greeterChosenCompositor = compositors[0]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterCompositorSelect(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.greeterSelectedComp > 0 {
|
||||
m.greeterSelectedComp--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.greeterSelectedComp < len(m.greeterCompositors)-1 {
|
||||
m.greeterSelectedComp++
|
||||
}
|
||||
case "enter", " ":
|
||||
m.greeterChosenCompositor = m.greeterCompositors[m.greeterSelectedComp]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterPasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.greeterPasswordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validateGreeterPassword(m.greeterPasswordInput)
|
||||
case "backspace":
|
||||
if len(m.greeterPasswordInput) > 0 {
|
||||
m.greeterPasswordInput = m.greeterPasswordInput[:len(m.greeterPasswordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.greeterPasswordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterInstalling(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.greeterProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.greeterProgress = greeterProgressMsg{}
|
||||
m.greeterLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) performGreeterInstall() tea.Cmd {
|
||||
progressChan := m.greeterInstallChan
|
||||
sudoPassword := m.greeterSudoPassword
|
||||
compositor := m.greeterChosenCompositor
|
||||
|
||||
return func() tea.Msg {
|
||||
go func() {
|
||||
logFunc := func(msg string) {
|
||||
progressChan <- greeterProgressMsg{step: msg, logOutput: msg}
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Checking greetd installation..."}
|
||||
if err := performGreeterInstallSteps(progressChan, logFunc, sudoPassword, compositor); err != nil {
|
||||
progressChan <- greeterProgressMsg{step: "Installation failed", complete: true, err: err}
|
||||
return
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Installation complete", complete: true}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) validateGreeterPassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return greeterPasswordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func performGreeterInstallSteps(progressChan chan greeterProgressMsg, logFunc func(string), sudoPassword string, compositor string) error {
|
||||
if err := greeter.EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Detecting DMS installation..."}
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Selected compositor: %s", compositor))
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Copying greeter files..."}
|
||||
if err := greeter.CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Configuring greetd..."}
|
||||
if err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Synchronizing DMS configurations..."}
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
core/internal/dms/handlers_mainmenu.go
Normal file
61
core/internal/dms/handlers_mainmenu.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateUpdate:
|
||||
m.state = StateUpdate
|
||||
m.selectedUpdateDep = 0
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateGreeterMenu:
|
||||
m.state = StateGreeterMenu
|
||||
m.selectedGreeterItem = 0
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
377
core/internal/dms/plugins_handlers.go
Normal file
377
core/internal/dms/plugins_handlers.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updatePluginsMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedPluginsMenuItem > 0 {
|
||||
m.selectedPluginsMenuItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems)-1 {
|
||||
m.selectedPluginsMenuItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems) {
|
||||
selectedAction := m.pluginsMenuItems[m.selectedPluginsMenuItem].Action
|
||||
switch selectedAction {
|
||||
case StatePluginsBrowse:
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginsLoading = true
|
||||
m.pluginsError = ""
|
||||
m.pluginsList = nil
|
||||
return m, loadPlugins
|
||||
case StatePluginsInstalled:
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
m.installedPluginsList = nil
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "up", "k":
|
||||
if m.selectedPluginIndex > 0 {
|
||||
m.selectedPluginIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList)-1 {
|
||||
m.selectedPluginIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
m.state = StatePluginDetail
|
||||
}
|
||||
case "/":
|
||||
m.state = StatePluginSearch
|
||||
m.pluginSearchQuery = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
case "i":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if !installed {
|
||||
return m, installPlugin(plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "enter":
|
||||
m.state = StatePluginsBrowse
|
||||
m.filterPlugins()
|
||||
case "backspace":
|
||||
if len(m.pluginSearchQuery) > 0 {
|
||||
m.pluginSearchQuery = m.pluginSearchQuery[:len(m.pluginSearchQuery)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.pluginSearchQuery += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) filterPlugins() {
|
||||
if m.pluginSearchQuery == "" {
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
rawPlugins := make([]plugins.Plugin, len(m.pluginsList))
|
||||
for i, p := range m.pluginsList {
|
||||
rawPlugins[i] = plugins.Plugin{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
searchResults := plugins.FuzzySearch(m.pluginSearchQuery, rawPlugins)
|
||||
searchResults = plugins.SortByFirstParty(searchResults)
|
||||
|
||||
filtered := make([]pluginInfo, len(searchResults))
|
||||
for i, p := range searchResults {
|
||||
filtered[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
|
||||
m.filteredPluginsList = filtered
|
||||
m.selectedPluginIndex = 0
|
||||
}
|
||||
|
||||
type pluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
func loadPlugins() tea.Msg {
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
return pluginsLoadedMsg{plugins: pluginList}
|
||||
}
|
||||
|
||||
func (m *Model) updatePluginInstallStatus() {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range m.pluginsList {
|
||||
p := plugins.Plugin{ID: plugin.ID}
|
||||
installed, err := manager.IsInstalled(p)
|
||||
if err == nil {
|
||||
m.pluginInstallStatus[plugin.Name] = installed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsInstalled(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
case "up", "k":
|
||||
if m.selectedInstalledIndex > 0 {
|
||||
m.selectedInstalledIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList)-1 {
|
||||
m.selectedInstalledIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
m.state = StatePluginInstalledDetail
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsInstalled
|
||||
case "u":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
return m, uninstallPlugin(plugin)
|
||||
}
|
||||
case "p":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
return m, updatePlugin(plugin)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type installedPluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginUninstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginInstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginUpdatedMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
func loadInstalledPlugins() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
installedNames, err := manager.ListInstalled()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
var installed []plugins.Plugin
|
||||
for _, id := range installedNames {
|
||||
for _, p := range allPlugins {
|
||||
if p.ID == id {
|
||||
installed = append(installed, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installed = plugins.SortByFirstParty(installed)
|
||||
|
||||
return installedPluginsLoadedMsg{plugins: installed}
|
||||
}
|
||||
|
||||
func installPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Install(p); err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginInstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Uninstall(p); err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Update(p); err != nil {
|
||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginUpdatedMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
367
core/internal/dms/plugins_views.go
Normal file
367
core/internal/dms/plugins_views.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderPluginsMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for i, item := range m.pluginsMenuItems {
|
||||
if i == m.selectedPluginsMenuItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Select | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsBrowse() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Browse Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.pluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Fetching plugins from registry..."))
|
||||
} else if m.pluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.pluginsError)))
|
||||
} else if len(m.filteredPluginsList) == 0 {
|
||||
if m.pluginSearchQuery != "" {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("No plugins match '%s'", m.pluginSearchQuery)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render("No plugins found in registry."))
|
||||
}
|
||||
} else {
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
for i, plugin := range m.filteredPluginsList {
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
installMarker := ""
|
||||
if installed {
|
||||
installMarker = " [Installed]"
|
||||
}
|
||||
|
||||
if i == m.selectedPluginIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.pluginsLoading || m.pluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: View/Install | /: Search | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.selectedPluginIndex >= len(m.filteredPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if installed {
|
||||
b.WriteString(labelStyle.Render("Status: "))
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
b.WriteString(installedStyle.Render("Installed"))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if installed {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("i: Install | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginSearch() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(titleStyle.Render("Search Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Query: "))
|
||||
b.WriteString(titleStyle.Render(m.pluginSearchQuery + "▌"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Enter: Search | Esc: Cancel"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsInstalled() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Installed Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.installedPluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Loading installed plugins..."))
|
||||
} else if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
} else if len(m.installedPluginsList) == 0 {
|
||||
b.WriteString(normalStyle.Render("No plugins installed."))
|
||||
} else {
|
||||
for i, plugin := range m.installedPluginsList {
|
||||
if i == m.selectedInstalledIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.installedPluginsLoading || m.installedPluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Details | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginInstalledDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
if m.selectedInstalledIndex >= len(m.installedPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("u: Uninstall | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func wrapText(text string, width int) string {
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var lines []string
|
||||
currentLine := words[0]
|
||||
|
||||
for _, word := range words[1:] {
|
||||
if len(currentLine)+1+len(word) <= width {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
lines = append(lines, currentLine)
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
152
core/internal/dms/views_common.go
Normal file
152
core/internal/dms/views_common.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderMainMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("dms"))
|
||||
b.WriteString("\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range m.menuItems {
|
||||
if i == m.selectedItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, q/Esc: Exit"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderShellView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Shell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Opening interactive shell..."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("This will launch a shell with DMS environment loaded."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Press any key to launch shell, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderAboutView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("About DankMaterialShell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("DMS Management Interface %s", m.version)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("DankMaterialShell is a comprehensive desktop environment"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("built around Quickshell, providing a modern Material Design"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("experience for Wayland compositors."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Components:"))
|
||||
b.WriteString("\n")
|
||||
if len(m.dependencies) == 0 {
|
||||
b.WriteString(normalStyle.Render("\n Component detection not supported on this platform."))
|
||||
}
|
||||
for _, dep := range m.dependencies {
|
||||
status := "✗"
|
||||
if dep.Status == 1 {
|
||||
status = "✓"
|
||||
}
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s %s", status, dep.Name)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Esc: Back to main menu"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderBanner() string {
|
||||
theme := tui.TerminalTheme()
|
||||
|
||||
logo := `
|
||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
return titleStyle.Render(logo)
|
||||
}
|
||||
529
core/internal/dms/views_features.go
Normal file
529
core/internal/dms/views_features.go
Normal file
@@ -0,0 +1,529 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderUpdateView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Update Dependencies"))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateDeps) == 0 {
|
||||
b.WriteString("Loading dependencies...\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
categories := m.categorizeDependencies()
|
||||
currentIndex := 0
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if !exists || len(deps) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
categoryStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7060ac")).
|
||||
Bold(true).
|
||||
MarginTop(1)
|
||||
|
||||
b.WriteString(categoryStyle.Render(category + ":"))
|
||||
b.WriteString("\n")
|
||||
|
||||
for _, dep := range deps {
|
||||
var statusText, icon, reinstallMarker string
|
||||
var style lipgloss.Style
|
||||
|
||||
if m.updateToggles[dep.Name] {
|
||||
reinstallMarker = "🔄 "
|
||||
if dep.Status == 0 {
|
||||
statusText = "Will be installed"
|
||||
} else {
|
||||
statusText = "Will be upgraded"
|
||||
}
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
} else {
|
||||
switch dep.Status {
|
||||
case 1:
|
||||
icon = "✓"
|
||||
statusText = "Installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
||||
case 0:
|
||||
icon = "○"
|
||||
statusText = "Not installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
case 2:
|
||||
icon = "△"
|
||||
statusText = "Needs update"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
case 3:
|
||||
icon = "!"
|
||||
statusText = "Needs reinstall"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
}
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s%s%-25s %s", reinstallMarker, icon, dep.Name, statusText)
|
||||
|
||||
if currentIndex == m.selectedUpdateDep {
|
||||
line = "▶ " + line
|
||||
selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7060ac")).Bold(true)
|
||||
b.WriteString(selectedStyle.Render(line))
|
||||
} else {
|
||||
line = " " + line
|
||||
b.WriteString(style.Render(line))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Space: Toggle, Enter: Update Selected, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Package installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.passwordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.passwordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.passwordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderProgressView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Updating Packages"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.updateProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.updateProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
progressBar := fmt.Sprintf("[%s%s] %.0f%%",
|
||||
strings.Repeat("█", int(m.updateProgress.progress*30)),
|
||||
strings.Repeat("░", 30-int(m.updateProgress.progress*30)),
|
||||
m.updateProgress.progress*100)
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render(progressBar))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Live Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 8
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.updateProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Update failed: %v", m.updateProgress.err)))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Error Logs:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 15
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.updateProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Update complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) getFilteredDeps() []DependencyInfo {
|
||||
categories := m.categorizeDependencies()
|
||||
var filtered []DependencyInfo
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if exists {
|
||||
filtered = append(filtered, deps...)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (m Model) getDepAtVisualIndex(index int) *DependencyInfo {
|
||||
filtered := m.getFilteredDeps()
|
||||
if index >= 0 && index < len(filtered) {
|
||||
return &filtered[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Greeter installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.greeterPasswordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.greeterPasswordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.greeterPasswordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterCompositorSelect() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Select Compositor"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Multiple compositors detected. Choose which one to use for the greeter:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
for i, comp := range m.greeterCompositors {
|
||||
if i == m.greeterSelectedComp {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", comp)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", comp)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Greeter Management"))
|
||||
b.WriteString("\n")
|
||||
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range greeterMenuItems {
|
||||
if i == m.selectedGreeterItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterInstalling() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Installing Greeter"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.greeterProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.greeterProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.greeterLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 10
|
||||
startIdx := 0
|
||||
if len(m.greeterLogs) > maxLines {
|
||||
startIdx = len(m.greeterLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.greeterLogs); i++ {
|
||||
if m.greeterLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.greeterLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.greeterProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Installation failed: %v", m.greeterProgress.err)))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.greeterProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Greeter installation complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("To test the greeter, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl start greetd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("To enable on boot, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl enable --now greetd"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
||||
categories := map[string][]DependencyInfo{
|
||||
"Shell": {},
|
||||
"Shared Components": {},
|
||||
"Hyprland Components": {},
|
||||
"Niri Components": {},
|
||||
}
|
||||
|
||||
excludeList := map[string]bool{
|
||||
"git": true,
|
||||
"polkit-agent": true,
|
||||
"jq": true,
|
||||
"xdg-desktop-portal": true,
|
||||
"xdg-desktop-portal-wlr": true,
|
||||
"xdg-desktop-portal-hyprland": true,
|
||||
"xdg-desktop-portal-gtk": true,
|
||||
}
|
||||
|
||||
for _, dep := range m.updateDeps {
|
||||
if excludeList[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
switch dep.Name {
|
||||
case "dms (DankMaterialShell)", "quickshell":
|
||||
categories["Shell"] = append(categories["Shell"], dep)
|
||||
case "hyprland", "hyprctl":
|
||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||
case "niri":
|
||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||
case "kitty", "alacritty", "ghostty":
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
default:
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
}
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
p.dmsProcessed = true
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(sourcePath)
|
||||
fullPath := sourcePath
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
fullPath = filepath.Join(baseDir, sourcePath)
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := expanded
|
||||
if !filepath.IsAbs(expanded) {
|
||||
fullPath = filepath.Join(baseDir, expanded)
|
||||
}
|
||||
|
||||
includedBinds, err := p.parseFileWithSource(fullPath)
|
||||
includedBinds, err := p.parseFileWithSource(expanded)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -521,10 +521,33 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
||||
keybinds, err := p.parseFileWithSource(dmsBindsPath)
|
||||
data, err := os.ReadFile(dmsBindsPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = dmsBindsPath
|
||||
|
||||
var keybinds []MangoWCKeyBinding
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
for lineNum, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
kb := p.getKeybindAtLineContent(line, lineNum)
|
||||
if kb == nil {
|
||||
continue
|
||||
}
|
||||
kb.Source = dmsBindsPath
|
||||
p.addBind(kb)
|
||||
keybinds = append(keybinds, *kb)
|
||||
}
|
||||
|
||||
p.currentSource = prevSource
|
||||
p.dmsProcessed = true
|
||||
return keybinds
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
||||
}
|
||||
|
||||
func (c *ColorMode) GTKTheme() string {
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyDest = "org.freedesktop.Notifications"
|
||||
notifyPath = "/org/freedesktop/Notifications"
|
||||
notifyInterface = "org.freedesktop.Notifications"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
AppName string
|
||||
Icon string
|
||||
Summary string
|
||||
Body string
|
||||
FilePath string
|
||||
Timeout int32
|
||||
}
|
||||
|
||||
func Send(n Notification) error {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dbus session failed: %w", err)
|
||||
}
|
||||
|
||||
if n.AppName == "" {
|
||||
n.AppName = "DMS"
|
||||
}
|
||||
if n.Timeout == 0 {
|
||||
n.Timeout = 5000
|
||||
}
|
||||
|
||||
var actions []string
|
||||
if n.FilePath != "" {
|
||||
actions = []string{
|
||||
"open", "Open",
|
||||
"folder", "Open Folder",
|
||||
}
|
||||
}
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
if n.FilePath != "" {
|
||||
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
||||
}
|
||||
|
||||
obj := conn.Object(notifyDest, notifyPath)
|
||||
call := obj.Call(
|
||||
notifyInterface+".Notify",
|
||||
0,
|
||||
n.AppName,
|
||||
uint32(0),
|
||||
n.Icon,
|
||||
n.Summary,
|
||||
n.Body,
|
||||
actions,
|
||||
hints,
|
||||
n.Timeout,
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("notify call failed: %w", call.Err)
|
||||
}
|
||||
|
||||
var notificationID uint32
|
||||
if err := call.Store(¬ificationID); err != nil {
|
||||
return fmt.Errorf("failed to get notification id: %w", err)
|
||||
}
|
||||
|
||||
if len(actions) > 0 && n.FilePath != "" {
|
||||
spawnActionListener(notificationID, n.FilePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func spawnActionListener(notificationID uint32, filePath string) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, "notify-action-generic", fmt.Sprintf("%d", notificationID), filePath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
|
||||
func RunActionListener(args []string) {
|
||||
if len(args) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
notificationID, err := strconv.ParseUint(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := args[1]
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(notifyPath),
|
||||
dbus.WithMatchInterface(notifyInterface),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
signals := make(chan *dbus.Signal, 10)
|
||||
conn.Signal(signals)
|
||||
|
||||
for sig := range signals {
|
||||
switch sig.Name {
|
||||
case notifyInterface + ".ActionInvoked":
|
||||
if len(sig.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
id, ok := sig.Body[0].(uint32)
|
||||
if !ok || id != uint32(notificationID) {
|
||||
continue
|
||||
}
|
||||
action, ok := sig.Body[1].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
handleAction(action, filePath)
|
||||
return
|
||||
|
||||
case notifyInterface + ".NotificationClosed":
|
||||
if len(sig.Body) < 1 {
|
||||
continue
|
||||
}
|
||||
id, ok := sig.Body[0].(uint32)
|
||||
if !ok || id != uint32(notificationID) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAction(action, filePath string) {
|
||||
switch action {
|
||||
case "open", "default":
|
||||
openPath(filePath)
|
||||
case "folder":
|
||||
openPath(filepath.Dir(filePath))
|
||||
}
|
||||
}
|
||||
|
||||
func openPath(path string) {
|
||||
cmd := exec.Command("xdg-open", path)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -111,15 +110,17 @@ func (m *Manager) updateAdapterState() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
powered, _ := poweredVar.Value().(bool)
|
||||
|
||||
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discovering, _ := discoveringVar.Value().(bool)
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Powered = dbusutil.AsOr(poweredVar, false)
|
||||
m.state.Discovering = dbusutil.AsOr(discoveringVar, false)
|
||||
m.state.Powered = powered
|
||||
m.state.Discovering = discovering
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -168,20 +169,65 @@ func (m *Manager) updateDevices() error {
|
||||
}
|
||||
|
||||
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
||||
return Device{
|
||||
Path: path,
|
||||
Address: dbusutil.GetOr(props, "Address", ""),
|
||||
Name: dbusutil.GetOr(props, "Name", ""),
|
||||
Alias: dbusutil.GetOr(props, "Alias", ""),
|
||||
Paired: dbusutil.GetOr(props, "Paired", false),
|
||||
Trusted: dbusutil.GetOr(props, "Trusted", false),
|
||||
Blocked: dbusutil.GetOr(props, "Blocked", false),
|
||||
Connected: dbusutil.GetOr(props, "Connected", false),
|
||||
Class: dbusutil.GetOr(props, "Class", uint32(0)),
|
||||
Icon: dbusutil.GetOr(props, "Icon", ""),
|
||||
RSSI: dbusutil.GetOr(props, "RSSI", int16(0)),
|
||||
LegacyPairing: dbusutil.GetOr(props, "LegacyPairing", false),
|
||||
dev := Device{Path: path}
|
||||
|
||||
if v, ok := props["Address"]; ok {
|
||||
if addr, ok := v.Value().(string); ok {
|
||||
dev.Address = addr
|
||||
}
|
||||
}
|
||||
if v, ok := props["Name"]; ok {
|
||||
if name, ok := v.Value().(string); ok {
|
||||
dev.Name = name
|
||||
}
|
||||
}
|
||||
if v, ok := props["Alias"]; ok {
|
||||
if alias, ok := v.Value().(string); ok {
|
||||
dev.Alias = alias
|
||||
}
|
||||
}
|
||||
if v, ok := props["Paired"]; ok {
|
||||
if paired, ok := v.Value().(bool); ok {
|
||||
dev.Paired = paired
|
||||
}
|
||||
}
|
||||
if v, ok := props["Trusted"]; ok {
|
||||
if trusted, ok := v.Value().(bool); ok {
|
||||
dev.Trusted = trusted
|
||||
}
|
||||
}
|
||||
if v, ok := props["Blocked"]; ok {
|
||||
if blocked, ok := v.Value().(bool); ok {
|
||||
dev.Blocked = blocked
|
||||
}
|
||||
}
|
||||
if v, ok := props["Connected"]; ok {
|
||||
if connected, ok := v.Value().(bool); ok {
|
||||
dev.Connected = connected
|
||||
}
|
||||
}
|
||||
if v, ok := props["Class"]; ok {
|
||||
if class, ok := v.Value().(uint32); ok {
|
||||
dev.Class = class
|
||||
}
|
||||
}
|
||||
if v, ok := props["Icon"]; ok {
|
||||
if icon, ok := v.Value().(string); ok {
|
||||
dev.Icon = icon
|
||||
}
|
||||
}
|
||||
if v, ok := props["RSSI"]; ok {
|
||||
if rssi, ok := v.Value().(int16); ok {
|
||||
dev.RSSI = rssi
|
||||
}
|
||||
}
|
||||
if v, ok := props["LegacyPairing"]; ok {
|
||||
if legacy, ok := v.Value().(bool); ok {
|
||||
dev.LegacyPairing = legacy
|
||||
}
|
||||
}
|
||||
|
||||
return dev
|
||||
}
|
||||
|
||||
func (m *Manager) startAgent() error {
|
||||
@@ -282,13 +328,17 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
||||
m.stateMutex.Lock()
|
||||
dirty := false
|
||||
|
||||
if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
if v, ok := changed["Powered"]; ok {
|
||||
if powered, ok := v.Value().(bool); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
if v, ok := changed["Discovering"]; ok {
|
||||
if discovering, ok := v.Value().(bool); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
m.stateMutex.Unlock()
|
||||
@@ -299,28 +349,31 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
||||
}
|
||||
|
||||
func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) {
|
||||
paired, hasPaired := dbusutil.Get[bool](changed, "Paired")
|
||||
pairedVar, hasPaired := changed["Paired"]
|
||||
_, hasConnected := changed["Connected"]
|
||||
_, hasTrusted := changed["Trusted"]
|
||||
|
||||
if hasPaired {
|
||||
devicePath := string(path)
|
||||
if paired {
|
||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||
if wasPending {
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
if paired, ok := pairedVar.Value().(bool); ok {
|
||||
if paired {
|
||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||
|
||||
if wasPending {
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
}
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
m.pendingPairings.Delete(devicePath)
|
||||
}
|
||||
} else {
|
||||
m.pendingPairings.Delete(devicePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,14 +37,6 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
handleSetConfig(conn, req, m)
|
||||
case "clipboard.store":
|
||||
handleStore(conn, req, m)
|
||||
case "clipboard.pinEntry":
|
||||
handlePinEntry(conn, req, m)
|
||||
case "clipboard.unpinEntry":
|
||||
handleUnpinEntry(conn, req, m)
|
||||
case "clipboard.getPinnedEntries":
|
||||
handleGetPinnedEntries(conn, req, m)
|
||||
case "clipboard.getPinnedCount":
|
||||
handleGetPinnedCount(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||
}
|
||||
@@ -213,9 +205,6 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||
cfg.Disabled = v
|
||||
}
|
||||
if v, ok := models.Get[float64](req, "maxPinned"); ok {
|
||||
cfg.MaxPinned = int(v)
|
||||
}
|
||||
|
||||
if err := m.SetConfig(cfg); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -241,43 +230,3 @@ func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
||||
}
|
||||
|
||||
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.PinEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
|
||||
}
|
||||
|
||||
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.UnpinEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
|
||||
}
|
||||
|
||||
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
|
||||
pinned := m.GetPinnedEntries()
|
||||
models.Respond(conn, req.ID, pinned)
|
||||
}
|
||||
|
||||
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||
count := m.GetPinnedCount()
|
||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||
}
|
||||
|
||||
@@ -389,11 +389,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
}
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
if count < m.config.MaxHistory {
|
||||
count++
|
||||
continue
|
||||
@@ -423,11 +419,6 @@ func encodeEntry(e Entry) ([]byte, error) {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||
if e.Pinned {
|
||||
buf.WriteByte(1)
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -471,12 +462,6 @@ func decodeEntry(data []byte) (Entry, error) {
|
||||
binary.Read(buf, binary.BigEndian, &e.Hash)
|
||||
}
|
||||
|
||||
if buf.Len() >= 1 {
|
||||
var pinnedByte byte
|
||||
binary.Read(buf, binary.BigEndian, &pinnedByte)
|
||||
e.Pinned = pinnedByte == 1
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -750,54 +735,19 @@ func (m *Manager) ClearHistory() {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete only non-pinned entries
|
||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var toDelete [][]byte
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil || !entry.Pinned {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range toDelete {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
_, err := tx.CreateBucket([]byte("clipboard"))
|
||||
return err
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to clear clipboard history: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b != nil {
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, _ := decodeEntry(v)
|
||||
if entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
if pinnedCount == 0 {
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
}
|
||||
if err := m.compactDB(); err != nil {
|
||||
log.Errorf("Failed to compact database: %v", err)
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
@@ -1010,10 +960,6 @@ func (m *Manager) clearOldEntries(days int) error {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Skip pinned entries
|
||||
if entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if entry.Timestamp.Before(cutoff) {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
@@ -1304,153 +1250,3 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) PinEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
// Check pinned count
|
||||
cfg := m.getConfig()
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
if pinnedCount >= cfg.MaxPinned {
|
||||
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Pinned = true
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put(itob(id), encoded)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) UnpinEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Pinned = false
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put(itob(id), encoded)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedEntries() []Entry {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pinned []Entry
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.Pinned {
|
||||
entry.Data = nil
|
||||
pinned = append(pinned, entry)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to get pinned entries: %v", err)
|
||||
}
|
||||
|
||||
return pinned
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedCount() int {
|
||||
if m.db == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Errorf("Failed to count pinned entries: %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ type Config struct {
|
||||
AutoClearDays int `json:"autoClearDays"`
|
||||
ClearAtStartup bool `json:"clearAtStartup"`
|
||||
Disabled bool `json:"disabled"`
|
||||
MaxPinned int `json:"maxPinned"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
@@ -28,7 +27,6 @@ func DefaultConfig() Config {
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
AutoClearDays: 0,
|
||||
ClearAtStartup: false,
|
||||
MaxPinned: 25,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +100,6 @@ type Entry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsImage bool `json:"isImage"`
|
||||
Hash uint64 `json:"hash,omitempty"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
type objectParams struct {
|
||||
bus string
|
||||
dest string
|
||||
path string
|
||||
iface string
|
||||
}
|
||||
|
||||
func extractObjectParams(p map[string]any, requirePath bool) (objectParams, error) {
|
||||
bus, err := params.String(p, "bus")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
dest, err := params.String(p, "dest")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
|
||||
var path string
|
||||
if requirePath {
|
||||
path, err = params.String(p, "path")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
} else {
|
||||
path = params.StringOpt(p, "path", "/")
|
||||
}
|
||||
|
||||
iface, err := params.String(p, "interface")
|
||||
if err != nil {
|
||||
return objectParams{}, err
|
||||
}
|
||||
|
||||
return objectParams{bus: bus, dest: dest, path: path, iface: iface}, nil
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||
switch req.Method {
|
||||
case "dbus.call":
|
||||
handleCall(conn, req, m)
|
||||
case "dbus.getProperty":
|
||||
handleGetProperty(conn, req, m)
|
||||
case "dbus.setProperty":
|
||||
handleSetProperty(conn, req, m)
|
||||
case "dbus.getAllProperties":
|
||||
handleGetAllProperties(conn, req, m)
|
||||
case "dbus.introspect":
|
||||
handleIntrospect(conn, req, m)
|
||||
case "dbus.listNames":
|
||||
handleListNames(conn, req, m)
|
||||
case "dbus.subscribe":
|
||||
handleSubscribe(conn, req, m, clientID)
|
||||
case "dbus.unsubscribe":
|
||||
handleUnsubscribe(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleCall(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
method, err := params.String(req.Params, "method")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var args []any
|
||||
if argsRaw, ok := params.Any(req.Params, "args"); ok {
|
||||
if argsSlice, ok := argsRaw.([]any); ok {
|
||||
args = argsSlice
|
||||
}
|
||||
}
|
||||
|
||||
result, err := m.Call(op.bus, op.dest, op.path, op.iface, method, args)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
property, err := params.String(req.Params, "property")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.GetProperty(op.bus, op.dest, op.path, op.iface, property)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
property, err := params.String(req.Params, "property")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
value, ok := params.Any(req.Params, "value")
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing 'value' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.SetProperty(op.bus, op.dest, op.path, op.iface, property, value); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||
}
|
||||
|
||||
func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.GetAllProperties(op.bus, op.dest, op.path, op.iface)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dest, err := params.String(req.Params, "dest")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
path := params.StringOpt(req.Params, "path", "/")
|
||||
|
||||
result, err := m.Introspect(bus, dest, path)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleListNames(conn net.Conn, req models.Request, m *Manager) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.ListNames(bus)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sender := params.StringOpt(req.Params, "sender", "")
|
||||
path := params.StringOpt(req.Params, "path", "")
|
||||
iface := params.StringOpt(req.Params, "interface", "")
|
||||
member := params.StringOpt(req.Params, "member", "")
|
||||
|
||||
result, err := m.Subscribe(clientID, bus, sender, path, iface, member)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) {
|
||||
subID, err := params.String(req.Params, "subscriptionId")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Unsubscribe(subID); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
systemConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
|
||||
sessionConn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
systemConn.Close()
|
||||
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
systemConn: systemConn,
|
||||
sessionConn: sessionConn,
|
||||
}
|
||||
|
||||
go m.processSystemSignals()
|
||||
go m.processSessionSignals()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) getConn(bus string) (*dbus.Conn, error) {
|
||||
switch bus {
|
||||
case "system":
|
||||
if m.systemConn == nil {
|
||||
return nil, fmt.Errorf("system bus not connected")
|
||||
}
|
||||
return m.systemConn, nil
|
||||
case "session":
|
||||
if m.sessionConn == nil {
|
||||
return nil, fmt.Errorf("session bus not connected")
|
||||
}
|
||||
return m.sessionConn, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid bus: %s (must be 'system' or 'session')", bus)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Call(bus, dest, path, iface, method string, args []any) (*CallResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
fullMethod := iface + "." + method
|
||||
|
||||
call := obj.Call(fullMethod, 0, args...)
|
||||
if call.Err != nil {
|
||||
return nil, fmt.Errorf("dbus call failed: %w", call.Err)
|
||||
}
|
||||
|
||||
return &CallResult{Values: call.Body}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
var variant dbus.Variant
|
||||
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, iface, property).Store(&variant)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get property: %w", err)
|
||||
}
|
||||
|
||||
return &PropertyResult{Value: dbusutil.Normalize(variant.Value())}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetProperty(bus, dest, path, iface, property string, value any) error {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
call := obj.Call("org.freedesktop.DBus.Properties.Set", 0, iface, property, dbus.MakeVariant(value))
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to set property: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetAllProperties(bus, dest, path, iface string) (map[string]any, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
var props map[string]dbus.Variant
|
||||
err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface).Store(&props)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get properties: %w", err)
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
for k, v := range props {
|
||||
result[k] = dbusutil.Normalize(v.Value())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Introspect(bus, dest, path string) (*IntrospectResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
|
||||
var xml string
|
||||
err = obj.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&xml)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to introspect: %w", err)
|
||||
}
|
||||
|
||||
return &IntrospectResult{XML: xml}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListNames(bus string) (*ListNamesResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var names []string
|
||||
err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list names: %w", err)
|
||||
}
|
||||
|
||||
return &ListNamesResult{Names: names}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(clientID, bus, sender, path, iface, member string) (*SubscribeResult, error) {
|
||||
conn, err := m.getConn(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subID := generateSubscriptionID()
|
||||
|
||||
parts := []string{"type='signal'"}
|
||||
if sender != "" {
|
||||
parts = append(parts, fmt.Sprintf("sender='%s'", sender))
|
||||
}
|
||||
if path != "" {
|
||||
parts = append(parts, fmt.Sprintf("path='%s'", path))
|
||||
}
|
||||
if iface != "" {
|
||||
parts = append(parts, fmt.Sprintf("interface='%s'", iface))
|
||||
}
|
||||
if member != "" {
|
||||
parts = append(parts, fmt.Sprintf("member='%s'", member))
|
||||
}
|
||||
matchRule := strings.Join(parts, ",")
|
||||
|
||||
call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule)
|
||||
if call.Err != nil {
|
||||
return nil, fmt.Errorf("failed to add match rule: %w", call.Err)
|
||||
}
|
||||
|
||||
sub := &signalSubscription{
|
||||
Bus: bus,
|
||||
Sender: sender,
|
||||
Path: path,
|
||||
Interface: iface,
|
||||
Member: member,
|
||||
ClientID: clientID,
|
||||
}
|
||||
m.subscriptions.Store(subID, sub)
|
||||
|
||||
log.Debugf("dbus: subscribed %s to %s", subID, matchRule)
|
||||
|
||||
return &SubscribeResult{SubscriptionID: subID}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(subID string) error {
|
||||
sub, ok := m.subscriptions.LoadAndDelete(subID)
|
||||
if !ok {
|
||||
return fmt.Errorf("subscription not found: %s", subID)
|
||||
}
|
||||
|
||||
conn, err := m.getConn(sub.Bus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := []string{"type='signal'"}
|
||||
if sub.Sender != "" {
|
||||
parts = append(parts, fmt.Sprintf("sender='%s'", sub.Sender))
|
||||
}
|
||||
if sub.Path != "" {
|
||||
parts = append(parts, fmt.Sprintf("path='%s'", sub.Path))
|
||||
}
|
||||
if sub.Interface != "" {
|
||||
parts = append(parts, fmt.Sprintf("interface='%s'", sub.Interface))
|
||||
}
|
||||
if sub.Member != "" {
|
||||
parts = append(parts, fmt.Sprintf("member='%s'", sub.Member))
|
||||
}
|
||||
matchRule := strings.Join(parts, ",")
|
||||
|
||||
call := conn.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule)
|
||||
if call.Err != nil {
|
||||
log.Warnf("dbus: failed to remove match rule: %v", call.Err)
|
||||
}
|
||||
|
||||
log.Debugf("dbus: unsubscribed %s", subID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) UnsubscribeClient(clientID string) {
|
||||
var toDelete []string
|
||||
m.subscriptions.Range(func(subID string, sub *signalSubscription) bool {
|
||||
if sub.ClientID == clientID {
|
||||
toDelete = append(toDelete, subID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, subID := range toDelete {
|
||||
if err := m.Unsubscribe(subID); err != nil {
|
||||
log.Warnf("dbus: failed to unsubscribe %s: %v", subID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SubscribeSignals(clientID string) chan SignalEvent {
|
||||
ch := make(chan SignalEvent, 64)
|
||||
existing, loaded := m.signalSubscribers.LoadOrStore(clientID, ch)
|
||||
if loaded {
|
||||
return existing
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) UnsubscribeSignals(clientID string) {
|
||||
if ch, ok := m.signalSubscribers.LoadAndDelete(clientID); ok {
|
||||
close(ch)
|
||||
}
|
||||
m.UnsubscribeClient(clientID)
|
||||
}
|
||||
|
||||
func (m *Manager) processSystemSignals() {
|
||||
if m.systemConn == nil {
|
||||
return
|
||||
}
|
||||
ch := make(chan *dbus.Signal, 256)
|
||||
m.systemConn.Signal(ch)
|
||||
|
||||
for sig := range ch {
|
||||
m.dispatchSignal("system", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) processSessionSignals() {
|
||||
if m.sessionConn == nil {
|
||||
return
|
||||
}
|
||||
ch := make(chan *dbus.Signal, 256)
|
||||
m.sessionConn.Signal(ch)
|
||||
|
||||
for sig := range ch {
|
||||
m.dispatchSignal("session", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) dispatchSignal(bus string, sig *dbus.Signal) {
|
||||
path := string(sig.Path)
|
||||
iface := ""
|
||||
member := sig.Name
|
||||
|
||||
if idx := strings.LastIndex(sig.Name, "."); idx != -1 {
|
||||
iface = sig.Name[:idx]
|
||||
member = sig.Name[idx+1:]
|
||||
}
|
||||
|
||||
m.subscriptions.Range(func(subID string, sub *signalSubscription) bool {
|
||||
if sub.Bus != bus {
|
||||
return true
|
||||
}
|
||||
if sub.Path != "" && sub.Path != path && !strings.HasPrefix(path, sub.Path) {
|
||||
return true
|
||||
}
|
||||
if sub.Interface != "" && sub.Interface != iface {
|
||||
return true
|
||||
}
|
||||
if sub.Member != "" && sub.Member != member {
|
||||
return true
|
||||
}
|
||||
|
||||
event := SignalEvent{
|
||||
SubscriptionID: subID,
|
||||
Sender: sig.Sender,
|
||||
Path: path,
|
||||
Interface: iface,
|
||||
Member: member,
|
||||
Body: dbusutil.NormalizeSlice(sig.Body),
|
||||
}
|
||||
|
||||
ch, ok := m.signalSubscribers.Load(sub.ClientID)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- event:
|
||||
default:
|
||||
log.Warnf("dbus: channel full for %s, dropping signal", subID)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
m.signalSubscribers.Range(func(clientID string, ch chan SignalEvent) bool {
|
||||
close(ch)
|
||||
m.signalSubscribers.Delete(clientID)
|
||||
return true
|
||||
})
|
||||
|
||||
if m.systemConn != nil {
|
||||
m.systemConn.Close()
|
||||
}
|
||||
if m.sessionConn != nil {
|
||||
m.sessionConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func generateSubscriptionID() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Warnf("dbus: failed to generate random subscription ID: %v", err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
|
||||
subscriptions syncmap.Map[string, *signalSubscription]
|
||||
signalSubscribers syncmap.Map[string, chan SignalEvent]
|
||||
}
|
||||
|
||||
type signalSubscription struct {
|
||||
Bus string
|
||||
Sender string
|
||||
Path string
|
||||
Interface string
|
||||
Member string
|
||||
ClientID string
|
||||
}
|
||||
|
||||
type SignalEvent struct {
|
||||
SubscriptionID string `json:"subscriptionId"`
|
||||
Sender string `json:"sender"`
|
||||
Path string `json:"path"`
|
||||
Interface string `json:"interface"`
|
||||
Member string `json:"member"`
|
||||
Body []any `json:"body"`
|
||||
}
|
||||
|
||||
type CallResult struct {
|
||||
Values []any `json:"values"`
|
||||
}
|
||||
|
||||
type PropertyResult struct {
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
type IntrospectResult struct {
|
||||
XML string `json:"xml"`
|
||||
}
|
||||
|
||||
type ListNamesResult struct {
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
|
||||
type SubscribeResult struct {
|
||||
SubscriptionID string `json:"subscriptionId"`
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -111,17 +110,61 @@ func (m *Manager) updateAccountsState() error {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Accounts.IconFile = dbusutil.GetOr(props, "IconFile", "")
|
||||
m.state.Accounts.RealName = dbusutil.GetOr(props, "RealName", "")
|
||||
m.state.Accounts.UserName = dbusutil.GetOr(props, "UserName", "")
|
||||
m.state.Accounts.AccountType = dbusutil.GetOr(props, "AccountType", int32(0))
|
||||
m.state.Accounts.HomeDirectory = dbusutil.GetOr(props, "HomeDirectory", "")
|
||||
m.state.Accounts.Shell = dbusutil.GetOr(props, "Shell", "")
|
||||
m.state.Accounts.Email = dbusutil.GetOr(props, "Email", "")
|
||||
m.state.Accounts.Language = dbusutil.GetOr(props, "Language", "")
|
||||
m.state.Accounts.Location = dbusutil.GetOr(props, "Location", "")
|
||||
m.state.Accounts.Locked = dbusutil.GetOr(props, "Locked", false)
|
||||
m.state.Accounts.PasswordMode = dbusutil.GetOr(props, "PasswordMode", int32(0))
|
||||
if v, ok := props["IconFile"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.IconFile = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["RealName"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.RealName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["UserName"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.UserName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["AccountType"]; ok {
|
||||
if val, ok := v.Value().(int32); ok {
|
||||
m.state.Accounts.AccountType = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["HomeDirectory"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.HomeDirectory = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Shell"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Shell = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Email"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Email = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Language"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Language = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Location"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Location = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Locked"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Accounts.Locked = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["PasswordMode"]; ok {
|
||||
if val, ok := v.Value().(int32); ok {
|
||||
m.state.Accounts.PasswordMode = val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -137,7 +180,7 @@ func (m *Manager) updateSettingsState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if colorScheme, ok := dbusutil.As[uint32](variant); ok {
|
||||
if colorScheme, ok := variant.Value().(uint32); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.ColorScheme = colorScheme
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -133,15 +132,37 @@ func (m *Manager) updateSessionState() error {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Active = dbusutil.GetOr(props, "Active", m.state.Active)
|
||||
m.state.IdleHint = dbusutil.GetOr(props, "IdleHint", m.state.IdleHint)
|
||||
m.state.IdleSinceHint = dbusutil.GetOr(props, "IdleSinceHint", m.state.IdleSinceHint)
|
||||
if lockedHint, ok := dbusutil.Get[bool](props, "LockedHint"); ok {
|
||||
m.state.LockedHint = lockedHint
|
||||
m.state.Locked = lockedHint
|
||||
if v, ok := props["Active"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Active = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["IdleHint"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.IdleHint = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["IdleSinceHint"]; ok {
|
||||
if val, ok := v.Value().(uint64); ok {
|
||||
m.state.IdleSinceHint = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["LockedHint"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.LockedHint = val
|
||||
m.state.Locked = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Type"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.SessionType = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Class"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.SessionClass = val
|
||||
}
|
||||
}
|
||||
m.state.SessionType = dbusutil.GetOr(props, "Type", m.state.SessionType)
|
||||
m.state.SessionClass = dbusutil.GetOr(props, "Class", m.state.SessionClass)
|
||||
if v, ok := props["User"]; ok {
|
||||
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
||||
if uid, ok := userArr[0].(uint32); ok {
|
||||
@@ -149,12 +170,36 @@ func (m *Manager) updateSessionState() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
m.state.UserName = dbusutil.GetOr(props, "Name", m.state.UserName)
|
||||
m.state.RemoteHost = dbusutil.GetOr(props, "RemoteHost", m.state.RemoteHost)
|
||||
m.state.Service = dbusutil.GetOr(props, "Service", m.state.Service)
|
||||
m.state.TTY = dbusutil.GetOr(props, "TTY", m.state.TTY)
|
||||
m.state.Display = dbusutil.GetOr(props, "Display", m.state.Display)
|
||||
m.state.Remote = dbusutil.GetOr(props, "Remote", m.state.Remote)
|
||||
if v, ok := props["Name"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.UserName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["RemoteHost"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.RemoteHost = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Service"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Service = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["TTY"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.TTY = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Display"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Display = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Remote"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Remote = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Seat"]; ok {
|
||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||
if seatID, ok := seatArr[0].(string); ok {
|
||||
@@ -162,7 +207,11 @@ func (m *Manager) updateSessionState() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr)
|
||||
if v, ok := props["VTNr"]; ok {
|
||||
if val, ok := v.Value().(uint32); ok {
|
||||
m.state.VTNr = val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package loginctl
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -118,28 +117,31 @@ func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) {
|
||||
for key, variant := range changes {
|
||||
switch key {
|
||||
case "Active":
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Active = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "IdleHint":
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.IdleHint = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "IdleSinceHint":
|
||||
if val, ok := dbusutil.As[uint64](variant); ok {
|
||||
if val, ok := variant.Value().(uint64); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.IdleSinceHint = val
|
||||
m.stateMutex.Unlock()
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
case "LockedHint":
|
||||
if val, ok := dbusutil.As[bool](variant); ok {
|
||||
if val, ok := variant.Value().(bool); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.LockedHint = val
|
||||
m.state.Locked = val
|
||||
|
||||
@@ -150,13 +150,21 @@ func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority),
|
||||
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric),
|
||||
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||
log.Warnf("Failed to set priority for %s: %v", connName, err)
|
||||
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority)).Run(); err != nil {
|
||||
log.Warnf("Failed to set autoconnect-priority for %v: %v", connName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||
log.Warnf("Failed to set ipv4.route-metric for %v: %v", connName, err)
|
||||
}
|
||||
|
||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||
log.Warnf("Failed to set ipv6.route-metric for %v: %v", connName, err)
|
||||
}
|
||||
|
||||
log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||
@@ -45,15 +43,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "theme.auto.") {
|
||||
if themeModeManager == nil {
|
||||
models.RespondError(conn, req.ID, "theme mode manager not initialized")
|
||||
return
|
||||
}
|
||||
thememode.HandleRequest(conn, req, themeModeManager)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "loginctl.") {
|
||||
if loginctlManager == nil {
|
||||
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
||||
@@ -165,15 +154,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "dbus.") {
|
||||
if dbusManager == nil {
|
||||
models.RespondError(conn, req.ID, "dbus manager not initialized")
|
||||
return
|
||||
}
|
||||
serverDbus.HandleRequest(conn, req, dbusManager, dbusClientID)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "clipboard.") {
|
||||
switch req.Method {
|
||||
case "clipboard.getConfig":
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||
@@ -67,11 +65,7 @@ var brightnessManager *brightness.Manager
|
||||
var wlrOutputManager *wlroutput.Manager
|
||||
var evdevManager *evdev.Manager
|
||||
var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||
var cupsSubscribers syncmap.Map[string, bool]
|
||||
@@ -369,27 +363,6 @@ func InitializeClipboardManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeDbusManager() error {
|
||||
manager, err := serverDbus.NewManager()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize dbus manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
dbusManager = manager
|
||||
|
||||
log.Info("DBus manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeThemeModeManager() error {
|
||||
manager := thememode.NewManager()
|
||||
themeModeManager = manager
|
||||
|
||||
log.Info("Theme mode automation manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -467,14 +440,6 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if themeModeManager != nil {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
return Capabilities{Capabilities: caps}
|
||||
}
|
||||
|
||||
@@ -533,14 +498,6 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if themeModeManager != nil {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
return ServerInfo{
|
||||
APIVersion: APIVersion,
|
||||
CLIVersion: CLIVersion,
|
||||
@@ -809,38 +766,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
||||
}()
|
||||
}
|
||||
|
||||
if shouldSubscribe("theme.auto") && themeModeManager != nil {
|
||||
wg.Add(1)
|
||||
themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto")
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer themeModeManager.Unsubscribe(clientID + "-theme-auto")
|
||||
|
||||
initialState := themeModeManager.GetState()
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}:
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case state, ok := <-themeAutoChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}:
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
||||
wg.Add(1)
|
||||
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
||||
@@ -1208,31 +1133,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
||||
}()
|
||||
}
|
||||
|
||||
if shouldSubscribe("dbus") && dbusManager != nil {
|
||||
wg.Add(1)
|
||||
dbusChan := dbusManager.SubscribeSignals(dbusClientID)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer dbusManager.UnsubscribeSignals(dbusClientID)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-dbusChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "dbus", Data: event}:
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(eventChan)
|
||||
@@ -1298,12 +1198,6 @@ func cleanupManagers() {
|
||||
if clipboardManager != nil {
|
||||
clipboardManager.Close()
|
||||
}
|
||||
if dbusManager != nil {
|
||||
dbusManager.Close()
|
||||
}
|
||||
if themeModeManager != nil {
|
||||
themeModeManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
@@ -1399,15 +1293,6 @@ func Start(printDocs bool) error {
|
||||
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
|
||||
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
|
||||
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
|
||||
log.Info("Theme automation:")
|
||||
log.Info(" theme.auto.getState - Get current theme automation state")
|
||||
log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)")
|
||||
log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])")
|
||||
log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)")
|
||||
log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)")
|
||||
log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)")
|
||||
log.Info(" theme.auto.trigger - Trigger immediate re-evaluation")
|
||||
log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)")
|
||||
log.Info("Bluetooth:")
|
||||
log.Info(" bluetooth.getState - Get current bluetooth state")
|
||||
log.Info(" bluetooth.startDiscovery - Start device discovery")
|
||||
@@ -1565,12 +1450,6 @@ func Start(printDocs bool) error {
|
||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := InitializeThemeModeManager(); err != nil {
|
||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
|
||||
fatalErrChan := make(chan error, 1)
|
||||
if wlrOutputManager != nil {
|
||||
go func() {
|
||||
@@ -1611,14 +1490,6 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := InitializeDbusManager(); err != nil {
|
||||
log.Warnf("DBus manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("")
|
||||
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
package thememode
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
if manager == nil {
|
||||
models.RespondError(conn, req.ID, "theme mode manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "theme.auto.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "theme.auto.setEnabled":
|
||||
handleSetEnabled(conn, req, manager)
|
||||
case "theme.auto.setMode":
|
||||
handleSetMode(conn, req, manager)
|
||||
case "theme.auto.setSchedule":
|
||||
handleSetSchedule(conn, req, manager)
|
||||
case "theme.auto.setLocation":
|
||||
handleSetLocation(conn, req, manager)
|
||||
case "theme.auto.setUseIPLocation":
|
||||
handleSetUseIPLocation(conn, req, manager)
|
||||
case "theme.auto.trigger":
|
||||
handleTrigger(conn, req, manager)
|
||||
case "theme.auto.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
|
||||
enabled, err := params.Bool(req.Params, "enabled")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetEnabled(enabled)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
|
||||
}
|
||||
|
||||
func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
|
||||
mode, err := params.String(req.Params, "mode")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if mode != "time" && mode != "location" {
|
||||
models.RespondError(conn, req.ID, "invalid mode")
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetMode(mode)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
|
||||
}
|
||||
|
||||
func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
|
||||
startHour, err := params.Int(req.Params, "startHour")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
startMinute, err := params.Int(req.Params, "startMinute")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
endHour, err := params.Int(req.Params, "endHour")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
endMinute, err := params.Int(req.Params, "endMinute")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetSchedule(startHour, startMinute, endHour, endMinute)
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||
lat, err := params.Float(req.Params, "latitude")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
lon, err := params.Float(req.Params, "longitude")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetLocation(lat, lon)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
|
||||
}
|
||||
|
||||
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||
use, err := params.Bool(req.Params, "use")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetUseIPLocation(use)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
|
||||
}
|
||||
|
||||
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
|
||||
manager.TriggerUpdate()
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &initialState,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
Result: &state,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
package thememode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultStartHour = 18
|
||||
defaultStartMinute = 0
|
||||
defaultEndHour = 6
|
||||
defaultEndMinute = 0
|
||||
defaultElevationTwilight = -6.0
|
||||
defaultElevationDaylight = 3.0
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
config Config
|
||||
configMutex sync.RWMutex
|
||||
|
||||
state *State
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
subscribers syncmap.Map[string, chan State]
|
||||
|
||||
locationMutex sync.RWMutex
|
||||
cachedIPLat *float64
|
||||
cachedIPLon *float64
|
||||
|
||||
stopChan chan struct{}
|
||||
updateTrigger chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
m := &Manager{
|
||||
config: Config{
|
||||
Enabled: false,
|
||||
Mode: "time",
|
||||
StartHour: defaultStartHour,
|
||||
StartMinute: defaultStartMinute,
|
||||
EndHour: defaultEndHour,
|
||||
EndMinute: defaultEndMinute,
|
||||
ElevationTwilight: defaultElevationTwilight,
|
||||
ElevationDaylight: defaultElevationDaylight,
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
m.updateState(time.Now())
|
||||
|
||||
m.wg.Add(1)
|
||||
go m.schedulerLoop()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
if m.state == nil {
|
||||
return State{Config: m.getConfig()}
|
||||
}
|
||||
stateCopy := *m.state
|
||||
return stateCopy
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan State {
|
||||
ch := make(chan State, 64)
|
||||
m.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(val)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetEnabled(enabled bool) {
|
||||
m.configMutex.Lock()
|
||||
if m.config.Enabled == enabled {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.Enabled = enabled
|
||||
m.configMutex.Unlock()
|
||||
m.TriggerUpdate()
|
||||
}
|
||||
|
||||
func (m *Manager) SetMode(mode string) {
|
||||
m.configMutex.Lock()
|
||||
if m.config.Mode == mode {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.Mode = mode
|
||||
m.configMutex.Unlock()
|
||||
m.TriggerUpdate()
|
||||
}
|
||||
|
||||
func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
|
||||
m.configMutex.Lock()
|
||||
changed := m.config.StartHour != startHour ||
|
||||
m.config.StartMinute != startMinute ||
|
||||
m.config.EndHour != endHour ||
|
||||
m.config.EndMinute != endMinute
|
||||
if !changed {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.StartHour = startHour
|
||||
m.config.StartMinute = startMinute
|
||||
m.config.EndHour = endHour
|
||||
m.config.EndMinute = endMinute
|
||||
m.configMutex.Unlock()
|
||||
m.TriggerUpdate()
|
||||
}
|
||||
|
||||
func (m *Manager) SetLocation(lat, lon float64) {
|
||||
m.configMutex.Lock()
|
||||
if m.config.Latitude != nil && m.config.Longitude != nil &&
|
||||
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.Latitude = &lat
|
||||
m.config.Longitude = &lon
|
||||
m.config.UseIPLocation = false
|
||||
m.configMutex.Unlock()
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = nil
|
||||
m.cachedIPLon = nil
|
||||
m.locationMutex.Unlock()
|
||||
|
||||
m.TriggerUpdate()
|
||||
}
|
||||
|
||||
func (m *Manager) SetUseIPLocation(use bool) {
|
||||
m.configMutex.Lock()
|
||||
if m.config.UseIPLocation == use {
|
||||
m.configMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.config.UseIPLocation = use
|
||||
if use {
|
||||
m.config.Latitude = nil
|
||||
m.config.Longitude = nil
|
||||
}
|
||||
m.configMutex.Unlock()
|
||||
|
||||
if use {
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = nil
|
||||
m.cachedIPLon = nil
|
||||
m.locationMutex.Unlock()
|
||||
}
|
||||
|
||||
m.TriggerUpdate()
|
||||
}
|
||||
|
||||
func (m *Manager) TriggerUpdate() {
|
||||
select {
|
||||
case m.updateTrigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
default:
|
||||
close(m.stopChan)
|
||||
}
|
||||
m.wg.Wait()
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
close(ch)
|
||||
m.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) schedulerLoop() {
|
||||
defer m.wg.Done()
|
||||
|
||||
var timer *time.Timer
|
||||
for {
|
||||
config := m.getConfig()
|
||||
now := time.Now()
|
||||
var isLight bool
|
||||
var next time.Time
|
||||
if config.Enabled {
|
||||
isLight, next = m.computeSchedule(now, config)
|
||||
} else {
|
||||
m.stateMutex.RLock()
|
||||
if m.state != nil {
|
||||
isLight = m.state.IsLight
|
||||
}
|
||||
m.stateMutex.RUnlock()
|
||||
next = now.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
m.updateStateWithValues(config, isLight, next)
|
||||
|
||||
waitDur := time.Until(next)
|
||||
if !config.Enabled {
|
||||
waitDur = 24 * time.Hour
|
||||
}
|
||||
if waitDur < time.Second {
|
||||
waitDur = time.Second
|
||||
}
|
||||
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
timer = time.NewTimer(waitDur)
|
||||
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-m.updateTrigger:
|
||||
timer.Stop()
|
||||
continue
|
||||
case <-timer.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) updateState(now time.Time) {
|
||||
config := m.getConfig()
|
||||
var isLight bool
|
||||
var next time.Time
|
||||
if config.Enabled {
|
||||
isLight, next = m.computeSchedule(now, config)
|
||||
} else {
|
||||
m.stateMutex.RLock()
|
||||
if m.state != nil {
|
||||
isLight = m.state.IsLight
|
||||
}
|
||||
m.stateMutex.RUnlock()
|
||||
next = now.Add(24 * time.Hour)
|
||||
}
|
||||
m.updateStateWithValues(config, isLight, next)
|
||||
}
|
||||
|
||||
func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
|
||||
newState := State{
|
||||
Config: config,
|
||||
IsLight: isLight,
|
||||
NextTransition: next,
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
if m.state != nil && statesEqual(m.state, &newState) {
|
||||
m.stateMutex.Unlock()
|
||||
return
|
||||
}
|
||||
m.state = &newState
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
state := m.GetState()
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
select {
|
||||
case ch <- state:
|
||||
default:
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) getConfig() Config {
|
||||
m.configMutex.RLock()
|
||||
defer m.configMutex.RUnlock()
|
||||
return m.config
|
||||
}
|
||||
|
||||
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
if config.Latitude != nil && config.Longitude != nil {
|
||||
return config.Latitude, config.Longitude
|
||||
}
|
||||
if !config.UseIPLocation {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||
m.locationMutex.RUnlock()
|
||||
return lat, lon
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
lat, lon, err := wayland.FetchIPLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.locationMutex.Unlock()
|
||||
|
||||
return lat, lon
|
||||
}
|
||||
|
||||
func statesEqual(a, b *State) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
|
||||
return false
|
||||
}
|
||||
return a.Config == b.Config
|
||||
}
|
||||
|
||||
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
if config.Mode == "location" {
|
||||
return m.computeLocationSchedule(now, config)
|
||||
}
|
||||
return computeTimeSchedule(now, config)
|
||||
}
|
||||
|
||||
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
startMinutes := config.StartHour*60 + config.StartMinute
|
||||
endMinutes := config.EndHour*60 + config.EndMinute
|
||||
currentMinutes := now.Hour()*60 + now.Minute()
|
||||
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
|
||||
|
||||
if startMinutes == endMinutes {
|
||||
next := startTime
|
||||
if !next.After(now) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
return true, next
|
||||
}
|
||||
|
||||
if startMinutes < endMinutes {
|
||||
if currentMinutes < startMinutes {
|
||||
return true, startTime
|
||||
}
|
||||
if currentMinutes >= endMinutes {
|
||||
return true, startTime.Add(24 * time.Hour)
|
||||
}
|
||||
return false, endTime
|
||||
}
|
||||
|
||||
if currentMinutes >= startMinutes {
|
||||
return false, endTime.Add(24 * time.Hour)
|
||||
}
|
||||
if currentMinutes < endMinutes {
|
||||
return false, endTime
|
||||
}
|
||||
return true, startTime
|
||||
}
|
||||
|
||||
func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
lat, lon := m.getLocation(config)
|
||||
if lat == nil || lon == nil {
|
||||
currentIsLight := false
|
||||
m.stateMutex.RLock()
|
||||
if m.state != nil {
|
||||
currentIsLight = m.state.IsLight
|
||||
}
|
||||
m.stateMutex.RUnlock()
|
||||
return currentIsLight, now.Add(10 * time.Minute)
|
||||
}
|
||||
|
||||
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
|
||||
if cond != wayland.SunNormal {
|
||||
if cond == wayland.SunMidnightSun {
|
||||
return true, startOfNextDay(now)
|
||||
}
|
||||
return false, startOfNextDay(now)
|
||||
}
|
||||
|
||||
if now.Before(times.Sunrise) {
|
||||
return false, times.Sunrise
|
||||
}
|
||||
if now.Before(times.Sunset) {
|
||||
return true, times.Sunset
|
||||
}
|
||||
|
||||
nextDay := startOfNextDay(now)
|
||||
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
||||
if nextCond != wayland.SunNormal {
|
||||
if nextCond == wayland.SunMidnightSun {
|
||||
return true, startOfNextDay(nextDay)
|
||||
}
|
||||
return false, startOfNextDay(nextDay)
|
||||
}
|
||||
|
||||
return false, nextTimes.Sunrise
|
||||
}
|
||||
|
||||
func startOfNextDay(t time.Time) time.Time {
|
||||
next := t.Add(24 * time.Hour)
|
||||
return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
|
||||
}
|
||||
|
||||
func validateHourMinute(hour, minute int) bool {
|
||||
if hour < 0 || hour > 23 {
|
||||
return false
|
||||
}
|
||||
if minute < 0 || minute > 59 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
|
||||
if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
|
||||
return errInvalidTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errInvalidTime = errors.New("invalid schedule time")
|
||||
@@ -1,23 +0,0 @@
|
||||
package thememode
|
||||
|
||||
import "time"
|
||||
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode"`
|
||||
StartHour int `json:"startHour"`
|
||||
StartMinute int `json:"startMinute"`
|
||||
EndHour int `json:"endHour"`
|
||||
EndMinute int `json:"endMinute"`
|
||||
Latitude *float64 `json:"latitude,omitempty"`
|
||||
Longitude *float64 `json:"longitude,omitempty"`
|
||||
UseIPLocation bool `json:"useIPLocation"`
|
||||
ElevationTwilight float64 `json:"elevationTwilight"`
|
||||
ElevationDaylight float64 `json:"elevationDaylight"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Config Config `json:"config"`
|
||||
IsLight bool `json:"isLight"`
|
||||
NextTransition time.Time `json:"nextTransition"`
|
||||
}
|
||||
@@ -626,7 +626,6 @@ func (m *Manager) schedulerLoop() {
|
||||
m.schedule.calcDay = time.Time{}
|
||||
m.scheduleMutex.Unlock()
|
||||
m.recalcSchedule(time.Now())
|
||||
m.updateStateFromSchedule()
|
||||
m.configMutex.RLock()
|
||||
enabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package dbusutil
|
||||
|
||||
import "github.com/godbus/dbus/v5"
|
||||
|
||||
func As[T any](v dbus.Variant) (T, bool) {
|
||||
val, ok := v.Value().(T)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func AsOr[T any](v dbus.Variant, def T) T {
|
||||
if val, ok := v.Value().(T); ok {
|
||||
return val
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func Get[T any](m map[string]dbus.Variant, key string) (T, bool) {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
return As[T](v)
|
||||
}
|
||||
|
||||
func GetOr[T any](m map[string]dbus.Variant, key string, def T) T {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
return AsOr(v, def)
|
||||
}
|
||||
|
||||
func Normalize(v any) any {
|
||||
switch val := v.(type) {
|
||||
case dbus.Variant:
|
||||
return Normalize(val.Value())
|
||||
case dbus.ObjectPath:
|
||||
return string(val)
|
||||
case []dbus.ObjectPath:
|
||||
result := make([]string, len(val))
|
||||
for i, p := range val {
|
||||
result[i] = string(p)
|
||||
}
|
||||
return result
|
||||
case map[string]dbus.Variant:
|
||||
result := make(map[string]any)
|
||||
for k, vv := range val {
|
||||
result[k] = Normalize(vv.Value())
|
||||
}
|
||||
return result
|
||||
case []any:
|
||||
result := make([]any, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = Normalize(item)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeSlice(values []any) []any {
|
||||
result := make([]any, len(values))
|
||||
for i, v := range values {
|
||||
result[i] = Normalize(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package dbusutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAs(t *testing.T) {
|
||||
t.Run("string", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
val, ok := As[string](v)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "hello", val)
|
||||
})
|
||||
|
||||
t.Run("bool", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(true)
|
||||
val, ok := As[bool](v)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, val)
|
||||
})
|
||||
|
||||
t.Run("int32", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(int32(42))
|
||||
val, ok := As[int32](v)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, int32(42), val)
|
||||
})
|
||||
|
||||
t.Run("wrong type", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
_, ok := As[int](v)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsOr(t *testing.T) {
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
val := AsOr(v, "default")
|
||||
assert.Equal(t, "hello", val)
|
||||
})
|
||||
|
||||
t.Run("wrong type uses default", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(123)
|
||||
val := AsOr(v, "default")
|
||||
assert.Equal(t, "default", val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
m := map[string]dbus.Variant{
|
||||
"name": dbus.MakeVariant("test"),
|
||||
"enabled": dbus.MakeVariant(true),
|
||||
"count": dbus.MakeVariant(int32(5)),
|
||||
}
|
||||
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
val, ok := Get[string](m, "name")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test", val)
|
||||
})
|
||||
|
||||
t.Run("missing key", func(t *testing.T) {
|
||||
_, ok := Get[string](m, "missing")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("wrong type", func(t *testing.T) {
|
||||
_, ok := Get[int](m, "name")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOr(t *testing.T) {
|
||||
m := map[string]dbus.Variant{
|
||||
"name": dbus.MakeVariant("test"),
|
||||
}
|
||||
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
val := GetOr(m, "name", "default")
|
||||
assert.Equal(t, "test", val)
|
||||
})
|
||||
|
||||
t.Run("missing uses default", func(t *testing.T) {
|
||||
val := GetOr(m, "missing", "default")
|
||||
assert.Equal(t, "default", val)
|
||||
})
|
||||
|
||||
t.Run("wrong type uses default", func(t *testing.T) {
|
||||
val := GetOr(m, "name", 42)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalize(t *testing.T) {
|
||||
t.Run("variant unwrap", func(t *testing.T) {
|
||||
v := dbus.MakeVariant("hello")
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("nested variant", func(t *testing.T) {
|
||||
v := dbus.MakeVariant(dbus.MakeVariant("nested"))
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, "nested", result)
|
||||
})
|
||||
|
||||
t.Run("object path", func(t *testing.T) {
|
||||
v := dbus.ObjectPath("/org/test")
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, "/org/test", result)
|
||||
})
|
||||
|
||||
t.Run("object path slice", func(t *testing.T) {
|
||||
v := []dbus.ObjectPath{"/org/a", "/org/b"}
|
||||
result := Normalize(v)
|
||||
assert.Equal(t, []string{"/org/a", "/org/b"}, result)
|
||||
})
|
||||
|
||||
t.Run("variant map", func(t *testing.T) {
|
||||
v := map[string]dbus.Variant{
|
||||
"key": dbus.MakeVariant("value"),
|
||||
}
|
||||
result := Normalize(v)
|
||||
expected := map[string]any{"key": "value"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("any slice", func(t *testing.T) {
|
||||
v := []any{dbus.MakeVariant("a"), dbus.ObjectPath("/b")}
|
||||
result := Normalize(v)
|
||||
expected := []any{"a", "/b"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("passthrough primitives", func(t *testing.T) {
|
||||
assert.Equal(t, "hello", Normalize("hello"))
|
||||
assert.Equal(t, 42, Normalize(42))
|
||||
assert.Equal(t, true, Normalize(true))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeSlice(t *testing.T) {
|
||||
input := []any{
|
||||
dbus.MakeVariant("a"),
|
||||
dbus.ObjectPath("/b"),
|
||||
"c",
|
||||
}
|
||||
result := NormalizeSlice(input)
|
||||
expected := []any{"a", "/b", "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,7 +29,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,6 +14,7 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -27,7 +28,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -33,6 +33,7 @@ Recommends: cava
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
# Recommended system packages
|
||||
Recommends: NetworkManager
|
||||
|
||||
@@ -24,8 +24,10 @@ Requires: dms-cli = %{version}-%{release}
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: wl-clipboard
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
@@ -20,9 +20,12 @@ Requires: accountsservice
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: wl-clipboard
|
||||
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
@@ -23,10 +23,12 @@ Requires: dgop
|
||||
|
||||
# Core utilities (Highly recommended for DMS functionality)
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Recommends: wl-clipboard
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -15,6 +15,7 @@ Depends: ${misc:Depends},
|
||||
quickshell-git | quickshell,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -28,7 +29,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -14,6 +14,7 @@ Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
cava,
|
||||
cliphist,
|
||||
danksearch,
|
||||
dgop,
|
||||
matugen,
|
||||
@@ -27,7 +28,8 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-layouts,
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
qt6ct,
|
||||
wl-clipboard
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769018530,
|
||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||
"lastModified": 1766651565,
|
||||
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
kirigami.unwrapped
|
||||
sonnet
|
||||
qtmultimedia
|
||||
qtimageformats
|
||||
];
|
||||
in
|
||||
{
|
||||
@@ -79,7 +78,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-kWHB/FN6Z2Ydh+VvNrDnbg18RuJSDAle4DHDAP4NpNk=";
|
||||
vendorHash = "sha256-9CnZFtjXXWYELRiBX2UbZvWopnl9Y1ILuK+xP6YQZ9U=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
Saffron Bloom
|
||||
Spicy Miso
|
||||
|
||||
53
quickshell/Common/Facts.qml
Normal file
53
quickshell/Common/Facts.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var facts: [
|
||||
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
|
||||
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
|
||||
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
|
||||
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
|
||||
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
|
||||
"There's a nebula out there that's actually colder than empty space itself.",
|
||||
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
|
||||
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
|
||||
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
|
||||
"Distant galaxies can move away from us faster than light because space itself is stretching.",
|
||||
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
|
||||
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
|
||||
"A day on Venus lasts longer than its entire year around the Sun.",
|
||||
"On Mercury, the time between sunrises is 176 Earth days long.",
|
||||
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
|
||||
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
|
||||
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
|
||||
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
|
||||
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
|
||||
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
|
||||
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
|
||||
"Counting to a billion at one number per second would take over 31 years.",
|
||||
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
|
||||
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
|
||||
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
|
||||
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
|
||||
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
|
||||
"Only around 5% of galaxies are ever reachable—even at light-speed.",
|
||||
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
|
||||
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
|
||||
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
|
||||
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
|
||||
"The Moon moves 3.8 centimeters farther from Earth every year.",
|
||||
"The universe creates 275 million new stars every single day.",
|
||||
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
|
||||
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
|
||||
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
|
||||
]
|
||||
|
||||
function getRandomFact() {
|
||||
return facts[Math.floor(Math.random() * facts.length)]
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,7 @@ const DMS_ACTIONS = [
|
||||
{ id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" },
|
||||
{ id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" },
|
||||
{ id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" },
|
||||
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" },
|
||||
{ id: "spawn dms ipc call workspace-rename open", label: "Workspace: Rename" }
|
||||
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
|
||||
];
|
||||
|
||||
const NIRI_ACTIONS = {
|
||||
|
||||
@@ -13,7 +13,7 @@ import "settings/SessionStore.js" as Store
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int sessionConfigVersion: 3
|
||||
readonly property int sessionConfigVersion: 2
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
property bool _parseError: false
|
||||
@@ -21,7 +21,6 @@ Singleton {
|
||||
property bool _isReadOnly: false
|
||||
property bool _hasUnsavedChanges: false
|
||||
property var _loadedSessionSnapshot: null
|
||||
readonly property var _hooks: ({})
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
@@ -82,17 +81,7 @@ Singleton {
|
||||
property bool nightModeUseIPLocation: false
|
||||
property string nightModeLocationProvider: ""
|
||||
|
||||
property bool themeModeAutoEnabled: false
|
||||
property string themeModeAutoMode: "time"
|
||||
property int themeModeStartHour: 18
|
||||
property int themeModeStartMinute: 0
|
||||
property int themeModeEndHour: 6
|
||||
property int themeModeEndMinute: 0
|
||||
property bool themeModeShareGammaSettings: true
|
||||
|
||||
property var pinnedApps: []
|
||||
property var barPinnedApps: []
|
||||
property int dockLauncherPosition: 0
|
||||
property var hiddenTrayIds: []
|
||||
property var recentColors: []
|
||||
property bool showThirdPartyPlugins: false
|
||||
@@ -113,12 +102,6 @@ Singleton {
|
||||
property string weatherLocation: "New York, NY"
|
||||
property string weatherCoordinates: "40.7128,-74.0060"
|
||||
|
||||
property var hiddenApps: []
|
||||
property var appOverrides: ({})
|
||||
property bool searchAppActions: true
|
||||
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadSettings();
|
||||
@@ -182,7 +165,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
|
||||
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
||||
}
|
||||
}
|
||||
@@ -196,10 +179,14 @@ Singleton {
|
||||
_isReadOnly = !writable;
|
||||
if (_isReadOnly) {
|
||||
_hasUnsavedChanges = _checkForUnsavedChanges();
|
||||
if (!wasReadOnly)
|
||||
console.info("SessionData: session.json is now read-only");
|
||||
} else {
|
||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||
_hasUnsavedChanges = false;
|
||||
if (wasReadOnly && _pendingMigration)
|
||||
if (wasReadOnly)
|
||||
console.info("SessionData: session.json is now writable");
|
||||
if (_pendingMigration)
|
||||
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
|
||||
}
|
||||
_pendingMigration = null;
|
||||
@@ -261,7 +248,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
|
||||
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
||||
}
|
||||
}
|
||||
@@ -274,11 +261,8 @@ Singleton {
|
||||
_checkSessionWritable();
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
Spec.set(root, key, value, saveSettings, _hooks);
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(settings) {
|
||||
console.info("SessionData: Migrating configuration from undefined to version 1");
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
if (settings.acMonitorTimeout !== undefined) {
|
||||
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
|
||||
@@ -453,7 +437,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,7 +534,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -588,7 +572,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -626,7 +610,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -664,7 +648,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -707,6 +691,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function setNightModeAutoEnabled(enabled) {
|
||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled);
|
||||
nightModeAutoEnabled = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
@@ -742,11 +727,13 @@ Singleton {
|
||||
}
|
||||
|
||||
function setLatitude(lat) {
|
||||
console.log("SessionData: Setting latitude to", lat);
|
||||
latitude = lat;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLongitude(lng) {
|
||||
console.log("SessionData: Setting longitude to", lng);
|
||||
longitude = lng;
|
||||
saveSettings();
|
||||
}
|
||||
@@ -756,51 +743,11 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeAutoEnabled(enabled) {
|
||||
themeModeAutoEnabled = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeAutoMode(mode) {
|
||||
themeModeAutoMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeStartHour(hour) {
|
||||
themeModeStartHour = hour;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeStartMinute(minute) {
|
||||
themeModeStartMinute = minute;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeEndHour(hour) {
|
||||
themeModeEndHour = hour;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeEndMinute(minute) {
|
||||
themeModeEndMinute = minute;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeShareGammaSettings(share) {
|
||||
themeModeShareGammaSettings = share;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setPinnedApps(apps) {
|
||||
pinnedApps = apps;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setDockLauncherPosition(position) {
|
||||
dockLauncherPosition = position;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function addPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
@@ -822,32 +769,6 @@ Singleton {
|
||||
return appId && pinnedApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function setBarPinnedApps(apps) {
|
||||
barPinnedApps = apps;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function addBarPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
var currentPinned = [...barPinnedApps];
|
||||
if (currentPinned.indexOf(appId) === -1) {
|
||||
currentPinned.push(appId);
|
||||
setBarPinnedApps(currentPinned);
|
||||
}
|
||||
}
|
||||
|
||||
function removeBarPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
var currentPinned = barPinnedApps.filter(id => id !== appId);
|
||||
setBarPinnedApps(currentPinned);
|
||||
}
|
||||
|
||||
function isBarPinnedApp(appId) {
|
||||
return appId && barPinnedApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function hideTrayId(trayId) {
|
||||
if (!trayId)
|
||||
return;
|
||||
@@ -985,66 +906,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function hideApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const current = [...hiddenApps];
|
||||
if (current.indexOf(appId) === -1) {
|
||||
current.push(appId);
|
||||
hiddenApps = current;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function showApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
hiddenApps = hiddenApps.filter(id => id !== appId);
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function isAppHidden(appId) {
|
||||
return appId && hiddenApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function setAppOverride(appId, overrides) {
|
||||
if (!appId)
|
||||
return;
|
||||
const newOverrides = Object.assign({}, appOverrides);
|
||||
if (!overrides || Object.keys(overrides).length === 0) {
|
||||
delete newOverrides[appId];
|
||||
} else {
|
||||
newOverrides[appId] = overrides;
|
||||
}
|
||||
appOverrides = newOverrides;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getAppOverride(appId) {
|
||||
if (!appId)
|
||||
return null;
|
||||
return appOverrides[appId] || null;
|
||||
}
|
||||
|
||||
function clearAppOverride(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const newOverrides = Object.assign({}, appOverrides);
|
||||
delete newOverrides[appId];
|
||||
appOverrides = newOverrides;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setSearchAppActions(enabled) {
|
||||
searchAppActions = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setVpnLastConnected(uuid) {
|
||||
vpnLastConnected = uuid || "";
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
|
||||
@@ -79,45 +79,6 @@ Singleton {
|
||||
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 dankBarCenterWidgetsModel: centerWidgetsModel
|
||||
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
||||
@@ -146,9 +107,7 @@ Singleton {
|
||||
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool padHours12Hour: false
|
||||
property bool useFahrenheit: false
|
||||
property string windSpeedUnit: "kmh"
|
||||
property bool nightModeEnabled: false
|
||||
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
||||
property int customAnimationDuration: 500
|
||||
@@ -247,7 +206,6 @@ Singleton {
|
||||
property bool reverseScrolling: false
|
||||
property bool dwlShowAllTags: false
|
||||
property string workspaceColorMode: "default"
|
||||
property string workspaceOccupiedColorMode: "none"
|
||||
property string workspaceUnfocusedColorMode: "default"
|
||||
property string workspaceUrgentColorMode: "default"
|
||||
property bool workspaceFocusedBorderEnabled: false
|
||||
@@ -274,31 +232,20 @@ Singleton {
|
||||
property string spotlightModalViewMode: "list"
|
||||
property string browserPickerViewMode: "grid"
|
||||
property var browserUsageHistory: ({})
|
||||
property string appPickerViewMode: "grid"
|
||||
property var filePickerUsageHistory: ({})
|
||||
property bool sortAppsAlphabetically: false
|
||||
property int appLauncherGridColumns: 4
|
||||
property bool spotlightCloseNiriOverview: true
|
||||
property var spotlightSectionViewModes: ({})
|
||||
onSpotlightSectionViewModesChanged: saveSettings()
|
||||
property var appDrawerSectionViewModes: ({})
|
||||
onAppDrawerSectionViewModesChanged: saveSettings()
|
||||
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 _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||
property string _legacyVpnLastConnected: ""
|
||||
readonly property string weatherLocation: SessionData.weatherLocation
|
||||
readonly property string weatherCoordinates: SessionData.weatherCoordinates
|
||||
property bool useAutoLocation: false
|
||||
property bool weatherEnabled: true
|
||||
|
||||
property string networkPreference: "auto"
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
property string iconTheme: "System Default"
|
||||
property var availableIconThemes: ["System Default"]
|
||||
@@ -416,11 +363,9 @@ Singleton {
|
||||
property bool matugenTemplateDgop: true
|
||||
property bool matugenTemplateKcolorscheme: true
|
||||
property bool matugenTemplateVscode: true
|
||||
property bool matugenTemplateEmacs: true
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
property bool dockSmartAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property bool dockOpenOnOverview: false
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
@@ -434,13 +379,6 @@ Singleton {
|
||||
property real dockBorderOpacity: 1.0
|
||||
property int dockBorderThickness: 1
|
||||
property bool dockIsolateDisplays: false
|
||||
property bool dockLauncherEnabled: false
|
||||
property string dockLauncherLogoMode: "apps"
|
||||
property string dockLauncherLogoCustomPath: ""
|
||||
property string dockLauncherLogoColorOverride: ""
|
||||
property int dockLauncherLogoSizeOffset: 0
|
||||
property real dockLauncherLogoBrightness: 0.5
|
||||
property real dockLauncherLogoContrast: 1
|
||||
|
||||
property bool notificationOverlayEnabled: false
|
||||
property int overviewRows: 2
|
||||
@@ -455,8 +393,6 @@ Singleton {
|
||||
property bool lockScreenShowDate: true
|
||||
property bool lockScreenShowProfileImage: true
|
||||
property bool lockScreenShowPasswordField: true
|
||||
property bool lockScreenShowMediaPlayer: true
|
||||
property bool lockScreenPowerOffMonitorsOnLock: false
|
||||
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 15
|
||||
@@ -556,8 +492,7 @@ Singleton {
|
||||
"shadowIntensity": 0,
|
||||
"shadowOpacity": 60,
|
||||
"shadowColorMode": "text",
|
||||
"shadowCustomColor": "#000000",
|
||||
"clickThrough": false
|
||||
"shadowCustomColor": "#000000"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1078,11 +1013,6 @@ Singleton {
|
||||
_legacyWeatherLocation = obj.weatherLocation;
|
||||
if (obj?.weatherCoordinates !== undefined)
|
||||
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
||||
if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
|
||||
_legacyVpnLastConnected = obj.vpnLastConnected;
|
||||
SessionData.vpnLastConnected = _legacyVpnLastConnected;
|
||||
SessionData.saveSettings();
|
||||
}
|
||||
|
||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||
_hasLoaded = true;
|
||||
@@ -1260,11 +1190,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock)
|
||||
if (use24HourClock) {
|
||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
||||
if (padHours12Hour)
|
||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||
} else {
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveClockDateFormat() {
|
||||
@@ -2316,11 +2246,6 @@ Singleton {
|
||||
_legacyWeatherLocation = obj.weatherLocation;
|
||||
if (obj.weatherCoordinates !== undefined)
|
||||
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
||||
if (obj.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
|
||||
_legacyVpnLastConnected = obj.vpnLastConnected;
|
||||
SessionData.vpnLastConnected = _legacyVpnLastConnected;
|
||||
SessionData.saveSettings();
|
||||
}
|
||||
|
||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||
_hasLoaded = true;
|
||||
|
||||
@@ -94,9 +94,6 @@ Singleton {
|
||||
property var matugenColors: ({})
|
||||
property var _pendingGenerateParams: null
|
||||
|
||||
property bool themeModeAutomationActive: false
|
||||
property bool dmsServiceWasDisconnected: true
|
||||
|
||||
readonly property var dank16: {
|
||||
const raw = matugenColors?.dank16;
|
||||
if (!raw)
|
||||
@@ -179,237 +176,6 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||
switchTheme(SettingsData.currentThemeName, false, false);
|
||||
}
|
||||
|
||||
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
|
||||
startThemeModeAutomation();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
enabled: typeof SessionData !== "undefined"
|
||||
|
||||
function onThemeModeAutoEnabledChanged() {
|
||||
if (SessionData.themeModeAutoEnabled) {
|
||||
root.startThemeModeAutomation();
|
||||
} else {
|
||||
root.stopThemeModeAutomation();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeAutoModeChanged() {
|
||||
if (root.themeModeAutomationActive) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeStartHourChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeStartMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeEndHourChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeEndMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeShareGammaSettingsChanged() {
|
||||
if (root.themeModeAutomationActive) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeStartHourChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeStartMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeEndHourChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeEndMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onLatitudeChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||
if (!SessionData.nightModeUseIPLocation &&
|
||||
SessionData.latitude !== 0.0 &&
|
||||
SessionData.longitude !== 0.0 &&
|
||||
typeof DMSService !== "undefined") {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
root.evaluateThemeMode();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onLongitudeChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||
if (!SessionData.nightModeUseIPLocation &&
|
||||
SessionData.latitude !== 0.0 &&
|
||||
SessionData.longitude !== 0.0 &&
|
||||
typeof DMSService !== "undefined") {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
root.evaluateThemeMode();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeUseIPLocationChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||
if (typeof DMSService !== "undefined") {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": SessionData.nightModeUseIPLocation
|
||||
}, response => {
|
||||
if (!response.error && !SessionData.nightModeUseIPLocation &&
|
||||
SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
root.evaluateThemeMode();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// React to gamma backend's isDay state changes for location-based mode
|
||||
Connections {
|
||||
target: DisplayService
|
||||
enabled: typeof DisplayService !== "undefined" &&
|
||||
typeof SessionData !== "undefined" &&
|
||||
SessionData.themeModeAutoEnabled &&
|
||||
SessionData.themeModeAutoMode === "location" &&
|
||||
!themeAutoBackendAvailable()
|
||||
|
||||
function onGammaIsDayChanged() {
|
||||
if (root.isLightMode !== DisplayService.gammaIsDay) {
|
||||
root.setLightMode(DisplayService.gammaIsDay, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
|
||||
|
||||
function onLoginctlEvent(event) {
|
||||
if (!SessionData.themeModeAutoEnabled) return;
|
||||
if (event.event === "unlock" || event.event === "resume") {
|
||||
if (!themeAutoBackendAvailable()) {
|
||||
root.evaluateThemeMode();
|
||||
return;
|
||||
}
|
||||
DMSService.sendRequest("theme.auto.trigger", {});
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeAutoStateUpdate(data) {
|
||||
if (!SessionData.themeModeAutoEnabled) {
|
||||
return;
|
||||
}
|
||||
applyThemeAutoState(data);
|
||||
}
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") {
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
|
||||
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
|
||||
if (themeAutoBackendAvailable() && SessionData.themeModeAutoEnabled) {
|
||||
DMSService.sendRequest("theme.auto.getState", null, response => {
|
||||
if (response && response.result) {
|
||||
applyThemeAutoState(response.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!SessionData.themeModeAutoEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": true
|
||||
}, response => {
|
||||
if (!response.error) {
|
||||
console.info("Theme automation: IP location enabled after connection");
|
||||
}
|
||||
});
|
||||
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (!response.error) {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
}, locationResponse => {
|
||||
if (locationResponse?.error) {
|
||||
console.warn("Theme automation: Failed to set location", locationResponse.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("Theme automation: No location configured");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyGreeterTheme(themeName) {
|
||||
@@ -725,9 +491,7 @@ Singleton {
|
||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||
|
||||
function screenTransition() {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.doScreenTransition();
|
||||
}
|
||||
CompositorService.isNiri && NiriService.doScreenTransition();
|
||||
}
|
||||
|
||||
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||
@@ -779,10 +543,8 @@ Singleton {
|
||||
}
|
||||
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
|
||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
|
||||
SessionData.setLightMode(light);
|
||||
}
|
||||
|
||||
if (!isGreeterMode) {
|
||||
// Skip with matugen because, our script runner will do it.
|
||||
if (!matugenAvailable) {
|
||||
@@ -790,7 +552,6 @@ Singleton {
|
||||
}
|
||||
generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toggleLightMode(savePrefs = true) {
|
||||
@@ -991,11 +752,9 @@ Singleton {
|
||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
||||
}
|
||||
|
||||
function barIconSize(barThickness, offset, noBackground) {
|
||||
function barIconSize(barThickness, offset) {
|
||||
const defaultOffset = offset !== undefined ? offset : -6;
|
||||
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
|
||||
|
||||
return Math.round((barThickness / 48) * (size + defaultOffset));
|
||||
return Math.round((barThickness / 48) * (iconSize + defaultOffset));
|
||||
}
|
||||
|
||||
function barTextSize(barThickness, fontScale) {
|
||||
@@ -1145,7 +904,7 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const skipTemplates = [];
|
||||
if (!SettingsData.runDmsMatugenTemplates) {
|
||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
|
||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
|
||||
} else {
|
||||
if (!SettingsData.matugenTemplateGtk)
|
||||
skipTemplates.push("gtk");
|
||||
@@ -1187,8 +946,6 @@ Singleton {
|
||||
skipTemplates.push("kcolorscheme");
|
||||
if (!SettingsData.matugenTemplateVscode)
|
||||
skipTemplates.push("vscode");
|
||||
if (!SettingsData.matugenTemplateEmacs)
|
||||
skipTemplates.push("emacs");
|
||||
}
|
||||
if (skipTemplates.length > 0) {
|
||||
args.push("--skip-templates", skipTemplates.join(","));
|
||||
@@ -1472,7 +1229,7 @@ Singleton {
|
||||
return `#${invR}${invG}${invB}`;
|
||||
}
|
||||
|
||||
property var baseLogoColor: {
|
||||
property string baseLogoColor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return "";
|
||||
const colorOverride = SettingsData.launcherLogoColorOverride;
|
||||
@@ -1485,7 +1242,7 @@ Singleton {
|
||||
return colorOverride;
|
||||
}
|
||||
|
||||
property var effectiveLogoColor: {
|
||||
property string effectiveLogoColor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return "";
|
||||
|
||||
@@ -1692,294 +1449,4 @@ Singleton {
|
||||
root.switchTheme(defaultTheme, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Theme mode automation functions
|
||||
function themeAutoBackendAvailable() {
|
||||
return typeof DMSService !== "undefined" &&
|
||||
DMSService.isConnected &&
|
||||
Array.isArray(DMSService.capabilities) &&
|
||||
DMSService.capabilities.includes("theme.auto");
|
||||
}
|
||||
|
||||
function applyThemeAutoState(state) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) {
|
||||
return;
|
||||
}
|
||||
if (state.isLight !== undefined && root.isLightMode !== state.isLight) {
|
||||
root.setLightMode(state.isLight, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
function syncTimeThemeSchedule() {
|
||||
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time";
|
||||
|
||||
if (!timeModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("theme.auto.setMode", {"mode": "time"});
|
||||
|
||||
const shareSettings = SessionData.themeModeShareGammaSettings;
|
||||
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
|
||||
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
|
||||
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
|
||||
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
|
||||
|
||||
DMSService.sendRequest("theme.auto.setSchedule", {
|
||||
"startHour": startHour,
|
||||
"startMinute": startMinute,
|
||||
"endHour": endHour,
|
||||
"endMinute": endMinute
|
||||
}, response => {
|
||||
if (response && response.error) {
|
||||
console.error("Theme automation: Failed to sync time schedule:", response.error);
|
||||
}
|
||||
});
|
||||
|
||||
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": true});
|
||||
DMSService.sendRequest("theme.auto.trigger", {});
|
||||
}
|
||||
|
||||
function syncLocationThemeSchedule() {
|
||||
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location";
|
||||
|
||||
if (!locationModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("theme.auto.setMode", {"mode": "location"});
|
||||
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("theme.auto.setUseIPLocation", {"use": true});
|
||||
} else {
|
||||
DMSService.sendRequest("theme.auto.setUseIPLocation", {"use": false});
|
||||
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("theme.auto.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": true});
|
||||
DMSService.sendRequest("theme.auto.trigger", {});
|
||||
}
|
||||
|
||||
function evaluateThemeMode() {
|
||||
if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeAutoBackendAvailable()) {
|
||||
DMSService.sendRequest("theme.auto.getState", null, response => {
|
||||
if (response && response.result) {
|
||||
applyThemeAutoState(response.result);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = SessionData.themeModeAutoMode;
|
||||
|
||||
if (mode === "location") {
|
||||
evaluateLocationBasedThemeMode();
|
||||
} else {
|
||||
evaluateTimeBasedThemeMode();
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateLocationBasedThemeMode() {
|
||||
if (typeof DisplayService !== "undefined") {
|
||||
const shouldBeLight = DisplayService.gammaIsDay;
|
||||
if (root.isLightMode !== shouldBeLight) {
|
||||
root.setLightMode(shouldBeLight, true, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SessionData.nightModeUseIPLocation &&
|
||||
SessionData.latitude !== 0.0 &&
|
||||
SessionData.longitude !== 0.0) {
|
||||
const shouldBeLight = calculateIsDaytime(
|
||||
SessionData.latitude,
|
||||
SessionData.longitude
|
||||
);
|
||||
if (root.isLightMode !== shouldBeLight) {
|
||||
root.setLightMode(shouldBeLight, true, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.themeModeAutomationActive) {
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
console.warn("Theme automation: Waiting for IP location from backend");
|
||||
} else {
|
||||
console.warn("Theme automation: Location mode requires coordinates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateTimeBasedThemeMode() {
|
||||
const shareSettings = SessionData.themeModeShareGammaSettings;
|
||||
|
||||
const startHour = shareSettings ?
|
||||
SessionData.nightModeStartHour : SessionData.themeModeStartHour;
|
||||
const startMinute = shareSettings ?
|
||||
SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
|
||||
const endHour = shareSettings ?
|
||||
SessionData.nightModeEndHour : SessionData.themeModeEndHour;
|
||||
const endMinute = shareSettings ?
|
||||
SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
|
||||
|
||||
const now = new Date();
|
||||
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
const startMinutes = startHour * 60 + startMinute;
|
||||
const endMinutes = endHour * 60 + endMinute;
|
||||
|
||||
let shouldBeLight;
|
||||
if (startMinutes < endMinutes) {
|
||||
shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes;
|
||||
} else {
|
||||
shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes;
|
||||
}
|
||||
|
||||
if (root.isLightMode !== shouldBeLight) {
|
||||
root.setLightMode(shouldBeLight, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateIsDaytime(lat, lng) {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getFullYear(), 0, 0);
|
||||
const diff = now - start;
|
||||
const dayOfYear = Math.floor(diff / 86400000);
|
||||
const latRad = lat * Math.PI / 180;
|
||||
|
||||
const declination = 23.45 * Math.sin((360/365) * (dayOfYear - 81) * Math.PI / 180);
|
||||
const declinationRad = declination * Math.PI / 180;
|
||||
|
||||
const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad);
|
||||
|
||||
if (cosHourAngle > 1) {
|
||||
return false; // Polar night
|
||||
}
|
||||
if (cosHourAngle < -1) {
|
||||
return true; // Midnight sun
|
||||
}
|
||||
|
||||
const hourAngle = Math.acos(cosHourAngle);
|
||||
const hourAngleDeg = hourAngle * 180 / Math.PI;
|
||||
|
||||
const sunriseHour = 12 - hourAngleDeg / 15;
|
||||
const sunsetHour = 12 + hourAngleDeg / 15;
|
||||
|
||||
const timeZoneOffset = now.getTimezoneOffset() / 60;
|
||||
const localSunrise = sunriseHour - lng / 15 - timeZoneOffset;
|
||||
const localSunset = sunsetHour - lng / 15 - timeZoneOffset;
|
||||
|
||||
const currentHour = now.getHours() + now.getMinutes() / 60;
|
||||
|
||||
const normalizeSunrise = ((localSunrise % 24) + 24) % 24;
|
||||
const normalizeSunset = ((localSunset % 24) + 24) % 24;
|
||||
|
||||
return currentHour >= normalizeSunrise && currentHour < normalizeSunset;
|
||||
}
|
||||
|
||||
// Helper function to send location to backend
|
||||
function sendLocationToBackend() {
|
||||
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {"use": true}, response => {
|
||||
if (response?.error) {
|
||||
console.warn("Theme automation: Failed to enable IP location", response.error);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {"use": false}, response => {
|
||||
if (!response.error) {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
}, locResp => {
|
||||
if (locResp?.error) {
|
||||
console.warn("Theme automation: Failed to set location", locResp.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: locationRetryTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: false
|
||||
property int retryCount: 0
|
||||
|
||||
onTriggered: {
|
||||
if (root.sendLocationToBackend()) {
|
||||
stop();
|
||||
retryCount = 0;
|
||||
root.evaluateThemeMode();
|
||||
} else {
|
||||
retryCount++;
|
||||
if (retryCount >= 10) {
|
||||
stop();
|
||||
retryCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startThemeModeAutomation() {
|
||||
root.themeModeAutomationActive = true;
|
||||
|
||||
root.syncTimeThemeSchedule();
|
||||
root.syncLocationThemeSchedule();
|
||||
|
||||
const sent = root.sendLocationToBackend();
|
||||
|
||||
if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") {
|
||||
locationRetryTimer.start();
|
||||
} else {
|
||||
root.evaluateThemeMode();
|
||||
}
|
||||
}
|
||||
|
||||
function stopThemeModeAutomation() {
|
||||
root.themeModeAutomationActive = false;
|
||||
if (typeof DMSService !== "undefined" && DMSService.isConnected) {
|
||||
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,20 +35,10 @@ var SPEC = {
|
||||
nightModeUseIPLocation: { def: false },
|
||||
nightModeLocationProvider: { def: "" },
|
||||
|
||||
themeModeAutoEnabled: { def: false },
|
||||
themeModeAutoMode: { def: "time" },
|
||||
themeModeStartHour: { def: 18 },
|
||||
themeModeStartMinute: { def: 0 },
|
||||
themeModeEndHour: { def: 6 },
|
||||
themeModeEndMinute: { def: 0 },
|
||||
themeModeShareGammaSettings: { def: true },
|
||||
|
||||
weatherLocation: { def: "New York, NY" },
|
||||
weatherCoordinates: { def: "40.7128,-74.0060" },
|
||||
|
||||
pinnedApps: { def: [] },
|
||||
barPinnedApps: { def: [] },
|
||||
dockLauncherPosition: { def: 0 },
|
||||
hiddenTrayIds: { def: [] },
|
||||
recentColors: { def: [] },
|
||||
showThirdPartyPlugins: { def: false },
|
||||
@@ -65,25 +55,9 @@ var SPEC = {
|
||||
enabledGpuPciIds: { def: [] },
|
||||
|
||||
wifiDeviceOverride: { def: "" },
|
||||
weatherHourlyDetailed: { def: true },
|
||||
|
||||
hiddenApps: { def: [] },
|
||||
appOverrides: { def: {} },
|
||||
searchAppActions: { def: true },
|
||||
|
||||
vpnLastConnected: { def: "" }
|
||||
weatherHourlyDetailed: { def: true }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
return Object.keys(SPEC).concat(["configVersion"]);
|
||||
}
|
||||
|
||||
function set(root, key, value, saveFn, hooks) {
|
||||
if (!(key in SPEC)) return;
|
||||
root[key] = value;
|
||||
var hookName = SPEC[key].onChange;
|
||||
if (hookName && hooks && hooks[hookName]) {
|
||||
hooks[hookName](root);
|
||||
}
|
||||
saveFn();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.pragma library
|
||||
|
||||
.import "./SessionSpec.js" as SpecModule
|
||||
.import "./SessionSpec.js" as SpecModule
|
||||
|
||||
function parse(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
@@ -68,11 +68,6 @@ function migrateToVersion(obj, targetVersion, settingsData) {
|
||||
session.configVersion = 2;
|
||||
}
|
||||
|
||||
if (currentVersion < 3) {
|
||||
console.info("SessionData: Migrating session to version 3");
|
||||
session.configVersion = 3;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@ var SPEC = {
|
||||
|
||||
use24HourClock: { def: true },
|
||||
showSeconds: { def: false },
|
||||
padHours12Hour: { def: false },
|
||||
useFahrenheit: { def: false },
|
||||
windSpeedUnit: { def: "kmh" },
|
||||
nightModeEnabled: { def: false },
|
||||
animationSpeed: { def: 1 },
|
||||
customAnimationDuration: { def: 500 },
|
||||
@@ -79,18 +77,16 @@ var SPEC = {
|
||||
privacyShowCameraIcon: { def: false },
|
||||
privacyShowScreenShareIcon: { def: false },
|
||||
|
||||
controlCenterWidgets: {
|
||||
def: [
|
||||
{ id: "volumeSlider", enabled: true, width: 50 },
|
||||
{ id: "brightnessSlider", enabled: true, width: 50 },
|
||||
{ id: "wifi", enabled: true, width: 50 },
|
||||
{ id: "bluetooth", enabled: true, width: 50 },
|
||||
{ id: "audioOutput", enabled: true, width: 50 },
|
||||
{ id: "audioInput", enabled: true, width: 50 },
|
||||
{ id: "nightMode", enabled: true, width: 50 },
|
||||
{ id: "darkMode", enabled: true, width: 50 }
|
||||
]
|
||||
},
|
||||
controlCenterWidgets: { def: [
|
||||
{ id: "volumeSlider", enabled: true, width: 50 },
|
||||
{ id: "brightnessSlider", enabled: true, width: 50 },
|
||||
{ id: "wifi", enabled: true, width: 50 },
|
||||
{ id: "bluetooth", enabled: true, width: 50 },
|
||||
{ id: "audioOutput", enabled: true, width: 50 },
|
||||
{ id: "audioInput", enabled: true, width: 50 },
|
||||
{ id: "nightMode", enabled: true, width: 50 },
|
||||
{ id: "darkMode", enabled: true, width: 50 }
|
||||
]},
|
||||
|
||||
showWorkspaceIndex: { def: false },
|
||||
showWorkspaceName: { def: false },
|
||||
@@ -104,7 +100,6 @@ var SPEC = {
|
||||
reverseScrolling: { def: false },
|
||||
dwlShowAllTags: { def: false },
|
||||
workspaceColorMode: { def: "default" },
|
||||
workspaceOccupiedColorMode: { def: "none" },
|
||||
workspaceUnfocusedColorMode: { def: "default" },
|
||||
workspaceUrgentColorMode: { def: "default" },
|
||||
workspaceFocusedBorderEnabled: { def: false },
|
||||
@@ -121,15 +116,13 @@ var SPEC = {
|
||||
keyboardLayoutNameCompactMode: { def: false },
|
||||
runningAppsCurrentWorkspace: { def: false },
|
||||
runningAppsGroupByApp: { def: false },
|
||||
appIdSubstitutions: {
|
||||
def: [
|
||||
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
|
||||
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
|
||||
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
|
||||
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
|
||||
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
|
||||
]
|
||||
},
|
||||
appIdSubstitutions: { def: [
|
||||
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
|
||||
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
|
||||
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
|
||||
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
|
||||
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
|
||||
]},
|
||||
centeringMode: { def: "index" },
|
||||
clockDateFormat: { def: "" },
|
||||
lockDateFormat: { def: "" },
|
||||
@@ -137,26 +130,16 @@ var SPEC = {
|
||||
|
||||
appLauncherViewMode: { def: "list" },
|
||||
spotlightModalViewMode: { def: "list" },
|
||||
browserPickerViewMode: { def: "grid" },
|
||||
browserUsageHistory: { def: {} },
|
||||
appPickerViewMode: { def: "grid" },
|
||||
filePickerUsageHistory: { def: {} },
|
||||
sortAppsAlphabetically: { def: false },
|
||||
appLauncherGridColumns: { def: 4 },
|
||||
spotlightCloseNiriOverview: { def: true },
|
||||
spotlightSectionViewModes: { def: {} },
|
||||
appDrawerSectionViewModes: { def: {} },
|
||||
niriOverviewOverlayEnabled: { def: true },
|
||||
dankLauncherV2Size: { def: "compact" },
|
||||
dankLauncherV2BorderEnabled: { def: false },
|
||||
dankLauncherV2BorderThickness: { def: 2 },
|
||||
dankLauncherV2BorderColor: { def: "primary" },
|
||||
dankLauncherV2ShowFooter: { def: true },
|
||||
|
||||
useAutoLocation: { def: false },
|
||||
weatherEnabled: { def: true },
|
||||
|
||||
networkPreference: { def: "auto" },
|
||||
vpnLastConnected: { def: "" },
|
||||
|
||||
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
|
||||
availableIconThemes: { def: ["System Default"], persist: false },
|
||||
@@ -245,11 +228,9 @@ var SPEC = {
|
||||
matugenTemplateDgop: { def: true },
|
||||
matugenTemplateKcolorscheme: { def: true },
|
||||
matugenTemplateVscode: { def: true },
|
||||
matugenTemplateEmacs: { def: true },
|
||||
|
||||
showDock: { def: false },
|
||||
dockAutoHide: { def: false },
|
||||
dockSmartAutoHide: { def: false },
|
||||
dockGroupByApp: { def: false },
|
||||
dockOpenOnOverview: { def: false },
|
||||
dockPosition: { def: 1 },
|
||||
@@ -263,13 +244,6 @@ var SPEC = {
|
||||
dockBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
||||
dockBorderThickness: { def: 1 },
|
||||
dockIsolateDisplays: { def: false },
|
||||
dockLauncherEnabled: { def: false },
|
||||
dockLauncherLogoMode: { def: "apps" },
|
||||
dockLauncherLogoCustomPath: { def: "" },
|
||||
dockLauncherLogoColorOverride: { def: "" },
|
||||
dockLauncherLogoSizeOffset: { def: 0 },
|
||||
dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit },
|
||||
dockLauncherLogoContrast: { def: 1, coerce: percentToUnit },
|
||||
|
||||
notificationOverlayEnabled: { def: false },
|
||||
overviewRows: { def: 2, persist: false },
|
||||
@@ -284,8 +258,6 @@ var SPEC = {
|
||||
lockScreenShowDate: { def: true },
|
||||
lockScreenShowProfileImage: { def: true },
|
||||
lockScreenShowPasswordField: { def: true },
|
||||
lockScreenShowMediaPlayer: { def: true },
|
||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
fprintdAvailable: { def: false, persist: false },
|
||||
@@ -309,7 +281,7 @@ var SPEC = {
|
||||
osdAlwaysShowValue: { def: false },
|
||||
osdPosition: { def: 5 },
|
||||
osdVolumeEnabled: { def: true },
|
||||
osdMediaVolumeEnabled: { def: true },
|
||||
osdMediaVolumeEnabled : { def: true },
|
||||
osdBrightnessEnabled: { def: true },
|
||||
osdIdleInhibitorEnabled: { def: true },
|
||||
osdMicMuteEnabled: { def: true },
|
||||
@@ -340,54 +312,51 @@ var SPEC = {
|
||||
niriOutputSettings: { def: {} },
|
||||
hyprlandOutputSettings: { def: {} },
|
||||
|
||||
barConfigs: {
|
||||
def: [{
|
||||
id: "default",
|
||||
name: "Main Bar",
|
||||
enabled: true,
|
||||
position: 0,
|
||||
screenPreferences: ["all"],
|
||||
showOnLastDisplay: true,
|
||||
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
|
||||
centerWidgets: ["music", "clock", "weather"],
|
||||
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
|
||||
spacing: 4,
|
||||
innerPadding: 4,
|
||||
bottomGap: 0,
|
||||
transparency: 1.0,
|
||||
widgetTransparency: 1.0,
|
||||
squareCorners: false,
|
||||
noBackground: false,
|
||||
gothCornersEnabled: false,
|
||||
gothCornerRadiusOverride: false,
|
||||
gothCornerRadiusValue: 12,
|
||||
borderEnabled: false,
|
||||
borderColor: "surfaceText",
|
||||
borderOpacity: 1.0,
|
||||
borderThickness: 1,
|
||||
widgetOutlineEnabled: false,
|
||||
widgetOutlineColor: "primary",
|
||||
widgetOutlineOpacity: 1.0,
|
||||
widgetOutlineThickness: 1,
|
||||
fontScale: 1.0,
|
||||
autoHide: false,
|
||||
autoHideDelay: 250,
|
||||
showOnWindowsOpen: false,
|
||||
openOnOverview: false,
|
||||
visible: true,
|
||||
popupGapsAuto: true,
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true,
|
||||
scrollEnabled: true,
|
||||
scrollXBehavior: "column",
|
||||
scrollYBehavior: "workspace",
|
||||
shadowIntensity: 0,
|
||||
shadowOpacity: 60,
|
||||
shadowColorMode: "text",
|
||||
shadowCustomColor: "#000000",
|
||||
clickThrough: false
|
||||
}], onChange: "updateBarConfigs"
|
||||
},
|
||||
barConfigs: { def: [{
|
||||
id: "default",
|
||||
name: "Main Bar",
|
||||
enabled: true,
|
||||
position: 0,
|
||||
screenPreferences: ["all"],
|
||||
showOnLastDisplay: true,
|
||||
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
|
||||
centerWidgets: ["music", "clock", "weather"],
|
||||
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
|
||||
spacing: 4,
|
||||
innerPadding: 4,
|
||||
bottomGap: 0,
|
||||
transparency: 1.0,
|
||||
widgetTransparency: 1.0,
|
||||
squareCorners: false,
|
||||
noBackground: false,
|
||||
gothCornersEnabled: false,
|
||||
gothCornerRadiusOverride: false,
|
||||
gothCornerRadiusValue: 12,
|
||||
borderEnabled: false,
|
||||
borderColor: "surfaceText",
|
||||
borderOpacity: 1.0,
|
||||
borderThickness: 1,
|
||||
widgetOutlineEnabled: false,
|
||||
widgetOutlineColor: "primary",
|
||||
widgetOutlineOpacity: 1.0,
|
||||
widgetOutlineThickness: 1,
|
||||
fontScale: 1.0,
|
||||
autoHide: false,
|
||||
autoHideDelay: 250,
|
||||
showOnWindowsOpen: false,
|
||||
openOnOverview: false,
|
||||
visible: true,
|
||||
popupGapsAuto: true,
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true,
|
||||
scrollEnabled: true,
|
||||
scrollXBehavior: "column",
|
||||
scrollYBehavior: "workspace",
|
||||
shadowIntensity: 0,
|
||||
shadowOpacity: 60,
|
||||
shadowColorMode: "text",
|
||||
shadowCustomColor: "#000000"
|
||||
}], onChange: "updateBarConfigs" },
|
||||
|
||||
desktopClockEnabled: { def: false },
|
||||
desktopClockStyle: { def: "analog" },
|
||||
@@ -436,13 +405,11 @@ var SPEC = {
|
||||
|
||||
desktopWidgetGroups: { def: [] },
|
||||
|
||||
builtInPluginSettings: { def: {} },
|
||||
launcherPluginVisibility: { def: {} },
|
||||
launcherPluginOrder: { def: [] }
|
||||
builtInPluginSettings: { def: {} }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
return Object.keys(SPEC).filter(function (k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
|
||||
return Object.keys(SPEC).filter(function(k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
|
||||
}
|
||||
|
||||
function set(root, key, value, saveFn, hooks) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.pragma library
|
||||
|
||||
.import "./SettingsSpec.js" as SpecModule
|
||||
.import "./SettingsSpec.js" as SpecModule
|
||||
|
||||
function parse(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Modals.Changelog
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Greeter
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.DankLauncherV2
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Modules.DankDash
|
||||
@@ -473,17 +473,15 @@ Item {
|
||||
PopoutService.settingsModalLoader = settingsModalLoader;
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
PopoutService.settingsModal = item;
|
||||
PopoutService._onSettingsModalLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
SettingsModal {
|
||||
id: settingsModal
|
||||
property bool wasShown: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.settingsModal = settingsModal;
|
||||
PopoutService._onSettingsModalLoaded();
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
wasShown = true;
|
||||
@@ -508,22 +506,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dankLauncherV2ModalLoader
|
||||
|
||||
active: false
|
||||
SpotlightModal {
|
||||
id: spotlightModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.dankLauncherV2ModalLoader = dankLauncherV2ModalLoader;
|
||||
}
|
||||
|
||||
DankLauncherV2Modal {
|
||||
id: dankLauncherV2Modal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.dankLauncherV2Modal = dankLauncherV2Modal;
|
||||
PopoutService._onDankLauncherV2ModalLoaded();
|
||||
}
|
||||
PopoutService.spotlightModal = spotlightModal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,11 +537,6 @@ Item {
|
||||
AppPickerModal {
|
||||
id: filePickerModal
|
||||
title: I18n.tr("Open with...")
|
||||
viewMode: SettingsData.appPickerViewMode || "grid"
|
||||
|
||||
onViewModeChanged: {
|
||||
SettingsData.set("appPickerViewMode", viewMode)
|
||||
}
|
||||
|
||||
function shellEscape(str) {
|
||||
return "'" + str.replace(/'/g, "'\\''") + "'";
|
||||
@@ -649,18 +631,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: workspaceRenameModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: PopoutService.workspaceRenameModalLoader = workspaceRenameModalLoader
|
||||
|
||||
WorkspaceRenameModal {
|
||||
id: workspaceRenameModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListModalLoader
|
||||
|
||||
@@ -786,7 +756,6 @@ Item {
|
||||
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
||||
dankBarRepeater: dankBarRepeater
|
||||
hyprlandOverviewLoader: hyprlandOverviewLoader
|
||||
workspaceRenameModalLoader: workspaceRenameModalLoader
|
||||
}
|
||||
|
||||
Variants {
|
||||
|
||||
@@ -15,7 +15,6 @@ Item {
|
||||
required property var hyprKeybindsModalLoader
|
||||
required property var dankBarRepeater
|
||||
required property var hyprlandOverviewLoader
|
||||
required property var workspaceRenameModalLoader
|
||||
|
||||
function getFirstBar() {
|
||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||
@@ -967,17 +966,6 @@ Item {
|
||||
return success ? `PLUGIN_DISABLE_SUCCESS: ${pluginId}` : `PLUGIN_DISABLE_FAILED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function toggle(pluginId: string): string {
|
||||
if (!pluginId)
|
||||
return "ERROR: No plugin ID specified";
|
||||
|
||||
if (!PluginService.availablePlugins[pluginId])
|
||||
return `PLUGIN_NOT_FOUND: ${pluginId}`;
|
||||
|
||||
const success = PluginService.togglePlugin(pluginId);
|
||||
return success ? `PLUGIN_TOGGLE_SUCCESS: ${pluginId}` : `PLUGIN_TOGGLE_FAILED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
const plugins = PluginService.getAvailablePlugins();
|
||||
if (plugins.length === 0)
|
||||
@@ -1026,167 +1014,6 @@ Item {
|
||||
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.toggleDankLauncherV2WithQuery(query);
|
||||
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.toggleDankLauncherV2WithQuery(query);
|
||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function info(message: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showInfo(message);
|
||||
return "TOAST_INFO_SUCCESS";
|
||||
}
|
||||
|
||||
function infoWith(message: string, details: string, command: string, category: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showInfo(message, details, command, category);
|
||||
return "TOAST_INFO_SUCCESS";
|
||||
}
|
||||
|
||||
function warn(message: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showWarning(message);
|
||||
return "TOAST_WARN_SUCCESS";
|
||||
}
|
||||
|
||||
function warnWith(message: string, details: string, command: string, category: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showWarning(message, details, command, category);
|
||||
return "TOAST_WARN_SUCCESS";
|
||||
}
|
||||
|
||||
function error(message: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showError(message);
|
||||
return "TOAST_ERROR_SUCCESS";
|
||||
}
|
||||
|
||||
function errorWith(message: string, details: string, command: string, category: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showError(message, details, command, category);
|
||||
return "TOAST_ERROR_SUCCESS";
|
||||
}
|
||||
|
||||
function hide(): string {
|
||||
ToastService.hideToast();
|
||||
return "TOAST_HIDE_SUCCESS";
|
||||
}
|
||||
|
||||
function dismiss(category: string): string {
|
||||
if (!category)
|
||||
return "ERROR: No category specified";
|
||||
|
||||
ToastService.dismissCategory(category);
|
||||
return "TOAST_DISMISS_SUCCESS";
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
if (!ToastService.toastVisible)
|
||||
return "hidden";
|
||||
|
||||
const levels = ["info", "warn", "error"];
|
||||
return `visible:${levels[ToastService.currentLevel]}:${ToastService.currentMessage}`;
|
||||
}
|
||||
|
||||
target: "toast"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
FirstLaunchService.showWelcome();
|
||||
@@ -1366,40 +1193,4 @@ Item {
|
||||
|
||||
target: "desktopWidget"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
}
|
||||
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
if (root.workspaceRenameModalLoader.item.visible) {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
}
|
||||
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||
}
|
||||
|
||||
target: "workspace-rename"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ Singleton {
|
||||
readonly property int viewportBuffer: 100
|
||||
readonly property int extendedBuffer: 200
|
||||
readonly property int keyboardHintsHeight: 80
|
||||
readonly property int headerHeight: 32
|
||||
readonly property int headerHeight: 40
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ Item {
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
focus: false
|
||||
|
||||
ClipboardHeader {
|
||||
@@ -25,10 +25,7 @@ Item {
|
||||
width: parent.width
|
||||
totalCount: modal.totalCount
|
||||
showKeyboardHints: modal.showKeyboardHints
|
||||
activeTab: modal.activeTab
|
||||
pinnedCount: modal.pinnedCount
|
||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
onTabChanged: tabName => modal.activeTab = tabName
|
||||
onClearAllClicked: {
|
||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
||||
modal.clearAll();
|
||||
@@ -73,20 +70,18 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
|
||||
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
// Recents Tab
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.unpinnedEntries
|
||||
values: clipboardContent.modal.clipboardEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "recents"
|
||||
|
||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||
spacing: Theme.spacingXS
|
||||
@@ -119,11 +114,11 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
text: I18n.tr("No clipboard entries found")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.unpinnedEntries.length === 0
|
||||
visible: clipboardContent.modal.clipboardEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
@@ -140,62 +135,13 @@ Item {
|
||||
listView: clipboardListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Saved Tab
|
||||
DankListView {
|
||||
id: savedListView
|
||||
anchors.fill: parent
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.pinnedEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
visible: modal.activeTab === "saved"
|
||||
|
||||
spacing: Theme.spacingXS
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No saved clipboard entries")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: clipboardContent.modal.pinnedEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
width: savedListView.width
|
||||
height: ClipboardConstants.itemHeight
|
||||
entry: modelData
|
||||
entryIndex: index + 1
|
||||
itemIndex: index
|
||||
isSelected: false
|
||||
modal: clipboardContent.modal
|
||||
listView: savedListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyboardHintsContainer
|
||||
width: parent.width
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingM : 0
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
@@ -210,7 +156,7 @@ Item {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.margins: Theme.spacingL
|
||||
visible: modal.showKeyboardHints
|
||||
wtypeAvailable: modal.wtypeAvailable
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ Rectangle {
|
||||
|
||||
signal copyRequested
|
||||
signal deleteRequested
|
||||
signal pinRequested
|
||||
signal unpinRequested
|
||||
|
||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||
@@ -52,7 +50,7 @@ Rectangle {
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 110
|
||||
width: parent.width - 68
|
||||
spacing: Theme.spacingM
|
||||
|
||||
ClipboardThumbnail {
|
||||
@@ -102,32 +100,20 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: entry.pinned ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: entry.pinned ? Theme.primarySelected : "transparent"
|
||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 80
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyRequested()
|
||||
|
||||
@@ -8,13 +8,10 @@ Item {
|
||||
|
||||
property int totalCount: 0
|
||||
property bool showKeyboardHints: false
|
||||
property string activeTab: "recents"
|
||||
property int pinnedCount: 0
|
||||
|
||||
signal keyboardHintsToggled
|
||||
signal clearAllClicked
|
||||
signal closeClicked
|
||||
signal tabChanged(string tabName)
|
||||
|
||||
height: ClipboardConstants.headerHeight
|
||||
|
||||
@@ -44,28 +41,10 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
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 {
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
||||
tooltipText: I18n.tr("Keyboard Shortcuts")
|
||||
onClicked: keyboardHintsToggled()
|
||||
}
|
||||
|
||||
@@ -73,7 +52,6 @@ Item {
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize
|
||||
iconColor: Theme.surfaceText
|
||||
tooltipText: I18n.tr("Clear All")
|
||||
onClicked: clearAllClicked()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ DankModal {
|
||||
|
||||
property int totalCount: 0
|
||||
property var clipboardEntries: []
|
||||
property var pinnedEntries: []
|
||||
property int pinnedCount: 0
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool keyboardNavigationActive: false
|
||||
@@ -29,7 +27,16 @@ DankModal {
|
||||
property int activeImageLoads: 0
|
||||
readonly property int maxConcurrentLoads: 3
|
||||
readonly property bool clipboardAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("clipboard"))
|
||||
readonly property bool wtypeAvailable: SessionService.wtypeAvailable
|
||||
property bool wtypeAvailable: false
|
||||
|
||||
Process {
|
||||
id: wtypeCheck
|
||||
command: ["which", "wtype"]
|
||||
running: true
|
||||
onExited: exitCode => {
|
||||
clipboardHistoryModal.wtypeAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wtypeProcess
|
||||
@@ -67,36 +74,22 @@ DankModal {
|
||||
|
||||
function updateFilteredModel() {
|
||||
const query = searchText.trim();
|
||||
let filtered = [];
|
||||
|
||||
if (query.length === 0) {
|
||||
filtered = internalEntries;
|
||||
clipboardEntries = internalEntries;
|
||||
} else {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
filtered = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
||||
clipboardEntries = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
|
||||
// Sort: pinned first, then by ID descending
|
||||
filtered.sort((a, b) => {
|
||||
if (a.pinned !== b.pinned)
|
||||
return b.pinned ? 1 : -1;
|
||||
return b.id - a.id;
|
||||
});
|
||||
|
||||
clipboardEntries = filtered;
|
||||
unpinnedEntries = filtered.filter(e => !e.pinned);
|
||||
totalCount = clipboardEntries.length;
|
||||
if (unpinnedEntries.length === 0) {
|
||||
if (clipboardEntries.length === 0) {
|
||||
keyboardNavigationActive = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex >= unpinnedEntries.length) {
|
||||
selectedIndex = unpinnedEntries.length - 1;
|
||||
} else if (selectedIndex >= clipboardEntries.length) {
|
||||
selectedIndex = clipboardEntries.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
property var internalEntries: []
|
||||
property var unpinnedEntries: []
|
||||
property string activeTab: "recents"
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
@@ -142,10 +135,6 @@ DankModal {
|
||||
return;
|
||||
}
|
||||
internalEntries = response.result || [];
|
||||
|
||||
pinnedEntries = internalEntries.filter(e => e.pinned);
|
||||
pinnedCount = pinnedEntries.length;
|
||||
|
||||
updateFilteredModel();
|
||||
});
|
||||
}
|
||||
@@ -182,77 +171,16 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
function deletePinnedEntry(entry) {
|
||||
clearConfirmDialog.show(I18n.tr("Delete Saved Item?"), I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."), function () {
|
||||
DMSService.sendRequest("clipboard.deleteEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = internalEntries.filter(e => e.id !== entry.id);
|
||||
updateFilteredModel();
|
||||
ToastService.showInfo(I18n.tr("Saved item deleted"));
|
||||
});
|
||||
}, function () {});
|
||||
}
|
||||
|
||||
function pinEntry(entry) {
|
||||
DMSService.sendRequest("clipboard.getPinnedCount", null, function (countResponse) {
|
||||
if (countResponse.error) {
|
||||
ToastService.showError(I18n.tr("Failed to check pin limit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPinned = 25; // TODO: Get from config
|
||||
if (countResponse.result.count >= maxPinned) {
|
||||
ToastService.showError(I18n.tr("Maximum pinned entries reached") + " (" + maxPinned + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("clipboard.pinEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to pin entry"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Entry pinned"));
|
||||
refreshClipboard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unpinEntry(entry) {
|
||||
DMSService.sendRequest("clipboard.unpinEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to unpin entry"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Entry unpinned"));
|
||||
refreshClipboard();
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const hasPinned = pinnedCount > 0;
|
||||
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
|
||||
|
||||
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
refreshClipboard();
|
||||
if (hasPinned) {
|
||||
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
|
||||
}
|
||||
});
|
||||
}, function () {});
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
totalCount = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
switch (selectedItem?.type) {
|
||||
case "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
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "plugin_browse":
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "app":
|
||||
if (selectedItem?.isCore)
|
||||
break;
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
if (SessionService.nvidiaCommand) {
|
||||
result.push({
|
||||
name: I18n.tr("Launch on dGPU"),
|
||||
icon: "memory",
|
||||
action: "launch_dgpu"
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
readonly property bool hasActions: {
|
||||
switch (selectedItem?.type) {
|
||||
case "app":
|
||||
return !selectedItem?.isCore;
|
||||
case "plugin":
|
||||
return getPluginContextMenuActions().length > 0;
|
||||
case "plugin_browse":
|
||||
return selectedItem?.actions?.length > 0;
|
||||
default:
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,157 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
function getFileIcon(filename) {
|
||||
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
|
||||
|
||||
switch (ext) {
|
||||
case "pdf":
|
||||
return "picture_as_pdf";
|
||||
case "doc":
|
||||
case "docx":
|
||||
case "odt":
|
||||
return "description";
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
case "ods":
|
||||
return "table_chart";
|
||||
case "ppt":
|
||||
case "pptx":
|
||||
case "odp":
|
||||
return "slideshow";
|
||||
case "txt":
|
||||
case "md":
|
||||
case "rst":
|
||||
return "article";
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "svg":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
case "flac":
|
||||
case "ogg":
|
||||
return "audio_file";
|
||||
case "mp4":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "webm":
|
||||
return "video_file";
|
||||
case "zip":
|
||||
case "tar":
|
||||
case "gz":
|
||||
case "7z":
|
||||
case "rar":
|
||||
return "folder_zip";
|
||||
case "js":
|
||||
case "ts":
|
||||
case "py":
|
||||
case "rs":
|
||||
case "go":
|
||||
case "java":
|
||||
case "c":
|
||||
case "cpp":
|
||||
case "h":
|
||||
return "code";
|
||||
case "html":
|
||||
case "css":
|
||||
case "htm":
|
||||
return "web";
|
||||
case "json":
|
||||
case "xml":
|
||||
case "yaml":
|
||||
case "yml":
|
||||
return "data_object";
|
||||
case "sh":
|
||||
case "bash":
|
||||
case "zsh":
|
||||
return "terminal";
|
||||
default:
|
||||
return "insert_drive_file";
|
||||
}
|
||||
}
|
||||
|
||||
function stripIconPrefix(iconName) {
|
||||
if (!iconName)
|
||||
return "extension";
|
||||
if (iconName.startsWith("unicode:"))
|
||||
return iconName.substring(8);
|
||||
if (iconName.startsWith("material:"))
|
||||
return iconName.substring(9);
|
||||
if (iconName.startsWith("image:"))
|
||||
return iconName.substring(6);
|
||||
return iconName;
|
||||
}
|
||||
|
||||
function detectIconType(iconName) {
|
||||
if (!iconName)
|
||||
return "material";
|
||||
if (iconName.startsWith("unicode:"))
|
||||
return "unicode";
|
||||
if (iconName.startsWith("material:"))
|
||||
return "material";
|
||||
if (iconName.startsWith("image:"))
|
||||
return "image";
|
||||
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
|
||||
return "image";
|
||||
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
|
||||
return "image";
|
||||
return "material";
|
||||
}
|
||||
|
||||
function evaluateCalculator(query) {
|
||||
if (!query || query.length === 0)
|
||||
return null;
|
||||
|
||||
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
|
||||
if (mathExpr.length < 2)
|
||||
return null;
|
||||
|
||||
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
|
||||
if (!hasMath)
|
||||
return null;
|
||||
|
||||
try {
|
||||
var sanitized = mathExpr.replace(/\^/g, "**");
|
||||
var result = Function('"use strict"; return (' + sanitized + ')')();
|
||||
|
||||
if (typeof result === "number" && isFinite(result)) {
|
||||
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
|
||||
return {
|
||||
expression: query,
|
||||
result: result,
|
||||
displayResult: displayResult
|
||||
};
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sortPluginIdsByOrder(pluginIds, order) {
|
||||
if (!order || order.length === 0)
|
||||
return pluginIds;
|
||||
var orderMap = {};
|
||||
for (var i = 0; i < order.length; i++)
|
||||
orderMap[order[i]] = i;
|
||||
return pluginIds.slice().sort(function (a, b) {
|
||||
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
|
||||
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
}
|
||||
|
||||
function sortPluginsOrdered(plugins, order) {
|
||||
if (!order || order.length === 0)
|
||||
return plugins;
|
||||
var orderMap = {};
|
||||
for (var i = 0; i < order.length; i++)
|
||||
orderMap[order[i]] = i;
|
||||
return plugins.sort(function (a, b) {
|
||||
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
|
||||
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
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: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
readonly property int baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWithQuery(query) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
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:spotlight"
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
.import "ControllerUtils.js" as Utils
|
||||
|
||||
function transformApp(app, override, defaultActions, primaryActionLabel) {
|
||||
var appId = app.id || app.execString || app.exec || "";
|
||||
|
||||
var actions = [];
|
||||
if (app.actions && app.actions.length > 0) {
|
||||
for (var i = 0; i < app.actions.length; i++) {
|
||||
actions.push({
|
||||
name: app.actions[i].name,
|
||||
icon: "play_arrow",
|
||||
actionData: app.actions[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: appId,
|
||||
type: "app",
|
||||
name: override?.name || app.name || "",
|
||||
subtitle: override?.comment || app.comment || "",
|
||||
icon: override?.icon || app.icon || "application-x-executable",
|
||||
iconType: "image",
|
||||
section: "apps",
|
||||
data: app,
|
||||
keywords: app.keywords || [],
|
||||
actions: actions,
|
||||
primaryAction: {
|
||||
name: primaryActionLabel,
|
||||
icon: "open_in_new",
|
||||
action: "launch"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformCoreApp(app, openLabel) {
|
||||
var iconName = "apps";
|
||||
var iconType = "material";
|
||||
|
||||
if (app.icon) {
|
||||
if (app.icon.startsWith("svg+corner:")) {
|
||||
iconType = "composite";
|
||||
} else if (app.icon.startsWith("material:")) {
|
||||
iconName = app.icon.substring(9);
|
||||
} else {
|
||||
iconName = app.icon;
|
||||
iconType = "image";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: app.builtInPluginId || app.action || "",
|
||||
type: "app",
|
||||
name: app.name || "",
|
||||
subtitle: app.comment || "",
|
||||
icon: iconName,
|
||||
iconType: iconType,
|
||||
iconFull: app.icon,
|
||||
section: "apps",
|
||||
data: app,
|
||||
isCore: true,
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: openLabel,
|
||||
icon: "open_in_new",
|
||||
action: "launch"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
||||
var rawIcon = item.icon || "extension";
|
||||
var icon = Utils.stripIconPrefix(rawIcon);
|
||||
var iconType = item.iconType;
|
||||
if (!iconType) {
|
||||
if (rawIcon.startsWith("material:"))
|
||||
iconType = "material";
|
||||
else if (rawIcon.startsWith("unicode:"))
|
||||
iconType = "unicode";
|
||||
else
|
||||
iconType = "image";
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.action || "",
|
||||
type: "plugin",
|
||||
name: item.name || "",
|
||||
subtitle: item.comment || "",
|
||||
icon: icon,
|
||||
iconType: iconType,
|
||||
section: "plugin_" + pluginId,
|
||||
data: item,
|
||||
pluginId: pluginId,
|
||||
isBuiltInLauncher: true,
|
||||
keywords: item.keywords || [],
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: openLabel,
|
||||
icon: "open_in_new",
|
||||
action: "execute"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
|
||||
var filename = file.path ? file.path.split("/").pop() : "";
|
||||
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
||||
|
||||
return {
|
||||
id: file.path || "",
|
||||
type: "file",
|
||||
name: filename,
|
||||
subtitle: dirname,
|
||||
icon: Utils.getFileIcon(filename),
|
||||
iconType: "material",
|
||||
section: "files",
|
||||
data: file,
|
||||
actions: [
|
||||
{
|
||||
name: openFolderLabel,
|
||||
icon: "folder_open",
|
||||
action: "open_folder"
|
||||
},
|
||||
{
|
||||
name: copyPathLabel,
|
||||
icon: "content_copy",
|
||||
action: "copy_path"
|
||||
}
|
||||
],
|
||||
primaryAction: {
|
||||
name: openLabel,
|
||||
icon: "open_in_new",
|
||||
action: "open"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformPluginItem(item, pluginId, selectLabel) {
|
||||
var rawIcon = item.icon || "extension";
|
||||
var icon = Utils.stripIconPrefix(rawIcon);
|
||||
var iconType = item.iconType;
|
||||
if (!iconType) {
|
||||
if (rawIcon.startsWith("material:"))
|
||||
iconType = "material";
|
||||
else if (rawIcon.startsWith("unicode:"))
|
||||
iconType = "unicode";
|
||||
else
|
||||
iconType = "image";
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id || item.name || "",
|
||||
type: "plugin",
|
||||
name: item.name || "",
|
||||
subtitle: item.comment || item.description || "",
|
||||
icon: icon,
|
||||
iconType: iconType,
|
||||
section: "plugin_" + pluginId,
|
||||
data: item,
|
||||
pluginId: pluginId,
|
||||
keywords: item.keywords || [],
|
||||
actions: item.actions || [],
|
||||
primaryAction: item.primaryAction || {
|
||||
name: selectLabel,
|
||||
icon: "check",
|
||||
action: "execute"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createCalculatorItem(calc, query, copyLabel) {
|
||||
return {
|
||||
id: "calculator_result",
|
||||
type: "calculator",
|
||||
name: calc.displayResult,
|
||||
subtitle: query + " =",
|
||||
icon: "calculate",
|
||||
iconType: "material",
|
||||
section: "calculator",
|
||||
data: {
|
||||
expression: calc.expression,
|
||||
result: calc.result
|
||||
},
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: copyLabel,
|
||||
icon: "content_copy",
|
||||
action: "copy"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
|
||||
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
|
||||
return {
|
||||
id: "browse_" + pluginId,
|
||||
type: "plugin_browse",
|
||||
name: plugin.name || pluginId,
|
||||
subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
|
||||
icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
|
||||
iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
|
||||
section: "browse_plugins",
|
||||
data: {
|
||||
pluginId: pluginId,
|
||||
plugin: plugin,
|
||||
isBuiltIn: isBuiltIn
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
name: "All",
|
||||
icon: isAllowed ? "visibility" : "visibility_off",
|
||||
action: "toggle_all_visibility"
|
||||
}
|
||||
],
|
||||
primaryAction: {
|
||||
name: browseLabel,
|
||||
icon: "arrow_forward",
|
||||
action: "browse_plugin"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,868 +0,0 @@
|
||||
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 (!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 (event.modifiers & Qt.ShiftModifier) {
|
||||
controller.pasteSelected();
|
||||
return;
|
||||
}
|
||||
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
|
||||
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
|
||||
height: showFooter ? 32 : 0
|
||||
visible: showFooter
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: categoryRow
|
||||
width: parent.width
|
||||
height: controller.activePluginCategories.length > 0 ? 36 : 0
|
||||
visible: controller.activePluginCategories.length > 0
|
||||
spacing: Theme.spacingS
|
||||
|
||||
clip: true
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: categoryDropdown
|
||||
width: Math.min(200, parent.width)
|
||||
compactMode: true
|
||||
dropdownWidth: 200
|
||||
popupWidth: 240
|
||||
maxPopupHeight: 300
|
||||
enableFuzzySearch: controller.activePluginCategories.length > 8
|
||||
currentValue: {
|
||||
const cats = controller.activePluginCategories;
|
||||
const current = controller.activePluginCategory;
|
||||
if (!current)
|
||||
return cats.length > 0 ? cats[0].name : "";
|
||||
for (let i = 0; i < cats.length; i++) {
|
||||
if (cats[i].id === current)
|
||||
return cats[i].name;
|
||||
}
|
||||
return cats.length > 0 ? cats[0].name : "";
|
||||
}
|
||||
options: {
|
||||
const cats = controller.activePluginCategories;
|
||||
const names = [];
|
||||
for (let i = 0; i < cats.length; i++)
|
||||
names.push(cats[i].name);
|
||||
return names;
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const cats = controller.activePluginCategories;
|
||||
for (let i = 0; i < cats.length; i++) {
|
||||
if (cats[i].name === value) {
|
||||
controller.setActivePluginCategory(cats[i].id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
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")
|
||||
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 bool isCoreApp: item?.type === "app" && item?.isCore
|
||||
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
|
||||
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
|
||||
readonly property string appId: {
|
||||
if (isCoreApp) {
|
||||
return item?.id || coreAppData?.builtInPluginId || "";
|
||||
}
|
||||
return desktopEntry?.id || desktopEntry?.execString || "";
|
||||
}
|
||||
readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
|
||||
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 (item?.type === "app") {
|
||||
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"
|
||||
});
|
||||
|
||||
if (isRegularApp && SessionService.nvidiaCommand) {
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "memory",
|
||||
text: I18n.tr("Launch on dGPU"),
|
||||
action: launchWithNvidia
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "launch",
|
||||
text: I18n.tr("Launch"),
|
||||
action: launchApp
|
||||
});
|
||||
|
||||
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 (isCoreApp) {
|
||||
if (!coreAppData)
|
||||
return;
|
||||
AppSearchService.executeCoreApp(coreAppData);
|
||||
controller?.itemExecuted();
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
function getFirstItemIndex(flatModel) {
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (!flatModel[i].isHeader)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findNextNonHeaderIndex(flatModel, startIndex) {
|
||||
for (var i = startIndex; i < flatModel.length; i++) {
|
||||
if (!flatModel[i].isHeader)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findPrevNonHeaderIndex(flatModel, startIndex) {
|
||||
for (var i = startIndex; i >= 0; i--) {
|
||||
if (!flatModel[i].isHeader)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getSectionBounds(flatModel, sectionId) {
|
||||
var start = -1, end = -1;
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
|
||||
start = i + 1;
|
||||
} else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
|
||||
end = i;
|
||||
} else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
count: end >= start ? end - start + 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
function getGridColumns(viewMode, gridColumns) {
|
||||
switch (viewMode) {
|
||||
case "tile":
|
||||
return 3;
|
||||
case "grid":
|
||||
return gridColumns;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
|
||||
if (actualViewMode === "list") {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var cols = getGridColumns(actualViewMode, gridColumns);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
var newPosInSection = posInSection + cols;
|
||||
|
||||
if (newPosInSection < bounds.count) {
|
||||
return bounds.start + newPosInSection;
|
||||
}
|
||||
|
||||
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
|
||||
return nextSection !== -1 ? nextSection : selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
|
||||
if (actualViewMode === "list") {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var cols = getGridColumns(actualViewMode, gridColumns);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
var newPosInSection = posInSection - cols;
|
||||
|
||||
if (newPosInSection >= 0) {
|
||||
return bounds.start + newPosInSection;
|
||||
}
|
||||
|
||||
var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1);
|
||||
return prevItem !== -1 ? prevItem : selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var viewMode = getSectionViewModeFn(entry.sectionId);
|
||||
if (viewMode === "list") {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
if (posInSection + 1 < bounds.count) {
|
||||
return bounds.start + posInSection + 1;
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var viewMode = getSectionViewModeFn(entry.sectionId);
|
||||
if (viewMode === "list") {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
if (posInSection > 0) {
|
||||
return bounds.start + posInSection - 1;
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculateNextSectionIndex(flatModel, selectedFlatIndex) {
|
||||
var currentSection = null;
|
||||
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
|
||||
currentSection = flatModel[selectedFlatIndex].sectionId;
|
||||
}
|
||||
|
||||
var foundCurrent = false;
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (flatModel[i].isHeader) {
|
||||
if (foundCurrent) {
|
||||
for (var j = i + 1; j < flatModel.length; j++) {
|
||||
if (!flatModel[j].isHeader)
|
||||
return j;
|
||||
}
|
||||
}
|
||||
if (flatModel[i].section.id === currentSection) {
|
||||
foundCurrent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculatePrevSectionIndex(flatModel, selectedFlatIndex) {
|
||||
var currentSection = null;
|
||||
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
|
||||
currentSection = flatModel[selectedFlatIndex].sectionId;
|
||||
}
|
||||
|
||||
var lastSectionStart = -1;
|
||||
var prevSectionStart = -1;
|
||||
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (flatModel[i].isHeader) {
|
||||
if (flatModel[i].section.id === currentSection) {
|
||||
break;
|
||||
}
|
||||
prevSectionStart = lastSectionStart;
|
||||
lastSectionStart = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSectionStart >= 0) {
|
||||
for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
|
||||
if (!flatModel[j].isHeader)
|
||||
return j;
|
||||
}
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var itemsToSkip = visibleItems || 8;
|
||||
var newIndex = selectedFlatIndex;
|
||||
|
||||
for (var i = 0; i < itemsToSkip; i++) {
|
||||
var next = findNextNonHeaderIndex(flatModel, newIndex + 1);
|
||||
if (next === -1)
|
||||
break;
|
||||
newIndex = next;
|
||||
}
|
||||
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var itemsToSkip = visibleItems || 8;
|
||||
var newIndex = selectedFlatIndex;
|
||||
|
||||
for (var i = 0; i < itemsToSkip; i++) {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1);
|
||||
if (prev === -1)
|
||||
break;
|
||||
newIndex = prev;
|
||||
}
|
||||
|
||||
return newIndex;
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
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 || allModeToggleArea.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
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: allModeToggle
|
||||
visible: root.item?.type === "plugin_browse"
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
property bool isAllowed: {
|
||||
if (root.item?.type !== "plugin_browse")
|
||||
return false;
|
||||
var pluginId = root.item?.data?.pluginId;
|
||||
if (!pluginId)
|
||||
return false;
|
||||
SettingsData.launcherPluginVisibility;
|
||||
return SettingsData.getPluginAllowWithoutTrigger(pluginId);
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
|
||||
size: 18
|
||||
color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: allModeToggleArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
var pluginId = root.item?.data?.pluginId;
|
||||
if (!pluginId)
|
||||
return;
|
||||
SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
.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
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
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 toplevelId: item?.data?.toplevelId ?? ""
|
||||
readonly property var waylandToplevel: {
|
||||
if (!toplevelId || !item?.pluginId)
|
||||
return null;
|
||||
const pluginInstance = PluginService.pluginInstances[item.pluginId];
|
||||
if (!pluginInstance?.getToplevelById)
|
||||
return null;
|
||||
return pluginInstance.getToplevelById(toplevelId);
|
||||
}
|
||||
readonly property bool hasScreencopy: waylandToplevel !== null
|
||||
|
||||
readonly property string iconValue: {
|
||||
if (!item)
|
||||
return "";
|
||||
if (hasScreencopy)
|
||||
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
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopyView
|
||||
anchors.fill: parent
|
||||
captureSource: root.waylandToplevel
|
||||
live: root.hasScreencopy
|
||||
visible: root.hasScreencopy
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
visible: !root.hasScreencopy
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: attributionBadge
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.spacingXS
|
||||
width: root.hasScreencopy ? 28 : 40
|
||||
height: root.hasScreencopy ? 28 : 16
|
||||
radius: root.hasScreencopy ? 14 : 4
|
||||
color: root.hasScreencopy ? Theme.surfaceContainer : "transparent"
|
||||
visible: attributionImage.status === Image.Ready
|
||||
opacity: 0.95
|
||||
|
||||
Image {
|
||||
id: attributionImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.hasScreencopy ? 4 : 0
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: root.item?.data?.attribution || ""
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,8 +11,7 @@ FloatingWindow {
|
||||
id: processListModal
|
||||
|
||||
property int currentTab: 0
|
||||
property string searchText: ""
|
||||
property string expandedPid: ""
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
property bool shouldHaveFocus: visible
|
||||
property alias shouldBeVisible: processListModal.visible
|
||||
|
||||
@@ -28,8 +27,9 @@ FloatingWindow {
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
if (processContextMenu.visible)
|
||||
if (processContextMenu.visible) {
|
||||
processContextMenu.close();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -61,63 +61,48 @@ FloatingWindow {
|
||||
show();
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + " B/s";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(1) + " KB/s";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
function nextTab() {
|
||||
currentTab = (currentTab + 1) % 4;
|
||||
}
|
||||
|
||||
function previousTab() {
|
||||
currentTab = (currentTab - 1 + 4) % 4;
|
||||
}
|
||||
|
||||
objectName: "processListModal"
|
||||
title: I18n.tr("System Monitor", "sysmon window title")
|
||||
minimumSize: Qt.size(750, 550)
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 720
|
||||
minimumSize: Qt.size(650, 400)
|
||||
implicitWidth: 900
|
||||
implicitHeight: 680
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
onCurrentTabChanged: {
|
||||
if (visible && currentTab === 0 && searchField.visible)
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
closingModal();
|
||||
searchText = "";
|
||||
expandedPid = "";
|
||||
if (processesTabLoader.item)
|
||||
processesTabLoader.item.reset();
|
||||
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
|
||||
} else {
|
||||
DgopService.addRef(["cpu", "memory", "network", "disk", "system"]);
|
||||
Qt.callLater(() => {
|
||||
if (currentTab === 0 && searchField.visible)
|
||||
searchField.forceActiveFocus();
|
||||
else if (contentFocusScope)
|
||||
if (contentFocusScope) {
|
||||
contentFocusScope.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
|
||||
PerformanceTab {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
|
||||
SystemTab {}
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: processContextMenu
|
||||
parentFocusItem: contentFocusScope
|
||||
onProcessKilled: {
|
||||
if (processesTabLoader.item)
|
||||
processesTabLoader.item.forceRefresh(3);
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
@@ -130,9 +115,6 @@ FloatingWindow {
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (processContextMenu.visible)
|
||||
return;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_1:
|
||||
currentTab = 0;
|
||||
@@ -146,43 +128,7 @@ FloatingWindow {
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_4:
|
||||
currentTab = 3;
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
nextTab();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
previousTab();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Escape:
|
||||
if (searchText.length > 0) {
|
||||
searchText = "";
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (currentTab === 0 && processesTabLoader.item?.keyboardNavigationActive) {
|
||||
processesTabLoader.item.reset();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
hide();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_F:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
searchField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentTab === 0 && processesTabLoader.item)
|
||||
processesTabLoader.item.handleKey(event);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -215,7 +161,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message")
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -225,14 +171,14 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
visible: DgopService.dgopAvailable
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 48
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -287,278 +233,166 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 52
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
text: I18n.tr("Processes"),
|
||||
icon: "list_alt"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Performance"),
|
||||
icon: "analytics"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Disks"),
|
||||
icon: "storage"
|
||||
},
|
||||
{
|
||||
text: I18n.tr("System"),
|
||||
icon: "computer"
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: currentTab = index
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
Layout.preferredWidth: 250
|
||||
Layout.preferredHeight: 40
|
||||
placeholderText: I18n.tr("Search processes...", "process search placeholder")
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
text: searchText
|
||||
visible: currentTab === 0
|
||||
onTextChanged: searchText = text
|
||||
ignoreUpDownKeys: true
|
||||
keyForwardTargets: [contentFocusScope]
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: Theme.spacingL
|
||||
Layout.topMargin: Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
id: processesTabLoader
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
sourceComponent: ProcessesView {
|
||||
searchText: processListModal.searchText
|
||||
expandedPid: processListModal.expandedPid
|
||||
contextMenu: processContextMenu
|
||||
onExpandedPidChanged: processListModal.expandedPid = expandedPid
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
sourceComponent: PerformanceView {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: disksTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
sourceComponent: DisksView {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTabLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 3
|
||||
visible: currentTab === 3
|
||||
sourceComponent: SystemView {}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
Layout.bottomMargin: Theme.spacingM
|
||||
color: "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
anchors.topMargin: 0
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes:", "process count label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: DgopService.processCount.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
Repeater {
|
||||
model: tabNames
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||
return tabIcons[index] || "tab";
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Uptime:", "uptime label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
Loader {
|
||||
id: processesTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.shortUptime || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: performanceTab
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
opacity: currentTab === 1 ? 1 : 0
|
||||
sourceComponent: performanceTabComponent
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "swap_horiz"
|
||||
size: 14
|
||||
color: Theme.info
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: systemTab
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: 14
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: 14
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.cpuUsage.toFixed(1) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: DgopService.cpuUsage > 80 ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "sd_card"
|
||||
size: 14
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.weight: Font.Bold
|
||||
color: DgopService.memoryUsage > 90 ? Theme.error : Theme.surfaceText
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +539,7 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingXS
|
||||
height: Theme.spacingS
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
@@ -717,7 +717,7 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingXS
|
||||
height: Theme.spacingS
|
||||
visible: !root.searchActive
|
||||
}
|
||||
|
||||
|
||||
237
quickshell/Modals/Spotlight/FileSearchController.qml
Normal file
237
quickshell/Modals/Spotlight/FileSearchController.qml
Normal file
@@ -0,0 +1,237 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
155
quickshell/Modals/Spotlight/FileSearchEntry.qml
Normal file
155
quickshell/Modals/Spotlight/FileSearchEntry.qml
Normal file
@@ -0,0 +1,155 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
269
quickshell/Modals/Spotlight/FileSearchResults.qml
Normal file
269
quickshell/Modals/Spotlight/FileSearchResults.qml
Normal file
@@ -0,0 +1,269 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
516
quickshell/Modals/Spotlight/SpotlightContent.qml
Normal file
516
quickshell/Modals/Spotlight/SpotlightContent.qml
Normal file
@@ -0,0 +1,516 @@
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
117
quickshell/Modals/Spotlight/SpotlightContextMenu.qml
Normal file
117
quickshell/Modals/Spotlight/SpotlightContextMenu.qml
Normal file
@@ -0,0 +1,117 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user